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;
}
}
|