Skip to main content

embedded_3dgfx/
texture.rs

1//! Texture mapping support for embedded 3D graphics
2//!
3//! This module provides texture storage, sampling, and management for UV-mapped
4//! 3D rendering. It uses static texture data and power-of-2 dimensions for
5//! efficient wrapping without divisions.
6
7use embedded_graphics_core::pixelcolor::Rgb565;
8use heapless::Vec as HeaplessVec;
9
10/// A 2D texture with RGB565 pixel data
11///
12/// Textures must have power-of-2 dimensions (8, 16, 32, 64, 128, 256, etc.)
13/// for efficient wrapping using bit masks instead of modulo operations.
14#[derive(Debug, Clone, Copy)]
15pub struct Texture {
16    /// Texture pixel data in RGB565 format
17    pub data: &'static [Rgb565],
18    /// Width of the texture (must be power of 2)
19    pub width: u32,
20    /// Height of the texture (must be power of 2)
21    pub height: u32,
22    /// Bit mask for wrapping width (width - 1)
23    width_mask: u32,
24    /// Bit mask for wrapping height (height - 1)
25    height_mask: u32,
26}
27
28impl Texture {
29    /// Create a new texture
30    ///
31    /// # Arguments
32    /// * `data` - Static RGB565 pixel array (must be width × height elements)
33    /// * `width` - Texture width (must be power of 2)
34    /// * `height` - Texture height (must be power of 2)
35    ///
36    /// # Panics
37    /// Panics if width or height is not a power of 2, or if data length doesn't match dimensions
38    pub fn new(data: &'static [Rgb565], width: u32, height: u32) -> Self {
39        assert!(width.is_power_of_two(), "Texture width must be power of 2");
40        assert!(
41            height.is_power_of_two(),
42            "Texture height must be power of 2"
43        );
44        assert_eq!(
45            data.len(),
46            (width * height) as usize,
47            "Texture data length must match width × height"
48        );
49
50        Self {
51            data,
52            width,
53            height,
54            width_mask: width - 1,
55            height_mask: height - 1,
56        }
57    }
58
59    /// Sample the texture at UV coordinates
60    ///
61    /// Uses nearest-neighbor sampling with wrapping (repeat mode).
62    /// UV coordinates are in the range [0.0, 1.0] where:
63    /// - (0, 0) is the top-left corner
64    /// - (1, 1) is the bottom-right corner
65    ///
66    /// # Arguments
67    /// * `u` - Horizontal texture coordinate (0.0-1.0+, wraps)
68    /// * `v` - Vertical texture coordinate (0.0-1.0+, wraps)
69    #[inline]
70    pub fn sample(&self, u: f32, v: f32) -> Rgb565 {
71        // Convert UV [0.0, 1.0] to texture coordinates [0, width/height)
72        let tex_x = (u * self.width as f32) as u32;
73        let tex_y = (v * self.height as f32) as u32;
74
75        // Wrap coordinates using bit masks (fast for power-of-2 dimensions)
76        let tex_x = tex_x & self.width_mask;
77        let tex_y = tex_y & self.height_mask;
78
79        // Lookup pixel
80        self.data[(tex_y * self.width + tex_x) as usize]
81    }
82
83    /// Sample the texture at UV coordinates (integer version for performance)
84    ///
85    /// Uses fixed-point UV coordinates (16.16 format) for faster inner loops.
86    ///
87    /// # Arguments
88    /// * `u_fixed` - Horizontal texture coordinate in 16.16 fixed-point
89    /// * `v_fixed` - Vertical texture coordinate in 16.16 fixed-point
90    #[inline]
91    pub fn sample_fixed(&self, u_fixed: u32, v_fixed: u32) -> Rgb565 {
92        // Convert from 16.16 fixed-point to texture coordinates
93        // Shift right by 16 to get integer part, then multiply by width/height
94        let tex_x = ((u_fixed >> 16) * self.width) >> 16;
95        let tex_y = ((v_fixed >> 16) * self.height) >> 16;
96
97        // Wrap coordinates
98        let tex_x = tex_x & self.width_mask;
99        let tex_y = tex_y & self.height_mask;
100
101        self.data[(tex_y * self.width + tex_x) as usize]
102    }
103
104    /// Get texture dimensions
105    pub fn dimensions(&self) -> (u32, u32) {
106        (self.width, self.height)
107    }
108}
109
110/// Texture manager for storing multiple textures
111///
112/// Uses a fixed-size heapless vector for no_std compatibility.
113/// The capacity N determines how many textures can be stored.
114pub struct TextureManager<const N: usize> {
115    textures: HeaplessVec<Texture, N>,
116}
117
118impl<const N: usize> TextureManager<N> {
119    /// Create a new empty texture manager
120    pub fn new() -> Self {
121        Self {
122            textures: HeaplessVec::new(),
123        }
124    }
125
126    /// Add a texture to the manager
127    ///
128    /// Returns the texture ID (index) that can be used to reference this texture.
129    ///
130    /// # Returns
131    /// `Some(texture_id)` if successful, `None` if the manager is full
132    pub fn add_texture(&mut self, texture: Texture) -> Option<u32> {
133        self.textures.push(texture).ok()?;
134        Some((self.textures.len() - 1) as u32)
135    }
136
137    /// Get a texture by ID
138    ///
139    /// # Arguments
140    /// * `id` - Texture ID returned by `add_texture()`
141    ///
142    /// # Returns
143    /// `Some(&Texture)` if the ID is valid, `None` otherwise
144    pub fn get(&self, id: u32) -> Option<&Texture> {
145        self.textures.get(id as usize)
146    }
147
148    /// Get the number of stored textures
149    pub fn len(&self) -> usize {
150        self.textures.len()
151    }
152
153    /// Check if the manager is empty
154    pub fn is_empty(&self) -> bool {
155        self.textures.is_empty()
156    }
157
158    /// Check if the manager is full
159    pub fn is_full(&self) -> bool {
160        self.textures.len() >= N
161    }
162}
163
164impl<const N: usize> Default for TextureManager<N> {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    extern crate std;
173    use super::*;
174    use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
175
176    #[test]
177    fn test_texture_creation() {
178        static DATA: [Rgb565; 64] = [Rgb565::CSS_RED; 64];
179        let texture = Texture::new(&DATA, 8, 8);
180
181        assert_eq!(texture.width, 8);
182        assert_eq!(texture.height, 8);
183        assert_eq!(texture.dimensions(), (8, 8));
184    }
185
186    #[test]
187    #[should_panic(expected = "width must be power of 2")]
188    fn test_texture_non_power_of_2_width() {
189        static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
190        let _texture = Texture::new(&DATA, 10, 6); // 10 is not power of 2
191    }
192
193    #[test]
194    #[should_panic(expected = "height must be power of 2")]
195    fn test_texture_non_power_of_2_height() {
196        static DATA: [Rgb565; 48] = [Rgb565::CSS_RED; 48];
197        let _texture = Texture::new(&DATA, 8, 6); // 6 is not power of 2
198    }
199
200    #[test]
201    #[should_panic(expected = "length must match")]
202    fn test_texture_wrong_data_length() {
203        static DATA: [Rgb565; 60] = [Rgb565::CSS_RED; 60];
204        let _texture = Texture::new(&DATA, 8, 8); // Should be 64 elements
205    }
206
207    #[test]
208    fn test_texture_sampling() {
209        static DATA: [Rgb565; 16] = [
210            Rgb565::CSS_RED,
211            Rgb565::CSS_GREEN,
212            Rgb565::CSS_BLUE,
213            Rgb565::CSS_YELLOW,
214            Rgb565::CSS_CYAN,
215            Rgb565::CSS_MAGENTA,
216            Rgb565::CSS_WHITE,
217            Rgb565::CSS_BLACK,
218            Rgb565::CSS_RED,
219            Rgb565::CSS_GREEN,
220            Rgb565::CSS_BLUE,
221            Rgb565::CSS_YELLOW,
222            Rgb565::CSS_CYAN,
223            Rgb565::CSS_MAGENTA,
224            Rgb565::CSS_WHITE,
225            Rgb565::CSS_BLACK,
226        ];
227
228        let texture = Texture::new(&DATA, 4, 4);
229
230        // Sample at corners
231        let tl = texture.sample(0.0, 0.0);
232        assert_eq!(tl, Rgb565::CSS_RED);
233
234        // Sample in middle (0.5, 0.5) -> (2, 2) -> index 10
235        let mid = texture.sample(0.5, 0.5);
236        assert_eq!(mid, Rgb565::CSS_BLUE);
237    }
238
239    #[test]
240    fn test_texture_wrapping() {
241        static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
242        let texture = Texture::new(&DATA, 4, 4);
243
244        // Sample beyond 1.0 should wrap
245        let wrapped = texture.sample(1.5, 1.5);
246        assert_eq!(wrapped, Rgb565::CSS_RED);
247    }
248
249    #[test]
250    fn test_texture_manager() {
251        static DATA1: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
252        static DATA2: [Rgb565; 64] = [Rgb565::CSS_GREEN; 64];
253
254        let mut manager = TextureManager::<4>::new();
255
256        assert!(manager.is_empty());
257        assert!(!manager.is_full());
258
259        let id1 = manager.add_texture(Texture::new(&DATA1, 4, 4));
260        assert_eq!(id1, Some(0));
261        assert_eq!(manager.len(), 1);
262
263        let id2 = manager.add_texture(Texture::new(&DATA2, 8, 8));
264        assert_eq!(id2, Some(1));
265        assert_eq!(manager.len(), 2);
266
267        // Retrieve textures
268        let tex1 = manager.get(0).unwrap();
269        assert_eq!(tex1.width, 4);
270
271        let tex2 = manager.get(1).unwrap();
272        assert_eq!(tex2.width, 8);
273    }
274
275    #[test]
276    fn test_texture_manager_full() {
277        static DATA: [Rgb565; 16] = [Rgb565::CSS_RED; 16];
278
279        let mut manager = TextureManager::<2>::new();
280
281        // Fill the manager
282        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
283        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_some());
284        assert!(manager.is_full());
285
286        // Try to add one more (should fail)
287        assert!(manager.add_texture(Texture::new(&DATA, 4, 4)).is_none());
288    }
289}