diff options
| author | Micha White <botahamec@outlook.com> | 2022-10-20 20:39:44 -0400 |
|---|---|---|
| committer | Micha White <botahamec@outlook.com> | 2022-10-20 20:39:44 -0400 |
| commit | 93347346e8bd8f7412ae03a0858dd307a1df2e0d (patch) | |
| tree | 17805956857c76b5fed3f47a821fcdf6141cf7a6 /alligator_render/src/texture.rs | |
| parent | e337741969160603f06a7f2b30cda375eeef99fb (diff) | |
Moved files into workspace
Diffstat (limited to 'alligator_render/src/texture.rs')
| -rw-r--r-- | alligator_render/src/texture.rs | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/alligator_render/src/texture.rs b/alligator_render/src/texture.rs new file mode 100644 index 0000000..e343508 --- /dev/null +++ b/alligator_render/src/texture.rs @@ -0,0 +1,287 @@ +use std::error::Error; +use std::num::NonZeroU32; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use image::error::DecodingError; +use image::{EncodableLayout, GenericImage, ImageError, RgbaImage}; +use texture_packer::TexturePacker; +use texture_packer::{ + exporter::{ExportResult, ImageExporter}, + TexturePackerConfig, +}; +use thiserror::Error; + +static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); + +/// The unique ID for a subtexture +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TextureId(usize); + +impl TextureId { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +/// These are the formats supported by the renderer. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ImageFormat { + Bmp, + Ico, + Farbfeld, +} + +impl ImageFormat { + const fn format(self) -> image::ImageFormat { + match self { + Self::Bmp => image::ImageFormat::Bmp, + Self::Ico => image::ImageFormat::Ico, + Self::Farbfeld => image::ImageFormat::Farbfeld, + } + } +} + +/// 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( + #[source] + #[from] + PackError, + ), + #[error("{}", .0)] + BadImage( + #[source] + #[from] + DecodingError, + ), // TODO don't export this + #[error("Unexpected Error (this is a bug in alligator_render): {}", .0)] + Unexpected(#[source] Box<dyn Error>), +} + +impl From<ImageError> for TextureError { + fn from(ie: ImageError) -> Self { + match ie { + ImageError::Decoding(de) => de.into(), + _ => Self::Unexpected(Box::new(ie)), + } + } +} + +/// 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 { + packer: TexturePacker<'static, image::RgbaImage, 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(&self, id: TextureId) -> Option<f32> { + let frame = self.texture_frame(id)?; + let property = frame.frame.$prop; + let value = property as f32 / self.$divisor as f32; + Some(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, 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::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + }); + + // 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 { + 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 + // TODO support RGBA16 + pub fn load_from_memory( + &mut self, + buf: &[u8], + format: ImageFormat, + ) -> Result<TextureId, TextureError> { + let img = image::load_from_memory_with_format(buf, format.format())?.into_rgba8(); + let id = TextureId::new(); + 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 + pub fn clear(&mut self) { + self.packer = TexturePacker::new_skyline(TexturePackerConfig { + max_width: self.width, + max_height: self.height, + ..Default::default() + }); + } + + /// Fill the GPU texture atlas + #[profiling::function] + pub 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; + } +} |
