Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/texture.rs
StratoSDK / crates / strato-renderer / src / texture.rs
1//! Texture management and atlas
2 
3use dashmap::DashMap;
4use image::{DynamicImage, ImageBuffer, Rgba};
5use std::sync::Arc;
6 
7/// Texture wrapper
8pub struct Texture {
9 texture: wgpu::Texture,
10 view: wgpu::TextureView,
11 sampler: wgpu::Sampler,
12 size: (u32, u32),
13}
14 
15impl Texture {
16 /// Create a new texture
17 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, image: &DynamicImage) -> Self {
18 let rgba = image.to_rgba8();
19 let size = (image.width(), image.height());
20 
21 let texture = device.create_texture(&wgpu::TextureDescriptor {
22 label: Some("Texture"),
23 size: wgpu::Extent3d {
24 width: size.0,
25 height: size.1,
26 depth_or_array_layers: 1,
27 },
28 mip_level_count: 1,
29 sample_count: 1,
30 dimension: wgpu::TextureDimension::D2,
31 format: wgpu::TextureFormat::Rgba8UnormSrgb,
32 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
33 view_formats: &[],
34 });
35 
36 queue.write_texture(
37 wgpu::ImageCopyTexture {
38 texture: &texture,
39 mip_level: 0,
40 origin: wgpu::Origin3d::ZERO,
41 aspect: wgpu::TextureAspect::All,
42 },
43 &rgba,
44 wgpu::ImageDataLayout {
45 offset: 0,
46 bytes_per_row: Some(4 * size.0),
47 rows_per_image: Some(size.1),
48 },
49 wgpu::Extent3d {
50 width: size.0,
51 height: size.1,
52 depth_or_array_layers: 1,
53 },
54 );
55 
56 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
57 
58 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
59 address_mode_u: wgpu::AddressMode::ClampToEdge,
60 address_mode_v: wgpu::AddressMode::ClampToEdge,
61 address_mode_w: wgpu::AddressMode::ClampToEdge,
62 mag_filter: wgpu::FilterMode::Linear,
63 min_filter: wgpu::FilterMode::Linear,
64 mipmap_filter: wgpu::FilterMode::Nearest,
65 ..Default::default()
66 });
67 
68 Self {
69 texture,
70 view,
71 sampler,
72 size,
73 }
74 }
75 
76 /// Create a white texture (for untextured rendering)
77 pub fn white(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
78 let white_pixel = ImageBuffer::<Rgba<u8>, _>::from_raw(1, 1, vec![255u8; 4]).unwrap();
79 let image = DynamicImage::ImageRgba8(white_pixel);
80 Self::new(device, queue, &image)
81 }
82 
83 /// Get texture view
84 pub fn view(&self) -> &wgpu::TextureView {
85 &self.view
86 }
87 
88 /// Get sampler
89 pub fn sampler(&self) -> &wgpu::Sampler {
90 &self.sampler
91 }
92 
93 /// Get size
94 pub fn size(&self) -> (u32, u32) {
95 self.size
96 }
97}
98 
99/// Texture atlas for efficient texture management
100pub struct TextureAtlas {
101 texture: Arc<Texture>,
102 regions: DashMap<String, AtlasRegion>,
103 next_position: parking_lot::Mutex<(u32, u32)>,
104 row_height: parking_lot::Mutex<u32>,
105 size: (u32, u32),
106}
107 
108/// Region within a texture atlas
109#[derive(Debug, Clone, Copy)]
110pub struct AtlasRegion {
111 pub x: u32,
112 pub y: u32,
113 pub width: u32,
114 pub height: u32,
115 pub tex_coords: TexCoords,
116}
117 
118/// Texture coordinates
119#[derive(Debug, Clone, Copy)]
120pub struct TexCoords {
121 pub min_u: f32,
122 pub min_v: f32,
123 pub max_u: f32,
124 pub max_v: f32,
125}
126 
127impl TextureAtlas {
128 /// Create a new texture atlas
129 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, size: (u32, u32)) -> Self {
130 let empty_image =
131 ImageBuffer::<Rgba<u8>, _>::from_fn(size.0, size.1, |_, _| Rgba([0, 0, 0, 0]));
132 let image = DynamicImage::ImageRgba8(empty_image);
133 let texture = Arc::new(Texture::new(device, queue, &image));
134 
135 Self {
136 texture,
137 regions: DashMap::new(),
138 next_position: parking_lot::Mutex::new((0, 0)),
139 row_height: parking_lot::Mutex::new(0),
140 size,
141 }
142 }
143 
144 /// Add an image to the atlas
145 pub fn add_image(
146 &self,
147 queue: &wgpu::Queue,
148 name: String,
149 image: &DynamicImage,
150 ) -> Option<AtlasRegion> {
151 let img_size = (image.width(), image.height());
152 
153 let mut next_pos = self.next_position.lock();
154 let mut row_height = self.row_height.lock();
155 
156 // Check if we need to move to next row
157 if next_pos.0 + img_size.0 > self.size.0 {
158 next_pos.0 = 0;
159 next_pos.1 += *row_height;
160 *row_height = 0;
161 }
162 
163 // Check if image fits in atlas
164 if next_pos.1 + img_size.1 > self.size.1 {
165 return None; // Atlas is full
166 }
167 
168 let region = AtlasRegion {
169 x: next_pos.0,
170 y: next_pos.1,
171 width: img_size.0,
172 height: img_size.1,
173 tex_coords: TexCoords {
174 min_u: next_pos.0 as f32 / self.size.0 as f32,
175 min_v: next_pos.1 as f32 / self.size.1 as f32,
176 max_u: (next_pos.0 + img_size.0) as f32 / self.size.0 as f32,
177 max_v: (next_pos.1 + img_size.1) as f32 / self.size.1 as f32,
178 },
179 };
180 
181 // Write image data to texture
182 let rgba = image.to_rgba8();
183 queue.write_texture(
184 wgpu::ImageCopyTexture {
185 texture: &self.texture.texture,
186 mip_level: 0,
187 origin: wgpu::Origin3d {
188 x: region.x,
189 y: region.y,
190 z: 0,
191 },
192 aspect: wgpu::TextureAspect::All,
193 },
194 &rgba,
195 wgpu::ImageDataLayout {
196 offset: 0,
197 bytes_per_row: Some(4 * img_size.0),
198 rows_per_image: Some(img_size.1),
199 },
200 wgpu::Extent3d {
201 width: img_size.0,
202 height: img_size.1,
203 depth_or_array_layers: 1,
204 },
205 );
206 
207 // Update position for next image
208 next_pos.0 += img_size.0;
209 *row_height = (*row_height).max(img_size.1);
210 
211 self.regions.insert(name, region);
212 Some(region)
213 }
214 
215 /// Get a region by name
216 pub fn get_region(&self, name: &str) -> Option<AtlasRegion> {
217 self.regions.get(name).map(|r| *r)
218 }
219 
220 /// Get the atlas texture
221 pub fn texture(&self) -> &Texture {
222 &self.texture
223 }
224 
225 /// Clear the atlas
226 pub fn clear(&self) {
227 self.regions.clear();
228 *self.next_position.lock() = (0, 0);
229 *self.row_height.lock() = 0;
230 }
231}
232