use std::error::Error; use std::num::NonZeroU32; use std::sync::Arc; use alligator_resources::texture::{LoadError, Rgba16Texture, TextureId, TextureManager}; use image::{EncodableLayout, GenericImage, RgbaImage}; use texture_packer::TexturePacker; use texture_packer::{ exporter::{ExportResult, ImageExporter}, TexturePackerConfig, }; use thiserror::Error; /// The texture did not fit in the texture atlas #[derive(Debug, Error)] #[error("{:?}", .0)] pub struct PackError(PackErrorInternal); // TODO this can be removed when a new texture packer is made type PackErrorInternal = impl std::fmt::Debug; #[derive(Error, Debug)] pub enum TextureError { #[error("{:?}", .0)] TextureTooLarge(#[from] PackError), #[error("{}", .0)] BadImage(#[from] LoadError), #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)] Unexpected(#[source] Box), } /// Simpler constructor for a wgpu extent3d const fn extent_3d(width: u32, height: u32) -> wgpu::Extent3d { wgpu::Extent3d { width, height, depth_or_array_layers: 1, } } /// A texture atlas, usable by the renderer // TODO make this Debug // TODO make these resizable pub struct TextureAtlas { textures: Arc, packer: TexturePacker<'static, Rgba16Texture, TextureId>, diffuse_texture: wgpu::Texture, diffuse_bind_group: wgpu::BindGroup, image: RgbaImage, width: u32, height: u32, changed: bool, } macro_rules! texture_info { ($name: ident, $prop: ident, $divisor: ident) => { pub fn $name(&mut self, id: TextureId) -> Result { let frame = match self.texture_frame(id) { Some(frame) => frame, None => { self.load_texture(id)?; self.texture_frame(id).unwrap() } }; let property = frame.frame.$prop; let value = property as f32 / self.$divisor as f32; Ok(value) } }; } impl TextureAtlas { /// Creates a new texture atlas, with the given size // TODO why is this u32? // TODO this is still too large pub fn new( device: &wgpu::Device, textures: Arc, width: u32, height: u32, ) -> (Self, wgpu::BindGroupLayout) { let atlas_size = extent_3d(width, height); let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Diffuse Texture"), size: atlas_size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], }); // TODO I don't think this refreshes anything let diffuse_texture_view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Texture Bind Group Layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }); let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Diffuse Bind Group"), layout: &texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&diffuse_sampler), }, ], }); ( Self { textures, packer: TexturePacker::new_skyline(TexturePackerConfig { max_width: width, max_height: height, allow_rotation: false, trim: false, texture_padding: 0, ..Default::default() }), diffuse_texture, diffuse_bind_group, width, height, image: RgbaImage::from_raw( width, height, vec![0; 4 * width as usize * height as usize], ) .unwrap(), changed: true, }, texture_bind_group_layout, ) } /// get the bind group for the texture pub(crate) const fn bind_group(&self) -> &wgpu::BindGroup { &self.diffuse_bind_group } /// Load a new subtexture from memory pub fn load_texture(&mut self, id: TextureId) -> Result { self.changed = true; let img = self.textures.load_texture(id)?; self.packer.pack_own(id, img).map_err(PackError)?; Ok(id) } /// Get the frame for s particular subtexture fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame> { self.packer.get_frame(&id) } texture_info!(texture_width, w, width); texture_info!(texture_height, h, height); texture_info!(texture_x, x, width); texture_info!(texture_y, y, height); /// Fill the cached image fn fill_image(&mut self) -> ExportResult<()> { let atlas = { profiling::scope!("export atlas"); ImageExporter::export(&self.packer)? }; profiling::scope!("copy image"); self.image .copy_from(&atlas, 0, 0) .expect("image cache is too small"); Ok(()) } /// Clear the texture atlas, and give it a new size pub fn clear(&mut self, width: u32, height: u32) { self.changed = true; self.width = width; self.height = height; self.packer = TexturePacker::new_skyline(TexturePackerConfig { max_width: self.width, max_height: self.height, ..Default::default() }); } /// Fill the GPU texture atlas #[profiling::function] pub(crate) fn fill_textures(&mut self, queue: &wgpu::Queue) { // saves time if nothing changed since the last time we did this // FIXME This doesn't do much good once we get procedurally generated animation // We'll have to create our own texture packer, with mutable subtextures, // and more efficient algorithms. This'll also make frame times more consistent if !self.changed { return; } let atlas_size = extent_3d(self.width, self.height); // put the packed texture into the base image if let Err(e) = self.fill_image() { log::error!("{}", e); } // copy that to the gpu queue.write_texture( wgpu::ImageCopyTexture { texture: &self.diffuse_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, self.image.as_bytes(), wgpu::ImageDataLayout { offset: 0, bytes_per_row: NonZeroU32::new(atlas_size.width * 4), rows_per_image: NonZeroU32::new(atlas_size.height), }, atlas_size, ); self.changed = false; } }