diff options
Diffstat (limited to 'render/src/texture.rs')
| -rw-r--r-- | render/src/texture.rs | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/render/src/texture.rs b/render/src/texture.rs new file mode 100644 index 0000000..76e77a8 --- /dev/null +++ b/render/src/texture.rs @@ -0,0 +1,250 @@ +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<dyn Error>), +} + +/// 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<TextureManager>, + 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<f32, TextureError> { + 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<TextureManager>, + 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<TextureId, TextureError> { + 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<TextureId>> { + 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; + } +} |
