StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Texture management and atlas |
| 2 | |
| 3 | use dashmap::DashMap; |
| 4 | use image::{DynamicImage, ImageBuffer, Rgba}; |
| 5 | use std::sync::Arc; |
| 6 | |
| 7 | /// Texture wrapper |
| 8 | pub struct Texture { |
| 9 | texture: wgpu::Texture, |
| 10 | view: wgpu::TextureView, |
| 11 | sampler: wgpu::Sampler, |
| 12 | size: (u32, u32), |
| 13 | } |
| 14 | |
| 15 | impl 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 |
| 100 | pub 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)] |
| 110 | pub 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)] |
| 120 | pub struct TexCoords { |
| 121 | pub min_u: f32, |
| 122 | pub min_v: f32, |
| 123 | pub max_u: f32, |
| 124 | pub max_v: f32, |
| 125 | } |
| 126 | |
| 127 | impl 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 |