Skip to main content

renderling/
atlas.rs

1//! Texture atlas.
2//!
3//! All images are packed into an atlas at staging time.
4//! Texture descriptors describe where in the atlas an image is,
5//! and how it should sample pixels. These descriptors are packed into a buffer
6//! on the GPU. This keeps the number of texture binds to a minimum (one, in
7//! most cases).
8//!
9//! ## NOTE:
10//! `Atlas` is a temporary work around until we can use bindless techniques
11//! on web.
12//!
13//! `Atlas` is only available on CPU. Not available in shaders.
14use crabslab::SlabItem;
15
16#[cfg(cpu)]
17mod atlas_image;
18#[cfg(cpu)]
19pub use atlas_image::*;
20#[cfg(cpu)]
21mod cpu;
22#[cfg(cpu)]
23pub use cpu::*;
24
25pub mod shader;
26
27/// Method of addressing the edges of a texture.
28#[derive(
29    Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, SlabItem, core::fmt::Debug,
30)]
31pub struct TextureModes {
32    pub s: TextureAddressMode,
33    pub t: TextureAddressMode,
34}
35
36/// Infinitely wrap the input between 0.0 and 1.0.
37///
38/// Only handles `input` >= 0.0.
39pub fn repeat(mut input: f32) -> f32 {
40    let gto = input >= 1.0;
41    input %= 1.0;
42    if gto && input == 0.0 {
43        1.0
44    } else {
45        input
46    }
47}
48
49/// Clamp the input between 0.0 and 1.0.
50pub fn clamp(input: f32) -> f32 {
51    if input > 1.0 {
52        1.0 - f32::EPSILON
53    } else if input < 0.0 {
54        0.0 + f32::EPSILON
55    } else {
56        input
57    }
58}
59
60/// How edges should be handled in texture addressing/wrapping.
61#[repr(u32)]
62#[derive(
63    Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, SlabItem, core::fmt::Debug,
64)]
65pub enum TextureAddressMode {
66    #[default]
67    ClampToEdge,
68    Repeat,
69    MirroredRepeat,
70}
71
72impl core::fmt::Display for TextureAddressMode {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        f.write_str(match *self {
75            TextureAddressMode::ClampToEdge => "clamp to edge",
76            TextureAddressMode::Repeat => "repeat",
77            TextureAddressMode::MirroredRepeat => "mirrored repeat",
78        })
79    }
80}
81
82impl TextureAddressMode {
83    /// Wrap the given s/t coord into a pixel index according to texture
84    /// addressing.
85    pub fn wrap(&self, input: f32) -> f32 {
86        match self {
87            TextureAddressMode::Repeat => {
88                let sign = if input >= 0.0 { 1.0f32 } else { -1.0 };
89                let input = repeat(input.abs());
90                if sign > 0.0 {
91                    input
92                } else {
93                    1.0 - input
94                }
95            }
96            TextureAddressMode::MirroredRepeat => {
97                let sign = if input >= 0.0 { 1.0f32 } else { -1.0 };
98                let i = input.abs();
99                // TODO: Remove this clippy allowance after <https://github.com/Rust-GPU/rust-gpu/pull/460>
100                // merges.
101                #[cfg_attr(
102                    cpu,
103                    expect(
104                        clippy::manual_is_multiple_of,
105                        reason = "rust-gpu is not yet on rustc 1.91, which introduced this lint"
106                    )
107                )]
108                let flip = ((i as u32) % 2) == 0;
109                let t = repeat(i);
110                if sign > 0.0 {
111                    if flip {
112                        t
113                    } else {
114                        1.0 - t
115                    }
116                } else if flip {
117                    1.0 - t
118                } else {
119                    t
120                }
121            }
122            _ => clamp(input),
123        }
124    }
125}
126
127#[cfg(test)]
128mod test {
129    use glam::{UVec2, UVec3, Vec2, Vec3Swizzles, Vec4Swizzles};
130
131    use crate::atlas::shader::AtlasTextureDescriptor;
132
133    use super::*;
134
135    #[test]
136    fn can_repeat() {
137        assert_eq!(0.0, TextureAddressMode::Repeat.wrap(0.0));
138        assert_eq!(1.0, TextureAddressMode::Repeat.wrap(2.0));
139        assert_eq!(1.0, TextureAddressMode::Repeat.wrap(3.0));
140    }
141
142    #[test]
143    /// Tests that clip coordinates can be converted into texture coords within
144    /// a specific `AtlasTexture`, and back again.
145    fn constrain_clip_coords_sanity() {
146        let atlas_texture = AtlasTextureDescriptor {
147            offset_px: UVec2::splat(0),
148            size_px: UVec2::splat(800),
149            layer_index: 0,
150            frame_index: 0,
151            modes: TextureModes {
152                s: TextureAddressMode::ClampToEdge,
153                t: TextureAddressMode::ClampToEdge,
154            },
155        };
156        let atlas_size = UVec3::new(1024, 1024, 4);
157        let corners @ [tl, tr, br, bl] = [
158            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TL,
159            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TR,
160            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR,
161            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BL,
162        ]
163        .map(|coord| {
164            atlas_texture.constrain_clip_coords_to_texture_space(coord.xy(), atlas_size.xy())
165        });
166        log::info!("uv_corners: {corners:#?}");
167
168        let clip_br = crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR.xy();
169        log::info!("clip_br: {clip_br}");
170        let input_uv_br = (clip_br * Vec2::new(1.0, -1.0) + Vec2::splat(1.0)) * Vec2::splat(0.5);
171        log::info!("input_uv_br: {input_uv_br}");
172        assert_eq!(Vec2::ONE, input_uv_br, "incorrect uv");
173
174        let d = 800.0 / 1024.0;
175        assert_eq!(Vec2::splat(0.0), tl, "incorrect tl");
176        assert_eq!(Vec2::new(d, 0.0), tr, "incorrect tr");
177        assert_eq!(Vec2::new(d, d), br, "incorrect br");
178        assert_eq!(Vec2::new(0.0, d), bl, "incorrect bl");
179
180        let corners = [
181            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TL,
182            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TR,
183            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR,
184            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BL,
185        ]
186        .map(|coord| atlas_texture.constrain_clip_coords(coord.xy(), atlas_size.xy()));
187        log::info!("clip_corners: {corners:#?}");
188        //     [
189        //     Vec2(
190        //         -1.0,
191        //         1.0,
192        //     ),
193        //     Vec2(
194        //         0.5625,
195        //         1.0,
196        //     ),
197        //     Vec2(
198        //         0.5625,
199        //         -0.5625,
200        //     ),
201        //     Vec2(
202        //         -1.0,
203        //         -0.5625,
204        //     ),
205        // ]
206    }
207}