use std::error::Error;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicUsize, Ordering};
use image::error::DecodingError;
use image::{DynamicImage, EncodableLayout, GenericImage, ImageError, RgbaImage};
use texture_packer::{
exporter::{ExportResult, ImageExporter},
MultiTexturePacker, TexturePackerConfig,
};
use thiserror::Error;
static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0);
#[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))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
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,
}
}
}
type PackError = impl std::fmt::Debug;
#[derive(Error, Debug)]
pub enum TextureError {
#[error("{:?}", .0)]
TextureTooLarge(PackError), // use an error with a source
#[error("{}", .0)]
BadImage(#[source] 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) => Self::BadImage(de),
_ => Self::Unexpected(Box::new(ie)),
}
}
}
// TODO make this Debug
// TODO make these resizable
pub struct TextureAtlases<'a> {
packer: MultiTexturePacker<'a, image::RgbaImage, TextureId>,
images: Vec<RgbaImage>,
width: u32,
height: u32,
}
impl<'a> Default for TextureAtlases<'a> {
fn default() -> Self {
Self::new(1024, 1024)
}
}
macro_rules! texture_info {
($name: ident, $prop: ident) => {
pub fn $name(&self, id: TextureId) -> Option<u32> {
let frame = self.texture_frame(id)?;
Some(frame.frame.$prop)
}
};
}
impl<'a> TextureAtlases<'a> {
/// Creates a new texture atlas, with the given size
// TODO why is this u32?
pub fn new(width: u32, height: u32) -> Self {
Self {
packer: MultiTexturePacker::new_skyline(TexturePackerConfig {
max_width: width,
max_height: height,
//trim: false,
..Default::default()
}),
width,
height,
images: Vec::with_capacity(1),
}
}
// 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(TextureError::TextureTooLarge)?;
Ok(id)
}
fn texture_frame(&self, id: TextureId) -> Option<&texture_packer::Frame<TextureId>> {
self.packer
.get_pages()
.iter()
.map(|a| a.get_frame(&id))
.next()?
}
texture_info!(texture_width, w);
texture_info!(texture_height, h);
texture_info!(texture_x, x);
texture_info!(texture_y, y);
const fn extent_3d(&self) -> wgpu::Extent3d {
wgpu::Extent3d {
width: self.width,
height: self.height,
depth_or_array_layers: 1,
}
}
fn atlases(&self) -> ExportResult<Box<[DynamicImage]>> {
self.packer
.get_pages()
.iter()
.map(ImageExporter::export)
.collect::<ExportResult<Vec<DynamicImage>>>()
.map(Vec::into_boxed_slice)
}
fn push_image(&mut self) -> &mut RgbaImage {
self.images.push(
RgbaImage::from_raw(
self.width,
self.height,
vec![0; 4 * self.width as usize * self.height as usize],
)
.expect("the image was the wrong size"),
);
self.images
.last_mut()
.expect("we just added an image to the list")
}
fn fill_images(&mut self) -> ExportResult<()> {
for (i, atlas) in self.atlases()?.iter().enumerate() {
#[allow(clippy::option_if_let_else)]
if let Some(image) = self.images.get_mut(i) {
image
.copy_from(atlas, 0, 0)
.expect("atlases shouldn't be too large");
} else {
let image = self.push_image();
image
.copy_from(atlas, 0, 0)
.expect("atlases shouldn't be too large");
}
}
Ok(())
}
fn images(&mut self) -> ExportResult<&[RgbaImage]> {
self.fill_images()?;
Ok(&self.images)
}
}
pub struct WgpuTextures {
atlases: TextureAtlases<'static>,
diffuse_texture: wgpu::Texture,
diffuse_bind_group: wgpu::BindGroup,
}
macro_rules! get_info {
($name: ident, $divisor: ident) => {
// TODO try to remove this
#[allow(clippy::cast_precision_loss)]
pub fn $name(&self, id: TextureId) -> Option<f32> {
self.atlases
.$name(id)
.map(|u| u as f32 / self.atlases.extent_3d().$divisor as f32)
}
};
}
impl WgpuTextures {
// TODO this is still too large
pub fn new(device: &wgpu::Device, width: u32, height: u32) -> (Self, wgpu::BindGroupLayout) {
let atlases = TextureAtlases::new(width, height);
let atlas_size = atlases.extent_3d();
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 {
atlases,
diffuse_texture,
diffuse_bind_group,
},
texture_bind_group_layout,
)
}
/// Loads a texture from memory, in the given file format
///
/// # Errors
///
/// This returns an error if the texture is not in the given format, or if
/// the texture is so large that it cannot fit in the texture atlas.
pub fn texture_from_memory(
&mut self,
texture: &[u8],
format: ImageFormat,
) -> Result<TextureId, TextureError> {
self.atlases.load_from_memory(texture, format)
}
get_info!(texture_width, width);
get_info!(texture_height, height);
get_info!(texture_x, width);
get_info!(texture_y, height);
pub const fn bind_group(&self) -> &wgpu::BindGroup {
&self.diffuse_bind_group
}
pub fn fill_textures(&mut self, queue: &wgpu::Queue) {
let atlas_size = self.atlases.extent_3d();
// put the packed texture into the base image
let Ok(atlases) = self.atlases.images() else { return };
let Some(atlas) = atlases.first() else { return };
// 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,
},
atlas.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,
);
}
}
|