retrofire_core/render/
tex.rs

1//! Textures and texture samplers.
2
3use crate::math::vec::{Vec2, Vector};
4use crate::util::buf::{AsSlice2, Buf2, Slice2};
5
6/// Basis of the texture space.
7#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
8pub struct Tex;
9
10/// Texture coordinate vector. Texture coordinates can be either absolute,
11/// in range (0, 0)..(w, h) for some texture with dimensions w and h, or
12/// relative, in range (0, 0)..(1, 1), in which case they are independent
13/// of the actual dimensions of the texture.
14pub type TexCoord = Vec2<Tex>;
15
16impl TexCoord {
17    /// Returns the u (horizontal) component of `self`.
18    pub const fn u(&self) -> f32 {
19        self.0[0]
20    }
21    /// Returns the v (vertical) component of `self`.
22    pub const fn v(&self) -> f32 {
23        self.0[1]
24    }
25}
26
27/// Returns a new texture coordinate with components `u` and `v`.
28#[inline]
29pub const fn uv(u: f32, v: f32) -> TexCoord {
30    Vector::new([u, v])
31}
32
33/// A texture type. Can contain either owned or borrowed pixel data.
34///
35/// Textures are used to render *texture mapped* geometry, by interpolating
36/// texture coordinates across polygon faces. To read, or *sample*, from a
37/// `Texture`, use one of the `Sampler*` types defined in this module.
38///
39/// Multiple textures can be packed into a single larger memory buffer, often
40/// called a "texture atlas" or "sprite sheet". Each texture borrows a region
41/// of the larger buffer.
42///
43/// * TODO Mipmapping
44/// * TODO Bilinear filtering sampler
45#[derive(Copy, Clone)]
46pub struct Texture<D> {
47    w: f32,
48    h: f32,
49    data: D,
50}
51
52impl<D> Texture<D> {
53    /// Returns the width of `Self` as `f32`.
54    #[inline]
55    pub fn width(&self) -> f32 {
56        self.w
57    }
58    /// Returns the height of `Self` as `f32`.
59    #[inline]
60    pub fn height(&self) -> f32 {
61        self.h
62    }
63}
64
65impl<C> From<Buf2<C>> for Texture<Buf2<C>> {
66    /// Creates a new texture from owned pixel data.
67    fn from(data: Buf2<C>) -> Self {
68        Self {
69            w: data.width() as f32,
70            h: data.height() as f32,
71            data,
72        }
73    }
74}
75
76impl<'a, C> From<Slice2<'a, C>> for Texture<Slice2<'a, C>> {
77    /// Creates a new texture from borrowed pixel data.
78    fn from(data: Slice2<'a, C>) -> Self {
79        Self {
80            w: data.width() as f32,
81            h: data.height() as f32,
82            data,
83        }
84    }
85}
86
87/// A texture sampler that repeats the texture infinitely modulo the texture
88/// dimensions. For performance reasons, `SamplerRepeatPot` only accepts
89/// textures with dimensions that are powers of two.
90#[derive(Copy, Clone, Debug)]
91pub struct SamplerRepeatPot {
92    w_mask: u32,
93    h_mask: u32,
94}
95
96impl SamplerRepeatPot {
97    /// Creates a new `SamplerRepeatPot` based on the dimensions of `tex`.
98    /// # Panics
99    /// If the width or height of `tex` is not a power of two.
100    pub fn new<C>(tex: &Texture<impl AsSlice2<C>>) -> Self {
101        let w = tex.width() as u32;
102        let h = tex.height() as u32;
103        assert!(w.is_power_of_two(), "width must be 2^n, was {w}");
104        assert!(h.is_power_of_two(), "height must be 2^n, was {h}");
105        Self { w_mask: w - 1, h_mask: h - 1 }
106    }
107
108    /// Returns the color in `tex` at `tc` in relative coordinates, such that
109    /// coordinates outside `0.0..1.0` are wrapped to the valid range.
110    ///
111    /// Uses nearest neighbor sampling.
112    pub fn sample<C: Copy>(
113        &self,
114        tex: &Texture<impl AsSlice2<C>>,
115        tc: TexCoord,
116    ) -> C {
117        let scaled_uv = uv(tex.width() * tc.u(), tex.height() * tc.v());
118        self.sample_abs(tex, scaled_uv)
119    }
120
121    /// Returns the color in `tex` at `tc` in absolute coordinates, such that
122    /// coordinates outside `0.0..tex.width()` and `0.0..tex.height()` are
123    /// wrapped to the valid range.
124    ///
125    /// Uses nearest neighbor sampling.
126    pub fn sample_abs<C: Copy>(
127        &self,
128        tex: &Texture<impl AsSlice2<C>>,
129        tc: TexCoord,
130    ) -> C {
131        use crate::math::float::f32;
132        // Convert first to signed int to avoid clamping to zero
133        let u = f32::floor(tc.u()) as i32 as u32 & self.w_mask;
134        let v = f32::floor(tc.v()) as i32 as u32 & self.h_mask;
135
136        tex.data.as_slice2()[[u, v]]
137    }
138}
139
140/// A texture sampler that clamps out-of-bounds coordinates
141/// to the nearest valid coordinate in both dimensions.
142#[derive(Copy, Clone, Debug)]
143pub struct SamplerClamp;
144
145#[cfg(feature = "fp")]
146impl SamplerClamp {
147    /// Returns the color in `tex` at `tc` such that coordinates outside
148    /// the range `0.0..1.0` are clamped to the range endpoints.
149    ///
150    /// Uses nearest neighbor sampling.
151    pub fn sample<C: Copy>(
152        &self,
153        tex: &Texture<impl AsSlice2<C>>,
154        tc: TexCoord,
155    ) -> C {
156        self.sample_abs(tex, uv(tc.u() * tex.w, tc.v() * tex.h))
157    }
158
159    /// Returns the color in `tex` at `tc` in absolute coordinates, such that
160    /// coordinates outside `0.0..tex.width()` and `0.0..tex.height()` are
161    /// clamped to the range endpoints.
162    ///
163    /// Uses nearest neighbor sampling.
164    pub fn sample_abs<C: Copy>(
165        &self,
166        tex: &Texture<impl AsSlice2<C>>,
167        tc: TexCoord,
168    ) -> C {
169        use crate::math::float::f32;
170        let u = f32::floor(tc.u().clamp(0.0, tex.w - 1.0)) as u32;
171        let v = f32::floor(tc.v().clamp(0.0, tex.h - 1.0)) as u32;
172        tex.data.as_slice2()[[u, v]]
173    }
174}
175
176/// A texture sampler that assumes all texture coordinates are within bounds.
177///
178/// Out-of-bounds coordinates may cause graphical glitches or runtime panics
179/// but not undefined behavior. In particular, if the texture data is a slice
180/// of a larger buffer, `SamplerOnce` may read out of bounds of the slice but
181/// not the backing buffer.
182#[derive(Copy, Clone, Debug)]
183pub struct SamplerOnce;
184
185impl SamplerOnce {
186    /// Returns the color in `tex` at `tc` such that both coordinates are
187    /// assumed to be in the range `0.0..1.0`.
188    ///
189    /// Uses nearest neighbor sampling. Passing out-of-range coordinates
190    /// to this function is sound (not UB) but is not otherwise specified.
191    ///
192    /// # Panics
193    /// May panic if `tc` is not in the valid range.
194    pub fn sample<C: Copy>(
195        &self,
196        tex: &Texture<impl AsSlice2<C>>,
197        tc: TexCoord,
198    ) -> C {
199        let scaled_uv = uv(tex.width() * tc.u(), tex.height() * tc.v());
200        self.sample_abs(tex, scaled_uv)
201    }
202    /// Returns the color in `tex` at `tc` such that the coordinates are
203    /// assumed to be in the ranges `0.0..tex.width()` and `0.0..tex.height()`
204    /// respectively.
205    ///
206    /// Uses nearest neighbor sampling. Passing out-of-range coordinates
207    /// to this function is sound (not UB) but is not otherwise specified.
208    ///
209    /// # Panics
210    /// May panic if `tc` is not in the valid range.
211    pub fn sample_abs<C: Copy>(
212        &self,
213        tex: &Texture<impl AsSlice2<C>>,
214        tc: TexCoord,
215    ) -> C {
216        let u = tc.u() as u32;
217        let v = tc.v() as u32;
218
219        let d = tex.data.as_slice2();
220        debug_assert!(u < d.width(), "u={u}");
221        debug_assert!(v < d.height(), "v={v}");
222
223        d[[u, v]]
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use alloc::vec;
230
231    use crate::math::color::{rgb, Color3};
232    use crate::util::buf::Buf2;
233
234    use super::*;
235
236    #[rustfmt::skip]
237    fn tex() -> Texture<Buf2<Color3>> {
238        Texture::from(Buf2::new_from(
239            (2, 2), vec![
240                rgb(0xFF, 0, 0),
241                rgb(0, 0xFF, 0),
242                rgb(0, 0, 0xFF),
243                rgb(0xFF, 0xFF, 0),
244           ]
245        ))
246    }
247
248    #[test]
249    #[cfg(feature = "fp")]
250    fn sampler_repeat_pot() {
251        let tex = tex();
252        let s = SamplerRepeatPot::new(&tex);
253
254        assert_eq!(s.sample(&tex, uv(-0.1, 0.0)), rgb(0, 0xFF, 0));
255        assert_eq!(s.sample(&tex, uv(0.0, -0.1)), rgb(0, 0, 0xFF));
256
257        assert_eq!(s.sample(&tex, uv(1.0, 0.0)), rgb(0xFF, 0, 0));
258        assert_eq!(s.sample(&tex, uv(0.0, 1.0)), rgb(0xFF, 0, 0));
259
260        assert_eq!(s.sample(&tex, uv(4.8, 0.2)), rgb(0, 0xFF, 0));
261        assert_eq!(s.sample(&tex, uv(0.2, 4.8)), rgb(0, 0, 0xFF));
262    }
263
264    #[test]
265    #[cfg(feature = "fp")]
266    fn sampler_clamp() {
267        let tex = tex();
268        let s = SamplerClamp;
269
270        assert_eq!(s.sample(&tex, uv(-1.0, 0.0)), rgb(0xFF, 0, 0));
271        assert_eq!(s.sample(&tex, uv(0.0, -1.0)), rgb(0xFF, 0, 0));
272
273        assert_eq!(s.sample(&tex, uv(1.5, 0.0)), rgb(0, 0xFF, 0));
274        assert_eq!(s.sample(&tex, uv(0.0, 1.5)), rgb(0, 0, 0xFF));
275
276        assert_eq!(s.sample(&tex, uv(1.5, 1.5)), rgb(0xFF, 0xFF, 0));
277    }
278
279    #[test]
280    fn sampler_once() {
281        let tex = tex();
282        let s = SamplerOnce;
283
284        assert_eq!(s.sample(&tex, uv(0.0, 0.0)), rgb(0xFF, 0, 0));
285        assert_eq!(s.sample(&tex, uv(0.5, 0.0)), rgb(0, 0xFF, 0));
286        assert_eq!(s.sample(&tex, uv(0.0, 0.5)), rgb(0, 0, 0xFF));
287        assert_eq!(s.sample(&tex, uv(0.5, 0.5)), rgb(0xFF, 0xFF, 0));
288    }
289}