1use core::f32::consts::{PI, TAU};
5use core::fmt;
6use core::fmt::Debug;
7use core::hash::Hash;
8
9use crate::assets::Unresolved;
10use crate::light::rgb;
11use crate::math::{UVec2, Vec3, Vec4};
12use crate::{Assets, Catalog, Color, TextureData};
13
14const DEFAULT: Color = Color::rgb(0.1, 0.1, 0.1);
17
18const RASTERIZED: UVec2 = UVec2::new(64, 32);
21
22const COEFFICIENT_WIDTH: u32 = 256;
25
26const GROUND_BLEND: f32 = 0.052_336;
30
31const SHAPES: [f32; 9] = [
34 0.282_095, 0.488_603, 0.488_603, 0.488_603, 1.092_548, 1.092_548, 0.315_392, 1.092_548,
35 0.546_274,
36];
37
38const TAKEN: [f32; 9] = [
42 1.0,
43 2.0 / 3.0,
44 2.0 / 3.0,
45 2.0 / 3.0,
46 0.25,
47 0.25,
48 0.25,
49 0.25,
50 0.25,
51];
52
53pub trait Skyboxes: Catalog + Clone + Debug + Eq + Hash {
59 fn build(&self, assets: &Assets) -> SkyboxData;
63}
64
65#[derive(Clone, Debug, Eq, Hash, PartialEq)]
70pub enum NoSkyboxes {}
71
72impl Catalog for NoSkyboxes {
73 fn catalog() -> Vec<Self> {
74 Vec::new()
75 }
76}
77
78impl Skyboxes for NoSkyboxes {
79 fn build(&self, _assets: &Assets) -> SkyboxData {
80 match *self {}
81 }
82}
83
84#[derive(Clone, Debug)]
89pub struct SkyboxData {
90 kind: Kind,
91 light: f32,
92 ground: Option<Color>,
93 unresolved: Unresolved,
96}
97
98#[derive(Clone, Debug)]
100enum Kind {
101 Equirect(TextureData),
102 Gradient(Gradient),
103}
104
105impl SkyboxData {
106 pub fn equirect(mut image: TextureData) -> Self {
112 let unresolved = image.take_unresolved();
113
114 Self {
115 unresolved,
116 ..Self::of(Kind::Equirect(image))
117 }
118 }
119
120 pub fn gradient(zenith: Color, horizon: Color, nadir: Color) -> Self {
126 Self::of(Kind::Gradient(Gradient::new(zenith, horizon, nadir)))
127 }
128
129 pub fn lit_by(mut self, fraction: f32) -> Self {
137 self.light = fraction.max(0.0);
138 self
139 }
140
141 pub fn with_ground(mut self, color: Color) -> Self {
149 self.ground = Some(color);
150 self
151 }
152
153 fn of(kind: Kind) -> Self {
154 Self {
155 kind,
156 light: 1.0,
157 ground: None,
158 unresolved: Unresolved::default(),
159 }
160 }
161
162 pub(crate) fn missing(unresolved: Unresolved) -> Self {
165 Self {
166 unresolved,
167 ..Self::default()
168 }
169 }
170
171 pub(crate) fn take_unresolved(&mut self) -> Unresolved {
173 self.unresolved.taken()
174 }
175
176 pub(crate) fn resident(&self) -> Result<Resident, SkyboxError> {
179 let largest = match &self.kind {
180 Kind::Equirect(image) => Mip::of(image)?,
181 Kind::Gradient(gradient) => Mip::between(*gradient),
182 };
183 let largest = match self.ground {
184 Some(ground) => largest.over(rgb(ground)),
185 None => largest,
186 };
187
188 Ok(Resident::of(largest).lit_by(self.light))
189 }
190}
191
192impl Default for SkyboxData {
193 fn default() -> Self {
195 Self::equirect(TextureData::rgba8(UVec2::new(2, 1), vec![0; 8]))
196 }
197}
198
199#[derive(Clone, Copy, Debug, PartialEq)]
201pub(crate) struct Gradient {
202 zenith: Color,
204 horizon: Color,
206 nadir: Color,
208}
209
210impl Gradient {
211 pub(crate) const fn new(zenith: Color, horizon: Color, nadir: Color) -> Self {
214 Self {
215 zenith,
216 horizon,
217 nadir,
218 }
219 }
220}
221
222impl Default for Gradient {
223 fn default() -> Self {
226 Self::new(DEFAULT, DEFAULT, DEFAULT)
227 }
228}
229
230#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
233pub(crate) enum SkyboxError {
234 Sides { width: u32, height: u32 },
236 Pixels {
238 width: u32,
239 height: u32,
240 bytes: usize,
241 },
242}
243
244impl fmt::Display for SkyboxError {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::Sides { width, height } => write!(
248 f,
249 "is {width}x{height}; a skybox image is twice as wide as it is tall"
250 ),
251 Self::Pixels {
252 width,
253 height,
254 bytes,
255 } => write!(
256 f,
257 "is {width}x{height} over {bytes} bytes, where every texel needs four"
258 ),
259 }
260 }
261}
262
263pub(crate) struct Resident {
266 largest: Mip,
267 halved: Vec<Mip>,
269 irradiance: Irradiance,
270 light: f32,
272}
273
274impl Resident {
275 pub(crate) fn gradient(gradient: Gradient) -> Self {
277 Self::of(Mip::between(gradient))
278 }
279
280 fn of(largest: Mip) -> Self {
283 let mut halved: Vec<Mip> = Vec::new();
284 while let Some(mip) = halved.last().unwrap_or(&largest).halved() {
285 halved.push(mip);
286 }
287 let narrow = halved.iter().fold(&largest, |narrowest, mip| {
290 match narrowest.size.x <= COEFFICIENT_WIDTH {
291 true => narrowest,
292 false => mip,
293 }
294 });
295
296 Self {
297 irradiance: Irradiance::of(narrow),
298 largest,
299 halved,
300 light: 1.0,
301 }
302 }
303
304 fn lit_by(mut self, fraction: f32) -> Self {
308 self.irradiance = self.irradiance.scaled(fraction);
309 self.light = fraction;
310 self
311 }
312
313 pub(crate) fn share(&self) -> f32 {
316 self.light
317 }
318
319 pub(crate) fn size(&self) -> UVec2 {
321 self.largest.size
322 }
323
324 pub(crate) fn mips(&self) -> impl Iterator<Item = &Mip> {
326 core::iter::once(&self.largest).chain(&self.halved)
327 }
328
329 pub(crate) fn mip_count(&self) -> u32 {
331 1 + self.halved.len() as u32
332 }
333
334 pub(crate) fn top_mip(&self) -> f32 {
337 self.halved.len() as f32
338 }
339
340 pub(crate) fn irradiance(&self) -> Irradiance {
343 self.irradiance
344 }
345}
346
347pub(crate) struct Mip {
350 size: UVec2,
351 texels: Vec<Vec3>,
352}
353
354impl Mip {
355 fn of(image: &TextureData) -> Result<Self, SkyboxError> {
357 let size = image.size();
358 let (width, height) = (size.x, size.y);
359 if height == 0 || width != 2 * height {
360 return Err(SkyboxError::Sides { width, height });
361 }
362 let bytes = image.pixels().len();
365 if bytes as u64 != 4 * u64::from(width) * u64::from(height) {
366 return Err(SkyboxError::Pixels {
367 width,
368 height,
369 bytes,
370 });
371 }
372
373 Ok(Self {
374 size,
375 texels: image
376 .pixels()
377 .chunks_exact(4)
378 .map(|texel| rgb(Color::of_srgb([texel[0], texel[1], texel[2]])))
379 .collect(),
380 })
381 }
382
383 fn between(gradient: Gradient) -> Self {
386 let zenith = rgb(gradient.zenith);
387 let horizon = rgb(gradient.horizon);
388 let nadir = rgb(gradient.nadir);
389
390 Self {
391 size: RASTERIZED,
392 texels: Grid::of(RASTERIZED)
393 .directions()
394 .map(|direction| match direction.y >= 0.0 {
395 true => horizon.lerp(zenith, direction.y),
396 false => horizon.lerp(nadir, -direction.y),
397 })
398 .collect(),
399 }
400 }
401
402 fn over(self, ground: Vec3) -> Self {
405 let texels = Grid::of(self.size)
406 .directions()
407 .zip(self.texels)
408 .map(|(direction, texel)| {
409 let share = (-direction.y / GROUND_BLEND).clamp(0.0, 1.0);
410 texel.lerp(ground, share)
411 })
412 .collect();
413
414 Self {
415 size: self.size,
416 texels,
417 }
418 }
419
420 fn halved(&self) -> Option<Self> {
423 if self.size.max_element() <= 1 {
424 return None;
425 }
426 let size = (self.size / 2).max(UVec2::ONE);
427
428 Some(Self {
429 size,
430 texels: (0..size.y)
431 .flat_map(|down| (0..size.x).map(move |across| UVec2::new(across, down)))
432 .map(|at| self.averaged(at))
433 .collect(),
434 })
435 }
436
437 pub(crate) fn size(&self) -> UVec2 {
439 self.size
440 }
441
442 pub(crate) fn texels(&self) -> &[Vec3] {
444 &self.texels
445 }
446
447 fn averaged(&self, at: UVec2) -> Vec3 {
450 let corner = at * 2;
451 let covered: Vec3 = [UVec2::ZERO, UVec2::X, UVec2::Y, UVec2::ONE]
452 .into_iter()
453 .map(|step| self.texel(corner + step))
454 .sum();
455
456 covered / 4.0
457 }
458
459 fn texel(&self, at: UVec2) -> Vec3 {
461 let held = at.min(self.size - UVec2::ONE);
462
463 self.texels[(held.y * self.size.x + held.x) as usize]
464 }
465}
466
467#[derive(Clone, Copy)]
472struct Grid {
473 size: UVec2,
474}
475
476impl Grid {
477 fn of(size: UVec2) -> Self {
478 Self { size }
479 }
480
481 fn directions(self) -> impl Iterator<Item = Vec3> {
483 self.rows().flat_map(Row::directions)
484 }
485
486 fn texels(self) -> impl Iterator<Item = (Vec3, f32)> {
489 self.rows().flat_map(|row| {
490 row.directions()
491 .map(move |direction| (direction, row.covered))
492 })
493 }
494
495 fn rows(self) -> impl Iterator<Item = Row> {
497 let size = self.size.as_vec2();
498 let texel = (PI / size.y) * (TAU / size.x);
499
500 (0..self.size.y).map(move |down| {
501 let latitude = (down as f32 + 0.5) / size.y * PI;
502 let (latitude_sin, latitude_cos) = latitude.sin_cos();
503 Row {
504 latitude_sin,
505 latitude_cos,
506 columns: self.size.x,
507 covered: latitude_sin * texel,
508 }
509 })
510 }
511}
512
513#[derive(Clone, Copy)]
516struct Row {
517 latitude_sin: f32,
518 latitude_cos: f32,
519 columns: u32,
520 covered: f32,
521}
522
523impl Row {
524 fn directions(self) -> impl Iterator<Item = Vec3> {
526 let columns = self.columns as f32;
527
528 (0..self.columns).map(move |across| {
529 let longitude = ((across as f32 + 0.5) / columns - 0.5) * TAU;
530 let (longitude_sin, longitude_cos) = longitude.sin_cos();
531 Vec3::new(
532 self.latitude_sin * longitude_sin,
533 self.latitude_cos,
534 -self.latitude_sin * longitude_cos,
535 )
536 })
537 }
538}
539
540#[derive(Clone, Copy, Debug)]
550pub(crate) struct Irradiance([Vec3; 9]);
551
552impl Irradiance {
553 fn of(mip: &Mip) -> Self {
555 let mut coefficients = [Vec3::ZERO; 9];
556 for (texel, (direction, covered)) in mip.texels().iter().zip(Grid::of(mip.size).texels()) {
557 let light = *texel * covered;
558 for (coefficient, shape) in coefficients.iter_mut().zip(Self::shapes(direction)) {
559 *coefficient += light * shape;
560 }
561 }
562
563 Self(core::array::from_fn(|at| {
564 coefficients[at] * TAKEN[at] * SHAPES[at] * SHAPES[at]
565 }))
566 }
567
568 fn scaled(self, fraction: f32) -> Self {
570 Self(self.0.map(|coefficient| coefficient * fraction))
571 }
572
573 pub(crate) fn lanes(&self) -> [Vec4; 9] {
576 self.0.map(|coefficient| coefficient.extend(0.0))
577 }
578
579 fn shapes(direction: Vec3) -> [f32; 9] {
582 let Vec3 { x, y, z } = direction;
583
584 [
585 1.0,
586 y,
587 z,
588 x,
589 x * y,
590 y * z,
591 3.0 * z * z - 1.0,
592 x * z,
593 x * x - y * y,
594 ]
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 fn flat(color: Color) -> (Resident, Resident) {
604 let encoded = |linear: f32| {
605 let encoded = match linear <= 0.003_130_8 {
606 true => linear * 12.92,
607 false => 1.055 * linear.powf(1.0 / 2.4) - 0.055,
608 };
609 (encoded * 255.0).round() as u8
610 };
611 let texel = [
612 encoded(color.red),
613 encoded(color.green),
614 encoded(color.blue),
615 u8::MAX,
616 ];
617 let image = TextureData::rgba8(UVec2::new(16, 8), texel.repeat(16 * 8));
618
619 (
620 Resident::gradient(Gradient::new(color, color, color)),
621 SkyboxData::equirect(image)
622 .resident()
623 .expect("that image is a sky"),
624 )
625 }
626
627 #[test]
628 fn a_sky_of_one_color_lands_that_color_on_a_surface_facing_anywhere() {
629 let color = Color::rgb(0.25, 0.5, 0.75);
630 let (gradient, image) = flat(color);
631
632 for sky in [gradient, image] {
633 let coefficients = sky.irradiance().0;
634
635 for (read, held) in [coefficients[0].x, coefficients[0].y, coefficients[0].z]
636 .into_iter()
637 .zip([color.red, color.green, color.blue])
638 {
639 assert!(
640 (read - held).abs() < 0.01,
641 "the first coefficient holds the color itself, got {read} against {held}"
642 );
643 }
644 for coefficient in &coefficients[1..] {
645 assert!(
646 coefficient.length() < 0.01,
647 "and no other holds anything, got {coefficient}"
648 );
649 }
650 }
651 }
652
653 #[test]
654 fn a_sky_bright_above_lands_more_light_on_a_surface_facing_up() {
655 let coefficients =
656 Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK))
657 .irradiance()
658 .0;
659
660 assert!(
661 coefficients[1].y > 0.0,
662 "the coefficient a normal reads through its own `+Y` rises with the \
663 light above, got {}",
664 coefficients[1].y
665 );
666 assert!(
667 coefficients[0].y > 0.0,
668 "and the sky lands light on a surface facing anywhere"
669 );
670 }
671
672 #[test]
673 fn an_image_not_twice_as_wide_as_it_is_tall_is_no_sky_at_all() {
674 let square = TextureData::rgba8(UVec2::splat(4), vec![u8::MAX; 4 * 4 * 4]);
675
676 assert_eq!(
677 SkyboxData::equirect(square).resident().err(),
678 Some(SkyboxError::Sides {
679 width: 4,
680 height: 4
681 })
682 );
683 assert_eq!(
684 SkyboxData::equirect(TextureData::default())
685 .resident()
686 .err(),
687 Some(SkyboxError::Sides {
688 width: 0,
689 height: 0
690 }),
691 "and an image with no texels at all is none either"
692 );
693 }
694
695 #[test]
696 fn a_sky_is_halved_down_to_one_texel() {
697 let sky = Resident::gradient(Gradient::default());
698 let sizes: Vec<UVec2> = sky.mips().map(Mip::size).collect();
699
700 assert_eq!(sizes.first(), Some(&RASTERIZED));
701 assert_eq!(sizes.last(), Some(&UVec2::ONE));
702 assert_eq!(sizes.len(), 7, "one mip per halving of the widest side");
703 assert_eq!(sky.mip_count(), 7);
704 assert_eq!(sky.top_mip(), 6.0, "the smallest of them is the last");
705 assert!(
706 sky.mips()
707 .all(|mip| mip.texels().len() == (mip.size().x * mip.size().y) as usize),
708 "each of them holds its own texels"
709 );
710 }
711
712 #[test]
713 fn the_smallest_mip_holds_the_average_of_the_sky() {
714 let sky = Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK));
715 let smallest = sky.mips().last().expect("the mips end at one texel");
716
717 assert_eq!(smallest.size(), UVec2::ONE);
718 let average = smallest.texels()[0].x;
719 assert!(
720 (0.1..0.4).contains(&average),
721 "a sky white above and black below averages between them, got {average}"
722 );
723 }
724
725 #[test]
726 fn a_ground_colors_every_texel_under_the_horizon_and_none_above_it() {
727 let sky = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
728 .with_ground(Color::rgb(0.5, 0.0, 0.0))
729 .resident()
730 .expect("a gradient is a sky");
731 let largest = sky.mips().next().expect("a sky has a largest mip");
732 let size = largest.size();
733 let top = largest.texel(UVec2::new(size.x / 2, 0));
734 let bottom = largest.texel(UVec2::new(size.x / 2, size.y - 1));
735
736 assert!(
737 top.abs_diff_eq(Vec3::ONE, 0.01),
738 "the zenith keeps the sky's own color, got {top}"
739 );
740 assert!(
741 bottom.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 0.01),
742 "and the nadir reads as the ground, got {bottom}"
743 );
744 }
745
746 #[test]
747 fn a_ground_darker_than_the_sky_lands_less_light_on_a_surface_facing_down() {
748 let coefficients = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
749 .with_ground(Color::BLACK)
750 .resident()
751 .expect("a gradient is a sky")
752 .irradiance()
753 .0;
754
755 assert!(
756 coefficients[1].x > 0.0,
757 "the coefficient a normal reads through its own `+Y` rises with the \
758 light above and not below, got {}",
759 coefficients[1].x
760 );
761 }
762}