summaryrefslogtreecommitdiff
path: root/alligator_render/src/texture.rs
diff options
context:
space:
mode:
authorMicha White <botahamec@outlook.com>2022-10-20 20:39:44 -0400
committerMicha White <botahamec@outlook.com>2022-10-20 20:39:44 -0400
commit93347346e8bd8f7412ae03a0858dd307a1df2e0d (patch)
tree17805956857c76b5fed3f47a821fcdf6141cf7a6 /alligator_render/src/texture.rs
parente337741969160603f06a7f2b30cda375eeef99fb (diff)
Moved files into workspace
Diffstat (limited to 'alligator_render/src/texture.rs')
-rw-r--r--alligator_render/src/texture.rs287
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;
+ }
+}