all_is_cubes/space/light/
data.rs

1//! Data structures for light storage and algorithms.
2
3use core::fmt;
4
5use euclid::default::Vector3D;
6use euclid::vec3;
7
8/// Acts as polyfill for float methods
9#[cfg(not(any(feature = "std", test)))]
10#[allow(
11    unused_imports,
12    reason = "unclear why this warns even though it is needed"
13)]
14use num_traits::float::Float as _;
15
16use crate::math::{PositiveSign, Rgb, ps32};
17
18#[cfg(doc)]
19use crate::space::{self, Space};
20
21/// One component of a `PackedLight`.
22pub(crate) type PackedLightScalar = u8;
23
24/// Special reasons for a cube having zero light in it.
25/// These may be used to help compute smoothed lighting across blocks.
26///
27/// The numeric values of this enum are used to transmit it to shaders by packing
28/// it into an "RGBA" color value. They should not be considered a stable API element.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30#[repr(u8)]
31pub(crate) enum LightStatus {
32    /// The cube's light value has never been computed, or it has been computed
33    /// as a wild guess that should be used only if no better data is available.
34    ///
35    /// This value may appear when:
36    ///
37    /// * [`space::Builder::palette_and_contents()`] was called without including light data.
38    /// * The light updater is speculatively copying from neighboring cubes.
39    Uninitialized = 0,
40    /// The cube has no surfaces to catch light and therefore the light value is not tracked.
41    NoRays = 1,
42    /// The cube contains an opaque block and therefore does not have any light entering.
43    Opaque = 128,
44    /// No special situation: if it's black then it's just dark.
45    Visible = 255,
46}
47
48/// A single cube of light within a [`Space`]; an [`Rgb`] value stored with reduced precision and
49/// range, plus metadata about the applicability of the result.
50#[derive(Clone, Copy, PartialEq, Eq)]
51pub struct PackedLight {
52    // LightStatus being other than Visible is mutually exclusive with value being nonzero,
53    // so we could in theory make this an enum, but that wouldn't actually compact the
54    // representation, and this representation maps to 8-bit-per-component RGBA which is
55    // what the shader expects.
56    value: Vector3D<PackedLightScalar>,
57    status: LightStatus,
58}
59// TODO: Once we've built out the rest of the game, do some performance testing and
60// decide whether having colored lighting is worth the compute and storage cost.
61// If memory vs. bit depth is an issue, consider switching to something like YCbCr
62// representation, or possibly something that GPUs specifically do well with.
63
64impl PackedLight {
65    // Note: When changing these scaling parameters, don't forget to update:
66    // * `PACKED_LIGHT_SCALAR_LOOKUP_TABLE`
67    // * The decoder in `all-is-cubes-gpu/src/in_wgpu/shaders/blocks-and-lines.wgsl`
68    const LOG_SCALE: f32 = 10.0;
69    const LOG_OFFSET: f32 = 144.0;
70
71    // pub(crate) const ZERO: Self = Self::none(LightStatus::Visible);
72    pub(crate) const OPAQUE: Self = Self::none(LightStatus::Opaque);
73    pub(crate) const NO_RAYS: Self = Self::none(LightStatus::NoRays);
74    pub(crate) const UNINITIALIZED_AND_BLACK: Self = Self::none(LightStatus::Uninitialized);
75    pub(crate) const ONE: PackedLight = {
76        let one_scalar = Self::LOG_OFFSET as PackedLightScalar;
77        PackedLight {
78            status: LightStatus::Visible,
79            value: Vector3D::new(one_scalar, one_scalar, one_scalar),
80        }
81    };
82
83    pub(crate) fn some(value: Rgb) -> Self {
84        PackedLight {
85            value: Vector3D::new(
86                Self::scalar_in(value.red()),
87                Self::scalar_in(value.green()),
88                Self::scalar_in(value.blue()),
89            ),
90            status: LightStatus::Visible,
91        }
92    }
93
94    pub(crate) const fn none(status: LightStatus) -> Self {
95        PackedLight {
96            value: Vector3D::new(0, 0, 0),
97            status,
98        }
99    }
100
101    pub(crate) fn guess(value: Rgb) -> Self {
102        PackedLight {
103            value: Vector3D::new(
104                Self::scalar_in(value.red()),
105                Self::scalar_in(value.green()),
106                Self::scalar_in(value.blue()),
107            ),
108            status: LightStatus::Uninitialized,
109        }
110    }
111
112    /// Returns the light level.
113    #[inline]
114    pub fn value(self) -> Rgb {
115        Rgb::new_ps(
116            Self::scalar_out_ps(self.value.x),
117            Self::scalar_out_ps(self.value.y),
118            Self::scalar_out_ps(self.value.z),
119        )
120    }
121
122    // TODO: Expose LightStatus once we are more confident in its API stability
123    pub(crate) fn status(self) -> LightStatus {
124        self.status
125    }
126
127    /// Returns true if the light value is meaningful, or false if it is
128    /// inside an opaque block or in empty unlit air (in which case [`Self::value`]
129    /// always returns zero).
130    pub(crate) fn valid(self) -> bool {
131        self.status == LightStatus::Visible
132    }
133
134    /// RGB color plus a fourth component which is a “weight” value which indicates how
135    /// much this color should actually contribute to the surface color. It is usually
136    /// 0 or 1, but is set slightly above zero for opaque blocks to create the ambient
137    /// occlusion effect.
138    pub(crate) fn value_with_ambient_occlusion(self) -> [f32; 4] {
139        [
140            Self::scalar_out(self.value.x),
141            Self::scalar_out(self.value.y),
142            Self::scalar_out(self.value.z),
143            match self.status {
144                LightStatus::Uninitialized => 0.0,
145                LightStatus::NoRays => 0.0,
146                // TODO: Make this a graphics option
147                LightStatus::Opaque => 0.25,
148                LightStatus::Visible => 1.0,
149            },
150        ]
151    }
152
153    #[inline]
154    #[doc(hidden)] // TODO: used by all_is_cubes_gpu; but it should be doable equivalently using public functions
155    pub fn as_texel(self) -> [u8; 4] {
156        let Self {
157            value: Vector3D {
158                x: r, y: g, z: b, ..
159            },
160            status,
161        } = self;
162        [r, g, b, status as u8]
163    }
164
165    /// Undoes the transformation of [`Self::as_texel()`].
166    /// For testing only.
167    #[doc(hidden)]
168    #[track_caller]
169    pub fn from_texel([r, g, b, s]: [u8; 4]) -> Self {
170        Self {
171            value: vec3(r, g, b),
172            status: match s {
173                0 => LightStatus::Uninitialized,
174                1 => LightStatus::NoRays,
175                128 => LightStatus::Opaque,
176                255 => LightStatus::Visible,
177                _ => panic!("invalid status value {s}"),
178            },
179        }
180    }
181
182    /// Computes a degree of difference between two [`PackedLight`] values, used to decide
183    /// update priority.
184    /// The value is zero if and only if the two inputs are equal.
185    #[inline]
186    pub(crate) fn difference_priority(self, other: PackedLight) -> PackedLightScalar {
187        fn abs_diff(a: PackedLightScalar, b: PackedLightScalar) -> PackedLightScalar {
188            a.max(b) - a.min(b)
189        }
190        let mut difference = abs_diff(self.value.x, other.value.x)
191            .max(abs_diff(self.value.y, other.value.y))
192            .max(abs_diff(self.value.z, other.value.z));
193
194        if other.status != self.status {
195            // A non-opaque block changing to an opaque one, or similar, changes the
196            // results of the rest of the algorithm so should be counted as a difference
197            // even if it's still changing zero to zero.
198            // TODO: Tune this number for fast settling and good results.
199            difference = difference.saturating_add(PackedLightScalar::MAX / 4);
200        }
201
202        difference
203    }
204
205    #[inline(always)]
206    fn scalar_in(value: PositiveSign<f32>) -> PackedLightScalar {
207        // Note that `as` is a saturating cast, so out of range values will be clamped.
208        (value.into_inner().log2() * Self::LOG_SCALE + Self::LOG_OFFSET).round()
209            as PackedLightScalar
210    }
211
212    /// Convert a `PackedLightScalar` value to a linear color component value.
213    /// This function is guaranteed (and tested) to only return finite non-negative floats.
214    #[inline(always)]
215    fn scalar_out(value: PackedLightScalar) -> f32 {
216        PACKED_LIGHT_SCALAR_LOOKUP_TABLE[usize::from(value)].into_inner()
217    }
218
219    /// Convert a `PackedLightScalar` value to a linear color component value.
220    /// This function is guaranteed (and tested) to only return finite non-negative floats.
221    #[inline(always)]
222    fn scalar_out_ps(value: PackedLightScalar) -> PositiveSign<f32> {
223        PACKED_LIGHT_SCALAR_LOOKUP_TABLE[usize::from(value)]
224    }
225
226    /// Convert a `PackedLightScalar` value to a linear color component value.
227    /// This function is guaranteed (and tested) to only return finite non-negative floats.
228    ///
229    /// This implementation is only used for testing and regenerating
230    /// [`PACKED_LIGHT_SCALAR_LOOKUP_TABLE`].
231    #[cfg(test)]
232    fn scalar_out_arithmetic(value: PackedLightScalar) -> f32 {
233        // Special representation to ensure we don't "round" zero up to a small nonzero value.
234        if value == 0 {
235            0.0
236        } else {
237            // Use pure-Rust implementation from `libm` to avoid platform-dependent rounding
238            // which would be inconsistent with our hardcoded lookup table.
239            // This function is supposed to
240            // *define* what belongs in the lookup table.
241            libm::exp2f((f32::from(value) - Self::LOG_OFFSET) / Self::LOG_SCALE)
242        }
243    }
244}
245
246impl fmt::Debug for PackedLight {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        write!(
249            f,
250            "PackedLight({}, {}, {}, {:?})",
251            self.value.x, self.value.y, self.value.z, self.status
252        )
253    }
254}
255
256impl From<Rgb> for PackedLight {
257    #[inline]
258    fn from(value: Rgb) -> Self {
259        PackedLight::some(value)
260    }
261}
262
263#[cfg(feature = "save")]
264impl From<PackedLight> for crate::save::schema::LightSerV1 {
265    fn from(p: PackedLight) -> Self {
266        use crate::save::schema::LightStatusSerV1 as S;
267        crate::save::schema::LightSerV1 {
268            value: p.value.into(),
269            status: match p.status {
270                LightStatus::Uninitialized => S::Uninitialized,
271                LightStatus::NoRays => S::NoRays,
272                LightStatus::Opaque => S::Opaque,
273                LightStatus::Visible => S::Visible,
274            },
275        }
276    }
277}
278
279#[cfg(feature = "save")]
280impl From<crate::save::schema::LightSerV1> for PackedLight {
281    fn from(ls: crate::save::schema::LightSerV1) -> Self {
282        use crate::save::schema::LightStatusSerV1 as S;
283        PackedLight {
284            value: Vector3D::from(ls.value),
285            status: match ls.status {
286                S::Uninitialized => LightStatus::Uninitialized,
287                S::NoRays => LightStatus::NoRays,
288                S::Opaque => LightStatus::Opaque,
289                S::Visible => LightStatus::Visible,
290            },
291        }
292    }
293}
294
295/// Precomputed lookup table of the results of [`PackedLight::scalar_out_arithmetic()`].
296/// This is more efficient than computing the function every time.
297/// 
298/// This table is validated and can be regenerated using the test `check_packed_light_table`.
299#[rustfmt::skip]
300#[allow(clippy::approx_constant)]
301static PACKED_LIGHT_SCALAR_LOOKUP_TABLE: [PositiveSign<f32>; 256] = [
302    ps32(0.0), ps32(4.9575945e-5), ps32(5.3134198e-5), ps32(5.69478e-5), ps32(6.1035156e-5),
303    ps32(6.541588e-5), ps32(7.0110975e-5), ps32(7.51431e-5), ps32(8.053635e-5), ps32(8.6316744e-5),
304    ps32(9.251202e-5), ps32(9.915189e-5), ps32(0.000106268395), ps32(0.0001138956), ps32(0.00012207031),
305    ps32(0.00013083176), ps32(0.00014022195), ps32(0.0001502862), ps32(0.0001610727), ps32(0.00017263349),
306    ps32(0.00018502404), ps32(0.00019830378), ps32(0.00021253679), ps32(0.0002277912), ps32(0.00024414063),
307    ps32(0.00026166352), ps32(0.0002804439), ps32(0.0003005724), ps32(0.0003221454), ps32(0.00034526698),
308    ps32(0.00037004807), ps32(0.00039660756), ps32(0.00042507358), ps32(0.0004555824), ps32(0.00048828125),
309    ps32(0.00052332703), ps32(0.0005608878), ps32(0.0006011448), ps32(0.0006442908), ps32(0.00069053395),
310    ps32(0.00074009615), ps32(0.0007932151), ps32(0.00085014716), ps32(0.0009111648), ps32(0.0009765625),
311    ps32(0.0010466541), ps32(0.0011217756), ps32(0.0012022896), ps32(0.0012885816), ps32(0.0013810679),
312    ps32(0.0014801923), ps32(0.0015864302), ps32(0.0017002943), ps32(0.0018223296), ps32(0.001953125),
313    ps32(0.0020933081), ps32(0.0022435512), ps32(0.0024045792), ps32(0.0025771633), ps32(0.0027621358),
314    ps32(0.0029603846), ps32(0.0031728605), ps32(0.0034005886), ps32(0.0036446592), ps32(0.00390625),
315    ps32(0.004186615), ps32(0.0044871024), ps32(0.0048091584), ps32(0.005154328), ps32(0.0055242716),
316    ps32(0.0059207673), ps32(0.006345721), ps32(0.0068011773), ps32(0.0072893207), ps32(0.0078125),
317    ps32(0.00837323), ps32(0.008974205), ps32(0.009618317), ps32(0.010308656), ps32(0.011048543),
318    ps32(0.011841535), ps32(0.012691442), ps32(0.013602355), ps32(0.014578641), ps32(0.015625),
319    ps32(0.01674646), ps32(0.01794841), ps32(0.019236634), ps32(0.020617312), ps32(0.022097087),
320    ps32(0.02368307), ps32(0.025382884), ps32(0.02720471), ps32(0.029157283), ps32(0.03125),
321    ps32(0.03349292), ps32(0.03589682), ps32(0.038473267), ps32(0.041234624), ps32(0.044194173),
322    ps32(0.04736614), ps32(0.050765768), ps32(0.05440942), ps32(0.058314566), ps32(0.0625),
323    ps32(0.06698584), ps32(0.07179365), ps32(0.07694653), ps32(0.08246925), ps32(0.088388346),
324    ps32(0.09473228), ps32(0.10153155), ps32(0.108818814), ps32(0.11662913), ps32(0.125),
325    ps32(0.13397168), ps32(0.1435873), ps32(0.15389305), ps32(0.1649385), ps32(0.17677669),
326    ps32(0.18946455), ps32(0.2030631), ps32(0.21763763), ps32(0.23325826), ps32(0.25),
327    ps32(0.26794338), ps32(0.2871746), ps32(0.3077861), ps32(0.32987696), ps32(0.35355338),
328    ps32(0.37892914), ps32(0.4061262), ps32(0.43527526), ps32(0.4665165), ps32(0.5),
329    ps32(0.53588676), ps32(0.57434916), ps32(0.6155722), ps32(0.6597539), ps32(0.70710677),
330    ps32(0.7578583), ps32(0.8122524), ps32(0.8705506), ps32(0.933033), ps32(1.0),
331    ps32(1.0717734), ps32(1.1486983), ps32(1.2311444), ps32(1.319508), ps32(1.4142135),
332    ps32(1.5157166), ps32(1.6245048), ps32(1.7411011), ps32(1.866066), ps32(2.0),
333    ps32(2.143547), ps32(2.297397), ps32(2.4622889), ps32(2.6390157), ps32(2.828427),
334    ps32(3.031433), ps32(3.2490096), ps32(3.482202), ps32(3.732132), ps32(4.0),
335    ps32(4.2870936), ps32(4.594794), ps32(4.9245777), ps32(5.278032), ps32(5.656854),
336    ps32(6.0628657), ps32(6.498019), ps32(6.964404), ps32(7.4642644), ps32(8.0),
337    ps32(8.574187), ps32(9.189588), ps32(9.849155), ps32(10.556064), ps32(11.313708),
338    ps32(12.125731), ps32(12.996038), ps32(13.928808), ps32(14.928529), ps32(16.0),
339    ps32(17.148375), ps32(18.379171), ps32(19.698313), ps32(21.112127), ps32(22.627417),
340    ps32(24.251463), ps32(25.992073), ps32(27.857622), ps32(29.857058), ps32(32.0),
341    ps32(34.29675), ps32(36.758343), ps32(39.396626), ps32(42.224255), ps32(45.254833),
342    ps32(48.502926), ps32(51.984146), ps32(55.715244), ps32(59.714115), ps32(64.0),
343    ps32(68.5935), ps32(73.516685), ps32(78.79325), ps32(84.44851), ps32(90.50967),
344    ps32(97.00585), ps32(103.96829), ps32(111.43049), ps32(119.42823), ps32(128.0),
345    ps32(137.187), ps32(147.03337), ps32(157.5865), ps32(168.89702), ps32(181.01933),
346    ps32(194.0117), ps32(207.93658), ps32(222.86098), ps32(238.85646), ps32(256.0),
347    ps32(274.37408), ps32(294.06674), ps32(315.173), ps32(337.79395), ps32(362.03867),
348    ps32(388.02353), ps32(415.87317), ps32(445.72195), ps32(477.71277), ps32(512.0),
349    ps32(548.74817), ps32(588.1335), ps32(630.346), ps32(675.5879), ps32(724.07733),
350    ps32(776.04706), ps32(831.74634), ps32(891.4439), ps32(955.42554), ps32(1024.0),
351    ps32(1097.4963), ps32(1176.267), ps32(1260.692), ps32(1351.1758), ps32(1448.1547),
352    ps32(1552.0941), ps32(1663.4927), ps32(1782.8878), ps32(1910.8511), ps32(2048.0),
353    ps32(2194.9927),
354];
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::math::ps32;
360    use alloc::vec::Vec;
361    use core::iter::once;
362
363    fn packed_light_test_values() -> impl Iterator<Item = PackedLight> {
364        (PackedLightScalar::MIN..PackedLightScalar::MAX)
365            .flat_map(|s| {
366                vec![
367                    PackedLight {
368                        value: Vector3D::new(s, 0, 0),
369                        status: LightStatus::Visible,
370                    },
371                    PackedLight {
372                        value: Vector3D::new(0, s, 0),
373                        status: LightStatus::Visible,
374                    },
375                    PackedLight {
376                        value: Vector3D::new(0, 0, s),
377                        status: LightStatus::Visible,
378                    },
379                    PackedLight {
380                        value: Vector3D::new(s, 127, 255),
381                        status: LightStatus::Visible,
382                    },
383                ]
384                .into_iter()
385            })
386            .chain(once(PackedLight::OPAQUE))
387            .chain(once(PackedLight::NO_RAYS))
388    }
389
390    /// Test that unpacking and packing doesn't shift the value, which could lead
391    /// to runaway light values.
392    #[test]
393    fn packed_light_roundtrip() {
394        for i in PackedLightScalar::MIN..PackedLightScalar::MAX {
395            assert_eq!(i, PackedLight::scalar_in(PackedLight::scalar_out_ps(i)));
396        }
397    }
398
399    /// Safety test: we want to skip the NaN/sign checks for constructing `Rgb`
400    /// from `PackedLight`, so it had better be valid for any possible input.
401    #[test]
402    fn packed_light_always_positive() {
403        for i in PackedLightScalar::MIN..PackedLightScalar::MAX {
404            let value = PackedLight::scalar_out(i);
405            assert!(value.is_finite() && value.is_sign_positive(), "{}", i);
406        }
407    }
408
409    #[test]
410    fn check_packed_light_table() {
411        let generated_table: Vec<f32> =
412            (0..=u8::MAX).map(PackedLight::scalar_out_arithmetic).collect();
413        print!("static PACKED_LIGHT_SCALAR_LOOKUP_TABLE: [PositiveSign<f32>; 256] = [");
414        for i in 0..=u8::MAX {
415            if i.is_multiple_of(5) {
416                print!("\n   ");
417            }
418            print!(" ps32({:?}),", generated_table[i as usize]);
419        }
420        println!("\n];");
421
422        pretty_assertions::assert_eq!(
423            PACKED_LIGHT_SCALAR_LOOKUP_TABLE
424                .iter()
425                .copied()
426                .map(PositiveSign::<f32>::into_inner)
427                .collect::<Vec<_>>(),
428            generated_table
429        );
430    }
431
432    /// Test out-of-range floats.
433    #[test]
434    fn packed_light_clipping_in() {
435        assert_eq!(
436            [
437                PackedLight::scalar_in(ps32(-0.0)),
438                PackedLight::scalar_in(ps32(0.0)),
439                PackedLight::scalar_in(ps32(1e-30)),
440                PackedLight::scalar_in(ps32(1e+30)),
441                PackedLight::scalar_in(PositiveSign::<f32>::INFINITY),
442            ],
443            [0, 0, 0, 255, 255],
444        );
445    }
446
447    #[test]
448    fn packed_light_rounds_to_nearest() {
449        for i in PackedLightScalar::MIN..PackedLightScalar::MAX {
450            let value = PackedLight::scalar_out_ps(i);
451            assert_eq!(
452                (
453                    PackedLight::scalar_in(value * ps32(0.9999)),
454                    i,
455                    PackedLight::scalar_in(value * ps32(1.0001)),
456                ),
457                (i, i, i)
458            );
459        }
460    }
461
462    #[test]
463    fn packed_light_is_packed() {
464        // Technically this is not guaranteed by the compiler, but if it's false something probably went wrong.
465        assert_eq!(size_of::<PackedLight>(), 4);
466    }
467
468    /// Demonstrate what range and step sizes we get out of the encoding.
469    #[test]
470    fn packed_light_extreme_values_out() {
471        assert_eq!(
472            [
473                PackedLight::scalar_out(0),
474                PackedLight::scalar_out(1),
475                PackedLight::scalar_out(2),
476                PackedLight::scalar_out(254),
477                PackedLight::scalar_out(255),
478            ],
479            [0.0, 4.9575945e-5, 5.3134198e-5, 2048.0, 2194.9927],
480        );
481    }
482
483    #[test]
484    fn packed_light_difference_vs_eq() {
485        for v1 in packed_light_test_values() {
486            for v2 in packed_light_test_values() {
487                assert_eq!(
488                    v1 == v2,
489                    v1.difference_priority(v2) == 0,
490                    "v1={v1:?} v2={v2:?}"
491                );
492            }
493        }
494    }
495}