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}