From f9f34459ccbcc2befc61f19b5ce8f2e49fd5d3f9 Mon Sep 17 00:00:00 2001 From: Ben Hansen Date: Wed, 18 Mar 2020 13:49:18 -0600 Subject: [PATCH 1/2] started gifs showcase --- Cargo.lock | 13 ++ Cargo.toml | 3 + code/showcase/gifs/Cargo.toml | 16 ++ code/showcase/gifs/src/bin/main.rs | 4 + code/showcase/gifs/src/lib.rs | 5 + code/showcase/gifs/src/loop.rs | 4 + code/showcase/gifs/src/model.rs | 176 ++++++++++++++++++ code/showcase/gifs/src/texture.rs | 106 +++++++++++ .../windowless/Cargo.toml | 0 .../windowless/src/main.rs | 0 .../windowless/src/shader.frag | 0 .../windowless/src/shader.vert | 0 12 files changed, 327 insertions(+) create mode 100644 code/showcase/gifs/Cargo.toml create mode 100644 code/showcase/gifs/src/bin/main.rs create mode 100644 code/showcase/gifs/src/lib.rs create mode 100644 code/showcase/gifs/src/loop.rs create mode 100644 code/showcase/gifs/src/model.rs create mode 100644 code/showcase/gifs/src/texture.rs rename code/{intermediate => showcase}/windowless/Cargo.toml (100%) rename code/{intermediate => showcase}/windowless/src/main.rs (100%) rename code/{intermediate => showcase}/windowless/src/shader.frag (100%) rename code/{intermediate => showcase}/windowless/src/shader.vert (100%) diff --git a/Cargo.lock b/Cargo.lock index d64e6d84..99ec9b22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,19 @@ dependencies = [ "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gifs" +version = "0.1.0" +dependencies = [ + "cgmath 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "glsl-to-spirv 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tobj 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "wgpu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winit 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "glsl-to-spirv" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index d267d7e8..03a56d14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,7 @@ members = [ # intermediate tutorials "code/intermediate/*", + + # showcase + "code/showcase/*", ] \ No newline at end of file diff --git a/code/showcase/gifs/Cargo.toml b/code/showcase/gifs/Cargo.toml new file mode 100644 index 00000000..57afd495 --- /dev/null +++ b/code/showcase/gifs/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "gifs" +version = "0.1.0" +authors = ["Ben Hansen "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image = "0.22.4" +winit = "0.20.0" +glsl-to-spirv = "0.1.7" +cgmath = "0.17.0" +failure = "0.1" +tobj = "0.1" +wgpu = "0.4.0" \ No newline at end of file diff --git a/code/showcase/gifs/src/bin/main.rs b/code/showcase/gifs/src/bin/main.rs new file mode 100644 index 00000000..7da3a480 --- /dev/null +++ b/code/showcase/gifs/src/bin/main.rs @@ -0,0 +1,4 @@ + +fn main() { + println!("Hello, world!"); +} diff --git a/code/showcase/gifs/src/lib.rs b/code/showcase/gifs/src/lib.rs new file mode 100644 index 00000000..025fffb5 --- /dev/null +++ b/code/showcase/gifs/src/lib.rs @@ -0,0 +1,5 @@ +mod model; +mod texture; + +pub use model::*; +pub use texture::*; \ No newline at end of file diff --git a/code/showcase/gifs/src/loop.rs b/code/showcase/gifs/src/loop.rs new file mode 100644 index 00000000..6a775e94 --- /dev/null +++ b/code/showcase/gifs/src/loop.rs @@ -0,0 +1,4 @@ +pub trait Loopable { + pub update(&mut self) -> Option>, + pub render(&mut self) -> Option>, +} \ No newline at end of file diff --git a/code/showcase/gifs/src/model.rs b/code/showcase/gifs/src/model.rs new file mode 100644 index 00000000..bf905ba3 --- /dev/null +++ b/code/showcase/gifs/src/model.rs @@ -0,0 +1,176 @@ +use std::path::Path; +use std::ops::Range; + +use crate::texture; + +pub trait Vertex { + fn desc<'a>() -> wgpu::VertexBufferDescriptor<'a>; +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct ModelVertex { + position: [f32; 3], + tex_coords: [f32; 2], + normal: [f32; 3], +} + +impl Vertex for ModelVertex { + fn desc<'a>() -> wgpu::VertexBufferDescriptor<'a> { + use std::mem; + wgpu::VertexBufferDescriptor { + stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::InputStepMode::Vertex, + attributes: &[ + wgpu::VertexAttributeDescriptor { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float3, + }, + wgpu::VertexAttributeDescriptor { + offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float2, + }, + wgpu::VertexAttributeDescriptor { + offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, + shader_location: 2, + format: wgpu::VertexFormat::Float3, + }, + ] + } + } +} + +pub struct Material { + pub name: String, + pub diffuse_texture: texture::Texture, + pub bind_group: wgpu::BindGroup, +} + +pub struct Mesh { + pub name: String, + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, + pub num_elements: u32, + pub material: usize, +} + +pub struct Model { + pub meshes: Vec, + pub materials: Vec, +} + + +impl Model { + pub fn load>(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, path: P) -> Result<(Self, Vec), failure::Error> { + let (obj_models, obj_materials) = tobj::load_obj(path.as_ref())?; + + // We're assuming that the texture files are stored with the obj file + let containing_folder = path.as_ref().parent().unwrap(); + + // Our `Texure` struct currently returns a `CommandBuffer` when it's created so we need to collect those and return them. + let mut command_buffers = Vec::new(); + + let mut materials = Vec::new(); + for mat in obj_materials { + let diffuse_path = mat.diffuse_texture; + let (diffuse_texture, cmds) = texture::Texture::load(&device, containing_folder.join(diffuse_path))?; + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout, + bindings: &[ + wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture.view), + }, + wgpu::Binding { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler), + }, + ] + }); + + materials.push(Material { + name: mat.name, + diffuse_texture, + bind_group, + }); + command_buffers.push(cmds); + } + + let mut meshes = Vec::new(); + for m in obj_models { + let mut vertices = Vec::new(); + for i in 0..m.mesh.positions.len() / 3 { + vertices.push(ModelVertex { + position: [ + m.mesh.positions[i * 3], + m.mesh.positions[i * 3 + 1], + m.mesh.positions[i * 3 + 2], + ], + tex_coords: [ + m.mesh.texcoords[i * 2], + m.mesh.texcoords[i * 2 + 1], + ], + normal: [ + m.mesh.normals[i * 3], + m.mesh.normals[i * 3 + 1], + m.mesh.normals[i * 3 + 2], + ], + }); + } + + let vertex_buffer = device + .create_buffer_mapped(vertices.len(), wgpu::BufferUsage::VERTEX) + .fill_from_slice(&vertices); + + let index_buffer = device + .create_buffer_mapped(m.mesh.indices.len(), wgpu::BufferUsage::INDEX) + .fill_from_slice(&m.mesh.indices); + + meshes.push(Mesh { + name: m.name, + vertex_buffer, + index_buffer, + num_elements: m.mesh.indices.len() as u32, + material: m.mesh.material_id.unwrap_or(0), + }); + } + + Ok((Self { meshes, materials, }, command_buffers)) + } +} + +pub trait DrawModel { + fn draw_mesh(&mut self, mesh: &Mesh, material: &Material, uniforms: &wgpu::BindGroup); + fn draw_mesh_instanced(&mut self, mesh: &Mesh, material: &Material, instances: Range, uniforms: &wgpu::BindGroup); + + fn draw_model(&mut self, model: &Model, uniforms: &wgpu::BindGroup); + fn draw_model_instanced(&mut self, model: &Model, instances: Range, uniforms: &wgpu::BindGroup); +} + +impl<'a> DrawModel for wgpu::RenderPass<'a> { + fn draw_mesh(&mut self, mesh: &Mesh, material: &Material, uniforms: &wgpu::BindGroup) { + self.draw_mesh_instanced(mesh, material, 0..1, uniforms); + } + + fn draw_mesh_instanced(&mut self, mesh: &Mesh, material: &Material, instances: Range, uniforms: &wgpu::BindGroup) { + self.set_vertex_buffers(0, &[(&mesh.vertex_buffer, 0)]); + self.set_index_buffer(&mesh.index_buffer, 0); + self.set_bind_group(0, &material.bind_group, &[]); + self.set_bind_group(1, &uniforms, &[]); + self.draw_indexed(0..mesh.num_elements, 0, instances); + } + + fn draw_model(&mut self, model: &Model, uniforms: &wgpu::BindGroup) { + self.draw_model_instanced(model, 0..1, uniforms); + } + + fn draw_model_instanced(&mut self, model: &Model, instances: Range, uniforms: &wgpu::BindGroup) { + for mesh in &model.meshes { + let material = &model.materials[mesh.material]; + self.draw_mesh_instanced(mesh, material, instances.clone(), uniforms); + } + } +} \ No newline at end of file diff --git a/code/showcase/gifs/src/texture.rs b/code/showcase/gifs/src/texture.rs new file mode 100644 index 00000000..1d7d851c --- /dev/null +++ b/code/showcase/gifs/src/texture.rs @@ -0,0 +1,106 @@ +use image::GenericImageView; +use std::path::Path; + + +pub struct Texture { + pub texture: wgpu::Texture, + pub view: wgpu::TextureView, + pub sampler: wgpu::Sampler, +} + +impl Texture { + pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + + pub fn load>(device: &wgpu::Device, path: P) -> Result<(Self, wgpu::CommandBuffer), failure::Error> { + let img = image::open(path)?; + Self::from_image(device, &img) + } + + pub fn create_depth_texture(device: &wgpu::Device, sc_desc: &wgpu::SwapChainDescriptor) -> Self { + let desc = wgpu::TextureDescriptor { + format: Self::DEPTH_FORMAT, + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + ..sc_desc.to_texture_desc() + }; + let texture = device.create_texture(&desc); + + let view = texture.create_default_view(); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, + }); + + Self { texture, view, sampler } + } + + pub fn from_bytes(device: &wgpu::Device, bytes: &[u8]) -> Result<(Self, wgpu::CommandBuffer), failure::Error> { + let img = image::load_from_memory(bytes)?; + Self::from_image(device, &img) + } + + pub fn from_image(device: &wgpu::Device, img: &image::DynamicImage) -> Result<(Self, wgpu::CommandBuffer), failure::Error> { + let rgba = img.to_rgba(); + let dimensions = img.dimensions(); + + let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth: 1, + }; + let texture = device.create_texture(&wgpu::TextureDescriptor { + size, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, + }); + + let buffer = device + .create_buffer_mapped(rgba.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&rgba); + + let mut encoder = device.create_command_encoder(&Default::default()); + + encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer: &buffer, + offset: 0, + row_pitch: 4 * dimensions.0, + image_height: dimensions.1, + }, + wgpu::TextureCopyView { + texture: &texture, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d::ZERO, + }, + size, + ); + + let cmd_buffer = encoder.finish(); + + let view = texture.create_default_view(); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, + }); + + Ok((Self { texture, view, sampler }, cmd_buffer)) + } +} \ No newline at end of file diff --git a/code/intermediate/windowless/Cargo.toml b/code/showcase/windowless/Cargo.toml similarity index 100% rename from code/intermediate/windowless/Cargo.toml rename to code/showcase/windowless/Cargo.toml diff --git a/code/intermediate/windowless/src/main.rs b/code/showcase/windowless/src/main.rs similarity index 100% rename from code/intermediate/windowless/src/main.rs rename to code/showcase/windowless/src/main.rs diff --git a/code/intermediate/windowless/src/shader.frag b/code/showcase/windowless/src/shader.frag similarity index 100% rename from code/intermediate/windowless/src/shader.frag rename to code/showcase/windowless/src/shader.frag diff --git a/code/intermediate/windowless/src/shader.vert b/code/showcase/windowless/src/shader.vert similarity index 100% rename from code/intermediate/windowless/src/shader.vert rename to code/showcase/windowless/src/shader.vert From be23faee0607d02161397bf011e0a701c3fb0654 Mon Sep 17 00:00:00 2001 From: Ben Hansen Date: Sun, 29 Mar 2020 19:51:50 -0600 Subject: [PATCH 2/2] finished gif showcase --- .gitignore | 4 +- Cargo.lock | 15 ++ code/showcase/framework/Cargo.toml | 16 ++ code/showcase/framework/src/buffer.rs | 74 +++++++ code/showcase/framework/src/camera.rs | 25 +++ code/showcase/framework/src/lib.rs | 9 + .../showcase/{gifs => framework}/src/model.rs | 0 .../{gifs => framework}/src/texture.rs | 37 +++- code/showcase/gifs/Cargo.toml | 5 +- code/showcase/gifs/src/bin/main.rs | 4 - code/showcase/gifs/src/lib.rs | 5 - code/showcase/gifs/src/loop.rs | 4 - code/showcase/gifs/src/main.rs | 203 ++++++++++++++++++ code/showcase/gifs/src/res/model.frag | 35 +++ code/showcase/gifs/src/res/model.vert | 34 +++ code/showcase/gifs/src/res/shader.frag | 7 + code/showcase/gifs/src/res/shader.vert | 11 + docs/.vuepress/config.js | 10 +- docs/.vuepress/dist | 2 +- docs/showcase/README.md | 3 + docs/showcase/gifs/README.md | 163 ++++++++++++++ docs/showcase/gifs/output.gif | Bin 0 -> 12447 bytes docs/showcase/gifs/the-output2.gif | Bin 0 -> 5894 bytes .../windowless/README.md | 0 .../windowless/image-output.png | Bin 25 files changed, 644 insertions(+), 22 deletions(-) create mode 100644 code/showcase/framework/Cargo.toml create mode 100644 code/showcase/framework/src/buffer.rs create mode 100644 code/showcase/framework/src/camera.rs create mode 100644 code/showcase/framework/src/lib.rs rename code/showcase/{gifs => framework}/src/model.rs (100%) rename code/showcase/{gifs => framework}/src/texture.rs (75%) delete mode 100644 code/showcase/gifs/src/bin/main.rs delete mode 100644 code/showcase/gifs/src/lib.rs delete mode 100644 code/showcase/gifs/src/loop.rs create mode 100644 code/showcase/gifs/src/main.rs create mode 100644 code/showcase/gifs/src/res/model.frag create mode 100644 code/showcase/gifs/src/res/model.vert create mode 100644 code/showcase/gifs/src/res/shader.frag create mode 100644 code/showcase/gifs/src/res/shader.vert create mode 100644 docs/showcase/README.md create mode 100644 docs/showcase/gifs/README.md create mode 100644 docs/showcase/gifs/output.gif create mode 100644 docs/showcase/gifs/the-output2.gif rename docs/{intermediate => showcase}/windowless/README.md (100%) rename docs/{intermediate => showcase}/windowless/image-output.png (100%) diff --git a/.gitignore b/.gitignore index 7326f103..14c26153 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules/ target/ .vscode/ -image.png \ No newline at end of file +/image.png +/output*.* +output/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 99ec9b22..f6bdbeca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,19 @@ name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "framework" +version = "0.1.0" +dependencies = [ + "cgmath 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "glsl-to-spirv 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tobj 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "wgpu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winit 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -545,6 +558,8 @@ version = "0.1.0" dependencies = [ "cgmath 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "framework 0.1.0", + "gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", "glsl-to-spirv 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)", "tobj 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/code/showcase/framework/Cargo.toml b/code/showcase/framework/Cargo.toml new file mode 100644 index 00000000..f556dab6 --- /dev/null +++ b/code/showcase/framework/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "framework" +version = "0.1.0" +authors = ["Ben Hansen "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image = "0.22.4" +winit = "0.20.0" +glsl-to-spirv = "0.1.7" +cgmath = "0.17.0" +failure = "0.1" +tobj = "0.1" +wgpu = "0.4.0" \ No newline at end of file diff --git a/code/showcase/framework/src/buffer.rs b/code/showcase/framework/src/buffer.rs new file mode 100644 index 00000000..1ea9be29 --- /dev/null +++ b/code/showcase/framework/src/buffer.rs @@ -0,0 +1,74 @@ +use std::mem; + +pub trait ToRaw { + type Output; + fn to_raw(&self) -> Self::Output; +} + +pub struct RawBuffer { + pub buffer: wgpu::Buffer, + pub data: Vec, +} + +impl RawBuffer { + + pub fn from_slice>(device: &wgpu::Device, data: &[T], usage: wgpu::BufferUsage) -> Self { + let raw_data = data.iter().map(ToRaw::to_raw).collect::>(); + Self::from_vec(device, raw_data, usage) + } + + + pub fn from_vec(device: &wgpu::Device, data: Vec, usage: wgpu::BufferUsage) -> Self { + let buffer = device + .create_buffer_mapped(data.len(), usage) + .fill_from_slice(&data); + Self::from_parts(buffer, data, usage) + } + + pub fn from_parts(buffer: wgpu::Buffer, data: Vec, usage: wgpu::BufferUsage) -> Self { + Self { buffer, data } + } + + pub fn buffer_size(&self) -> wgpu::BufferAddress { + (self.data.len() * mem::size_of::()) as wgpu::BufferAddress + } +} + +pub struct Buffer, R: Copy + 'static> { + pub data: Vec, + pub raw_buffer: RawBuffer, + usage: wgpu::BufferUsage, +} + +impl, R: Copy + 'static> Buffer { + pub fn uniform(device: &wgpu::Device, datum: U) -> Self { + let data = vec![datum]; + let usage = wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST; + Self::with_usage(device, data, usage) + } + + pub fn storage(device: &wgpu::Device, data: Vec) -> Self { + let usage = wgpu::BufferUsage::STORAGE | wgpu::BufferUsage::COPY_DST; + Self::with_usage(device, data, usage) + } + + pub fn staging(device: &wgpu::Device, other: &Self) -> Self { + let buffer_size = other.raw_buffer.buffer_size(); + let usage = wgpu::BufferUsage::COPY_SRC | wgpu::BufferUsage::MAP_READ | wgpu::BufferUsage::MAP_WRITE; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + size: buffer_size, + usage, + }); + let raw_buffer = RawBuffer::from_parts(buffer, Vec::new(), usage); + Self::from_parts(Vec::new(), raw_buffer, usage) + } + + pub fn with_usage(device: &wgpu::Device, data: Vec, usage: wgpu::BufferUsage) -> Self { + let raw_buffer = RawBuffer::from_slice(device, &data, usage); + Self::from_parts(data, raw_buffer, usage) + } + + pub fn from_parts(data: Vec, raw_buffer: RawBuffer, usage: wgpu::BufferUsage) -> Self { + Self { data, raw_buffer, usage } + } +} \ No newline at end of file diff --git a/code/showcase/framework/src/camera.rs b/code/showcase/framework/src/camera.rs new file mode 100644 index 00000000..9dda9765 --- /dev/null +++ b/code/showcase/framework/src/camera.rs @@ -0,0 +1,25 @@ +#[cfg_attr(rustfmt, rustfmt_skip)] +pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( + 1.0, 0.0, 0.0, 0.0, + 0.0, -1.0, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.0, + 0.0, 0.0, 0.5, 1.0, +); + +pub struct Camera { + eye: cgmath::Point3, + target: cgmath::Point3, + up: cgmath::Vector3, + aspect: f32, + fovy: f32, + znear: f32, + zfar: f32, +} + +impl Camera { + pub fn build_view_projection_matrix(&self) -> cgmath::Matrix4 { + let view = cgmath::Matrix4::look_at(self.eye, self.target, self.up); + let proj = cgmath::perspective(cgmath::Deg(self.fovy), self.aspect, self.znear, self.zfar); + return proj * view; + } +} \ No newline at end of file diff --git a/code/showcase/framework/src/lib.rs b/code/showcase/framework/src/lib.rs new file mode 100644 index 00000000..34ed1bc9 --- /dev/null +++ b/code/showcase/framework/src/lib.rs @@ -0,0 +1,9 @@ +mod buffer; +mod camera; +mod model; +mod texture; + +pub use buffer::*; +pub use camera::*; +pub use model::*; +pub use texture::*; \ No newline at end of file diff --git a/code/showcase/gifs/src/model.rs b/code/showcase/framework/src/model.rs similarity index 100% rename from code/showcase/gifs/src/model.rs rename to code/showcase/framework/src/model.rs diff --git a/code/showcase/gifs/src/texture.rs b/code/showcase/framework/src/texture.rs similarity index 75% rename from code/showcase/gifs/src/texture.rs rename to code/showcase/framework/src/texture.rs index 1d7d851c..5befb3f3 100644 --- a/code/showcase/gifs/src/texture.rs +++ b/code/showcase/framework/src/texture.rs @@ -1,11 +1,15 @@ use image::GenericImageView; use std::path::Path; +use std::mem; + +use crate::buffer; pub struct Texture { pub texture: wgpu::Texture, pub view: wgpu::TextureView, pub sampler: wgpu::Sampler, + pub desc: wgpu::TextureDescriptor, } impl Texture { @@ -22,6 +26,10 @@ impl Texture { usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, ..sc_desc.to_texture_desc() }; + Self::from_descriptor(device, desc) + } + + pub fn from_descriptor(device: &wgpu::Device, desc: wgpu::TextureDescriptor) -> Self { let texture = device.create_texture(&desc); let view = texture.create_default_view(); @@ -37,7 +45,7 @@ impl Texture { compare_function: wgpu::CompareFunction::Always, }); - Self { texture, view, sampler } + Self { texture, view, sampler, desc } } pub fn from_bytes(device: &wgpu::Device, bytes: &[u8]) -> Result<(Self, wgpu::CommandBuffer), failure::Error> { @@ -54,7 +62,7 @@ impl Texture { height: dimensions.1, depth: 1, }; - let texture = device.create_texture(&wgpu::TextureDescriptor { + let desc = wgpu::TextureDescriptor { size, array_layer_count: 1, mip_level_count: 1, @@ -62,7 +70,8 @@ impl Texture { dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST, - }); + }; + let texture = device.create_texture(&desc); let buffer = device .create_buffer_mapped(rgba.len(), wgpu::BufferUsage::COPY_SRC) @@ -101,6 +110,24 @@ impl Texture { compare_function: wgpu::CompareFunction::Always, }); - Ok((Self { texture, view, sampler }, cmd_buffer)) + Ok((Self { texture, view, sampler, desc }, cmd_buffer)) + } + + pub fn prepare_buffer_rgba(&self, device: &wgpu::Device) -> buffer::RawBuffer<[f32;4]> { + let num_pixels = self.desc.size.width * self.desc.size.height * self.desc.size.depth; + + let buffer_size = num_pixels * mem::size_of::<[f32;4]>() as u32; + let buffer_usage = wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ; + let buffer_desc = wgpu::BufferDescriptor { + size: buffer_size as wgpu::BufferAddress, + usage: buffer_usage, + }; + let buffer = device.create_buffer(&buffer_desc); + + let data = Vec::with_capacity(num_pixels as usize); + + let raw_buffer = buffer::RawBuffer::from_parts(buffer, data, buffer_usage); + + raw_buffer } -} \ No newline at end of file +} \ No newline at end of file diff --git a/code/showcase/gifs/Cargo.toml b/code/showcase/gifs/Cargo.toml index 57afd495..77843adb 100644 --- a/code/showcase/gifs/Cargo.toml +++ b/code/showcase/gifs/Cargo.toml @@ -13,4 +13,7 @@ glsl-to-spirv = "0.1.7" cgmath = "0.17.0" failure = "0.1" tobj = "0.1" -wgpu = "0.4.0" \ No newline at end of file +wgpu = "0.4.0" +gif = "0.10.3" + +framework = { path = "../framework" } \ No newline at end of file diff --git a/code/showcase/gifs/src/bin/main.rs b/code/showcase/gifs/src/bin/main.rs deleted file mode 100644 index 7da3a480..00000000 --- a/code/showcase/gifs/src/bin/main.rs +++ /dev/null @@ -1,4 +0,0 @@ - -fn main() { - println!("Hello, world!"); -} diff --git a/code/showcase/gifs/src/lib.rs b/code/showcase/gifs/src/lib.rs deleted file mode 100644 index 025fffb5..00000000 --- a/code/showcase/gifs/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod model; -mod texture; - -pub use model::*; -pub use texture::*; \ No newline at end of file diff --git a/code/showcase/gifs/src/loop.rs b/code/showcase/gifs/src/loop.rs deleted file mode 100644 index 6a775e94..00000000 --- a/code/showcase/gifs/src/loop.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub trait Loopable { - pub update(&mut self) -> Option>, - pub render(&mut self) -> Option>, -} \ No newline at end of file diff --git a/code/showcase/gifs/src/main.rs b/code/showcase/gifs/src/main.rs new file mode 100644 index 00000000..09a9f4a5 --- /dev/null +++ b/code/showcase/gifs/src/main.rs @@ -0,0 +1,203 @@ +extern crate framework; + +use std::mem; +use std::sync::{Arc, Mutex}; + +fn main() { + let adapter = wgpu::Adapter::request(&Default::default()).unwrap(); + let (device, mut queue) = adapter.request_device(&Default::default()); + + let colors = [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.2], + [0.0, 0.2, 0.2], + [0.2, 0.2, 0.2], + [0.2, 0.2, 0.2], + [0.0, 0.2, 0.2], + [0.0, 0.0, 0.2], + [0.0, 0.0, 0.0], + ]; + + // create a texture to render to + let texture_size = 256u32; + let rt_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: texture_size, + height: texture_size, + depth: 1, + }, + array_layer_count: colors.len() as u32, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::OUTPUT_ATTACHMENT, + }; + let render_target = framework::Texture::from_descriptor(&device, rt_desc); + + // create a buffer to copy the texture to so we can get the data + let pixel_size = mem::size_of::<[u8;4]>() as u32; + let buffer_size = (pixel_size * texture_size * texture_size) as wgpu::BufferAddress; + let buffer_desc = wgpu::BufferDescriptor { + size: buffer_size, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, + }; + let output_buffer = device.create_buffer(&buffer_desc); + + // a simple render pipeline that draws a triangle + let render_pipeline = create_render_pipeline(&device, &render_target); + + // we need to store this in and arc-mutex so we can pass it to the mapping function + let frames = Arc::new(Mutex::new(Vec::new())); + + for c in &colors { + let mut encoder = device.create_command_encoder(&Default::default()); + + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: &render_target.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + // modify the clear color so the gif changes + clear_color: wgpu::Color { + r: c[0], + g: c[1], + b: c[2], + a: 1.0, + } + } + ], + depth_stencil_attachment: None, + }); + + rpass.set_pipeline(&render_pipeline); + rpass.draw(0..3, 0..1); + + drop(rpass); + + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &render_target.texture, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d::ZERO, + }, + wgpu::BufferCopyView { + buffer: &output_buffer, + offset: 0, + row_pitch: pixel_size * texture_size, + image_height: texture_size, + }, + render_target.desc.size + ); + + queue.submit(&[encoder.finish()]); + + let frames_clone = frames.clone(); + output_buffer.map_read_async(0, buffer_size, move |result: wgpu::BufferMapAsyncResult<&[u8]>| { + match result { + Ok(mapping) => { + let data = Vec::from(mapping.data); + let mut f = frames_clone.lock().unwrap(); + (*f).push(data); + } + _ => { eprintln!("Something went wrong") } + } + }); + + // wait for the GPU to finish + device.poll(true); + } + + let mut frames = Arc::try_unwrap(frames) + .unwrap() + .into_inner() + .unwrap(); + + save_gif("output.gif", &mut frames, 10, texture_size as u16).unwrap(); +} + +fn save_gif(path: &str, frames: &mut Vec>, speed: i32, size: u16) -> Result<(), failure::Error> { + use gif::{Frame, Encoder, Repeat, SetParameter}; + + let mut image = std::fs::File::create(path)?; + let mut encoder = Encoder::new(&mut image, size, size, &[])?; + encoder.set(Repeat::Infinite)?; + + for mut frame in frames { + encoder.write_frame(&Frame::from_rgba_speed(size, size, &mut frame, speed))?; + } + + Ok(()) +} + +// The image crate currently doesn't support looping gifs, so I'm not using this +// code. I'm keeping it around in case image adds looping support. +#[allow(unused)] +fn save_gif_old(path: &str, frames: &mut Vec>, speed: i32, size: u16) -> Result<(), failure::Error> { + let output = std::fs::File::create(path)?; + let mut encoder = image::gif::Encoder::new(output); + + for mut data in frames { + let frame = image::gif::Frame::from_rgba_speed(size, size, &mut data, speed); + encoder.encode(&frame)?; + } + + Ok(()) +} + + +fn create_render_pipeline(device: &wgpu::Device, target: &framework::Texture) -> wgpu::RenderPipeline { + let vs_src = include_str!("res/shader.vert"); + let fs_src = include_str!("res/shader.frag"); + let vs_spirv = glsl_to_spirv::compile(vs_src, glsl_to_spirv::ShaderType::Vertex).unwrap(); + let fs_spirv = glsl_to_spirv::compile(fs_src, glsl_to_spirv::ShaderType::Fragment).unwrap(); + let vs_data = wgpu::read_spirv(vs_spirv).unwrap(); + let fs_data = wgpu::read_spirv(fs_spirv).unwrap(); + let vs_module = device.create_shader_module(&vs_data); + let fs_module = device.create_shader_module(&fs_data); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &render_pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::Back, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[ + wgpu::ColorStateDescriptor { + format: target.desc.format, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }, + ], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + render_pipeline +} + diff --git a/code/showcase/gifs/src/res/model.frag b/code/showcase/gifs/src/res/model.frag new file mode 100644 index 00000000..cbba46f5 --- /dev/null +++ b/code/showcase/gifs/src/res/model.frag @@ -0,0 +1,35 @@ +#version 450 + +layout(location=0) in vec2 v_tex_coords; +layout(location=1) in vec3 v_normal; +layout(location=2) in vec3 v_position; + +layout(location=0) out vec4 f_color; + +layout(set = 0, binding = 0) uniform texture2D t_diffuse; +layout(set = 0, binding = 1) uniform sampler s_diffuse; + +layout(set=1, binding=2) +uniform Lights { + vec3 u_light; +}; + +const vec3 ambient_color = vec3(0.0, 0.0, 0.0); +const vec3 specular_color = vec3(1.0, 1.0, 1.0); + +const float shininess = 32; + +void main() { + vec4 diffuse_color = texture(sampler2D(t_diffuse, s_diffuse), v_tex_coords); + float diffuse_term = max(dot(normalize(v_normal), normalize(u_light)), 0); + + vec3 camera_dir = normalize(-v_position); + + // This is an aproximation of the actual reflection vector, aka what + // angle you have to look at the object to be blinded by the light + vec3 half_direction = normalize(normalize(u_light) + camera_dir); + float specular_term = pow(max(dot(normalize(v_normal), half_direction), 0.0), shininess); + + f_color = vec4(ambient_color, 1.0) + vec4(specular_term * specular_color, 1.0) + diffuse_term * diffuse_color; + +} \ No newline at end of file diff --git a/code/showcase/gifs/src/res/model.vert b/code/showcase/gifs/src/res/model.vert new file mode 100644 index 00000000..9b40c94d --- /dev/null +++ b/code/showcase/gifs/src/res/model.vert @@ -0,0 +1,34 @@ +#version 450 + +layout(location=0) in vec3 a_position; +layout(location=1) in vec2 a_tex_coords; +layout(location=2) in vec3 a_normal; + +layout(location=0) out vec2 v_tex_coords; +layout(location=1) out vec3 v_normal; +layout(location=2) out vec3 v_position; + +layout(set=1, binding=0) +uniform Uniforms { + mat4 u_view_proj; +}; + +layout(set=1, binding=1) +buffer Instances { + mat4 s_models[]; +}; + +void main() { + v_tex_coords = a_tex_coords; + + mat4 model = s_models[gl_InstanceIndex]; + + // Rotate the normals with respect to the model, ignoring scaling + mat3 normal_matrix = mat3(transpose(inverse(mat3(model)))); + v_normal = normal_matrix * a_normal; + + gl_Position = u_view_proj * model * vec4(a_position, 1.0); + + // Get the position relative to the view for the lighting calc + v_position = gl_Position.xyz / gl_Position.w; +} \ No newline at end of file diff --git a/code/showcase/gifs/src/res/shader.frag b/code/showcase/gifs/src/res/shader.frag new file mode 100644 index 00000000..49493067 --- /dev/null +++ b/code/showcase/gifs/src/res/shader.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location=0) out vec4 f_color; + +void main() { + f_color = vec4(0.3, 0.2, 0.1, 1.0); +} \ No newline at end of file diff --git a/code/showcase/gifs/src/res/shader.vert b/code/showcase/gifs/src/res/shader.vert new file mode 100644 index 00000000..ee871f69 --- /dev/null +++ b/code/showcase/gifs/src/res/shader.vert @@ -0,0 +1,11 @@ +#version 450 + +const vec2 positions[3] = vec2[3]( + vec2(0.0, -0.5), + vec2(-0.5, 0.5), + vec2(0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} \ No newline at end of file diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index f0d139f8..25501efb 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -37,9 +37,17 @@ module.exports = { collapsable: false, children: [ '/intermediate/tutorial10-lighting/', - '/intermediate/windowless/', ], }, + { + title: 'Showcase', + collapsable: true, + children: [ + '/showcase/', + '/showcase/windowless/', + '/showcase/gifs/', + ] + }, '/news/' ] } diff --git a/docs/.vuepress/dist b/docs/.vuepress/dist index bb7e290c..50afb738 160000 --- a/docs/.vuepress/dist +++ b/docs/.vuepress/dist @@ -1 +1 @@ -Subproject commit bb7e290ca411ae49b3eb2e20b3dcde36af454ae1 +Subproject commit 50afb738454fd9a4d69e42bea313f79386300908 diff --git a/docs/showcase/README.md b/docs/showcase/README.md new file mode 100644 index 00000000..98250e1d --- /dev/null +++ b/docs/showcase/README.md @@ -0,0 +1,3 @@ +# Foreward + +The articles in this section are not meant to be tutorials. They are showcases of the various things you can do with `wgpu`. I won't go over specifics of creating `wgpu` resources, as those will be covered elsewhere. The code for these examples is still available however, and will be accessible on Github. \ No newline at end of file diff --git a/docs/showcase/gifs/README.md b/docs/showcase/gifs/README.md new file mode 100644 index 00000000..2334b926 --- /dev/null +++ b/docs/showcase/gifs/README.md @@ -0,0 +1,163 @@ +# Creating gifs + +Sometimes you've created a nice simulation/animation, and you want to show it off. While you can record a video, that might be a bit overkill to break our your video recording if you just want something to post on twitter. That's where what [GIF](https://en.wikipedia.org/wiki/GIF)s are for. + +Also, GIF is pronounced GHIF, not JIF as JIF is not only [peanut butter](https://en.wikipedia.org/wiki/Jif_%28peanut_butter%29), it is also a [different image format](https://filext.com/file-extension/JIF). + +## How are we making the GIF? + +We're going to create a function using the [gif crate](https://docs.rs/gif/) to encode the actual image. + +```rust +fn save_gif(path: &str, frames: &mut Vec>, speed: i32, size: u16) -> Result<(), failure::Error> { + use gif::{Frame, Encoder, Repeat, SetParameter}; + + let mut image = std::fs::File::create(path)?; + let mut encoder = Encoder::new(&mut image, size, size, &[])?; + encoder.set(Repeat::Infinite)?; + + for mut frame in frames { + encoder.write_frame(&Frame::from_rgba_speed(size, size, &mut frame, speed))?; + } + + Ok(()) +} +``` + + + + + + +All we need to use this code is the frames of the GIF, how fast it should run, and the size of the GIF (you could use width and height seperately, but I didn't). + +## How do we make the frames? + +If you checked out the [windowless showcase](../windowless/#a-triangle-without-a-window), you'll know that we render directly to a `wgpu::Texture`. We'll create a texture to render to and a buffer the copy the output to. + +```rust +// create a texture to render to +let texture_size = 256u32; +let rt_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: texture_size, + height: texture_size, + depth: 1, + }, + array_layer_count: colors.len() as u32, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::OUTPUT_ATTACHMENT, +}; +let render_target = framework::Texture::from_descriptor(&device, rt_desc); + +// create a buffer to copy the texture to so we can get the data +let pixel_size = mem::size_of::<[u8;4]>() as u32; +let buffer_size = (pixel_size * texture_size * texture_size) as wgpu::BufferAddress; +let buffer_desc = wgpu::BufferDescriptor { + size: buffer_size, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, +}; +let output_buffer = device.create_buffer(&buffer_desc); +``` + +With that we can render a frame, and then copy that frame to a `Vec`. + +```rust +// we need to store this in and arc-mutex so we can pass it to the mapping function +let frames = Arc::new(Mutex::new(Vec::new())); + +for c in &colors { + let mut encoder = device.create_command_encoder(&Default::default()); + + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: &render_target.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + // modify the clear color so the gif changes + clear_color: wgpu::Color { + r: c[0], + g: c[1], + b: c[2], + a: 1.0, + } + } + ], + depth_stencil_attachment: None, + }); + + rpass.set_pipeline(&render_pipeline); + rpass.draw(0..3, 0..1); + + drop(rpass); + + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &render_target.texture, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d::ZERO, + }, + wgpu::BufferCopyView { + buffer: &output_buffer, + offset: 0, + row_pitch: pixel_size * texture_size, + image_height: texture_size, + }, + render_target.desc.size + ); + + queue.submit(&[encoder.finish()]); + + let frames_clone = frames.clone(); + output_buffer.map_read_async(0, buffer_size, move |result: wgpu::BufferMapAsyncResult<&[u8]>| { + match result { + Ok(mapping) => { + let data = Vec::from(mapping.data); + let mut f = frames_clone.lock().unwrap(); + (*f).push(data); + } + _ => { eprintln!("Something went wrong") } + } + }); + + // wait for the GPU to finish + device.poll(true); +} +``` + +Once that's done we can pull the frame data our of the `Arc>`, and pass it into `save_gif()`. + +```rust +let mut frames = Arc::try_unwrap(frames) + .unwrap() + .into_inner() + .unwrap(); + +save_gif("output.gif", &mut frames, 1, texture_size as u16).unwrap(); +``` + +That's the gist of it. We can improve things using a texture array, and sending the draw commands all at once, but this gets the idea across. With the shader I wrote we get the following GIF. + + +![./output.gif](./output.gif) + + \ No newline at end of file diff --git a/docs/showcase/gifs/output.gif b/docs/showcase/gifs/output.gif new file mode 100644 index 0000000000000000000000000000000000000000..cf3a3c828b46f4ea0062b7bcb833304c278c0a33 GIT binary patch literal 12447 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 E04n$f)Bpeg literal 0 HcmV?d00001 diff --git a/docs/intermediate/windowless/README.md b/docs/showcase/windowless/README.md similarity index 100% rename from docs/intermediate/windowless/README.md rename to docs/showcase/windowless/README.md diff --git a/docs/intermediate/windowless/image-output.png b/docs/showcase/windowless/image-output.png similarity index 100% rename from docs/intermediate/windowless/image-output.png rename to docs/showcase/windowless/image-output.png