1use core::f32::consts::{PI, TAU};
5use core::fmt;
6use core::fmt::Debug;
7use core::hash::Hash;
8
9use crate::light::rgb;
10use crate::math::{UVec2, Vec3, Vec4};
11use crate::{Assets, Catalog, Color, TextureData};
12
13const DEFAULT: Color = Color::rgb(0.1, 0.1, 0.1);
16
17const RASTERIZED: UVec2 = UVec2::new(64, 32);
20
21const COEFFICIENT_WIDTH: u32 = 256;
24
25const GROUND_BLEND: f32 = 0.052_336;
29
30const SHAPES: [f32; 9] = [
33 0.282_095, 0.488_603, 0.488_603, 0.488_603, 1.092_548, 1.092_548, 0.315_392, 1.092_548,
34 0.546_274,
35];
36
37const TAKEN: [f32; 9] = [
41 1.0,
42 2.0 / 3.0,
43 2.0 / 3.0,
44 2.0 / 3.0,
45 0.25,
46 0.25,
47 0.25,
48 0.25,
49 0.25,
50];
51
52pub trait Skyboxes: Catalog + Clone + Debug + Eq + Hash {
58 fn build(&self, assets: &Assets) -> SkyboxData;
62}
63
64#[derive(Clone, Debug, Eq, Hash, PartialEq)]
69pub enum NoSkyboxes {}
70
71impl Catalog for NoSkyboxes {
72 fn catalog() -> Vec<Self> {
73 Vec::new()
74 }
75}
76
77impl Skyboxes for NoSkyboxes {
78 fn build(&self, _assets: &Assets) -> SkyboxData {
79 match *self {}
80 }
81}
82
83#[derive(Clone, Debug)]
88pub struct SkyboxData {
89 kind: Kind,
90 light: f32,
91 ground: Option<Color>,
92}
93
94#[derive(Clone, Debug)]
96enum Kind {
97 Equirect(TextureData),
98 Gradient(Gradient),
99}
100
101impl SkyboxData {
102 pub fn equirect(image: TextureData) -> Self {
108 Self::of(Kind::Equirect(image))
109 }
110
111 pub fn gradient(zenith: Color, horizon: Color, nadir: Color) -> Self {
117 Self::of(Kind::Gradient(Gradient::new(zenith, horizon, nadir)))
118 }
119
120 pub fn lit_by(mut self, fraction: f32) -> Self {
128 self.light = fraction.max(0.0);
129 self
130 }
131
132 pub fn with_ground(mut self, color: Color) -> Self {
140 self.ground = Some(color);
141 self
142 }
143
144 fn of(kind: Kind) -> Self {
145 Self {
146 kind,
147 light: 1.0,
148 ground: None,
149 }
150 }
151
152 pub(crate) fn resident(&self) -> Result<Resident, SkyboxError> {
155 let largest = match &self.kind {
156 Kind::Equirect(image) => Mip::of(image)?,
157 Kind::Gradient(gradient) => Mip::between(*gradient),
158 };
159 let largest = match self.ground {
160 Some(ground) => largest.over(rgb(ground)),
161 None => largest,
162 };
163
164 Ok(Resident::of(largest).lit_by(self.light))
165 }
166}
167
168impl Default for SkyboxData {
169 fn default() -> Self {
171 Self::equirect(TextureData::rgba8(UVec2::new(2, 1), vec![0; 8]))
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq)]
177pub(crate) struct Gradient {
178 zenith: Color,
180 horizon: Color,
182 nadir: Color,
184}
185
186impl Gradient {
187 pub(crate) const fn new(zenith: Color, horizon: Color, nadir: Color) -> Self {
190 Self {
191 zenith,
192 horizon,
193 nadir,
194 }
195 }
196}
197
198impl Default for Gradient {
199 fn default() -> Self {
202 Self::new(DEFAULT, DEFAULT, DEFAULT)
203 }
204}
205
206#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
209pub(crate) enum SkyboxError {
210 Sides { width: u32, height: u32 },
212 Pixels {
214 width: u32,
215 height: u32,
216 bytes: usize,
217 },
218}
219
220impl fmt::Display for SkyboxError {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 match self {
223 Self::Sides { width, height } => write!(
224 f,
225 "is {width}x{height}; a skybox image is twice as wide as it is tall"
226 ),
227 Self::Pixels {
228 width,
229 height,
230 bytes,
231 } => write!(
232 f,
233 "is {width}x{height} over {bytes} bytes, where every texel needs four"
234 ),
235 }
236 }
237}
238
239pub(crate) struct Resident {
242 largest: Mip,
243 halved: Vec<Mip>,
245 irradiance: Irradiance,
246 light: f32,
248}
249
250impl Resident {
251 pub(crate) fn gradient(gradient: Gradient) -> Self {
253 Self::of(Mip::between(gradient))
254 }
255
256 fn of(largest: Mip) -> Self {
259 let mut halved: Vec<Mip> = Vec::new();
260 while let Some(mip) = halved.last().unwrap_or(&largest).halved() {
261 halved.push(mip);
262 }
263 let narrow = halved.iter().fold(&largest, |narrowest, mip| {
266 match narrowest.size.x <= COEFFICIENT_WIDTH {
267 true => narrowest,
268 false => mip,
269 }
270 });
271
272 Self {
273 irradiance: Irradiance::of(narrow),
274 largest,
275 halved,
276 light: 1.0,
277 }
278 }
279
280 fn lit_by(mut self, fraction: f32) -> Self {
284 self.irradiance = self.irradiance.scaled(fraction);
285 self.light = fraction;
286 self
287 }
288
289 pub(crate) fn share(&self) -> f32 {
292 self.light
293 }
294
295 pub(crate) fn size(&self) -> UVec2 {
297 self.largest.size
298 }
299
300 pub(crate) fn mips(&self) -> impl Iterator<Item = &Mip> {
302 core::iter::once(&self.largest).chain(&self.halved)
303 }
304
305 pub(crate) fn mip_count(&self) -> u32 {
307 1 + self.halved.len() as u32
308 }
309
310 pub(crate) fn top_mip(&self) -> f32 {
313 self.halved.len() as f32
314 }
315
316 pub(crate) fn irradiance(&self) -> Irradiance {
319 self.irradiance
320 }
321}
322
323pub(crate) struct Mip {
326 size: UVec2,
327 texels: Vec<Vec3>,
328}
329
330impl Mip {
331 fn of(image: &TextureData) -> Result<Self, SkyboxError> {
333 let size = image.size();
334 let (width, height) = (size.x, size.y);
335 if height == 0 || width != 2 * height {
336 return Err(SkyboxError::Sides { width, height });
337 }
338 let bytes = image.pixels().len();
341 if bytes as u64 != 4 * u64::from(width) * u64::from(height) {
342 return Err(SkyboxError::Pixels {
343 width,
344 height,
345 bytes,
346 });
347 }
348
349 Ok(Self {
350 size,
351 texels: image
352 .pixels()
353 .chunks_exact(4)
354 .map(|texel| rgb(Color::of_srgb([texel[0], texel[1], texel[2]])))
355 .collect(),
356 })
357 }
358
359 fn between(gradient: Gradient) -> Self {
362 let zenith = rgb(gradient.zenith);
363 let horizon = rgb(gradient.horizon);
364 let nadir = rgb(gradient.nadir);
365
366 Self {
367 size: RASTERIZED,
368 texels: Grid::of(RASTERIZED)
369 .directions()
370 .map(|direction| match direction.y >= 0.0 {
371 true => horizon.lerp(zenith, direction.y),
372 false => horizon.lerp(nadir, -direction.y),
373 })
374 .collect(),
375 }
376 }
377
378 fn over(self, ground: Vec3) -> Self {
381 let texels = Grid::of(self.size)
382 .directions()
383 .zip(self.texels)
384 .map(|(direction, texel)| {
385 let share = (-direction.y / GROUND_BLEND).clamp(0.0, 1.0);
386 texel.lerp(ground, share)
387 })
388 .collect();
389
390 Self {
391 size: self.size,
392 texels,
393 }
394 }
395
396 fn halved(&self) -> Option<Self> {
399 if self.size.max_element() <= 1 {
400 return None;
401 }
402 let size = (self.size / 2).max(UVec2::ONE);
403
404 Some(Self {
405 size,
406 texels: (0..size.y)
407 .flat_map(|down| (0..size.x).map(move |across| UVec2::new(across, down)))
408 .map(|at| self.averaged(at))
409 .collect(),
410 })
411 }
412
413 pub(crate) fn size(&self) -> UVec2 {
415 self.size
416 }
417
418 pub(crate) fn texels(&self) -> &[Vec3] {
420 &self.texels
421 }
422
423 fn averaged(&self, at: UVec2) -> Vec3 {
426 let corner = at * 2;
427 let covered: Vec3 = [UVec2::ZERO, UVec2::X, UVec2::Y, UVec2::ONE]
428 .into_iter()
429 .map(|step| self.texel(corner + step))
430 .sum();
431
432 covered / 4.0
433 }
434
435 fn texel(&self, at: UVec2) -> Vec3 {
437 let held = at.min(self.size - UVec2::ONE);
438
439 self.texels[(held.y * self.size.x + held.x) as usize]
440 }
441}
442
443#[derive(Clone, Copy)]
448struct Grid {
449 size: UVec2,
450}
451
452impl Grid {
453 fn of(size: UVec2) -> Self {
454 Self { size }
455 }
456
457 fn directions(self) -> impl Iterator<Item = Vec3> {
459 self.rows().flat_map(Row::directions)
460 }
461
462 fn texels(self) -> impl Iterator<Item = (Vec3, f32)> {
465 self.rows().flat_map(|row| {
466 row.directions()
467 .map(move |direction| (direction, row.covered))
468 })
469 }
470
471 fn rows(self) -> impl Iterator<Item = Row> {
473 let size = self.size.as_vec2();
474 let texel = (PI / size.y) * (TAU / size.x);
475
476 (0..self.size.y).map(move |down| {
477 let latitude = (down as f32 + 0.5) / size.y * PI;
478 let (latitude_sin, latitude_cos) = latitude.sin_cos();
479 Row {
480 latitude_sin,
481 latitude_cos,
482 columns: self.size.x,
483 covered: latitude_sin * texel,
484 }
485 })
486 }
487}
488
489#[derive(Clone, Copy)]
492struct Row {
493 latitude_sin: f32,
494 latitude_cos: f32,
495 columns: u32,
496 covered: f32,
497}
498
499impl Row {
500 fn directions(self) -> impl Iterator<Item = Vec3> {
502 let columns = self.columns as f32;
503
504 (0..self.columns).map(move |across| {
505 let longitude = ((across as f32 + 0.5) / columns - 0.5) * TAU;
506 let (longitude_sin, longitude_cos) = longitude.sin_cos();
507 Vec3::new(
508 self.latitude_sin * longitude_sin,
509 self.latitude_cos,
510 -self.latitude_sin * longitude_cos,
511 )
512 })
513 }
514}
515
516#[derive(Clone, Copy, Debug)]
526pub(crate) struct Irradiance([Vec3; 9]);
527
528impl Irradiance {
529 fn of(mip: &Mip) -> Self {
531 let mut coefficients = [Vec3::ZERO; 9];
532 for (texel, (direction, covered)) in mip.texels().iter().zip(Grid::of(mip.size).texels()) {
533 let light = *texel * covered;
534 for (coefficient, shape) in coefficients.iter_mut().zip(Self::shapes(direction)) {
535 *coefficient += light * shape;
536 }
537 }
538
539 Self(core::array::from_fn(|at| {
540 coefficients[at] * TAKEN[at] * SHAPES[at] * SHAPES[at]
541 }))
542 }
543
544 fn scaled(self, fraction: f32) -> Self {
546 Self(self.0.map(|coefficient| coefficient * fraction))
547 }
548
549 pub(crate) fn lanes(&self) -> [Vec4; 9] {
552 self.0.map(|coefficient| coefficient.extend(0.0))
553 }
554
555 fn shapes(direction: Vec3) -> [f32; 9] {
558 let Vec3 { x, y, z } = direction;
559
560 [
561 1.0,
562 y,
563 z,
564 x,
565 x * y,
566 y * z,
567 3.0 * z * z - 1.0,
568 x * z,
569 x * x - y * y,
570 ]
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 fn flat(color: Color) -> (Resident, Resident) {
580 let encoded = |linear: f32| {
581 let encoded = match linear <= 0.003_130_8 {
582 true => linear * 12.92,
583 false => 1.055 * linear.powf(1.0 / 2.4) - 0.055,
584 };
585 (encoded * 255.0).round() as u8
586 };
587 let texel = [
588 encoded(color.red),
589 encoded(color.green),
590 encoded(color.blue),
591 u8::MAX,
592 ];
593 let image = TextureData::rgba8(UVec2::new(16, 8), texel.repeat(16 * 8));
594
595 (
596 Resident::gradient(Gradient::new(color, color, color)),
597 SkyboxData::equirect(image)
598 .resident()
599 .expect("that image is a sky"),
600 )
601 }
602
603 #[test]
604 fn a_sky_of_one_color_lands_that_color_on_a_surface_facing_anywhere() {
605 let color = Color::rgb(0.25, 0.5, 0.75);
606 let (gradient, image) = flat(color);
607
608 for sky in [gradient, image] {
609 let coefficients = sky.irradiance().0;
610
611 for (read, held) in [coefficients[0].x, coefficients[0].y, coefficients[0].z]
612 .into_iter()
613 .zip([color.red, color.green, color.blue])
614 {
615 assert!(
616 (read - held).abs() < 0.01,
617 "the first coefficient holds the color itself, got {read} against {held}"
618 );
619 }
620 for coefficient in &coefficients[1..] {
621 assert!(
622 coefficient.length() < 0.01,
623 "and no other holds anything, got {coefficient}"
624 );
625 }
626 }
627 }
628
629 #[test]
630 fn a_sky_bright_above_lands_more_light_on_a_surface_facing_up() {
631 let coefficients =
632 Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK))
633 .irradiance()
634 .0;
635
636 assert!(
637 coefficients[1].y > 0.0,
638 "the coefficient a normal reads through its own `+Y` rises with the \
639 light above, got {}",
640 coefficients[1].y
641 );
642 assert!(
643 coefficients[0].y > 0.0,
644 "and the sky lands light on a surface facing anywhere"
645 );
646 }
647
648 #[test]
649 fn an_image_not_twice_as_wide_as_it_is_tall_is_no_sky_at_all() {
650 let square = TextureData::rgba8(UVec2::splat(4), vec![u8::MAX; 4 * 4 * 4]);
651
652 assert_eq!(
653 SkyboxData::equirect(square).resident().err(),
654 Some(SkyboxError::Sides {
655 width: 4,
656 height: 4
657 })
658 );
659 assert_eq!(
660 SkyboxData::equirect(TextureData::default())
661 .resident()
662 .err(),
663 Some(SkyboxError::Sides {
664 width: 0,
665 height: 0
666 }),
667 "and an image with no texels at all is none either"
668 );
669 }
670
671 #[test]
672 fn a_sky_is_halved_down_to_one_texel() {
673 let sky = Resident::gradient(Gradient::default());
674 let sizes: Vec<UVec2> = sky.mips().map(Mip::size).collect();
675
676 assert_eq!(sizes.first(), Some(&RASTERIZED));
677 assert_eq!(sizes.last(), Some(&UVec2::ONE));
678 assert_eq!(sizes.len(), 7, "one mip per halving of the widest side");
679 assert_eq!(sky.mip_count(), 7);
680 assert_eq!(sky.top_mip(), 6.0, "the smallest of them is the last");
681 assert!(
682 sky.mips()
683 .all(|mip| mip.texels().len() == (mip.size().x * mip.size().y) as usize),
684 "each of them holds its own texels"
685 );
686 }
687
688 #[test]
689 fn the_smallest_mip_holds_the_average_of_the_sky() {
690 let sky = Resident::gradient(Gradient::new(Color::WHITE, Color::BLACK, Color::BLACK));
691 let smallest = sky.mips().last().expect("the mips end at one texel");
692
693 assert_eq!(smallest.size(), UVec2::ONE);
694 let average = smallest.texels()[0].x;
695 assert!(
696 (0.1..0.4).contains(&average),
697 "a sky white above and black below averages between them, got {average}"
698 );
699 }
700
701 #[test]
702 fn a_ground_colors_every_texel_under_the_horizon_and_none_above_it() {
703 let sky = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
704 .with_ground(Color::rgb(0.5, 0.0, 0.0))
705 .resident()
706 .expect("a gradient is a sky");
707 let largest = sky.mips().next().expect("a sky has a largest mip");
708 let size = largest.size();
709 let top = largest.texel(UVec2::new(size.x / 2, 0));
710 let bottom = largest.texel(UVec2::new(size.x / 2, size.y - 1));
711
712 assert!(
713 top.abs_diff_eq(Vec3::ONE, 0.01),
714 "the zenith keeps the sky's own color, got {top}"
715 );
716 assert!(
717 bottom.abs_diff_eq(Vec3::new(0.5, 0.0, 0.0), 0.01),
718 "and the nadir reads as the ground, got {bottom}"
719 );
720 }
721
722 #[test]
723 fn a_ground_darker_than_the_sky_lands_less_light_on_a_surface_facing_down() {
724 let coefficients = SkyboxData::gradient(Color::WHITE, Color::WHITE, Color::WHITE)
725 .with_ground(Color::BLACK)
726 .resident()
727 .expect("a gradient is a sky")
728 .irradiance()
729 .0;
730
731 assert!(
732 coefficients[1].x > 0.0,
733 "the coefficient a normal reads through its own `+Y` rises with the \
734 light above and not below, got {}",
735 coefficients[1].x
736 );
737 }
738}