est_render/gpu/texture/
atlas.rs

1use std::collections::HashMap;
2
3use crate::{math::{Point2, RectF}, utils::ArcRef};
4
5use super::{
6    super::GPUInner,
7    Texture,
8    TextureError,
9    TextureBuilder,
10    TextureUsage,
11    TextureFormat,
12};
13
14/// Represents a texture atlas containing multiple textures
15/// and their UV coordinates
16#[derive(Debug, Clone)]
17pub struct TextureAtlas {
18    pub(crate) texture: Texture,
19    pub(crate) items: HashMap<String, TextureAtlasCoord>,
20}
21
22#[derive(Debug, Clone)]
23pub(crate) struct TextureAtlasCoord {
24    pub rect_uv: RectF,
25    pub size: Point2,
26}
27
28impl TextureAtlas {
29    pub(crate) fn new(texture: Texture, items: HashMap<String, TextureAtlasCoord>) -> Self {
30        Self { texture, items }
31    }
32
33    /// Retrieves the UV rectangle and size for a given texture ID
34    pub fn get_id(&self, id: &str) -> Option<(RectF, Point2)> {
35        self.items.get(id).map(|coord| (coord.rect_uv, coord.size))
36    }
37
38    /// Get the texture associated with this atlas
39    pub fn get_texture(&self) -> &Texture {
40        &self.texture
41    }
42
43    /// Get the size of the texture atlas
44    pub fn get_texture_size(&self) -> Point2 {
45        let inner = self.texture.inner.borrow();
46
47        Point2::new(inner.size.x as i32, inner.size.y as i32)
48    }
49}
50
51const MAX_WIDTH_SIZE: i32 = 2048;
52
53#[derive(Debug, Clone)]
54pub struct TextureAtlasBuilder {
55    pub(crate) gpu: ArcRef<GPUInner>,
56    pub(crate) items: HashMap<String, ItemQueue>,
57}
58
59#[derive(Debug, Clone)]
60pub(crate) enum ItemQueue {
61    File(String),           // id, file path
62    Memory(Vec<u8>),        // id, raw data
63    Raw(Vec<u8>, u32, u32), // id, raw data, width, height
64}
65
66#[derive(Debug, Clone)]
67pub enum TextureAtlasBuilderError {
68    EmptyAtlas,
69    ExceedsMaxSize(i32, i32),
70    FileNotFound(String),
71    InvalidData(String),
72    TextureCreationError(TextureError),
73}
74
75impl std::fmt::Display for TextureAtlasBuilderError {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            TextureAtlasBuilderError::EmptyAtlas => write!(f, "Texture atlas cannot be empty"),
79            TextureAtlasBuilderError::ExceedsMaxSize(width, height) => write!(
80                f,
81                "Texture atlas exceeds maximum size: {}x{}",
82                width, height
83            ),
84            TextureAtlasBuilderError::FileNotFound(file) => {
85                write!(f, "Texture file not found: {}", file)
86            }
87            TextureAtlasBuilderError::InvalidData(id) => {
88                write!(f, "Invalid texture data for id: {}", id)
89            }
90            TextureAtlasBuilderError::TextureCreationError(err) => {
91                write!(f, "Texture creation error: {}", err)
92            }
93        }
94    }
95}
96
97impl TextureAtlasBuilder {
98    pub(crate) fn new(gpu: ArcRef<GPUInner>) -> Self {
99        Self {
100            items: HashMap::new(),
101            gpu,
102        }
103    }
104
105    pub fn add_texture_file(mut self, id: &str, file: &str) -> Self {
106        self.items
107            .insert(id.to_string(), ItemQueue::File(file.to_string()));
108        self
109    }
110
111    pub fn add_texture_file_buf(mut self, id: &str, data: &[u8]) -> Self {
112        self.items
113            .insert(id.to_string(), ItemQueue::Memory(data.to_vec()));
114        self
115    }
116
117    pub fn add_texture_raw(mut self, id: &str, data: Vec<u8>, width: u32, height: u32) -> Self {
118        self.items
119            .insert(id.to_string(), ItemQueue::Raw(data, width, height));
120        self
121    }
122
123    pub fn build(self) -> Result<TextureAtlas, TextureAtlasBuilderError> {
124        if self.items.is_empty() {
125            return Err(TextureAtlasBuilderError::EmptyAtlas);
126        }
127
128        let mut texture_items = HashMap::new();
129
130        for (id, item) in self.items {
131            use image::GenericImageView;
132
133            let (texture_data, size) = match item {
134                ItemQueue::File(file) => {
135                    if !std::path::Path::new(&file).exists() {
136                        return Err(TextureAtlasBuilderError::FileNotFound(file));
137                    }
138
139                    let canonical_path = std::fs::canonicalize(&file)
140                        .map_err(|_| TextureAtlasBuilderError::FileNotFound(file.clone()))?;
141
142                    let image = image::open(&canonical_path)
143                        .map_err(|_| TextureAtlasBuilderError::InvalidData(file.clone()))?;
144
145                    let (width, height) = image.dimensions();
146                    let data = image.to_rgba8();
147
148                    (data.to_vec(), Point2::new(width as i32, height as i32))
149                }
150                ItemQueue::Memory(data) => {
151                    let image = image::load_from_memory(&data)
152                        .map_err(|_| TextureAtlasBuilderError::InvalidData(id.clone()))?;
153
154                    let (width, height) = image.dimensions();
155                    let data = image.to_rgba8();
156
157                    (data.to_vec(), Point2::new(width as i32, height as i32))
158                }
159                ItemQueue::Raw(data, width, height) => {
160                    if data.len() != (width * height * 4) as usize {
161                        return Err(TextureAtlasBuilderError::InvalidData(id.clone()));
162                    }
163
164                    let size = Point2::new(width as i32, height as i32);
165                    (data, size)
166                }
167            };
168
169            texture_items.insert(id.to_string(), (texture_data, size));
170        }
171
172        let rect_config = rect_packer::Config {
173            width: MAX_WIDTH_SIZE as i32,
174            height: MAX_WIDTH_SIZE as i32,
175            border_padding: 1,
176            rectangle_padding: 1,
177        };
178
179        let mut packer = rect_packer::Packer::new(rect_config);
180        let mut placemenets = HashMap::new();
181        let mut atlas_size = Point2::new(0, 0);
182
183        for (id, (_, size)) in &texture_items {
184            if size.x > MAX_WIDTH_SIZE || size.y > MAX_WIDTH_SIZE {
185                return Err(TextureAtlasBuilderError::ExceedsMaxSize(
186                    size.x,
187                    size.y,
188                ));
189            }
190
191            let rect = packer.pack(size.x, size.y, false)
192                .ok_or_else(|| {
193                TextureAtlasBuilderError::InvalidData(format!(
194                    "Failed to pack texture with id: {}",
195                    id
196                ))
197            })?;
198
199            placemenets.insert(id.clone(), rect);
200            atlas_size.x = atlas_size.x.max(rect.x + rect.width);
201            atlas_size.y = atlas_size.y.max(rect.y + rect.height);
202        }
203
204        if atlas_size.x > MAX_WIDTH_SIZE || atlas_size.y > MAX_WIDTH_SIZE {
205            return Err(TextureAtlasBuilderError::ExceedsMaxSize(atlas_size.x, atlas_size.y));
206        }
207
208        let mut texture_data = vec![0; (atlas_size.x * atlas_size.y * 4) as usize];
209        let mut items = HashMap::new();
210        for (id, rect) in placemenets {
211            let (data, size) = texture_items.get(&id).ok_or_else(|| {
212                TextureAtlasBuilderError::InvalidData(format!("Missing data for id: {}", id))
213            })?;
214
215            let atlas_w = atlas_size.x as f32;
216            let atlas_h = atlas_size.y as f32;
217            let half_texel_x = 0.5 / atlas_w;
218            let half_texel_y = 0.5 / atlas_h;
219
220            let rect_uv = RectF::new(
221                (rect.x as f32 + half_texel_x) / atlas_w,
222                (rect.y as f32 + half_texel_y) / atlas_h,
223                (rect.x as f32 + rect.width as f32 - half_texel_x) / atlas_w,
224                (rect.y as f32 + rect.height as f32 - half_texel_y) / atlas_h,
225            );
226
227            let size = Point2::new(size.x, size.y);
228
229            for j in 0..size.y {
230                for i in 0..size.x {
231                    let src_index = ((j * size.x + i) * 4) as usize;
232                    let dst_index = (((rect.y + j) * atlas_size.x + (rect.x + i)) * 4) as usize;
233
234                    texture_data[dst_index..dst_index + 4]
235                        .copy_from_slice(&data[src_index..src_index + 4]);
236                }
237            }
238
239            items.insert(
240                id,
241                TextureAtlasCoord {
242                    rect_uv,
243                    size,
244                },
245            );
246        }
247
248        let format = if self.gpu.borrow().is_srgb() {
249            TextureFormat::Rgba8UnormSrgb
250        } else {
251            TextureFormat::Rgba8Unorm
252        };
253
254        let texture = TextureBuilder::new(self.gpu)
255            .set_raw_image(&texture_data, atlas_size, format)
256            .set_usage(TextureUsage::Sampler)
257            .build()
258            .map_err(TextureAtlasBuilderError::TextureCreationError)?;
259
260        Ok(TextureAtlas::new(texture, items))
261    }
262}