Skip to main content

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