1use pfx_load::{RoomLamp, RoomSky, Sky};
2
3use crate::lights::LocalLight;
4use crate::math::{cross, dot, length, normalize, scale, sub};
5
6pub const ROOM_MEAN: f32 = 0.80;
7pub const ROOM_WIDTH: u32 = 1024;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Lamp {
11 pub az: f32,
12 pub el: f32,
13 pub width: f32,
14 pub height: f32,
15 pub power: f32,
16 pub colour: [f32; 3],
17}
18
19pub const ROOM_LAMPS: [Lamp; 4] = [
20 Lamp {
21 az: 133.8,
22 el: 27.8,
23 width: 4.0,
24 height: 4.0,
25 power: 420.0,
26 colour: [0.904, 1.012, 1.163],
27 },
28 Lamp {
29 az: -1.1,
30 el: 22.7,
31 width: 15.5,
32 height: 1.5,
33 power: 147.0,
34 colour: [0.719, 1.087, 0.97],
35 },
36 Lamp {
37 az: 21.4,
38 el: 4.7,
39 width: 6.0,
40 height: 6.0,
41 power: 386.0,
42 colour: [0.874, 1.02, 1.173],
43 },
44 Lamp {
45 az: -146.4,
46 el: 1.5,
47 width: 5.0,
48 height: 5.0,
49 power: 502.0,
50 colour: [0.881, 1.016, 1.193],
51 },
52];
53
54pub const ROOM_FLOOR: [f32; 3] = [0.103, 0.131, 0.135];
55pub const ROOM_WALL: [f32; 3] = [0.020, 0.025, 0.026];
56pub const ROOM_CEILING: [f32; 3] = [0.0012, 0.0012, 0.0019];
57
58pub fn softbox_room(turn_deg: f32) -> RoomSky {
59 RoomSky {
60 width: ROOM_WIDTH,
61 floor: ROOM_FLOOR,
62 wall: ROOM_WALL,
63 ceiling: ROOM_CEILING,
64 lights: ROOM_LAMPS
65 .iter()
66 .map(|lamp| RoomLamp {
67 name: String::new(),
68 az: lamp.az - turn_deg,
69 el: lamp.el,
70 power: lamp.power,
71 color: lamp.colour,
72 width: lamp.width,
73 height: lamp.height,
74 soft: 0.18,
75 slats: 0.0,
76 open: 1.0,
77 })
78 .collect(),
79 }
80}
81
82pub fn room(turn_deg: f32, strength: f32) -> Sky {
83 let sky = Sky::room(&softbox_room(turn_deg));
84 let mean = solid_angle_mean(&sky);
85 sky.exposed(ROOM_MEAN / mean.max(1e-6) * strength)
86}
87
88pub fn solid_angle_mean(sky: &Sky) -> f32 {
89 let (mut total, mut weight) = (0.0f64, 0.0f64);
90 for (y, row) in sky.texels.chunks(sky.width as usize).enumerate() {
91 let theta = (y as f64 + 0.5) / f64::from(sky.height) * std::f64::consts::PI;
92 let w = theta.sin();
93 for texel in row {
94 total += w * f64::from(0.2126 * texel[0] + 0.7152 * texel[1] + 0.0722 * texel[2]);
95 weight += w;
96 }
97 }
98 (total / weight.max(1e-12)) as f32
99}
100
101pub fn engine_axes(blender: [f32; 3]) -> [f32; 3] {
102 [blender[0], blender[2], -blender[1]]
103}
104
105#[derive(Clone, Debug, PartialEq)]
106pub struct Rig {
107 pub key: f32,
108 pub fill: f32,
109 pub rim: f32,
110 pub key_from: [f32; 3],
111 pub key_color: Option<[f32; 3]>,
112 pub rim_color: Vec<[f32; 3]>,
113 pub strength: f32,
114 pub turn: f32,
115}
116
117impl Default for Rig {
118 fn default() -> Self {
119 Self {
120 key: 1.0,
121 fill: 0.35,
122 rim: 0.8,
123 key_from: [-2.2, -2.6, 3.4],
124 key_color: None,
125 rim_color: Vec::new(),
126 strength: 0.6,
127 turn: 25.0,
128 }
129 }
130}
131
132#[derive(Clone, Copy, Debug, PartialEq)]
133pub struct Softbox {
134 pub at: [f32; 3],
135 pub size: f32,
136 pub power: f32,
137 pub colour: [f32; 3],
138}
139
140pub const RIG_TARGET: [f32; 3] = [0.0, 0.0, 0.1];
141
142impl Rig {
143 pub fn boxes(&self) -> Vec<Softbox> {
144 let white = [1.0; 3];
145 let rim = |k: usize| {
146 self.rim_color
147 .get(k.min(self.rim_color.len().saturating_sub(1)))
148 .copied()
149 .unwrap_or(white)
150 };
151 let mut boxes = vec![
152 Softbox {
153 at: self.key_from,
154 size: 3.0,
155 power: self.key,
156 colour: self.key_color.unwrap_or(white),
157 },
158 Softbox {
159 at: [3.0, -1.2, 2.2],
160 size: 2.4,
161 power: self.fill,
162 colour: white,
163 },
164 Softbox {
165 at: [0.6, 3.4, 2.6],
166 size: 2.0,
167 power: self.rim,
168 colour: rim(0),
169 },
170 ];
171 if self.rim_color.len() > 1 {
172 boxes.push(Softbox {
173 at: [-0.8, 3.2, 2.4],
174 size: 2.0,
175 power: self.rim * 0.8,
176 colour: rim(1),
177 });
178 }
179 boxes
180 }
181
182 pub fn lights(&self) -> Vec<LocalLight> {
183 self.boxes().iter().map(|softbox| softbox.light()).collect()
184 }
185
186 pub fn sky(&self) -> Sky {
187 room(self.turn, self.strength)
188 }
189}
190
191impl Softbox {
192 pub fn light(&self) -> LocalLight {
193 let centre = engine_axes(self.at);
194 let aim = normalize(sub(engine_axes(RIG_TARGET), centre));
195 let back = scale(aim, -1.0);
196 let up = [0.0, 1.0, 0.0];
197 let along = sub(up, scale(back, dot(up, back)));
198 let y = if length(along) > 1e-6 {
199 normalize(along)
200 } else {
201 normalize(cross(back, [1.0, 0.0, 0.0]))
202 };
203 let x = cross(y, back);
204 let distance2 = dot(self.at, self.at);
205 LocalLight::rect(
206 centre,
207 scale(y, self.size * 0.3),
208 scale(x, self.size * 0.5),
209 self.colour,
210 self.power * distance2,
211 )
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::lights::LightShape;
219
220 #[test]
221 fn the_room_has_the_mean_it_documents() {
222 let sky = room(25.0, 1.0);
223 assert_eq!((sky.width, sky.height), (ROOM_WIDTH, ROOM_WIDTH / 2));
224 assert!((solid_angle_mean(&sky) - ROOM_MEAN).abs() < 1e-3);
225 let dim = room(0.0, 0.5);
226 assert!((solid_angle_mean(&dim) - ROOM_MEAN * 0.5).abs() < 1e-3);
227 softbox_room(25.0).check().unwrap();
228 let w = sky.width as usize;
229 let row = |y: usize| {
230 sky.texels[y * w..(y + 1) * w]
231 .iter()
232 .map(|t| t[0])
233 .sum::<f32>()
234 / w as f32
235 };
236 let (floor, ceiling) = (row(sky.height as usize - 1), row(0));
237 let peak = sky.texels.iter().map(|t| t[0]).fold(0.0, f32::max);
238 assert!(
239 floor > 10.0 * ceiling && peak > 40.0 * floor,
240 "floor {floor} ceiling {ceiling} peak {peak}"
241 );
242 assert!(floor < 0.5, "floor {floor}");
243 }
244
245 #[test]
246 fn the_room_turns_its_lamps() {
247 let still = room(0.0, 1.0);
248 let turned = room(90.0, 1.0);
249 let w = still.width as usize;
250 let row = (still.height as usize / 2) - 14;
251 let brightest = |sky: &Sky| {
252 (0..w)
253 .max_by(|&a, &b| sky.texels[row * w + a][0].total_cmp(&sky.texels[row * w + b][0]))
254 .unwrap()
255 };
256 let shift = (brightest(&turned) as i64 - brightest(&still) as i64).rem_euclid(w as i64);
257 assert!((shift - w as i64 / 4).abs() <= 2, "{shift}");
258 }
259
260 #[test]
261 fn softboxes_face_the_target_with_blenders_power() {
262 let rig = Rig::default();
263 let lights = rig.lights();
264 assert_eq!(lights.len(), 3);
265 let key = lights[0];
266 assert!(key.valid());
267 assert_eq!(key.position, [-2.2, 3.4, 2.6]);
268 let LightShape::Rect {
269 half_u,
270 half_v,
271 two_sided,
272 } = key.shape
273 else {
274 panic!("the key is a rect");
275 };
276 assert!(!two_sided);
277 let facing = normalize(cross(half_u, half_v));
278 let aim = normalize(sub(engine_axes(RIG_TARGET), key.position));
279 assert!(dot(facing, aim) > 0.9999, "{facing:?} {aim:?}");
280 assert!((length(half_u) - 0.9).abs() < 1e-5 && (length(half_v) - 1.5).abs() < 1e-5);
281 assert!(half_u[1] > 0.0);
282 let distance2 = 2.2f32 * 2.2 + 2.6 * 2.6 + 3.4 * 3.4;
283 assert!((key.intensity - distance2).abs() < 1e-4);
284 assert!((key.area() - 3.0 * 1.8).abs() < 1e-4);
285 let colours = Rig {
286 rim_color: vec![[1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
287 ..Rig::default()
288 };
289 let lights = colours.lights();
290 assert_eq!(lights.len(), 4);
291 assert_eq!(lights[2].colour, [1.0, 0.0, 0.0]);
292 assert_eq!(lights[3].colour, [0.0, 0.0, 1.0]);
293 assert!((lights[3].intensity - 0.64 * (0.64 + 3.2 * 3.2 + 2.4 * 2.4)).abs() < 1e-4);
294 }
295}