retrofire_core/render/
tex.rs

1//! Textures and texture samplers.
2
3use crate::geom::Normal3;
4use crate::math::{Point2u, Vec2, Vec3, Vector, pt2, splat, vec2};
5use crate::util::{
6    Dims,
7    buf::{AsSlice2, Buf2, Slice2},
8};
9
10/// Basis of the texture space.
11#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
12pub struct Tex;
13
14/// Texture coordinate vector. Texture coordinates can be either absolute,
15/// in range (0, 0)..(w, h) for some texture with dimensions w and h, or
16/// relative, in range (0, 0)..(1, 1), in which case they are independent
17/// of the actual dimensions of the texture.
18pub type TexCoord = Vec2<Tex>;
19
20/// A texture type. Can contain either owned or borrowed pixel data.
21///
22/// Textures are used to render *texture mapped* geometry, by interpolating
23/// texture coordinates across polygon faces. To read, or *sample*, from a
24/// `Texture`, use one of the `Sampler*` types defined in this module.
25///
26/// Multiple textures can be packed into a single larger memory buffer, often
27/// called a "texture atlas" or "sprite sheet". Each texture borrows a region
28/// of the larger buffer.
29///
30/// * TODO Mipmapping
31/// * TODO Bilinear filtering sampler
32#[derive(Copy, Clone)]
33pub struct Texture<D> {
34    w: f32,
35    h: f32,
36    data: D,
37}
38
39#[derive(Clone)]
40pub struct Atlas<C> {
41    pub layout: Layout,
42    pub texture: Texture<Buf2<C>>,
43}
44
45/// Method of arranging sub-textures in an [atlas][Atlas].
46#[derive(Copy, Clone, Debug)]
47pub enum Layout {
48    /// A regular grid of equal-sized cells.
49    Grid { sub_dims: Dims },
50}
51
52/// Returns a new texture coordinate with components `u` and `v`.
53#[inline]
54pub const fn uv(u: f32, v: f32) -> TexCoord {
55    Vector::new([u, v])
56}
57
58/// Returns a texture coordinate in a cube map.
59///
60/// A cube map texture is a composite of six subtextures in a 3x2 grid.
61/// Each subtexture corresponds to one of the six cardinal directions:
62/// right (+x), left (-x), top (+y), bottom (-y), front (+z), back (-z).
63///
64/// The subtexture is chosen based on which component of `dir` has the greatest
65/// absolute value. The texture coordinates within the subtexture are based on
66/// the zy, xz, or xy components of `pos` such that the range  [-1.0, 1.0] is
67/// transformed to the range of uv values in the appropriate subtexture.
68///
69/// ```text
70///     u
71///     0     1/3    2/3     1
72/// v 0 +------+------+------+
73///     |      |      |      |
74///     |  +x  |  +y  |  +z  |
75///   1 |      |      |      |
76///   / +--zy--+--xz--+--xy--+
77///   2 |      |      |      |
78///     |  -x  |  -y  |  -z  |
79///     |      |      |      |
80///   1 +------+------+------+
81///
82/// ```
83pub fn cube_map(pos: Vec3, dir: Normal3) -> TexCoord {
84    // -1.0..1.0 -> 0.0..1.0
85    let [x, y, z] = (0.5 * pos + splat(0.5))
86        .clamp(&splat(0.0), &splat(1.0))
87        .0;
88    // TODO implement vec::abs
89    let [ax, ay, az] = dir.map(f32::abs).0;
90
91    // TODO implement vec::argmax
92    let (max_i, mut u, mut v) = if az > ax && az > ay {
93        // xy plane
94        (2, x, y)
95    } else if ay > ax && ay > az {
96        // xz plane left-handed - mirror x
97        (1, 1.0 - x, z)
98    } else {
99        // zy plane left-handed - mirror z
100        (0, 1.0 - z, y)
101    };
102    if dir[max_i] < 0.0 {
103        u = 1.0 - u;
104        v += 1.0;
105    }
106    uv((u + max_i as f32) / 3.0, v / 2.0)
107}
108
109//
110// Inherent impls
111//
112
113impl TexCoord {
114    /// Returns the u (horizontal) component of `self`.
115    pub const fn u(&self) -> f32 {
116        self.0[0]
117    }
118    /// Returns the v (vertical) component of `self`.
119    pub const fn v(&self) -> f32 {
120        self.0[1]
121    }
122}
123
124impl<D> Texture<D> {
125    /// Returns the width of `self` as `f32`.
126    #[inline]
127    pub fn width(&self) -> f32 {
128        self.w
129    }
130    /// Returns the height of `self` as `f32`.
131    #[inline]
132    pub fn height(&self) -> f32 {
133        self.h
134    }
135    /// Returns the pixel data of `self`.
136    pub fn data(&self) -> &D {
137        &self.data
138    }
139}
140
141impl<C> Atlas<C> {
142    /// Creates a new texture atlas from a texture.
143    pub fn new(layout: Layout, texture: Texture<Buf2<C>>) -> Self {
144        Self { layout, texture }
145    }
146
147    /// Returns the top-left and bottom-right pixel coordinates
148    /// of the sub-texture with index `i`.
149    fn rect(&self, i: u32) -> [Point2u; 2] {
150        match self.layout {
151            Layout::Grid { sub_dims: (sub_w, sub_h) } => {
152                let subs_per_row = self.texture.data.width() / sub_w;
153                let top_left =
154                    pt2(i % subs_per_row * sub_w, i / subs_per_row * sub_h);
155                [top_left, top_left + vec2(sub_w as i32, sub_h as i32)]
156            }
157        }
158    }
159
160    /// Returns the sub-texture with index `i`.
161    ///
162    /// # Panics
163    /// If `i` is out of bounds.
164    // TODO Improve error reporting
165    pub fn get(&self, i: u32) -> Texture<Slice2<'_, C>> {
166        let [p0, p1] = self.rect(i);
167        self.texture.data.slice(p0..p1).into()
168    }
169
170    /// Returns the texture coordinates of the sub-texture with index `i`.
171    ///
172    /// The coordinates are the top-left, top-right, bottom-left, and
173    /// bottom-right corners of the texture, in that order.
174    ///
175    /// Note that currently this method does not check `i` is actually a valid
176    /// index and may return coordinates with values greater than one.
177    // TODO Error handling, more readable result type
178    pub fn coords(&self, i: u32) -> [TexCoord; 4] {
179        let tex_w = self.texture.width();
180        let tex_h = self.texture.height();
181        let [(x0, y0), (x1, y1)] = self
182            .rect(i)
183            .map(|p| (p.x() as f32 / tex_w, p.y() as f32 / tex_h));
184        [uv(x0, y0), uv(x1, y0), uv(x0, y1), uv(x1, y1)]
185    }
186}
187
188//
189// Trait impls
190//
191
192impl<C> From<Buf2<C>> for Texture<Buf2<C>> {
193    /// Creates a new texture from owned pixel data.
194    fn from(data: Buf2<C>) -> Self {
195        Self {
196            w: data.width() as f32,
197            h: data.height() as f32,
198            data,
199        }
200    }
201}
202
203impl<'a, C> From<Slice2<'a, C>> for Texture<Slice2<'a, C>> {
204    /// Creates a new texture from borrowed pixel data.
205    fn from(data: Slice2<'a, C>) -> Self {
206        Self {
207            w: data.width() as f32,
208            h: data.height() as f32,
209            data,
210        }
211    }
212}
213
214/// A texture sampler that repeats the texture infinitely modulo the texture
215/// dimensions. For performance reasons, `SamplerRepeatPot` only accepts
216/// textures with dimensions that are powers of two.
217#[derive(Copy, Clone, Debug)]
218pub struct SamplerRepeatPot {
219    w_mask: u32,
220    h_mask: u32,
221}
222
223impl SamplerRepeatPot {
224    /// Creates a new `SamplerRepeatPot` based on the dimensions of `tex`.
225    /// # Panics
226    /// If the width or height of `tex` is not a power of two.
227    pub fn new<C>(tex: &Texture<impl AsSlice2<C>>) -> Self {
228        let w = tex.width() as u32;
229        let h = tex.height() as u32;
230        assert!(w.is_power_of_two(), "width must be 2^n, was {w}");
231        assert!(h.is_power_of_two(), "height must be 2^n, was {h}");
232        Self { w_mask: w - 1, h_mask: h - 1 }
233    }
234
235    /// Returns the color in `tex` at `tc` in relative coordinates, such that
236    /// coordinates outside `0.0..1.0` are wrapped to the valid range.
237    ///
238    /// Uses nearest neighbor sampling.
239    pub fn sample<C: Copy>(
240        &self,
241        tex: &Texture<impl AsSlice2<C>>,
242        tc: TexCoord,
243    ) -> C {
244        let scaled_uv = uv(tex.width() * tc.u(), tex.height() * tc.v());
245        self.sample_abs(tex, scaled_uv)
246    }
247
248    /// Returns the color in `tex` at `tc` in absolute coordinates, such that
249    /// coordinates outside `0.0..tex.width()` and `0.0..tex.height()` are
250    /// wrapped to the valid range.
251    ///
252    /// Uses nearest neighbor sampling.
253    pub fn sample_abs<C: Copy>(
254        &self,
255        tex: &Texture<impl AsSlice2<C>>,
256        tc: TexCoord,
257    ) -> C {
258        use crate::math::float::f32;
259        // Convert first to signed int to avoid clamping to zero
260        let u = f32::floor(tc.u()) as i32 as u32 & self.w_mask;
261        let v = f32::floor(tc.v()) as i32 as u32 & self.h_mask;
262
263        tex.data.as_slice2()[[u, v]]
264    }
265}
266
267/// A texture sampler that clamps out-of-bounds coordinates
268/// to the nearest valid coordinate in both dimensions.
269#[derive(Copy, Clone, Debug)]
270pub struct SamplerClamp;
271
272impl SamplerClamp {
273    /// Returns the color in `tex` at `tc` such that coordinates outside
274    /// the range `0.0..1.0` are clamped to the range endpoints.
275    ///
276    /// Uses nearest neighbor sampling.
277    pub fn sample<C: Copy>(
278        &self,
279        tex: &Texture<impl AsSlice2<C>>,
280        tc: TexCoord,
281    ) -> C {
282        self.sample_abs(tex, uv(tc.u() * tex.w, tc.v() * tex.h))
283    }
284
285    /// Returns the color in `tex` at `tc` in absolute coordinates, such that
286    /// coordinates outside `0.0..tex.width()` and `0.0..tex.height()` are
287    /// clamped to the range endpoints.
288    ///
289    /// Uses nearest neighbor sampling.
290    pub fn sample_abs<C: Copy>(
291        &self,
292        tex: &Texture<impl AsSlice2<C>>,
293        tc: TexCoord,
294    ) -> C {
295        use crate::math::float::f32;
296        let u = f32::floor(tc.u().clamp(0.0, tex.w - 1.0)) as u32;
297        let v = f32::floor(tc.v().clamp(0.0, tex.h - 1.0)) as u32;
298        tex.data.as_slice2()[[u, v]]
299    }
300}
301
302/// A texture sampler that assumes all texture coordinates are within bounds.
303///
304/// Out-of-bounds coordinates may cause graphical glitches or runtime panics
305/// but not undefined behavior. In particular, if the texture data is a slice
306/// of a larger buffer, `SamplerOnce` may read out of bounds of the slice but
307/// not of the backing buffer.
308#[derive(Copy, Clone, Debug)]
309pub struct SamplerOnce;
310
311impl SamplerOnce {
312    /// Returns the color in `tex` at `tc` such that both coordinates are
313    /// assumed to be in the range `0.0..1.0`.
314    ///
315    /// Uses nearest neighbor sampling. Passing out-of-range coordinates
316    /// to this function is sound (not UB) but is not otherwise specified.
317    ///
318    /// # Panics
319    /// May panic if `tc` is not in the valid range.
320    pub fn sample<C: Copy>(
321        &self,
322        tex: &Texture<impl AsSlice2<C>>,
323        tc: TexCoord,
324    ) -> C {
325        let scaled_uv = uv(tex.width() * tc.u(), tex.height() * tc.v());
326        self.sample_abs(tex, scaled_uv)
327    }
328    /// Returns the color in `tex` at `tc` such that the coordinates are
329    /// assumed to be in the ranges `0.0..tex.width()` and `0.0..tex.height()`
330    /// respectively.
331    ///
332    /// Uses nearest neighbor sampling. Passing out-of-range coordinates
333    /// to this function is sound (not UB) but is not otherwise specified.
334    ///
335    /// # Panics
336    /// May panic if `tc` is not in the valid range.
337    pub fn sample_abs<C: Copy>(
338        &self,
339        tex: &Texture<impl AsSlice2<C>>,
340        tc: TexCoord,
341    ) -> C {
342        let u = tc.u() as u32;
343        let v = tc.v() as u32;
344
345        let d = tex.data.as_slice2();
346        debug_assert!(u < d.width(), "u={u}");
347        debug_assert!(v < d.height(), "v={v}");
348
349        d[[u, v]]
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use alloc::vec;
356
357    use crate::math::{Color3, Linear, rgb};
358    use crate::util::buf::Buf2;
359
360    use super::*;
361
362    #[rustfmt::skip]
363    fn tex() -> Texture<Buf2<Color3>> {
364        Texture::from(Buf2::new_from(
365            (2, 2), vec![
366                rgb(0xFF, 0, 0),
367                rgb(0, 0xFF, 0),
368                rgb(0, 0, 0xFF),
369                rgb(0xFF, 0xFF, 0),
370           ]
371        ))
372    }
373
374    #[test]
375    fn sampler_repeat_pot() {
376        let tex = tex();
377        let s = SamplerRepeatPot::new(&tex);
378
379        assert_eq!(s.sample(&tex, uv(-0.1, 0.0)), rgb(0, 0xFF, 0));
380        assert_eq!(s.sample(&tex, uv(0.0, -0.1)), rgb(0, 0, 0xFF));
381
382        assert_eq!(s.sample(&tex, uv(1.0, 0.0)), rgb(0xFF, 0, 0));
383        assert_eq!(s.sample(&tex, uv(0.0, 1.0)), rgb(0xFF, 0, 0));
384
385        assert_eq!(s.sample(&tex, uv(4.8, 0.2)), rgb(0, 0xFF, 0));
386        assert_eq!(s.sample(&tex, uv(0.2, 4.8)), rgb(0, 0, 0xFF));
387    }
388
389    #[test]
390    fn sampler_clamp() {
391        let tex = tex();
392        let s = SamplerClamp;
393
394        assert_eq!(s.sample(&tex, uv(-1.0, 0.0)), rgb(0xFF, 0, 0));
395        assert_eq!(s.sample(&tex, uv(0.0, -1.0)), rgb(0xFF, 0, 0));
396
397        assert_eq!(s.sample(&tex, uv(1.5, 0.0)), rgb(0, 0xFF, 0));
398        assert_eq!(s.sample(&tex, uv(0.0, 1.5)), rgb(0, 0, 0xFF));
399
400        assert_eq!(s.sample(&tex, uv(1.5, 1.5)), rgb(0xFF, 0xFF, 0));
401    }
402
403    #[test]
404    fn sampler_once() {
405        let tex = tex();
406        let s = SamplerOnce;
407
408        assert_eq!(s.sample(&tex, uv(0.0, 0.0)), rgb(0xFF, 0, 0));
409        assert_eq!(s.sample(&tex, uv(0.5, 0.0)), rgb(0, 0xFF, 0));
410        assert_eq!(s.sample(&tex, uv(0.0, 0.5)), rgb(0, 0, 0xFF));
411        assert_eq!(s.sample(&tex, uv(0.5, 0.5)), rgb(0xFF, 0xFF, 0));
412    }
413
414    #[test]
415    fn cube_mapping() {
416        let zero = Vec3::zero();
417        let tc = cube_map(zero, Vec3::X);
418        assert_eq!(tc, uv(1.0 / 6.0, 0.25));
419        let tc = cube_map(zero, -Vec3::X);
420        assert_eq!(tc, uv(1.0 / 6.0, 0.75));
421
422        let tc = cube_map(zero, Vec3::Y);
423        assert_eq!(tc, uv(3.0 / 6.0, 0.25));
424        let tc = cube_map(zero, -Vec3::Y);
425        assert_eq!(tc, uv(3.0 / 6.0, 0.75));
426
427        let tc = cube_map(zero, Vec3::Z);
428        assert_eq!(tc, uv(5.0 / 6.0, 0.25));
429        let tc = cube_map(zero, -Vec3::Z);
430        assert_eq!(tc, uv(5.0 / 6.0, 0.75));
431    }
432}