renderling/
math.rs

1//! Mathematical helper types and functions.
2//!
3//! Primarily this module adds some traits to help using `glam` types on the GPU
4//! without panicking, as well as a few traits to aid in writing generic shader
5//! code that can be run on the CPU.
6//!
7//! Lastly, it provides some common geometry and constants used in many shaders.
8use core::ops::Mul;
9use spirv_std::{
10    image::{sample_with, Cubemap, Image2d, Image2dArray, ImageWithMethods},
11    Image, Sampler,
12};
13
14use glam::*;
15pub use spirv_std::num_traits::{clamp, Float, Zero};
16
17pub trait Fetch<Coords> {
18    type Output;
19
20    fn fetch(&self, coords: Coords) -> Self::Output;
21}
22
23impl Fetch<UVec2> for Image!(2D, type=f32, sampled, depth) {
24    type Output = Vec4;
25
26    fn fetch(&self, coords: UVec2) -> Self::Output {
27        self.fetch_with(coords, sample_with::lod(0))
28    }
29}
30
31impl Fetch<UVec2> for Image!(2D, type=f32, sampled, depth, multisampled=true) {
32    type Output = Vec4;
33
34    fn fetch(&self, coords: UVec2) -> Self::Output {
35        // TODO: check whether this is doing what we think it's doing.
36        // (We think its doing roughly the same thing as the non-multisampled version above)
37        self.fetch_with(coords, sample_with::sample_index(0))
38    }
39}
40
41pub trait IsSampler: Copy + Clone {}
42
43impl IsSampler for () {}
44
45impl IsSampler for Sampler {}
46
47pub trait Sample2d {
48    type Sampler: IsSampler;
49
50    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4;
51}
52
53impl Sample2d for Image2d {
54    type Sampler = Sampler;
55
56    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4 {
57        self.sample_by_lod(sampler, uv, lod)
58    }
59}
60
61impl Sample2d for Image!(2D, type=f32, sampled, depth) {
62    type Sampler = Sampler;
63
64    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4 {
65        self.sample_by_lod(sampler, uv, lod)
66    }
67}
68
69pub trait Sample2dArray {
70    type Sampler: IsSampler;
71
72    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4;
73}
74
75impl Sample2dArray for Image2dArray {
76    type Sampler = Sampler;
77
78    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4 {
79        self.sample_by_lod(sampler, uv, lod)
80    }
81}
82
83impl Sample2dArray for Image!(2D, type=f32, sampled, arrayed, depth) {
84    type Sampler = Sampler;
85
86    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4 {
87        self.sample_by_lod(sampler, uv, lod)
88    }
89}
90
91pub trait SampleCube {
92    type Sampler: IsSampler;
93
94    fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4;
95}
96
97impl SampleCube for Cubemap {
98    type Sampler = Sampler;
99
100    fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4 {
101        self.sample_by_lod(sampler, uv, lod)
102    }
103}
104
105#[cfg(not(target_arch = "spirv"))]
106mod cpu {
107
108    use super::*;
109
110    /// A CPU texture with no dimensions that always returns the same
111    /// value when sampled.
112    pub struct ConstTexture(Vec4);
113
114    impl Fetch<UVec2> for ConstTexture {
115        type Output = Vec4;
116
117        fn fetch(&self, _coords: UVec2) -> Self::Output {
118            self.0
119        }
120    }
121
122    impl Sample2d for ConstTexture {
123        type Sampler = ();
124
125        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: glam::Vec2, _lod: f32) -> Vec4 {
126            self.0
127        }
128    }
129
130    impl Sample2dArray for ConstTexture {
131        type Sampler = ();
132
133        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: glam::Vec3, _lod: f32) -> glam::Vec4 {
134            self.0
135        }
136    }
137
138    impl SampleCube for ConstTexture {
139        type Sampler = ();
140
141        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: Vec3, _lod: f32) -> glam::Vec4 {
142            self.0
143        }
144    }
145
146    impl ConstTexture {
147        pub fn new(value: Vec4) -> Self {
148            Self(value)
149        }
150    }
151
152    #[derive(Debug)]
153    pub struct CpuTexture2d<P: image::Pixel, Container> {
154        pub image: image::ImageBuffer<P, Container>,
155        convert_fn: fn(&P) -> Vec4,
156    }
157
158    impl<P: image::Pixel, Container> CpuTexture2d<P, Container> {
159        pub fn from_image(
160            image: image::ImageBuffer<P, Container>,
161            convert_fn: fn(&P) -> Vec4,
162        ) -> Self {
163            Self { image, convert_fn }
164        }
165    }
166
167    impl<P, Container> Fetch<UVec2> for CpuTexture2d<P, Container>
168    where
169        P: image::Pixel,
170        Container: std::ops::Deref<Target = [P::Subpixel]>,
171    {
172        type Output = Vec4;
173
174        fn fetch(&self, coords: UVec2) -> Self::Output {
175            let x = coords.x.clamp(0, self.image.width() - 1);
176            let y = coords.y.clamp(0, self.image.height() - 1);
177            let p = self.image.get_pixel(x, y);
178            (self.convert_fn)(p)
179        }
180    }
181
182    impl<P, Container> Sample2d for CpuTexture2d<P, Container>
183    where
184        P: image::Pixel,
185        Container: std::ops::Deref<Target = [P::Subpixel]>,
186    {
187        type Sampler = ();
188
189        fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec2, _lod: f32) -> Vec4 {
190            // TODO: lerp the CPU texture sampling
191            // TODO: use configurable wrap mode on CPU sampling
192            let px = uv.x.clamp(0.0, 1.0) * (self.image.width() as f32 - 1.0);
193            let py = uv.y.clamp(0.0, 1.0) * (self.image.height() as f32 - 1.0);
194            self.fetch(UVec2::new(px.round() as u32, py.round() as u32))
195        }
196    }
197
198    pub struct CpuTexture2dArray<P: image::Pixel, Container> {
199        pub images: Vec<image::ImageBuffer<P, Container>>,
200        convert_fn: fn(&P) -> Vec4,
201    }
202
203    impl<P: image::Pixel, Container> CpuTexture2dArray<P, Container> {
204        pub fn from_images(
205            images: impl IntoIterator<Item = image::ImageBuffer<P, Container>>,
206            convert_fn: fn(&P) -> Vec4,
207        ) -> Self {
208            let images = images.into_iter().collect();
209            Self { images, convert_fn }
210        }
211    }
212
213    impl<P, Container> Sample2dArray for CpuTexture2dArray<P, Container>
214    where
215        P: image::Pixel,
216        Container: std::ops::Deref<Target = [P::Subpixel]>,
217    {
218        type Sampler = ();
219
220        /// Panics if `uv.z` is greater than length of images.
221        fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec3, _lod: f32) -> Vec4 {
222            // TODO: lerp the CPU texture sampling
223            // TODO: use configurable wrap mode on CPU sampling
224            let img = &self.images[uv.z as usize];
225            let px = (uv.x.clamp(0.0, 1.0) * (img.width() as f32 - 1.0)).round() as u32;
226            let py = (uv.y.clamp(0.0, 1.0) * (img.height() as f32 - 1.0)).round() as u32;
227            println!("sampling: ({px}, {py})");
228            let p = img.get_pixel(px, py);
229            (self.convert_fn)(p)
230        }
231    }
232
233    /// A CPU-side cubemap texture.
234    ///
235    /// Provided primarily for testing purposes.
236    #[derive(Default)]
237    pub struct CpuCubemap {
238        pub images: [image::DynamicImage; 6],
239    }
240
241    impl SampleCube for CpuCubemap {
242        type Sampler = ();
243
244        fn sample_by_lod(
245            &self,
246            _sampler: Self::Sampler,
247            direction: glam::Vec3,
248            _lod: f32,
249        ) -> glam::Vec4 {
250            crate::cubemap::cpu_sample_cubemap(&self.images, direction)
251        }
252    }
253
254    /// Convert a u8 in range 0-255 to an f32 in range 0.0 - 1.0.
255    pub fn scaled_u8_to_f32(u: u8) -> f32 {
256        u as f32 / 255.0
257    }
258
259    pub fn luma_u8_to_vec4(p: &image::Luma<u8>) -> Vec4 {
260        let shade = scaled_u8_to_f32(p.0[0]);
261        Vec3::splat(shade).extend(1.0)
262    }
263
264    /// Convert an f32 in range 0.0 - 1.0 into a u8 in range 0-255.
265    pub fn scaled_f32_to_u8(f: f32) -> u8 {
266        (f * 255.0) as u8
267    }
268
269    /// Convert a u32 in rang 0-u32::MAX to a u8 in rang 0-255.
270    pub fn scaled_u32_to_u8(u: u32) -> u8 {
271        ((u as f32 / u32::MAX as f32) * 255.0) as u8
272    }
273}
274#[cfg(not(target_arch = "spirv"))]
275pub use cpu::*;
276
277/// Additional/replacement methods for glam vector types.
278///
279/// These are required because `naga` (`wgpu`'s translation layer) doesn't like
280/// certain contstants like `f32::INFINITY` or `f32::NaN`, which cause errors in
281/// naga's WGSL output.
282///
283/// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`
284/// for more info.
285pub trait IsVector {
286    /// Type returned by the `orthogonal_vectors` extension function.
287    type OrthogonalVectors;
288
289    /// Normalize or return zero.
290    fn alt_norm_or_zero(&self) -> Self;
291
292    /// Return a vector with `signum_or_zero` applied to each component.
293    fn signum_or_zero(&self) -> Self;
294
295    /// Returns the dot product of a vector with itself (the square of its
296    /// length).
297    fn dot2(&self) -> f32;
298
299    /// Returns normalized orthogonal vectors.
300    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors;
301}
302
303impl IsVector for glam::Vec2 {
304    type OrthogonalVectors = Vec2;
305
306    fn alt_norm_or_zero(&self) -> Self {
307        if self.length().is_zero() {
308            glam::Vec2::ZERO
309        } else {
310            self.normalize()
311        }
312    }
313
314    fn signum_or_zero(&self) -> Self {
315        Vec2::new(signum_or_zero(self.x), signum_or_zero(self.y))
316    }
317
318    fn dot2(&self) -> f32 {
319        self.dot(*self)
320    }
321
322    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors {
323        Vec3::new(self.x, self.y, 0.0).cross(Vec3::Z).xy()
324    }
325}
326
327impl IsVector for glam::Vec3 {
328    type OrthogonalVectors = [Vec3; 2];
329
330    fn alt_norm_or_zero(&self) -> Self {
331        if self.length().is_zero() {
332            glam::Vec3::ZERO
333        } else {
334            self.normalize()
335        }
336    }
337
338    fn signum_or_zero(&self) -> Self {
339        Vec3::new(
340            signum_or_zero(self.x),
341            signum_or_zero(self.y),
342            signum_or_zero(self.z),
343        )
344    }
345
346    fn dot2(&self) -> f32 {
347        self.dot(*self)
348    }
349
350    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors {
351        // From https://graphics.pixar.com/library/OrthonormalB/paper.pdf
352        let s = self.alt_norm_or_zero();
353        let sign = signum_or_zero(s.z);
354        let a = -1.0 / (sign + s.z);
355        let b = s.x * s.y * a;
356        [
357            Self::new(1.0 + sign * s.x * s.x * a, sign * b, -sign * s.x),
358            Self::new(b, sign + s.y * s.y * a, -s.y),
359        ]
360    }
361}
362
363/// Quantize an f32
364///
365/// Determine the distance from a point to a line segment.
366pub fn distance_to_line(p: Vec3, a: Vec3, b: Vec3) -> f32 {
367    let ab_distance = a.distance(b);
368    if ab_distance <= f32::EPSILON {
369        p.distance(a)
370    } else {
371        let tri_area = (p - a).cross(p - b).length();
372        tri_area / ab_distance
373    }
374}
375
376/// Additional/replacement methods for glam matrix types.
377///
378/// These are required because `naga` (`wgpu`'s translation layer) doesn't like
379/// certain contstants like `f32::INFINITY` or `f32::NaN`, which cause errors in
380/// naga's WGSL output.
381///
382/// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`
383/// for more info.
384pub trait IsMatrix {
385    /// Extracts `scale`, `rotation` and `translation` from `self`. The input
386    /// matrix is expected to be a 3D affine transformation matrix otherwise
387    /// the output will be invalid.
388    ///
389    /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant
390    /// of `self` is zero or if the resulting scale vector contains any zero
391    /// elements when `glam_assert` is enabled.
392    ///
393    /// This is required instead of using
394    /// [`glam::Mat4::to_scale_rotation_translation`], because that uses
395    /// f32::signum, which compares against `f32::NAN`, which causes an error
396    /// in naga's WGSL output.
397    ///
398    /// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`
399    /// for more info.
400    fn to_scale_rotation_translation_or_id(&self) -> (glam::Vec3, glam::Quat, glam::Vec3);
401}
402
403/// From the columns of a 3x3 rotation matrix.
404///
405/// All of this because we can't use NaNs.
406#[inline]
407fn from_rotation_axes(x_axis: glam::Vec3, y_axis: glam::Vec3, z_axis: glam::Vec3) -> glam::Quat {
408    // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`
409    let (m00, m01, m02) = x_axis.into();
410    let (m10, m11, m12) = y_axis.into();
411    let (m20, m21, m22) = z_axis.into();
412    if m22 <= 0.0 {
413        // x^2 + y^2 >= z^2 + w^2
414        let dif10 = m11 - m00;
415        let omm22 = 1.0 - m22;
416        if dif10 <= 0.0 {
417            // x^2 >= y^2
418            let four_xsq = omm22 - dif10;
419            let inv4x = 0.5 / four_xsq.sqrt();
420            glam::Quat::from_xyzw(
421                four_xsq * inv4x,
422                (m01 + m10) * inv4x,
423                (m02 + m20) * inv4x,
424                (m12 - m21) * inv4x,
425            )
426        } else {
427            // y^2 >= x^2
428            let four_ysq = omm22 + dif10;
429            let inv4y = 0.5 / four_ysq.sqrt();
430            glam::Quat::from_xyzw(
431                (m01 + m10) * inv4y,
432                four_ysq * inv4y,
433                (m12 + m21) * inv4y,
434                (m20 - m02) * inv4y,
435            )
436        }
437    } else {
438        // z^2 + w^2 >= x^2 + y^2
439        let sum10 = m11 + m00;
440        let opm22 = 1.0 + m22;
441        if sum10 <= 0.0 {
442            // z^2 >= w^2
443            let four_zsq = opm22 - sum10;
444            let inv4z = 0.5 / four_zsq.sqrt();
445            glam::Quat::from_xyzw(
446                (m02 + m20) * inv4z,
447                (m12 + m21) * inv4z,
448                four_zsq * inv4z,
449                (m01 - m10) * inv4z,
450            )
451        } else {
452            // w^2 >= z^2
453            let four_wsq = opm22 + sum10;
454            let inv4w = 0.5 / four_wsq.sqrt();
455            glam::Quat::from_xyzw(
456                (m12 - m21) * inv4w,
457                (m20 - m02) * inv4w,
458                (m01 - m10) * inv4w,
459                four_wsq * inv4w,
460            )
461        }
462    }
463}
464
465const fn srt_id() -> (Vec3, Quat, Vec3) {
466    (Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)
467}
468
469impl IsMatrix for glam::Mat4 {
470    #[inline]
471    fn to_scale_rotation_translation_or_id(&self) -> (glam::Vec3, glam::Quat, glam::Vec3) {
472        let det = self.determinant();
473        if det == 0.0 {
474            crate::println!("det == 0.0, returning identity");
475            return srt_id();
476        }
477
478        let det_sign = if det >= 0.0 { 1.0 } else { -1.0 };
479
480        let scale = glam::Vec3::new(
481            self.x_axis.length() * det_sign,
482            self.y_axis.length(),
483            self.z_axis.length(),
484        );
485
486        if !scale.cmpne(glam::Vec3::ZERO).all() {
487            return srt_id();
488        }
489
490        let inv_scale = scale.recip();
491
492        let rotation = from_rotation_axes(
493            self.x_axis.mul(inv_scale.x).xyz(),
494            self.y_axis.mul(inv_scale.y).xyz(),
495            self.z_axis.mul(inv_scale.z).xyz(),
496        );
497
498        let translation = self.w_axis.xyz();
499
500        (scale, rotation, translation)
501    }
502}
503
504/// Returns `1.0` if `n` is greater than or equal to `0.0`.
505/// Returns `1.0` if `n` is greater than or equal to `-0.0`.
506/// Returns `-1.0` if `n` is less than `0.0`.
507/// Returns `0.0` if `n` is `NaN`.
508pub fn signum_or_zero(n: f32) -> f32 {
509    ((n >= 0.0) as u32) as f32 - ((n < 0.0) as u32) as f32
510}
511
512/// Return `1.0` when `value` is greater than or equal to `edge` and `0.0` where
513/// `value` is less than `edge`.
514#[inline(always)]
515pub fn step_ge(value: f32, edge: f32) -> f32 {
516    ((value >= edge) as u32) as f32
517}
518
519/// Return `1.0` when `value` is less than or equal to `edge`
520/// and `0.0` when `value` is greater than `edge`.
521#[inline(always)]
522pub fn step_le(value: f32, edge: f32) -> f32 {
523    ((value <= edge) as u32) as f32
524}
525
526pub fn smoothstep(edge_in: f32, edge_out: f32, mut x: f32) -> f32 {
527    // Scale, and clamp x to 0..1 range
528    x = clamp((x - edge_in) / (edge_out - edge_in), 0.0, 1.0);
529    x * x * (3.0 - 2.0 * x)
530}
531
532pub fn triangle_face_normal(p1: Vec3, p2: Vec3, p3: Vec3) -> Vec3 {
533    let a = p1 - p2;
534    let b = p1 - p3;
535    let n: Vec3 = a.cross(b).alt_norm_or_zero();
536    #[cfg(cpu)]
537    debug_assert_ne!(
538        Vec3::ZERO,
539        n,
540        "normal is zero - p1: {p1}, p2: {p2}, p3: {p3}"
541    );
542    n
543}
544
545/// Convert a color from a hexadecimal number (eg. `0x52b14eff`) into a Vec4.
546pub fn hex_to_vec4(color: u32) -> Vec4 {
547    let r = ((color >> 24) & 0xFF) as f32 / 255.0;
548    let g = ((color >> 16) & 0xFF) as f32 / 255.0;
549    let b = ((color >> 8) & 0xFF) as f32 / 255.0;
550    let a = (color & 0xFF) as f32 / 255.0;
551
552    Vec4::new(r, g, b, a)
553}
554
555pub const UNIT_QUAD_CCW: [Vec3; 6] = {
556    let tl = Vec3::new(-0.5, 0.5, 0.0);
557    let tr = Vec3::new(0.5, 0.5, 0.0);
558    let bl = Vec3::new(-0.5, -0.5, 0.0);
559    let br = Vec3::new(0.5, -0.5, 0.0);
560    [bl, br, tr, tr, tl, bl]
561};
562
563pub const CLIP_QUAD_CCW: [Vec3; 6] = {
564    let tl = Vec3::new(-1.0, 1.0, 0.0);
565    let tr = Vec3::new(1.0, 1.0, 0.0);
566    let bl = Vec3::new(-1.0, -1.0, 0.0);
567    let br = Vec3::new(1.0, -1.0, 0.0);
568    [bl, br, tr, tr, tl, bl]
569};
570
571pub const CLIP_SPACE_COORD_QUAD_CCW_TL: Vec4 = Vec4::new(-1.0, 1.0, 0.5, 1.0);
572pub const CLIP_SPACE_COORD_QUAD_CCW_BL: Vec4 = Vec4::new(-1.0, -1.0, 0.5, 1.0);
573pub const CLIP_SPACE_COORD_QUAD_CCW_TR: Vec4 = Vec4::new(1.0, 1.0, 0.5, 1.0);
574pub const CLIP_SPACE_COORD_QUAD_CCW_BR: Vec4 = Vec4::new(1.0, -1.0, 0.5, 1.0);
575
576pub const CLIP_SPACE_COORD_QUAD_CCW: [Vec4; 6] = {
577    [
578        CLIP_SPACE_COORD_QUAD_CCW_BL,
579        CLIP_SPACE_COORD_QUAD_CCW_BR,
580        CLIP_SPACE_COORD_QUAD_CCW_TR,
581        CLIP_SPACE_COORD_QUAD_CCW_TR,
582        CLIP_SPACE_COORD_QUAD_CCW_TL,
583        CLIP_SPACE_COORD_QUAD_CCW_BL,
584    ]
585};
586
587pub const UV_COORD_QUAD_CCW: [Vec2; 6] = {
588    let tl = Vec2::new(0.0, 0.0);
589    let tr = Vec2::new(1.0, 0.0);
590    let bl = Vec2::new(0.0, 1.0);
591    let br = Vec2::new(1.0, 1.0);
592    [bl, br, tr, tr, tl, bl]
593};
594
595pub const POINTS_2D_TEX_QUAD: [Vec2; 6] = {
596    let tl = Vec2::new(0.0, 0.0);
597    let tr = Vec2::new(1.0, 0.0);
598    let bl = Vec2::new(0.0, 1.0);
599    let br = Vec2::new(1.0, 1.0);
600    [tl, bl, tr, tr, bl, br]
601};
602
603/// Points around the unit cube.
604///
605///    y           1_____2     _____
606///    |           /    /|    /|    |  (same box, left and front sides removed)
607///    |___x     0/___3/ |   /7|____|6
608///   /           |    | /   | /    /
609/// z/            |____|/   4|/____/5
610pub const UNIT_POINTS: [Vec3; 8] = {
611    let p0 = Vec3::new(-0.5, 0.5, 0.5);
612    let p1 = Vec3::new(-0.5, 0.5, -0.5);
613    let p2 = Vec3::new(0.5, 0.5, -0.5);
614    let p3 = Vec3::new(0.5, 0.5, 0.5);
615
616    let p4 = Vec3::new(-0.5, -0.5, 0.5);
617    let p7 = Vec3::new(-0.5, -0.5, -0.5);
618    let p6 = Vec3::new(0.5, -0.5, -0.5);
619    let p5 = Vec3::new(0.5, -0.5, 0.5);
620
621    [p0, p1, p2, p3, p4, p5, p6, p7]
622};
623
624/// Triangle faces of the unit cube, winding CCW.
625pub const UNIT_INDICES: [usize; 36] = [
626    0, 2, 1, 0, 3, 2, // top
627    0, 4, 3, 4, 5, 3, // front
628    3, 6, 2, 3, 5, 6, // right
629    1, 7, 0, 7, 4, 0, // left
630    4, 6, 5, 4, 7, 6, // bottom
631    2, 7, 1, 2, 6, 7, // back
632];
633
634#[cfg(not(target_arch = "spirv"))]
635pub fn unit_cube() -> Vec<(Vec3, Vec3)> {
636    UNIT_INDICES
637        .chunks_exact(3)
638        .flat_map(|chunk| match chunk {
639            [a, b, c] => {
640                let a = UNIT_POINTS[*a];
641                let b = UNIT_POINTS[*b];
642                let c = UNIT_POINTS[*c];
643                let n = triangle_face_normal(a, b, c);
644                [(a, n), (b, n), (c, n)]
645            }
646            _ => unreachable!(),
647        })
648        .collect::<Vec<_>>()
649}
650
651/// Points on the unit cube that create a triangle-list mesh.
652///
653/// Use [`unit_cube`] for a mesh that includes normals.
654///
655/// `rust-gpu` doesn't like nested/double indexing so we do this here.
656/// See [this comment on discord](https://discord.com/channels/750717012564770887/750717499737243679/1131395331368693770)
657pub const CUBE: [Vec3; 36] = {
658    let p0 = Vec3::new(-0.5, 0.5, 0.5);
659    let p1 = Vec3::new(-0.5, 0.5, -0.5);
660    let p2 = Vec3::new(0.5, 0.5, -0.5);
661    let p3 = Vec3::new(0.5, 0.5, 0.5);
662    let p4 = Vec3::new(-0.5, -0.5, 0.5);
663    let p7 = Vec3::new(-0.5, -0.5, -0.5);
664    let p6 = Vec3::new(0.5, -0.5, -0.5);
665    let p5 = Vec3::new(0.5, -0.5, 0.5);
666    convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7])
667};
668
669pub fn reflect(i: Vec3, n: Vec3) -> Vec3 {
670    let n = n.alt_norm_or_zero();
671    i - 2.0 * n.dot(i) * n
672}
673
674pub fn is_inside_clip_space(p: Vec3) -> bool {
675    p.x.abs() <= 1.0 && p.y.abs() <= 1.0 && p.z.abs() <= 1.0
676}
677
678pub const fn convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]: [Vec3; 8]) -> [Vec3; 36] {
679    [
680        p0, p2, p1, p0, p3, p2, // top
681        p0, p4, p3, p4, p5, p3, // front
682        p3, p6, p2, p3, p5, p6, // right
683        p1, p7, p0, p7, p4, p0, // left
684        p4, p6, p5, p4, p7, p6, // bottom
685        p2, p7, p1, p2, p6, p7, // back
686    ]
687}
688
689/// An PCG PRNG that is optimized for GPUs, in that it is fast to evaluate and accepts
690/// sequential ids as it's initial state without sacrificing on RNG quality.
691///
692/// * <https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/>
693/// * <https://jcgt.org/published/0009/03/02/>
694///
695/// Thanks to Firestar99 at
696/// <https://github.com/Firestar99/nanite-at-home/blob/c55915d16ad3b5b4b706d8017633f0870dd2603e/space-engine-shader/src/utils/gpurng.rs#L19>
697pub struct GpuRng(pub u32);
698
699impl GpuRng {
700    pub fn new(state: u32) -> GpuRng {
701        Self(state)
702    }
703
704    pub fn gen(&mut self) -> u32 {
705        let state = self.0;
706        self.0 = if cfg!(gpu) {
707            self.0 * 747796405 + 2891336453
708        } else {
709            self.0.wrapping_sub(747796405).wrapping_add(2891336453)
710        };
711        let word = (state >> ((state >> 28) + 4)) ^ state;
712        let word = if cfg!(gpu) {
713            word * 277803737
714        } else {
715            word.wrapping_mul(277803737)
716        };
717        (word >> 22) ^ word
718    }
719
720    pub fn gen_u32(&mut self, min: u32, max: u32) -> u32 {
721        let range = max - min;
722        let percent = self.gen_f32(0.0, 1.0);
723        min + (range as f32 * percent).round() as u32
724    }
725
726    pub fn gen_f32(&mut self, min: f32, max: f32) -> f32 {
727        let range = max - min;
728        let numerator = self.gen();
729        let percentage = numerator as f32 / u32::MAX as f32;
730        min + range * percentage
731    }
732
733    pub fn gen_vec3(&mut self, min: Vec3, max: Vec3) -> Vec3 {
734        let x = self.gen_f32(min.x, max.x);
735        let y = self.gen_f32(min.y, max.y);
736        let z = self.gen_f32(min.z, max.z);
737        Vec3::new(x, y, z)
738    }
739
740    pub fn gen_vec2(&mut self, min: Vec2, max: Vec2) -> Vec2 {
741        let x = self.gen_f32(min.x, max.x);
742        let y = self.gen_f32(min.y, max.y);
743        Vec2::new(x, y)
744    }
745}
746
747/// Convert a pixel coordinate in screen space (origin is top left, Y increases downwards)
748/// to normalized device coordinates (origin is center, Y increses upwards).
749pub fn convert_pixel_to_ndc(pixel_coord: Vec2, viewport_size: UVec2) -> Vec2 {
750    // Normalize the point to the range [0.0, 1.0];
751    let mut normalized = pixel_coord / viewport_size.as_vec2();
752    // Flip the Y axis to increase upward
753    normalized.y = 1.0 - normalized.y;
754    // Move the origin to the center
755    (normalized * 2.0) - 1.0
756}
757
758#[cfg(test)]
759mod test {
760    use super::*;
761
762    #[test]
763    fn step_sanity() {
764        assert_eq!(0.0, step_le(0.0, -0.33333));
765        assert_eq!(1.0, step_le(0.0, 0.33333));
766        assert_eq!(1.0, step_le(0.0, 0.0));
767    }
768
769    #[test]
770    #[allow(clippy::bool_comparison)]
771    fn nan_sanity() {
772        let n = f32::NAN;
773        assert!(n.is_nan());
774        assert!((n <= 0.0) == false);
775        assert!((n > 0.0) == false);
776    }
777
778    #[test]
779    fn signum_sanity() {
780        assert_eq!(1.0, signum_or_zero(0.33));
781        assert_eq!(1.0, signum_or_zero(0.0));
782        assert_eq!(1.0, signum_or_zero(-0.0));
783        assert_eq!(-1.0, signum_or_zero(-0.33));
784
785        let nan = f32::NAN;
786        assert_eq!(0.0, signum_or_zero(nan));
787    }
788}