Skip to main content

pfx_trace/
rig.rs

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}