diff options
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | examples/bmp.rs | 40 | ||||
| -rw-r--r-- | examples/res/sample.bmp | bin | 0 -> 1000138 bytes | |||
| -rw-r--r-- | examples/res/square.ico | bin | 0 -> 270398 bytes | |||
| -rw-r--r-- | examples/square.rs | 19 | ||||
| -rw-r--r-- | shaders/sprite.wgsl | 13 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/renderer.rs | 201 | ||||
| -rw-r--r-- | src/texture.rs | 59 |
9 files changed, 298 insertions, 41 deletions
@@ -20,4 +20,7 @@ texture_packer = "0.24" name = "black" [[example]] -name = "square"
\ No newline at end of file +name = "square" + +[[example]] +name = "bmp" diff --git a/examples/bmp.rs b/examples/bmp.rs new file mode 100644 index 0000000..b1c2117 --- /dev/null +++ b/examples/bmp.rs @@ -0,0 +1,40 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::num::NonZeroU32; + +use alligator_render::{config::WindowMode, ImageFormat, Instance, RenderWindowConfig, Renderer}; +use winit::event_loop::EventLoop; + +fn main() { + // configure the render window + let config = RenderWindowConfig { + title: "Bumper Sticker", + instance_capacity: 1, + default_width: NonZeroU32::new(1280).unwrap(), + default_height: NonZeroU32::new(720).unwrap(), + mode: WindowMode::BorderlessFullscreen, + vsync: false, + ..Default::default() + }; + + let texture = include_bytes!("res/sample.bmp"); + + let event_loop = EventLoop::new(); + let mut renderer = Renderer::new(&config, &event_loop).unwrap(); + + let texture = renderer + .texture_from_mem(texture, ImageFormat::Bmp) + .unwrap(); + let width = renderer.texture_width(texture).unwrap(); + let height = renderer.texture_height(texture).unwrap(); + let x = renderer.texture_x(texture).unwrap(); + let y = renderer.texture_y(texture).unwrap(); + + renderer.push_instance(Instance { + texture_size: [width, height], + texture_coordinates: [x, y], + ..Default::default() + }); + + renderer.run(event_loop); +} diff --git a/examples/res/sample.bmp b/examples/res/sample.bmp Binary files differnew file mode 100644 index 0000000..b31b58e --- /dev/null +++ b/examples/res/sample.bmp diff --git a/examples/res/square.ico b/examples/res/square.ico Binary files differnew file mode 100644 index 0000000..43d5a8c --- /dev/null +++ b/examples/res/square.ico diff --git a/examples/square.rs b/examples/square.rs index d03a4a9..f95bd00 100644 --- a/examples/square.rs +++ b/examples/square.rs @@ -1,6 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use alligator_render::{Instance, RenderWindowConfig, Renderer}; +use alligator_render::{ImageFormat, Instance, RenderWindowConfig, Renderer}; use winit::event_loop::EventLoop; fn main() { @@ -11,9 +11,24 @@ fn main() { ..Default::default() }; + let texture = include_bytes!("res/square.ico"); + let event_loop = EventLoop::new(); let mut renderer = Renderer::new(&config, &event_loop).unwrap(); - renderer.push_instance(Instance::default()); + + let texture = renderer + .texture_from_mem(texture, ImageFormat::Ico) + .unwrap(); + let width = renderer.texture_width(texture).unwrap(); + let height = renderer.texture_height(texture).unwrap(); + let x = renderer.texture_x(texture).unwrap(); + let y = renderer.texture_y(texture).unwrap(); + + renderer.push_instance(Instance { + texture_size: [width, height], + texture_coordinates: [x, y], + ..Default::default() + }); renderer.run(event_loop); } diff --git a/shaders/sprite.wgsl b/shaders/sprite.wgsl index 22225f7..61944a1 100644 --- a/shaders/sprite.wgsl +++ b/shaders/sprite.wgsl @@ -42,13 +42,22 @@ fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { let position4d = vec4<f32>(position2d, 0.0, 1.0); let position = camera * position4d; + let tex_coords = vec2<f32>(model.position[0] + 0.5, 1.0 - (model.position[1] + 0.5)); + out.clip_position = position; out.texture_atlas_index = instance.texture_atlas_index; - out.texture_coordinates = (model.position + 0.5) * instance.texture_size + instance.texture_coordinates; + out.texture_coordinates = tex_coords * instance.texture_size + instance.texture_coordinates; return out; } +@group(1) @binding(0) +var t_diffuse: texture_2d<f32>; +@group(1) @binding(1) +var s_diffuse: sampler; + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - return vec4<f32>(1.0, 1.0, 1.0, 0.0); + //return vec4<f32>(1.0, 1.0, 1.0, 0.0); + return textureSample(t_diffuse, s_diffuse, in.texture_coordinates); + //return textureSample(t_diffuse, s_diffuse, ); }
\ No newline at end of file @@ -16,4 +16,6 @@ pub(crate) use camera::Camera; pub use config::RenderWindowConfig; pub use instance::Instance; pub use renderer::Renderer; +pub use texture::ImageFormat; +pub(crate) use texture::TextureAtlases; pub(crate) use vertex::Vertex; diff --git a/src/renderer.rs b/src/renderer.rs index d45233b..14a7b6a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,9 +1,13 @@ use std::{convert::TryInto, mem::size_of, num::NonZeroU32}; use crate::{ - camera::CameraUniform, instance::InstanceId, vertex::SQUARE, Camera, Instance, - RenderWindowConfig, Vertex, + camera::CameraUniform, + instance::InstanceId, + texture::{TextureError, TextureId}, + vertex::SQUARE, + Camera, ImageFormat, Instance, RenderWindowConfig, TextureAtlases, Vertex, }; +use image::{EncodableLayout, GenericImage}; use pollster::FutureExt; use thiserror::Error; use wgpu::{include_wgsl, util::DeviceExt}; @@ -39,7 +43,7 @@ pub enum NewRendererError { WindowInitError(#[from] OsError), } -#[derive(Debug)] +// TODO make this Debug pub struct Renderer { // TODO move some of this data elsewhere surface: wgpu::Surface, @@ -56,6 +60,10 @@ pub struct Renderer { camera: Camera, camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, + textures: TextureAtlases<'static>, + texture_size: wgpu::Extent3d, + diffuse_texture: wgpu::Texture, + diffuse_bind_group: wgpu::BindGroup, window: Window, } @@ -222,17 +230,6 @@ impl Renderer { }], }); - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Sprite Render Pipeline Layout"), - bind_group_layouts: &[&camera_bind_group_layout], - push_constant_ranges: &[], - }); - - // set up a pipeline for sprite rendering - let render_pipeline = - Self::sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout); - // the vertex buffer used for rendering squares let square_vertices = SQUARE .len() @@ -249,6 +246,71 @@ impl Renderer { let (instance_buffer, instance_buffer_size) = Self::new_instance_buffer(&device, &instances); + // TODO make this configurable + let textures = TextureAtlases::new(window.inner_size().width, window.inner_size().height); + let texture_size = textures.extent_3d(); // textures.extent_3d(); + let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("diffuse texture"), + size: texture_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()); + // TODO make this configurable + 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), + }, + ], + }); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Sprite Render Pipeline Layout"), + bind_group_layouts: &[&camera_bind_group_layout, &texture_bind_group_layout], + push_constant_ranges: &[], + }); + + // set up a pipeline for sprite rendering + let render_pipeline = + Self::sprite_render_pipeline(&device, surface_config.format, &render_pipeline_layout); + Ok(Self { surface, surface_config, @@ -264,6 +326,10 @@ impl Renderer { camera, camera_buffer, camera_bind_group, + textures, + texture_size, + diffuse_texture, + diffuse_bind_group, window, }) } @@ -304,23 +370,6 @@ impl Renderer { self.window.set_title(title); } - fn expand_instance_buffer(&mut self) { - (self.instance_buffer, self.instance_buffer_size) = - Self::new_instance_buffer(&self.device, &self.instances); - } - - fn fill_instance_buffer(&mut self) { - if self.instances.len() > self.instance_buffer_size { - self.expand_instance_buffer(); - } - - self.queue.write_buffer( - &self.instance_buffer, - 0 as wgpu::BufferAddress, - bytemuck::cast_slice(&self.instances), - ); - } - /// Add an instance to the renderer, and returns an `InstanceId` to the /// instance. This id becomes invalid if the instances are cleared. pub fn push_instance(&mut self, instance: Instance) -> InstanceId { @@ -354,6 +403,61 @@ impl Renderer { &mut self.camera } + /// 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_mem( + &mut self, + texture: &[u8], + format: ImageFormat, + ) -> Result<TextureId, TextureError> { + self.textures.load_from_memory(texture, format) + } + + pub fn texture_width(&self, id: TextureId) -> Option<f32> { + self.textures + .texture_width(id) + .map(|u| u as f32 / self.texture_size.width as f32) + } + + pub fn texture_height(&self, id: TextureId) -> Option<f32> { + self.textures + .texture_height(id) + .map(|u| u as f32 / self.texture_size.height as f32) + } + + pub fn texture_x(&self, id: TextureId) -> Option<f32> { + self.textures + .texture_x(id) + .map(|u| u as f32 / self.texture_size.width as f32) + } + + pub fn texture_y(&self, id: TextureId) -> Option<f32> { + self.textures + .texture_y(id) + .map(|u| u as f32 / self.texture_size.height as f32) + } + + fn expand_instance_buffer(&mut self) { + (self.instance_buffer, self.instance_buffer_size) = + Self::new_instance_buffer(&self.device, &self.instances); + } + + fn fill_instance_buffer(&mut self) { + if self.instances.len() > self.instance_buffer_size { + self.expand_instance_buffer(); + } + + self.queue.write_buffer( + &self.instance_buffer, + 0 as wgpu::BufferAddress, + bytemuck::cast_slice(&self.instances), + ); + } + fn refresh_camera_buffer(&mut self) { self.queue.write_buffer( &self.camera_buffer, @@ -362,6 +466,39 @@ impl Renderer { ); } + // TODO optimize this + fn fill_textures(&mut self) { + // create a base image + let mut image = image::RgbaImage::from_vec( + self.texture_size.width, + self.texture_size.height, + vec![0; 4 * self.texture_size.width as usize * self.texture_size.height as usize], + ) + .unwrap(); + + // put the packed texture into the base image + let Ok(atlases) = self.textures.atlases() else { return }; + let Some(atlas) = atlases.first() else { return }; + image.copy_from(atlas, 0, 0).unwrap(); + + // copy that to the gpu + self.queue.write_texture( + wgpu::ImageCopyTexture { + texture: &self.diffuse_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + image.as_bytes(), + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(self.texture_size.width * 4), + rows_per_image: NonZeroU32::new(self.texture_size.height), + }, + self.texture_size, + ); + } + /// Renders a new frame to the window /// /// # Errors @@ -392,6 +529,7 @@ impl Renderer { .expect("expected less than 3 billion instances"); self.refresh_camera_buffer(); + self.fill_textures(); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -409,6 +547,7 @@ impl Renderer { render_pass.set_pipeline(&self.render_pipeline); render_pass.set_bind_group(0, &self.camera_bind_group, &[]); + render_pass.set_bind_group(1, &self.diffuse_bind_group, &[]); render_pass.set_vertex_buffer(0, self.square_vertex_buffer.slice(..)); render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); render_pass.draw(0..self.square_vertices, 0..num_instances); diff --git a/src/texture.rs b/src/texture.rs index 8a36334..9184304 100644 --- a/src/texture.rs +++ b/src/texture.rs @@ -12,9 +12,10 @@ use thiserror::Error; static NEXT_TEXTURE_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -struct TextureId(usize); +pub struct TextureId(usize); impl TextureId { + #[allow(clippy::new_without_default)] pub fn new() -> Self { Self(NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed)) } @@ -58,15 +59,16 @@ impl From<ImageError> for TextureError { } } -struct TextureAtlases<'a> { +// TODO make this Debug +pub struct TextureAtlases<'a> { packer: MultiTexturePacker<'a, image::RgbaImage, TextureId>, + width: u32, + height: u32, } impl<'a> Default for TextureAtlases<'a> { fn default() -> Self { - Self { - packer: MultiTexturePacker::new_skyline(TexturePackerConfig::default()), - } + Self::new(1024, 1024) } } @@ -78,8 +80,11 @@ impl<'a> TextureAtlases<'a> { packer: MultiTexturePacker::new_skyline(TexturePackerConfig { max_width: width, max_height: height, + //trim: false, ..Default::default() }), + width, + height, } } @@ -98,6 +103,50 @@ impl<'a> TextureAtlases<'a> { 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()? + } + + pub fn texture_width(&self, id: TextureId) -> Option<u32> { + let frame = self.texture_frame(id)?; + Some(frame.frame.w) + } + + pub fn texture_height(&self, id: TextureId) -> Option<u32> { + let frame = self.texture_frame(id)?; + Some(frame.frame.h) + } + + pub fn texture_x(&self, id: TextureId) -> Option<u32> { + let frame = self.texture_frame(id)?; + Some(frame.frame.x) + } + + pub fn texture_y(&self, id: TextureId) -> Option<u32> { + let frame = self.texture_frame(id)?; + Some(frame.frame.y) + } + + pub(crate) const fn bytes_per_row(&self) -> u32 { + self.width * 4 + } + + pub(crate) const fn height(&self) -> u32 { + self.height + } + + pub(crate) const fn extent_3d(&self) -> wgpu::Extent3d { + wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + } + } + pub(crate) fn atlases(&self) -> ExportResult<Box<[DynamicImage]>> { self.packer .get_pages() |
