Skip to main content

pfx_load/
room.rs

1use serde::{Deserialize, Serialize};
2
3use crate::Sky;
4use crate::vec3::{cross, dot};
5
6const MAX_WIDTH: u32 = 8192;
7
8#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
9#[serde(deny_unknown_fields)]
10pub struct RoomSky {
11    #[serde(default = "room_width")]
12    pub width: u32,
13    #[serde(default = "room_floor")]
14    pub floor: [f32; 3],
15    #[serde(default = "room_wall")]
16    pub wall: [f32; 3],
17    #[serde(default = "room_ceiling")]
18    pub ceiling: [f32; 3],
19    #[serde(default)]
20    pub lights: Vec<RoomLamp>,
21}
22
23#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct RoomLamp {
26    #[serde(default, skip_serializing_if = "String::is_empty")]
27    pub name: String,
28    #[serde(default)]
29    pub az: f32,
30    #[serde(default = "lamp_el")]
31    pub el: f32,
32    #[serde(default = "lamp_power")]
33    pub power: f32,
34    #[serde(default = "lamp_color")]
35    pub color: [f32; 3],
36    #[serde(default = "lamp_width")]
37    pub width: f32,
38    #[serde(default = "lamp_height")]
39    pub height: f32,
40    #[serde(default = "lamp_soft")]
41    pub soft: f32,
42    #[serde(default)]
43    pub slats: f32,
44    #[serde(default = "lamp_open")]
45    pub open: f32,
46}
47
48impl Default for RoomSky {
49    fn default() -> Self {
50        Self {
51            width: room_width(),
52            floor: room_floor(),
53            wall: room_wall(),
54            ceiling: room_ceiling(),
55            lights: Vec::new(),
56        }
57    }
58}
59
60impl Default for RoomLamp {
61    fn default() -> Self {
62        Self {
63            name: String::new(),
64            az: 0.0,
65            el: lamp_el(),
66            power: lamp_power(),
67            color: lamp_color(),
68            width: lamp_width(),
69            height: lamp_height(),
70            soft: lamp_soft(),
71            slats: 0.0,
72            open: lamp_open(),
73        }
74    }
75}
76
77fn room_width() -> u32 {
78    64
79}
80fn room_floor() -> [f32; 3] {
81    [0.5, 0.5, 0.5]
82}
83fn room_wall() -> [f32; 3] {
84    [0.5, 0.5, 0.5]
85}
86fn room_ceiling() -> [f32; 3] {
87    [0.5, 0.5, 0.5]
88}
89fn lamp_el() -> f32 {
90    0.0
91}
92fn lamp_power() -> f32 {
93    1.0
94}
95fn lamp_color() -> [f32; 3] {
96    [1.0, 1.0, 1.0]
97}
98fn lamp_width() -> f32 {
99    45.0
100}
101fn lamp_height() -> f32 {
102    45.0
103}
104fn lamp_soft() -> f32 {
105    0.1
106}
107fn lamp_open() -> f32 {
108    1.0
109}
110
111impl RoomSky {
112    pub fn check(&self) -> Result<(), String> {
113        if !(2..=MAX_WIDTH).contains(&self.width) {
114            return Err(format!(
115                "room width is {}, it must be 2 to {MAX_WIDTH}",
116                self.width
117            ));
118        }
119        let colour = |c: &[f32; 3]| c.iter().all(|v| v.is_finite() && *v >= 0.0);
120        if !colour(&self.floor) || !colour(&self.wall) || !colour(&self.ceiling) {
121            return Err("room floor, wall and ceiling must be finite and nonnegative".into());
122        }
123        for lamp in &self.lights {
124            if !colour(&lamp.color)
125                || [lamp.az, lamp.el, lamp.open].iter().any(|v| !v.is_finite())
126                || [lamp.power, lamp.soft, lamp.slats]
127                    .iter()
128                    .any(|v| !v.is_finite() || *v < 0.0)
129                || [lamp.width, lamp.height]
130                    .iter()
131                    .any(|v| !v.is_finite() || *v <= 0.0 || *v >= 180.0)
132            {
133                return Err(format!(
134                    "room lamp {:?} needs finite values, nonnegative power, colour, soft and slats, and a width and height between 0 and 180 degrees",
135                    lamp.name
136                ));
137            }
138        }
139        Ok(())
140    }
141}
142
143struct Lamp {
144    center: [f32; 3],
145    t1: [f32; 3],
146    t2: [f32; 3],
147    half: [f32; 2],
148    soft: f32,
149    color: [f32; 3],
150    slats: f32,
151    open: f32,
152}
153
154impl Lamp {
155    fn new(lamp: &RoomLamp) -> Self {
156        let az = lamp.az.to_radians();
157        let el = lamp.el.to_radians();
158        let center = [az.sin() * el.cos(), el.sin(), az.cos() * el.cos()];
159        let t1 = norm(cross([0.0, 1.0, 0.0], center));
160        let t2 = cross(center, t1);
161        Self {
162            center,
163            t1,
164            t2,
165            half: [
166                (lamp.width.to_radians() * 0.5).tan(),
167                (lamp.height.to_radians() * 0.5).tan(),
168            ],
169            soft: lamp.soft,
170            color: lamp.color.map(|x| x * lamp.power),
171            slats: lamp.slats,
172            open: lamp.open,
173        }
174    }
175
176    fn weight(&self, d: [f32; 3]) -> f32 {
177        let along = dot(d, self.center);
178        if along <= 0.05 {
179            return 0.0;
180        }
181        let u = dot(d, self.t1) / along / self.half[0];
182        let v = dot(d, self.t2) / along / self.half[1];
183        let edge = |q: f32| ((1.0 - q.abs()) / self.soft.max(1e-3)).clamp(0.0, 1.0);
184        let mut w = edge(u) * edge(v);
185        if self.slats > 0.0 && w > 0.0 {
186            let s = ((v * 0.5 + 0.5) * self.slats).fract();
187            w *= ((self.open - s) / 0.04).clamp(0.0, 1.0) * (s / 0.04).clamp(0.0, 1.0);
188        }
189        w
190    }
191}
192
193impl Sky {
194    pub fn room(room: &RoomSky) -> Sky {
195        let width = room.width;
196        let height = width / 2;
197        let lamps: Vec<Lamp> = room.lights.iter().map(Lamp::new).collect();
198        let mut texels = Vec::with_capacity(width as usize * height as usize);
199        for y in 0..height {
200            for x in 0..width {
201                let theta = (y as f32 + 0.5) / height as f32 * std::f32::consts::PI;
202                let phi = ((x as f32 + 0.5) / width as f32 - 0.5) * std::f32::consts::TAU;
203                let d = [
204                    theta.sin() * phi.sin(),
205                    theta.cos(),
206                    -theta.sin() * phi.cos(),
207                ];
208                let mut c = if d[1] < 0.0 {
209                    mix(room.wall, room.floor, (-d[1]).powf(0.5))
210                } else {
211                    mix(room.wall, room.ceiling, d[1].powf(0.6))
212                };
213                for lamp in &lamps {
214                    let w = lamp.weight(d);
215                    for (ck, lk) in c.iter_mut().zip(lamp.color) {
216                        *ck += lk * w;
217                    }
218                }
219                texels.push([c[0], c[1], c[2], 1.0]);
220            }
221        }
222        Sky {
223            width,
224            height,
225            texels,
226        }
227    }
228}
229
230fn norm(a: [f32; 3]) -> [f32; 3] {
231    let l = dot(a, a).sqrt().max(1e-9);
232    [a[0] / l, a[1] / l, a[2] / l]
233}
234
235fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
236    [
237        a[0] + (b[0] - a[0]) * t,
238        a[1] + (b[1] - a[1]) * t,
239        a[2] + (b[2] - a[2]) * t,
240    ]
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use serde_json::{Value, json};
247
248    fn reference_room(spec: &Value) -> (u32, u32, Vec<[f32; 4]>) {
249        let num = |v: &Value, d: f32| v.as_f64().map(|x| x as f32).unwrap_or(d);
250        let rgb = |v: &Value, d: [f32; 3]| match v.as_array() {
251            Some(a) if a.len() == 3 => [num(&a[0], d[0]), num(&a[1], d[1]), num(&a[2], d[2])],
252            _ => d,
253        };
254        let width = num(&spec["width"], 64.0) as u32;
255        let height = width / 2;
256        let floor = rgb(&spec["floor"], [0.5, 0.5, 0.5]);
257        let wall = rgb(&spec["wall"], [0.5, 0.5, 0.5]);
258        let ceiling = rgb(&spec["ceiling"], [0.5, 0.5, 0.5]);
259        struct Lamp {
260            center: [f32; 3],
261            t1: [f32; 3],
262            t2: [f32; 3],
263            half: [f32; 2],
264            soft: f32,
265            color: [f32; 3],
266            slats: f32,
267            open: f32,
268        }
269        let lamps: Vec<Lamp> = spec["lights"]
270            .as_array()
271            .map(|list| {
272                list.iter()
273                    .map(|l| {
274                        let az = num(&l["az"], 0.0).to_radians();
275                        let el = num(&l["el"], 0.0).to_radians();
276                        let c = [az.sin() * el.cos(), el.sin(), az.cos() * el.cos()];
277                        let up = [0.0, 1.0, 0.0];
278                        let t1 = norm(cross(up, c));
279                        let t2 = cross(c, t1);
280                        let power = num(&l["power"], 1.0);
281                        let tint = rgb(&l["color"], [1.0, 1.0, 1.0]);
282                        Lamp {
283                            center: c,
284                            t1,
285                            t2,
286                            half: [
287                                (num(&l["width"], 45.0).to_radians() * 0.5).tan(),
288                                (num(&l["height"], 45.0).to_radians() * 0.5).tan(),
289                            ],
290                            soft: num(&l["soft"], 0.1),
291                            color: tint.map(|x| x * power),
292                            slats: num(&l["slats"], 0.0),
293                            open: num(&l["open"], 1.0),
294                        }
295                    })
296                    .collect()
297            })
298            .unwrap_or_default();
299        let mut texels = Vec::with_capacity((width * height) as usize);
300        for y in 0..height {
301            for x in 0..width {
302                let theta = (y as f32 + 0.5) / height as f32 * std::f32::consts::PI;
303                let phi = ((x as f32 + 0.5) / width as f32 - 0.5) * std::f32::consts::TAU;
304                let d = [
305                    theta.sin() * phi.sin(),
306                    theta.cos(),
307                    -theta.sin() * phi.cos(),
308                ];
309                let mut c = if d[1] < 0.0 {
310                    let t = (-d[1]).powf(0.5);
311                    mix(wall, floor, t)
312                } else {
313                    let t = d[1].powf(0.6);
314                    mix(wall, ceiling, t)
315                };
316                for lamp in &lamps {
317                    let along = dot(d, lamp.center);
318                    if along <= 0.05 {
319                        continue;
320                    }
321                    let u = dot(d, lamp.t1) / along / lamp.half[0];
322                    let v = dot(d, lamp.t2) / along / lamp.half[1];
323                    let edge = |q: f32| ((1.0 - q.abs()) / lamp.soft.max(1e-3)).clamp(0.0, 1.0);
324                    let mut w = edge(u) * edge(v);
325                    if lamp.slats > 0.0 && w > 0.0 {
326                        let s = ((v * 0.5 + 0.5) * lamp.slats).fract();
327                        w *= ((lamp.open - s) / 0.04).clamp(0.0, 1.0)
328                            * ((s - 0.0) / 0.04).clamp(0.0, 1.0).max(0.0);
329                    }
330                    for (ck, lk) in c.iter_mut().zip(lamp.color) {
331                        *ck += lk * w;
332                    }
333                }
334                texels.push([c[0], c[1], c[2], 1.0]);
335            }
336        }
337        (width, height, texels)
338    }
339
340    fn made_up() -> Value {
341        json!({
342            "width": 32,
343            "floor": [0.2, 0.3, 0.4],
344            "wall": [0.45, 0.5, 0.55],
345            "ceiling": [0.7, 0.75, 0.8],
346            "lights": [
347                {"name": "north", "az": 15.0, "el": 25.0, "power": 2.0, "color": [0.9, 0.8, 0.7],
348                 "width": 20.0, "height": 12.0, "soft": 0.2, "slats": 5.0, "open": 0.6}
349            ]
350        })
351    }
352
353    fn every_field() -> Value {
354        json!({
355            "width": 512,
356            "floor": [0.2, 0.12, 0.05],
357            "wall": [0.25, 0.2, 0.15],
358            "ceiling": [0.6, 0.5, 0.4],
359            "lights": [
360                {"name": "blinds", "az": -70.0, "el": 18.0, "power": 9.0, "color": [1.0, 0.88, 0.7],
361                 "width": 40.0, "height": 34.0, "soft": 0.05, "slats": 9.0, "open": 0.62},
362                {"az": 120.0, "el": 55.0, "power": 2.5, "color": [0.8, 0.9, 1.0],
363                 "width": 25.0, "height": 60.0, "soft": 0.2, "slats": 3.5, "open": 0.4},
364                {"az": 200.0, "el": -10.0, "power": 0.7, "color": [1.0, 1.0, 1.0],
365                 "width": 120.0, "height": 15.0, "soft": 0.0, "slats": 0.0, "open": 1.0}
366            ]
367        })
368    }
369
370    fn matches(spec: Value) {
371        let (width, height, want) = reference_room(&spec);
372        let room: RoomSky = serde_json::from_value(spec).unwrap();
373        room.check().unwrap();
374        let sky = Sky::room(&room);
375        assert_eq!((sky.width, sky.height), (width, height));
376        assert_eq!(sky.texels.len(), want.len());
377        for (got, want) in sky.texels.iter().zip(&want) {
378            for k in 0..4 {
379                assert!((got[k] - want[k]).abs() <= 1e-6, "{got:?} against {want:?}");
380            }
381        }
382    }
383
384    #[test]
385    fn the_room_matches_a_reference_texel_for_texel() {
386        matches(made_up());
387        matches(json!({}));
388        matches(every_field());
389    }
390
391    #[test]
392    fn a_spec_deserializes_with_its_names_and_neutral_defaults() {
393        let room: RoomSky = serde_json::from_value(made_up()).unwrap();
394        assert_eq!(room.width, 32);
395        assert_eq!(room.lights.len(), 1);
396        assert_eq!(room.lights[0].name, "north");
397        assert_eq!(room.lights[0].slats, 5.0);
398        assert_eq!(room.lights[0].open, 0.6);
399        let empty: RoomSky = serde_json::from_value(json!({})).unwrap();
400        assert_eq!(empty, RoomSky::default());
401        let lamp: RoomLamp = serde_json::from_value(json!({})).unwrap();
402        assert_eq!(lamp, RoomLamp::default());
403        assert!(serde_json::from_value::<RoomSky>(json!({"lamps": []})).is_err());
404    }
405
406    #[test]
407    fn the_slats_cut_dark_bands_through_a_lamp() {
408        let lamp = RoomLamp {
409            az: 0.0,
410            el: 0.0,
411            width: 40.0,
412            height: 40.0,
413            soft: 0.01,
414            slats: 4.0,
415            open: 0.5,
416            ..RoomLamp::default()
417        };
418        let lit = Lamp::new(&RoomLamp {
419            slats: 0.0,
420            ..lamp.clone()
421        });
422        let slatted = Lamp::new(&lamp);
423        let mut dark = 0;
424        let mut bright = 0;
425        for step in 0..64 {
426            let v = (step as f32 + 0.5) / 64.0 * 1.6 - 0.8;
427            let d = norm([0.0, v * slatted.half[1], 1.0]);
428            assert!((lit.weight(d) - 1.0).abs() < 1e-6);
429            let w = slatted.weight(d);
430            if w < 1e-6 {
431                dark += 1;
432            } else if w > 0.999 {
433                bright += 1;
434            }
435        }
436        assert!(dark > 16 && bright > 16, "{dark} dark and {bright} bright");
437    }
438
439    #[test]
440    fn checking_refuses_a_broken_room() {
441        assert!(RoomSky::default().check().is_ok());
442        let narrow = RoomSky {
443            width: 1,
444            ..RoomSky::default()
445        };
446        assert!(narrow.check().is_err());
447        let dark = RoomSky {
448            floor: [-0.1, 0.0, 0.0],
449            ..RoomSky::default()
450        };
451        assert!(dark.check().is_err());
452        let wide = RoomSky {
453            lights: vec![RoomLamp {
454                width: 180.0,
455                ..RoomLamp::default()
456            }],
457            ..RoomSky::default()
458        };
459        assert!(wide.check().is_err());
460        let nan = RoomSky {
461            lights: vec![RoomLamp {
462                power: f32::NAN,
463                ..RoomLamp::default()
464            }],
465            ..RoomSky::default()
466        };
467        assert!(nan.check().is_err());
468    }
469}