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}