diff --git a/Cargo.toml b/Cargo.toml index 89a2c405..f368fc8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,8 @@ members = [ "crates/renderling", "crates/renderling-shader", "crates/renderling-gpui", - "crates/renderling-derive", + "crates/crabslab", + "crates/crabslab-derive", ] exclude = ["./shaders"] @@ -14,11 +15,13 @@ exclude = ["./shaders"] resolver = "2" [workspace.dependencies] +async-channel = "1.8" bytemuck = { version = "1.13.0", features = ["derive"] } futures-lite = "1.13" gltf = { git = 'https://github.com/gltf-rs/gltf.git', features = ["KHR_lights_punctual", "KHR_materials_unlit", "KHR_materials_emissive_strength", "extras"] } image = "0.24" log = "0.4" -glam = "0.24.2" +glam = { version = "0.24.2", default-features = false } +snafu = "0.7" winit = { version = "0.27" } wgpu = { version = "0.17" } diff --git a/DEVLOG.md b/DEVLOG.md index 2eb990a4..942d6786 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,118 @@ # devlog +## Sat Dec 23, 2023 + +I've ported over a majority of the tests to the GLTF-on-the-slab implementation. +I'm currently working on the big PBR test and having trouble with the skybox, which +is rendering all black... + +Debugging rabbit hole: +* So is it even running? + - Yes, logging shows that it's running. +* Could it be it needs to be run in its own render pass? +* Before I even check that, I see that the skybox's vertex shader uses the `instance_index` as the `Id` of the camera, and I'm passing `0..1` as the instance range in the draw call. + - So we need a way to pass the camera's `Id` to the skybox. + - I just added it as a field on `Skybox` + - Using that new field fixed that issue. Now I have an issue with bloom. + +After fixing the skybox rendering it seems bloom isn't running. + +Debugging rabbit hole: +* So is it even running? + - Yes, logging shows that it's running. +* Is the result being used downstream during tonemapping? + - It seems to be. +* Let's check to see that there isn't something funky when configuring the graph. + - Nothing I can tell there. +* Maybe print out the brightness texture and make sure it's populated? +* Losing steam here, especially since bloom needs to be re-done as "physically based". + +### Physically Based Bloom + +## Thu Dec 21, 2023 + +It's the solstice! My Dad's birthday, and another bug hunt in `renderling`. + +### Porting gltf_images test +The test `gltf_images` tests our image decoding by loading a GLTF file and then +creating a new staged object that uses the image's texture. + +It's currently coming out all black, and it should come out like +![gltf_images test](test_img/gltf_images.png). + +I recently got rid of the distinction between "native" vertex data and GLTF vertex +data. Now there is only GLTF vertex data and the "native" `Vertex` meshes can be +conveniently staged (marshalled to the GPU) using a helper function that creates +a `GltfPrimitive` complete with `GltfAccessors` etc. + +Debbuging rabbit hole: +* Let's compare old vs new vertex shaders + - It doesn't seem to be the vertices, because the staged vertices (read from the GPU) are equal to the original mesh. + - The staged vertices are equal to the original CPU-side mesh, but the computed vertex values are different from legacy. + - It looks like transforms on `RenderUnits` are not transforming their child primitive's geometry + - Got it! It was because `GltfNode`'s `Default` instance was setting `scale` to `Vec3::ZERO`. + +## Wed Dec 20, 2023 + +I think I'm going to keep going with this idea of making GLTF the internal representation of the +renderer. + +## Tue Dec 19, 2023 + +### Thoughts on GLTF +GLTF on-the-slab has been a boon to this project and I'm tempted to make it the main way we do +rendering. I just want to write this down somewhere so I don't forget. Currently when loading +a GLTF file we traverse the GLTF document and store the whole thing on the GPU's slab. Then +the user has to specify which nodes (or a scene) to draw, which traverses one more time, linking +the `RenderUnit`s to the primitives within the GLTF. I _think_ it might be cognitively easier +to have GLTF nodes somehow be the base unit of rendering ... but I also have plans for supporting +SDFs and I'm not sure how that all fits together. + +* [At least one other person is thinking about putting SDFs in GLTF using an extension](https://community.khronos.org/t/signed-distance-field-representation-of-geometry-extension/109575) + +Anyway - I'll keep going with the momentum I have and think about refactoring towards this in the future. + +## Mon Dec 18, 2023 + +### Simple Texture GLTF Example +* The `simple_texture` test is rendering the texture upside-down. +* There are _no rotation transformations_ in its node's hierarchy. +* What does the atlas look like? + - It's not the atlas, the two tests (slabbed and the previous non-slabbed) have + identical atlas images. +* So what about UV coords? + - Comparing runs of the vertex shaders shows that the UV coords' Y components are flipped. + - So, 0.0 is 1.0 and 1.0 is 0.0 +* So is there something doing this intentionally? + - Nothing that I can easily see in the `gltf_support` modules... + - It has something to do with the accessor. + - I can see in the GLTF file that the accessor's byte offset is 48, but somehow in + my code it comes out 12... + - It was because the accessor's offset was not being taken into account. + +### Analytical Directional Lights +I got analytical lighting working (at least for directional lights) on the stage. +The problem I was having was that the shaders use `Camera.position` in lighting +equations, but that was defaulting to `Vec3::ZERO`. Previously in the "scene" +version of the renderer (which I'm porting over to "stage") the camera's position +was set automatically when setting the projection and/or view. +I had to run both versions of the vertex AND fragement shaders to track this down. Ugh! + +## Fri Dec 8, 2023 + +I've been having trouble getting the new GLTF files on-the-slab method to pass my +previous tests. Mainly because of little things I had forgotten. Little bits of +state that need to be updated to run the shaders. The most recent was that the +size of the atlas needs to be updated on the GPU when the atlas changes. + +I'm moving over tests from `renderling/scene/gltf_support.rs` to +`renderling/stage/gltf_support.rs` one at a time. + +## Thu Dec 7, 2023 + +Ongoing work to get GLTF files on-the-slab working. When this work is done GLTF +file imports should be lightening fast. + ## Wed Nov 15, 2023 I resubmitted the NLNet grant proposal with expanded scope to take care of [the diff --git a/NOTES.md b/NOTES.md index 828e181c..1e93736f 100644 --- a/NOTES.md +++ b/NOTES.md @@ -17,6 +17,7 @@ Just pro-cons on tech choices and little things I don't want to forget whil impl - using cargo and Rust module system - expressions! - type checking! + - traits! - editor tooling! ## cons / limititions @@ -28,6 +29,7 @@ Just pro-cons on tech choices and little things I don't want to forget whil impl * for loops are hit or miss, sometimes they work and sometimes they don't - see [this rust-gpu issue](https://github.com/EmbarkStudios/rust-gpu/issues/739) - see [conversation with eddyb on discord](https://discord.com/channels/750717012564770887/750717499737243679/threads/1092283362217046066) +* can't use `.max` or `.min` * meh, but no support for dynamically sized arrays (how would that work in no-std?) - see [conversation on discord](https://discord.com/channels/750717012564770887/750717499737243679/1091813590400516106) * can't use bitwise rotate_left or rotate_right diff --git a/crates/renderling-derive/Cargo.toml b/crates/crabslab-derive/Cargo.toml similarity index 91% rename from crates/renderling-derive/Cargo.toml rename to crates/crabslab-derive/Cargo.toml index 5ed681c9..21dab827 100644 --- a/crates/renderling-derive/Cargo.toml +++ b/crates/crabslab-derive/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "renderling-derive" +name = "crabslab-derive" version = "0.1.0" edition = "2021" diff --git a/crates/renderling-derive/src/lib.rs b/crates/crabslab-derive/src/lib.rs similarity index 89% rename from crates/renderling-derive/src/lib.rs rename to crates/crabslab-derive/src/lib.rs index 236210f4..76464ffe 100644 --- a/crates/renderling-derive/src/lib.rs +++ b/crates/crabslab-derive/src/lib.rs @@ -31,7 +31,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { _ => { return Err(syn::Error::new( name.span(), - "deriving Slabbed only supports structs".to_string(), + "deriving SlabItem only supports structs".to_string(), )) } }; @@ -41,7 +41,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { .map(|field| { let ty = &field.ty; quote! { - <#ty as renderling_shader::slab::Slabbed>::slab_size() + <#ty as crabslab::SlabItem>::slab_size() } }) .collect(); @@ -69,7 +69,7 @@ fn get_params(input: &DeriveInput) -> syn::Result { }) } -#[proc_macro_derive(Slabbed)] +#[proc_macro_derive(SlabItem)] pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input: DeriveInput = syn::parse_macro_input!(input); let name = &input.ident; @@ -88,8 +88,7 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre /// Adds a `CanFetch<'lt>` bound on each of the system data types. fn constrain_system_data_types(clause: &mut WhereClause, tys: &[Type]) { for ty in tys.iter() { - let where_predicate: WherePredicate = - syn::parse_quote!(#ty : renderling_shader::slab::Slabbed); + let where_predicate: WherePredicate = syn::parse_quote!(#ty : crabslab::SlabItem); clause.predicates.push(where_predicate); } } @@ -129,9 +128,11 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre FieldName::Ident(field) => Ident::new(&format!("offset_of_{}", field), field.span()), }; offsets.push(quote! { - pub fn #ident() -> usize { - #(<#offset_tys as renderling_shader::slab::Slabbed>::slab_size()+)* - 0 + pub fn #ident() -> crabslab::Offset<#ty> { + crabslab::Offset::new( + #(<#offset_tys as crabslab::SlabItem>::slab_size()+)* + 0 + ) } }); offset_tys.push(ty.clone()); @@ -145,7 +146,7 @@ pub fn derive_from_slab(input: proc_macro::TokenStream) -> proc_macro::TokenStre } #[automatically_derived] - impl #impl_generics renderling_shader::slab::Slabbed for #name #ty_generics #where_clause + impl #impl_generics crabslab::SlabItem for #name #ty_generics #where_clause { fn slab_size() -> usize { #(#sizes)+* diff --git a/crates/crabslab/Cargo.toml b/crates/crabslab/Cargo.toml new file mode 100644 index 00000000..2e618e4d --- /dev/null +++ b/crates/crabslab/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "crabslab" +version = "0.1.0" +edition = "2021" +description = "Slab allocator focused on GPU compute (rust-gpu)" +repository = "https://github.com/schell/renderling" +license = "MIT OR Apache-2.0" +keywords = ["game", "graphics", "shader", "rendering"] +categories = ["rendering", "game-development", "graphics"] +readme = "README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["wgpu", "glam", "futures-lite"] +futures-lite = ["dep:futures-lite"] +glam = ["dep:glam"] +wgpu = ["dep:wgpu", "dep:bytemuck", "dep:snafu", "dep:async-channel", "dep:log"] + +[dependencies] +async-channel = {workspace=true, optional=true} +bytemuck = {workspace=true, optional=true} +futures-lite = {workspace=true, optional=true} +log = {workspace=true, optional=true} +crabslab-derive = { version = "0.1.0", path = "../crabslab-derive" } +snafu = {workspace=true, optional=true} +wgpu = {workspace=true, optional=true} + +[target.'cfg(not(target_arch = "spirv"))'.dependencies] +glam = { workspace = true, features = ["std"], optional = true } + +[target.'cfg(target_arch = "spirv")'.dependencies] +glam = { version = "0.24.2", default-features = false, features = ["libm"], optional = true } diff --git a/crates/crabslab/README.md b/crates/crabslab/README.md new file mode 100644 index 00000000..5756011b --- /dev/null +++ b/crates/crabslab/README.md @@ -0,0 +1,34 @@ +
+ slabcraft for crabs +
+ +## what +`crabslab` is a slab implementation focused on marshalling data from CPUs to GPUs. + +## why +### Opinion +Working with shaders is much easier using a slab. + +### rust-gpu +This crate was made to work with [`rust-gpu`](https://github.com/EmbarkStudios/rust-gpu/). +Specifically, using this crate it is possible to pack your types into a buffer on the CPU +and then read your types from the slab on the GPU (in Rust). + +### Other no-std platforms +Even though this crate was written with `rust-gpu` in mind, it should work in other `no-std` +contexts. + +## how +`crabslab` includes: +* a few traits: + - `Slab` + - `GrowableSlab` + - `SlabItem` +* a derive macro for `SlabItem` +* a few structs for working with various slabs + - `Id` + - `Array` + - `Offset` +* a helper struct `CpuSlab` +* a feature-gated helper for using slabs with `wgpu` - `WgpuBuffer` + - [example](src/wgpu_slab.rs#L344) diff --git a/crates/crabslab/crabslab.png b/crates/crabslab/crabslab.png new file mode 100644 index 00000000..454cce23 Binary files /dev/null and b/crates/crabslab/crabslab.png differ diff --git a/crates/renderling-shader/src/array.rs b/crates/crabslab/src/array.rs similarity index 60% rename from crates/renderling-shader/src/array.rs rename to crates/crabslab/src/array.rs index 35591901..9d97af11 100644 --- a/crates/renderling-shader/src/array.rs +++ b/crates/crabslab/src/array.rs @@ -1,9 +1,29 @@ //! A slab-allocated array. use core::marker::PhantomData; -use crate::id::Id; -use crate::slab::Slabbed; +use crate::{id::Id, slab::SlabItem}; +#[derive(Clone, Copy)] +pub struct ArrayIter { + array: Array, + index: usize, +} + +impl Iterator for ArrayIter { + type Item = Id; + + fn next(&mut self) -> Option { + if self.index >= self.array.len() { + None + } else { + let id = self.array.at(self.index); + self.index += 1; + Some(id) + } + } +} + +/// A pointer to contiguous `T` elements in a slab. #[repr(C)] pub struct Array { // u32 offset in the slab @@ -25,17 +45,32 @@ impl Clone for Array { impl Copy for Array {} +/// An `Id` is an `Array` with a length of 1. +impl From> for Array { + fn from(id: Id) -> Self { + Self { + index: id.inner(), + len: 1, + _phantom: PhantomData, + } + } +} + impl core::fmt::Debug for Array { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct(if self.is_null() { - "Array (null)" + if self.is_null() { + f.write_fmt(core::format_args!( + "Array<{}>(null)", + core::any::type_name::() + )) } else { - "Array" - }) - .field("index", &self.index) - .field("len", &self.len) - .field("_phantom", &self._phantom) - .finish() + f.write_fmt(core::format_args!( + "Array<{}>({}, {})", + core::any::type_name::(), + self.index, + self.len + )) + } } } @@ -45,7 +80,7 @@ impl PartialEq for Array { } } -impl Slabbed for Array { +impl SlabItem for Array { fn slab_size() -> usize { 2 } @@ -71,7 +106,7 @@ impl Slabbed for Array { } } -impl Default for Array { +impl Default for Array { fn default() -> Self { Self { index: u32::MAX, @@ -108,7 +143,7 @@ impl Array { pub fn at(&self, index: usize) -> Id where - T: Slabbed, + T: SlabItem, { if index >= self.len() { Id::NONE @@ -121,10 +156,17 @@ impl Array { self.index as usize } + pub fn iter(&self) -> ArrayIter { + ArrayIter { + array: *self, + index: 0, + } + } + /// Convert this array into a `u32` array. pub fn into_u32_array(self) -> Array where - T: Slabbed, + T: SlabItem, { Array { index: self.index, @@ -132,4 +174,14 @@ impl Array { _phantom: PhantomData, } } + + #[cfg(not(target_arch = "spirv"))] + /// Return the slice of the slab that this array represents. + pub fn sub_slab<'a>(&'a self, slab: &'a [u32]) -> &[u32] + where + T: SlabItem, + { + let arr = self.into_u32_array(); + &slab[arr.index as usize..(arr.index + arr.len) as usize] + } } diff --git a/crates/renderling-shader/src/id.rs b/crates/crabslab/src/id.rs similarity index 56% rename from crates/renderling-shader/src/id.rs rename to crates/crabslab/src/id.rs index f7a2d5f4..7ca1444d 100644 --- a/crates/renderling-shader/src/id.rs +++ b/crates/crabslab/src/id.rs @@ -1,13 +1,13 @@ //! Typed identifiers that can also be used as indices. use core::marker::PhantomData; -use crate::{self as renderling_shader, slab::Slabbed}; +use crate::{self as crabslab, slab::SlabItem}; pub const ID_NONE: u32 = u32::MAX; -/// An identifier. +/// An identifier that can be used to read or write a type from/into the slab. #[repr(transparent)] -#[derive(bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(SlabItem)] pub struct Id(pub(crate) u32, PhantomData); impl PartialOrd for Id { @@ -71,10 +71,18 @@ impl Default for Id { impl core::fmt::Debug for Id { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Id") - .field("type", &core::any::type_name::()) - .field("index", &self.0) - .finish() + if self.is_none() { + f.write_fmt(core::format_args!( + "Id<{}>(null)", + &core::any::type_name::(), + )) + } else { + f.write_fmt(core::format_args!( + "Id<{}>({})", + &core::any::type_name::(), + &self.0 + )) + } } } @@ -136,17 +144,85 @@ impl Id { } } +/// The offset of a field relative a parent's `Id`. +/// +/// Offset functions are automatically derived for `SlabItem` structs. +/// +/// ```rust +/// use crabslab::{Id, Offset, Slab, SlabItem}; +/// +/// #[derive(Debug, Default, PartialEq, SlabItem)] +/// pub struct Parent { +/// pub child_a: u32, +/// pub child_b: u32, +/// } +/// +/// let mut slab = [0u32; 10]; +/// +/// let parent_id = Id::new(3); +/// let parent = Parent { +/// child_a: 0, +/// child_b: 1, +/// }; +/// slab.write(parent_id, &parent); +/// assert_eq!(parent, slab.read(parent_id)); +/// +/// slab.write(parent_id + Parent::offset_of_child_a(), &42); +/// let a = slab.read(parent_id + Parent::offset_of_child_a()); +/// assert_eq!(42, a); +/// ``` +pub struct Offset { + pub offset: u32, + _phantom: PhantomData, +} + +impl core::ops::Add> for Offset { + type Output = Id; + + fn add(self, rhs: Id) -> Self::Output { + Id::new(self.offset + rhs.0) + } +} + +impl core::ops::Add> for Id { + type Output = Id; + + fn add(self, rhs: Offset) -> Self::Output { + Id::new(self.0 + rhs.offset) + } +} + +impl From> for Id { + fn from(value: Offset) -> Self { + Id::new(value.offset) + } +} + +impl Offset { + pub const fn new(offset: usize) -> Self { + Self { + offset: offset as u32, + _phantom: PhantomData, + } + } +} + #[cfg(test)] mod test { - use crate::stage::GpuEntity; - use super::*; + #[derive(SlabItem)] + struct MyEntity { + name: u32, + age: f32, + destiny: [u32; 3], + } + #[test] fn id_size() { assert_eq!( std::mem::size_of::(), - std::mem::size_of::>(), + std::mem::size_of::>(), "id is not u32" ); } diff --git a/crates/crabslab/src/lib.rs b/crates/crabslab/src/lib.rs new file mode 100644 index 00000000..22aa6848 --- /dev/null +++ b/crates/crabslab/src/lib.rs @@ -0,0 +1,18 @@ +#![cfg_attr(target_arch = "spirv", no_std)] +//! Creating and crafting a tasty slab of memory. + +mod array; +pub use array::*; + +mod id; +pub use id::*; + +mod slab; +pub use slab::*; + +#[cfg(feature = "wgpu")] +mod wgpu_slab; +#[cfg(feature = "wgpu")] +pub use wgpu_slab::*; + +pub use crabslab_derive::SlabItem; diff --git a/crates/renderling-shader/src/slab.rs b/crates/crabslab/src/slab.rs similarity index 57% rename from crates/renderling-shader/src/slab.rs rename to crates/crabslab/src/slab.rs index 3c357742..1a90c36d 100644 --- a/crates/renderling-shader/src/slab.rs +++ b/crates/crabslab/src/slab.rs @@ -1,18 +1,17 @@ -//! Slab storage and ops used for storing on CPU and extracting on GPU. -use core::marker::PhantomData; +//! Slab traits. +use core::{default::Default, marker::PhantomData}; +pub use crabslab_derive::SlabItem; -pub use renderling_derive::Slabbed; - -use crate::id::Id; +use crate::{array::Array, id::Id}; /// Determines the "size" of a type when stored in a slab of `&[u32]`, /// and how to read/write it from/to the slab. /// -/// `Slabbed` can be automatically derived for struct and tuple types, +/// `SlabItem` can be automatically derived for struct and tuple types, /// so long as those types are relatively simple. So far, autoderiving /// fields with these types will **not compile** on one or more targets: /// * `PhantomData` - will not compile on `target_arch = "spirv"` -pub trait Slabbed: core::any::Any + Sized { +pub trait SlabItem: core::any::Any + Sized { /// The number of `u32`s this type occupies in a slab of `&[u32]`. fn slab_size() -> usize; @@ -31,7 +30,7 @@ pub trait Slabbed: core::any::Any + Sized { fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize; } -impl Slabbed for bool { +impl SlabItem for bool { fn slab_size() -> usize { 1 } @@ -48,7 +47,7 @@ impl Slabbed for bool { } } -impl Slabbed for u32 { +impl SlabItem for u32 { fn slab_size() -> usize { 1 } @@ -72,7 +71,7 @@ impl Slabbed for u32 { } } -impl Slabbed for f32 { +impl SlabItem for f32 { fn slab_size() -> usize { 1 } @@ -96,7 +95,7 @@ impl Slabbed for f32 { } } -impl Slabbed for Option { +impl SlabItem for Option { fn slab_size() -> usize { 1 + T::slab_size() } @@ -126,9 +125,9 @@ impl Slabbed for Option { } } -impl Slabbed for [T; N] { +impl SlabItem for [T; N] { fn slab_size() -> usize { - ::slab_size() * N + ::slab_size() * N } fn read_slab(&mut self, mut index: usize, slab: &[u32]) -> usize { @@ -146,7 +145,8 @@ impl Slabbed for [T; N] { } } -impl Slabbed for glam::Mat4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Mat4 { fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { let Self { x_axis, @@ -178,7 +178,8 @@ impl Slabbed for glam::Mat4 { } } -impl Slabbed for glam::Vec2 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Vec2 { fn slab_size() -> usize { 2 } @@ -199,7 +200,7 @@ impl Slabbed for glam::Vec2 { } } -impl Slabbed for glam::Vec3 { +impl SlabItem for glam::Vec3 { fn slab_size() -> usize { 3 } @@ -221,7 +222,8 @@ impl Slabbed for glam::Vec3 { } } -impl Slabbed for glam::Vec4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::Vec4 { fn slab_size() -> usize { 4 } @@ -242,7 +244,7 @@ impl Slabbed for glam::Vec4 { } } -impl Slabbed for glam::Quat { +impl SlabItem for glam::Quat { fn slab_size() -> usize { 16 } @@ -264,7 +266,8 @@ impl Slabbed for glam::Quat { } } -impl Slabbed for glam::UVec2 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec2 { fn slab_size() -> usize { 2 } @@ -282,7 +285,8 @@ impl Slabbed for glam::UVec2 { } } -impl Slabbed for glam::UVec3 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec3 { fn slab_size() -> usize { 3 } @@ -302,7 +306,8 @@ impl Slabbed for glam::UVec3 { } } -impl Slabbed for glam::UVec4 { +#[cfg(feature = "glam")] +impl SlabItem for glam::UVec4 { fn slab_size() -> usize { 4 } @@ -327,7 +332,7 @@ impl Slabbed for glam::UVec4 { } } -impl Slabbed for PhantomData { +impl SlabItem for PhantomData { fn slab_size() -> usize { 0 } @@ -341,20 +346,21 @@ impl Slabbed for PhantomData { } } +/// Trait for slabs of `u32`s that can store many types. pub trait Slab { /// Return the number of u32 elements in the slab. fn len(&self) -> usize; /// Returns whether the slab may contain the value with the given id. - fn contains(&self, id: Id) -> bool { + fn contains(&self, id: Id) -> bool { id.index() + T::slab_size() <= self.len() } /// Read the type from the slab using the Id as the index. - fn read(&self, id: Id) -> T; + fn read(&self, id: Id) -> T; #[cfg(not(target_arch = "spirv"))] - fn read_vec(&self, array: crate::array::Array) -> Vec { + fn read_vec(&self, array: crate::array::Array) -> Vec { let mut vec = Vec::with_capacity(array.len()); for i in 0..array.len() { let id = array.at(i); @@ -365,13 +371,35 @@ pub trait Slab { /// Write the type into the slab at the index. /// - /// Return the next index, or the same index if writing would overlap the slab. - fn write(&mut self, t: &T, index: usize) -> usize; + /// Return the next index, or the same index if writing would overlap the + /// slab. + fn write_indexed(&mut self, t: &T, index: usize) -> usize; /// Write a slice of the type into the slab at the index. /// - /// Return the next index, or the same index if writing would overlap the slab. - fn write_slice(&mut self, t: &[T], index: usize) -> usize; + /// Return the next index, or the same index if writing would overlap the + /// slab. + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize; + + /// Write the type into the slab at the position of the given `Id`. + /// + /// This likely performs a partial write if the given `Id` is out of bounds. + fn write(&mut self, id: Id, t: &T) { + let _ = self.write_indexed(t, id.index()); + } + + /// Write contiguous elements into the slab at the position of the given + /// `Array`. + /// + /// ## NOTE + /// This does nothing if the length of `Array` is greater than the length of + /// `data`. + fn write_array(&mut self, array: Array, data: &[T]) { + if array.len() > data.len() { + return; + } + let _ = self.write_indexed_slice(data, array.starting_index()); + } } impl Slab for [u32] { @@ -379,17 +407,17 @@ impl Slab for [u32] { self.len() } - fn read(&self, id: Id) -> T { + fn read(&self, id: Id) -> T { let mut t = T::default(); let _ = t.read_slab(id.index(), self); t } - fn write(&mut self, t: &T, index: usize) -> usize { + fn write_indexed(&mut self, t: &T, index: usize) -> usize { t.write_slab(index, self) } - fn write_slice(&mut self, t: &[T], index: usize) -> usize { + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { let mut index = index; for item in t { index = item.write_slab(index, self); @@ -404,16 +432,178 @@ impl Slab for Vec { self.len() } - fn read(&self, id: Id) -> T { + fn read(&self, id: Id) -> T { self.as_slice().read(id) } - fn write(&mut self, t: &T, index: usize) -> usize { - self.as_mut_slice().write(t, index) + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + self.as_mut_slice().write_indexed(t, index) } - fn write_slice(&mut self, t: &[T], index: usize) -> usize { - self.as_mut_slice().write_slice(t, index) + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + self.as_mut_slice().write_indexed_slice(t, index) + } +} + +/// Trait for slabs of `u32`s that can store many types, and can grow to fit. +pub trait GrowableSlab: Slab { + /// Return the current capacity of the slab. + fn capacity(&self) -> usize; + + /// Reserve enough space on the slab to fit the given capacity. + fn reserve_capacity(&mut self, capacity: usize); + + /// Increment the length of the slab by `n` u32s. + /// + /// Returns the previous length. + fn increment_len(&mut self, n: usize) -> usize; + + + /// Expands the slab to fit the given number of `T`s, if necessary. + fn maybe_expand_to_fit(&mut self, len: usize) { + let size = T::slab_size(); + let capacity = self.capacity(); + //log::trace!( + // "append_slice: {size} * {ts_len} + {len} ({}) >= {capacity}", + // size * ts_len + len + //); + let capacity_needed = self.len() + size * len; + if capacity_needed > capacity { + let mut new_capacity = capacity * 2; + while new_capacity < capacity_needed { + new_capacity = (new_capacity * 2).max(2); + } + self.reserve_capacity(new_capacity); + } + } + + /// Preallocate space for one `T` element, but don't write anything to the + /// buffer. + /// + /// The returned `Id` can be used to write later with [`Self::write`]. + /// + /// NOTE: This changes the next available buffer index and may change the + /// buffer capacity. + fn allocate(&mut self) -> Id { + self.maybe_expand_to_fit::(1); + let index = self.increment_len(T::slab_size()); + Id::from(index) + } + + /// Preallocate space for `len` `T` elements, but don't write to + /// the buffer. + /// + /// This can be used to allocate space for a bunch of elements that get + /// written later with [`Self::write_array`]. + /// + /// NOTE: This changes the length of the buffer and may change the capacity. + fn allocate_array(&mut self, len: usize) -> Array { + if len == 0 { + return Array::default(); + } + self.maybe_expand_to_fit::(len); + let index = self.increment_len(T::slab_size() * len); + Array::new(index as u32, len as u32) + } + + /// Append to the end of the buffer. + /// + /// Returns the `Id` of the written element. + fn append(&mut self, t: &T) -> Id { + let id = self.allocate::(); + // IGNORED: safe because we just allocated the id + let _ = self.write(id, t); + id + } + + /// Append a slice to the end of the buffer, resizing if necessary + /// and returning a slabbed array. + /// + /// Returns the `Array` of the written elements. + fn append_array(&mut self, ts: &[T]) -> Array { + let array = self.allocate_array::(ts.len()); + // IGNORED: safe because we just allocated the array + let _ = self.write_array(array, ts); + array + } +} + +/// A wrapper around a `GrowableSlab` that provides convenience methods for +/// working with CPU-side slabs. +/// +/// Working with slabs on the CPU is much more convenient because the underlying +/// buffer `B` is often a growable type, like `Vec`. This wrapper provides +/// methods for appending to the end of the buffer with automatic resizing and +/// for preallocating space for elements that will be written later. +pub struct CpuSlab { + slab: B, +} + +impl AsRef for CpuSlab { + fn as_ref(&self) -> &B { + &self.slab + } +} + +impl AsMut for CpuSlab { + fn as_mut(&mut self) -> &mut B { + &mut self.slab + } +} + +impl Slab for CpuSlab { + fn len(&self) -> usize { + self.slab.len() + } + + fn read(&self, id: Id) -> T { + self.slab.read(id) + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + self.slab.write_indexed(t, index) + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + self.slab.write_indexed_slice(t, index) + } +} + +impl GrowableSlab for CpuSlab { + fn capacity(&self) -> usize { + self.slab.capacity() + } + + fn reserve_capacity(&mut self, capacity: usize) { + self.slab.reserve_capacity(capacity); + } + + fn increment_len(&mut self, n: usize) -> usize { + self.slab.increment_len(n) + } +} + +impl CpuSlab { + /// Create a new `SlabBuffer` with the given slab. + pub fn new(slab: B) -> Self { + Self { slab } + } +} + +#[cfg(not(target_arch = "spirv"))] +impl GrowableSlab for Vec { + fn capacity(&self) -> usize { + Vec::capacity(self) + } + + fn reserve_capacity(&mut self, capacity: usize) { + Vec::reserve(self, capacity - self.capacity()); + } + + fn increment_len(&mut self, n: usize) -> usize { + let index = self.len(); + self.extend(core::iter::repeat(0).take(n)); + index } } @@ -421,23 +611,30 @@ impl Slab for Vec { mod test { use glam::Vec4; - use crate::{array::Array, stage::Vertex}; + use crate::{self as crabslab, Array, CpuSlab, SlabItem}; use super::*; + #[derive(Debug, Default, PartialEq, SlabItem)] + struct Vertex { + position: Vec4, + color: Vec4, + uv: glam::Vec2, + } + #[test] fn slab_array_readwrite() { let mut slab = [0u32; 16]; - slab.write(&42, 0); - slab.write(&666, 1); + slab.write_indexed(&42, 0); + slab.write_indexed(&666, 1); let t = slab.read(Id::<[u32; 2]>::new(0)); assert_eq!([42, 666], t); let t: Vec = slab.read_vec(Array::new(0, 2)); assert_eq!([42, 666], t[..]); - slab.write_slice(&[1, 2, 3, 4], 2); + slab.write_indexed_slice(&[1, 2, 3, 4], 2); let t: Vec = slab.read_vec(Array::new(2, 4)); assert_eq!([1, 2, 3, 4], t[..]); - slab.write_slice(&[[1.0, 2.0, 3.0, 4.0], [5.5, 6.5, 7.5, 8.5]], 0); + slab.write_indexed_slice(&[[1.0, 2.0, 3.0, 4.0], [5.5, 6.5, 7.5, 8.5]], 0); let arr = Array::<[f32; 4]>::new(0, 2); assert_eq!(Id::new(0), arr.at(0)); @@ -481,10 +678,10 @@ mod test { let mut slab = vec![0u32; geometry_slab_size + Array::::slab_size()]; let index = 0usize; let vertices = Array::::new(index as u32, geometry.len() as u32); - let index = slab.write_slice(&geometry, index); + let index = slab.write_indexed_slice(&geometry, index); assert_eq!(geometry_slab_size, index); let vertices_id = Id::>::from(index); - let index = slab.write(&vertices, index); + let index = slab.write_indexed(&vertices, index); assert_eq!(geometry_slab_size + Array::::slab_size(), index); assert_eq!(Vertex::slab_size() * 6, vertices_id.index()); assert!(slab.contains(vertices_id),); @@ -492,4 +689,27 @@ mod test { let array = slab.read(vertices_id); assert_eq!(vertices, array); } + + #[test] + fn cpuslab_sanity() { + let mut slab = CpuSlab::new(vec![]); + let v = Vertex { + position: Vec4::new(0.5, -0.5, 0.0, 1.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }; + let id = slab.append(&v); + assert_eq!(Id::new(0), id); + assert_eq!(v, slab.read(id)); + + let f32s = [1.1, 2.2, 3.3, 4.4f32]; + let array = slab.append_array(&f32s); + assert_eq!(1.1, slab.read(array.at(0))); + assert_eq!(2.2, slab.read(array.at(1))); + assert_eq!(3.3, slab.read(array.at(2))); + assert_eq!(4.4, slab.read(array.at(3))); + + let f32_vec = slab.read_vec(array); + assert_eq!(f32s, f32_vec[..]); + } } diff --git a/crates/crabslab/src/wgpu_slab.rs b/crates/crabslab/src/wgpu_slab.rs new file mode 100644 index 00000000..ec7af5f4 --- /dev/null +++ b/crates/crabslab/src/wgpu_slab.rs @@ -0,0 +1,376 @@ +//! CPU side of slab storage using `wgpu`. +use std::{ + ops::Deref, + sync::{atomic::AtomicUsize, Arc}, +}; + +use crate::{GrowableSlab, Id, Slab, SlabItem}; +use snafu::{IntoError, ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum SlabError { + #[snafu(display( + "Out of capacity. Tried to write {type_is}(slab size={slab_size}) at {index} but capacity \ + is {capacity}", + ))] + Capacity { + type_is: &'static str, + slab_size: usize, + index: usize, + capacity: usize, + }, + + #[snafu(display( + "Out of capacity. Tried to write an array of {elements} {type_is}(each of slab \ + size={slab_size}) at {index} but capacity is {capacity}", + ))] + ArrayCapacity { + type_is: &'static str, + elements: usize, + slab_size: usize, + index: usize, + capacity: usize, + }, + + #[snafu(display( + "Array({type_is}) length mismatch. Tried to write {data_len} elements into array of \ + length {array_len}", + ))] + ArrayLen { + type_is: &'static str, + array_len: usize, + data_len: usize, + }, + + #[snafu(display("Async recv error: {source}"))] + AsyncRecv { source: async_channel::RecvError }, + + #[snafu(display("Async error: {source}"))] + Async { source: wgpu::BufferAsyncError }, +} + +pub fn print_slab(slab: &[u32], starting_index: usize) { + for (u, i) in slab.iter().zip(starting_index..) { + println!("{i:02}: {u:032b} {u:010} {:?}", f32::from_bits(*u)); + } +} + +/// A slab buffer used by the stage to store heterogeneous objects. +pub struct WgpuBuffer { + pub(crate) buffer: wgpu::Buffer, + device: Arc, + queue: Arc, + // The number of u32 elements currently stored in the buffer. + // + // This is the next index to write into. + len: AtomicUsize, + // The total number of u32 elements that can be stored in the buffer. + capacity: AtomicUsize, +} + +impl Slab for WgpuBuffer { + fn len(&self) -> usize { + self.len.load(std::sync::atomic::Ordering::Relaxed) + } + + fn read(&self, id: Id) -> T { + futures_lite::future::block_on(self.read_async(id)).unwrap() + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + let byte_offset = index * std::mem::size_of::(); + let size = T::slab_size(); + let mut bytes = vec![0u32; size]; + let _ = bytes.write_indexed(t, 0); + let capacity = self.capacity(); + if index + size > capacity { + log::error!( + "could not write to slab: {}", + CapacitySnafu { + type_is: std::any::type_name::(), + slab_size: T::slab_size(), + index, + capacity + } + .into_error(snafu::NoneError) + ); + return index; + } + let encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + self.queue.write_buffer( + &self.buffer, + byte_offset as u64, + bytemuck::cast_slice(bytes.as_slice()), + ); + self.queue.submit(std::iter::once(encoder.finish())); + + index + size + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + let capacity = self.capacity(); + let size = T::slab_size() * t.len(); + if index + size > capacity { + log::error!( + "could not write array to slab: {}", + ArrayCapacitySnafu { + capacity, + type_is: std::any::type_name::(), + elements: t.len(), + slab_size: T::slab_size(), + index + } + .into_error(snafu::NoneError) + ); + return index; + } + let encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + let mut u32_data = vec![0u32; size]; + let _ = u32_data.write_indexed_slice(t, 0); + let byte_offset = index * std::mem::size_of::(); + self.queue.write_buffer( + &self.buffer, + byte_offset as u64, + bytemuck::cast_slice(&u32_data), + ); + self.queue.submit(std::iter::once(encoder.finish())); + + index + size + } +} + +impl GrowableSlab for WgpuBuffer { + fn capacity(&self) -> usize { + self.capacity.load(std::sync::atomic::Ordering::Relaxed) + } + + /// Resize the slab buffer. + /// + /// This creates a new buffer and writes the data from the old into the new. + fn reserve_capacity(&mut self, new_capacity: usize) { + let capacity = self.capacity(); + if new_capacity > capacity { + log::trace!("resizing buffer from {capacity} to {new_capacity}"); + let len = self.len(); + let new_buffer = Self::new_buffer(&self.device, new_capacity, self.buffer.usage()); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + // UNWRAP: if we can't lock we want to panic + encoder.copy_buffer_to_buffer( + &self.buffer, + 0, + &new_buffer, + 0, + (len * std::mem::size_of::()) as u64, + ); + self.queue.submit(std::iter::once(encoder.finish())); + self.buffer = new_buffer; + self.capacity + .store(new_capacity, std::sync::atomic::Ordering::Relaxed); + } + } + + fn increment_len(&mut self, n: usize) -> usize { + self.len.fetch_add(n, std::sync::atomic::Ordering::Relaxed) + } +} + +impl WgpuBuffer { + fn new_buffer( + device: &wgpu::Device, + capacity: usize, + usage: wgpu::BufferUsages, + ) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("SlabBuffer"), + size: (capacity * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::STORAGE + | wgpu::BufferUsages::COPY_DST + | wgpu::BufferUsages::COPY_SRC + | usage, + mapped_at_creation: false, + }) + } + + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new( + device: impl Into>, + queue: impl Into>, + capacity: usize, + ) -> Self { + Self::new_usage(device, queue, capacity, wgpu::BufferUsages::empty()) + } + + /// Create a new slab buffer with a capacity of `capacity` u32 elements. + pub fn new_usage( + device: impl Into>, + queue: impl Into>, + capacity: usize, + usage: wgpu::BufferUsages, + ) -> Self { + let device = device.into(); + let queue = queue.into(); + Self { + buffer: Self::new_buffer(&device, capacity, usage), + len: AtomicUsize::new(0).into(), + capacity: AtomicUsize::new(capacity).into(), + device, + queue, + } + } + + #[cfg(feature = "futures-lite")] + /// Read from the slab buffer synchronously. + pub fn block_on_read_raw(&self, start: usize, len: usize) -> Result, SlabError> { + futures_lite::future::block_on(self.read_raw(start, len)) + } + + /// Read from the slab buffer. + pub async fn read_raw(&self, start: usize, len: usize) -> Result, SlabError> { + let byte_offset = start * std::mem::size_of::(); + let length = len * std::mem::size_of::(); + let output_buffer_size = length as u64; + let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("SlabBuffer::read_raw"), + size: output_buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + log::trace!( + "copy_buffer_to_buffer byte_offset:{byte_offset}, \ + output_buffer_size:{output_buffer_size}", + ); + encoder.copy_buffer_to_buffer( + &self.buffer, + byte_offset as u64, + &output_buffer, + 0, + output_buffer_size, + ); + self.queue.submit(std::iter::once(encoder.finish())); + + let buffer_slice = output_buffer.slice(..); + let (tx, rx) = async_channel::bounded(1); + buffer_slice.map_async(wgpu::MapMode::Read, move |res| tx.try_send(res).unwrap()); + self.device.poll(wgpu::Maintain::Wait); + rx.recv() + .await + .context(AsyncRecvSnafu)? + .context(AsyncSnafu)?; + let bytes = buffer_slice.get_mapped_range(); + Ok(bytemuck::cast_slice(bytes.deref()).to_vec()) + } + + /// Read from the slab buffer. + pub async fn read_async(&self, id: Id) -> Result { + let vec = self.read_raw(id.index(), T::slab_size()).await?; + let t = Slab::read(vec.as_slice(), Id::::new(0)); + Ok(t) + } + + /// Get the underlying buffer. + pub fn get_buffer(&self) -> &wgpu::Buffer { + &self.buffer + } +} + +#[cfg(test)] +mod test { + use crate::CpuSlab; + + use super::*; + + fn get_device_and_queue() -> (wgpu::Device, wgpu::Queue) { + // The instance is a handle to our GPU + // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU + let backends = wgpu::Backends::all(); + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + }); + + let limits = wgpu::Limits::default(); + + let adapter = futures_lite::future::block_on(instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: None, + force_fallback_adapter: false, + }, + )) + .unwrap(); + + let info = adapter.get_info(); + log::trace!( + "using adapter: '{}' backend:{:?} driver:'{}'", + info.name, + info.backend, + info.driver + ); + + futures_lite::future::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + features: + // this one is a funny requirement, it seems it is needed if + // using storage buffers in vertex shaders, even if those + // shaders are read-only + wgpu::Features::VERTEX_WRITABLE_STORAGE, + limits, + label: None, + }, + None, // Trace path + )) + .unwrap() + } + + #[test] + fn slab_buffer_roundtrip() { + let (device, queue) = get_device_and_queue(); + let buffer = WgpuBuffer::new(device, queue, 2); + let mut slab = CpuSlab::new(buffer); + slab.append(&42); + slab.append(&1); + let id = Id::<[u32; 2]>::new(0); + let t = slab.read(id); + assert_eq!([42, 1], t, "read back what we wrote"); + + println!("overflow"); + let id = Id::::new(2); + slab.write(id, &666); + assert_eq!(2, slab.len()); + + println!("append"); + slab.append(&666); + let id = Id::<[u32; 3]>::new(0); + let t = slab.read(id); + assert_eq!([42, 1, 666], t); + + println!("append slice"); + let a = glam::Vec3::new(0.0, 0.0, 0.0); + let b = glam::Vec3::new(1.0, 1.0, 1.0); + let c = glam::Vec3::new(2.0, 2.0, 2.0); + let points = vec![a, b, c]; + let array = slab.append_array(&points); + let slab_u32 = + futures_lite::future::block_on(slab.as_ref().read_raw(0, slab.len())).unwrap(); + let points_out = slab_u32.read_vec::(array); + assert_eq!(points, points_out); + + println!("append slice 2"); + let points = vec![a, a, a, a, b, b, b, c, c]; + let array = slab.append_array(&points); + let slab_u32 = + futures_lite::future::block_on(slab.as_ref().read_raw(0, slab.len())).unwrap(); + let points_out = slab_u32.read_vec::(array); + assert_eq!(points, points_out); + } +} diff --git a/crates/example/src/gltf.rs b/crates/example/src/gltf.rs index 7577b990..1753b0cb 100644 --- a/crates/example/src/gltf.rs +++ b/crates/example/src/gltf.rs @@ -10,7 +10,7 @@ use instant::Instant; use renderling::{ debug::DebugChannel, math::{Mat4, Vec3, Vec4}, - GltfLoader, GpuEntity, RenderGraphConfig, Renderling, Scene, SceneImage, ScreenSize, + GltfLoader, GpuEntity, RenderGraphConfig, Renderling, Scene, SceneImage, ScreenSize, Stage, TweenProperty, UiDrawObjects, UiMode, UiVertex, ViewMut, }; use renderling_gpui::{Element, Gpui}; diff --git a/crates/img-diff/src/lib.rs b/crates/img-diff/src/lib.rs index c6b93f01..98220bf1 100644 --- a/crates/img-diff/src/lib.rs +++ b/crates/img-diff/src/lib.rs @@ -6,6 +6,21 @@ use std::path::Path; const TEST_IMG_DIR: &str = "../../test_img"; const TEST_OUTPUT_DIR: &str = "../../test_output"; +const PIXEL_MAGNITUDE_THRESHOLD: f32 = 0.1; +const IMAGE_DIFF_THRESHOLD: f32 = 0.05; + +fn checkerboard_background_color(x: u32, y: u32) -> Vec4 { + let size = 16; + let x_square_index = x / size; + let x_grey = x_square_index % 2 == 0; + let y_square_index = y / size; + let y_grey = y_square_index % 2 == 0; + if (x_grey && y_grey) || (!x_grey && !y_grey) { + Vec4::from([0.5, 0.5, 0.5, 1.0]) + } else { + Vec4::from([1.0, 1.0, 1.0, 1.0]) + } +} #[derive(Debug, Snafu)] enum ImgDiffError { @@ -13,6 +28,27 @@ enum ImgDiffError { ImageSize, } +pub struct DiffCfg { + /// The threshold for a pixel to be considered different. + /// + /// Difference is measured as the magnitude of vector subtraction + /// between the two pixels. + pub pixel_threshold: f32, + /// The percentage of "different" pixels (as determined using + /// `pixel_threshold`) to "correct" pixels that the image must contain + /// before it is considered an error. + pub image_threshold: f32, +} + +impl Default for DiffCfg { + fn default() -> Self { + Self { + pixel_threshold: PIXEL_MAGNITUDE_THRESHOLD, + image_threshold: IMAGE_DIFF_THRESHOLD, + } + } +} + fn get_results( left_image: &Rgba32FImage, right_image: &Rgba32FImage, @@ -29,11 +65,14 @@ fn get_results( if left_pixel == right_pixel { None } else { + // pre-multiply alpha let left_pixel = Vec4::from(left_pixel.0); + let left_pixel = (left_pixel * left_pixel.w).xyz(); let right_pixel = Vec4::from(right_pixel.0); + let right_pixel = (right_pixel * right_pixel.w).xyz(); let delta = (left_pixel - right_pixel).abs(); if delta.length() > threshold { - Some((x, y, delta)) + Some((x, y, delta.extend(1.0))) } else { None } @@ -47,8 +86,22 @@ fn get_results( } else { let mut output_image = image::ImageBuffer::from_pixel(width, height, Rgba([0.0, 0.0, 0.0, 0.0])); + + for x in 0..width { + for y in 0..height { + output_image.put_pixel(x, y, Rgba(checkerboard_background_color(x, y).into())); + } + } + for (x, y, delta) in results { - let color = delta.xyz().extend(1.0); + let bg = checkerboard_background_color(x, y); + let a = 1.0 - delta.z; + let color = Vec4::new( + bg.x * a + delta.x, + bg.y * a + delta.y, + bg.z * a + delta.z, + 1.0, + ); output_image.put_pixel(x, y, Rgba(color.into())); } Ok(Some((diffs, output_image))) @@ -61,11 +114,28 @@ pub fn save(filename: &str, seen: impl Into) { seen.into().save(path).unwrap(); } -pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { +pub fn assert_eq_cfg( + filename: &str, + lhs: impl Into, + rhs: impl Into, + cfg: DiffCfg, +) { let lhs = lhs.into(); let lhs = lhs.into_rgba32f(); let rhs = rhs.into().into_rgba32f(); - if let Some((diffs, diff_image)) = get_results(&lhs, &rhs, 0.5).unwrap() { + let DiffCfg { + pixel_threshold, + image_threshold, + } = cfg; + if let Some((diffs, diff_image)) = get_results(&lhs, &rhs, pixel_threshold).unwrap() { + println!("{filename} has {diffs} pixel differences (threshold={pixel_threshold})"); + let percent_diff = diffs as f32 / (lhs.width() * lhs.height()) as f32; + println!("{filename}'s image is {percent_diff} different (threshold={image_threshold})"); + if percent_diff < image_threshold { + return; + } + + let mut dir = Path::new(TEST_OUTPUT_DIR).join(filename); dir.set_extension(""); std::fs::create_dir_all(&dir).expect("cannot create test output dir"); @@ -93,7 +163,11 @@ pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { +pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { + assert_eq_cfg(filename, lhs, rhs, DiffCfg::default()) +} + +pub fn assert_img_eq_cfg(filename: &str, seen: impl Into, cfg: DiffCfg) { let cwd = std::env::current_dir().expect("no cwd"); let lhs = image::open(Path::new(TEST_IMG_DIR).join(filename)).unwrap_or_else(|_| { panic!( @@ -101,5 +175,9 @@ pub fn assert_img_eq(filename: &str, seen: impl Into) { cwd.join(filename).display() ) }); - assert_eq(filename, lhs, seen) + assert_eq_cfg(filename, lhs, seen, cfg) +} + +pub fn assert_img_eq(filename: &str, seen: impl Into) { + assert_img_eq_cfg(filename, seen, DiffCfg::default()) } diff --git a/crates/loading-bytes/Cargo.toml b/crates/loading-bytes/Cargo.toml index da4ce86a..07df88fa 100644 --- a/crates/loading-bytes/Cargo.toml +++ b/crates/loading-bytes/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] async-fs = "^1.6" js-sys = "^0.3" -snafu = "^0.7" +snafu = {workspace = true} wasm-bindgen = "^0.2" wasm-bindgen-futures = "^0.4" web-sys = { version = "^0.3", features = ["Request", "RequestInit", "Response"] } \ No newline at end of file diff --git a/crates/renderling-shader/Cargo.toml b/crates/renderling-shader/Cargo.toml index 777e9ac0..27529f16 100644 --- a/crates/renderling-shader/Cargo.toml +++ b/crates/renderling-shader/Cargo.toml @@ -6,22 +6,25 @@ description = "Shared types and functions for renderling shaders compiled w/ rus # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["lib", "dylib"] + [features] default = [] [dependencies] -bytemuck = { workspace = true } -renderling-derive = { version = "0.1.0", path = "../renderling-derive" } spirv-std = "0.9" [target.'cfg(not(target_arch = "spirv"))'.dependencies] -glam = { workspace = true, features = ["bytemuck"] } +glam = { workspace = true, features = ["std"] } +crabslab = { version = "0.1.0", path = "../crabslab" } [target.'cfg(not(target_arch = "spirv"))'.dev-dependencies] -glam = { workspace = true, features = ["debug-glam-assert", "bytemuck"] } +glam = { workspace = true, features = ["std", "debug-glam-assert"] } [target.'cfg(target_arch = "spirv")'.dependencies] -glam = { version = "0.24.2", default-features = false, features = ["libm", "bytemuck"] } +glam = { version = "0.24.2", default-features = false, features = ["libm"] } +crabslab = { version = "0.1.0", path = "../crabslab", default-features = false, features = ["glam"] } [dev-dependencies] image = { workspace = true } diff --git a/crates/renderling-shader/src/bits.rs b/crates/renderling-shader/src/bits.rs index 6f224353..193a48fd 100644 --- a/crates/renderling-shader/src/bits.rs +++ b/crates/renderling-shader/src/bits.rs @@ -2,7 +2,7 @@ use core::ops::RangeInclusive; -use crate::{id::Id, slab::Slab}; +use crabslab::{Id, Slab}; /// Statically define a shift/mask range as a literal range of bits. pub const fn bits(range: RangeInclusive) -> (u32, u32) { @@ -102,9 +102,9 @@ pub fn extract_u16( // slab of u32s slab: &[u32], ) -> (u32, usize, usize) { - // NOTE: This should only have two entries, but we'll still handle the case where - // the extraction is not aligned to a u32 boundary by reading as if it were, and then - // re-aligning. + // NOTE: This should only have two entries, but we'll still handle the case + // where the extraction is not aligned to a u32 boundary by reading as if it + // were, and then re-aligning. const SHIFT_MASKS: [((u32, u32), usize, usize); 4] = [ (U16_0_BITS, 2, 0), (U16_0_BITS, 2, 0), @@ -340,17 +340,4 @@ mod test { [a, b, c, d, e, f, g] ); } - - #[test] - fn indices_sanity() { - let slab: [u32; 20] = [ - 65536, 2, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216, 0, 0, - 1065353216, 0, 0, 1065353216, - ]; - let u32_index = 9usize; - let byte_offset = 0usize; - let (a, u32_index, byte_offset) = extract_u32(u32_index, byte_offset, &slab); - let (b, u32_index, byte_offset) = extract_u32(u32_index, byte_offset, &slab); - let (c, u32_index, byte_offset) = extract_u32(u32_index, byte_offset, &slab); - } } diff --git a/crates/renderling-shader/src/convolution.rs b/crates/renderling-shader/src/convolution.rs index d84321f2..9f89cd93 100644 --- a/crates/renderling-shader/src/convolution.rs +++ b/crates/renderling-shader/src/convolution.rs @@ -1,6 +1,7 @@ //! Convolution shaders. //! //! These shaders convolve various functions to produce cached maps. +use crabslab::{Id, Slab, SlabItem}; use glam::{UVec2, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -11,7 +12,7 @@ use spirv_std::{ #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use crate::{pbr, stage::GpuConstants, IsVector}; +use crate::{pbr, stage::Camera, IsVector}; fn radical_inverse_vdc(mut bits: u32) -> f32 { bits = (bits << 16u32) | (bits >> 16u32); @@ -148,31 +149,46 @@ pub fn integrate_brdf_doesnt_work(mut n_dot_v: f32, roughness: f32) -> Vec2 { Vec2::new(a, b) } +/// Used by [`vertex_prefilter_environment_cubemap`] to read the camera and +/// roughness values from the slab. +#[derive(Default, SlabItem)] +pub struct VertexPrefilterEnvironmentCubemapIds { + pub camera: Id, + pub roughness: Id, +} + +/// Uses the `instance_index` as the [`Id`] of a [`PrefilterEnvironmentIds`]. +/// roughness value. #[spirv(vertex)] pub fn vertex_prefilter_environment_cubemap( - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - in_pos: Vec3, + #[spirv(instance_index)] instance_index: u32, + #[spirv(vertex_index)] vertex_id: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], out_pos: &mut Vec3, + out_roughness: &mut f32, #[spirv(position)] gl_pos: &mut Vec4, ) { + let in_pos = crate::math::CUBE[vertex_id as usize]; + let VertexPrefilterEnvironmentCubemapIds { camera, roughness } = + slab.read(Id::new(instance_index)); + let camera = slab.read(camera); + *out_roughness = slab.read(roughness); *out_pos = in_pos; - *gl_pos = constants.camera_projection * constants.camera_view * in_pos.extend(1.0); + *gl_pos = camera.projection * camera.view * in_pos.extend(1.0); } /// Lambertian prefilter. #[spirv(fragment)] pub fn fragment_prefilter_environment_cubemap( - #[spirv(uniform, descriptor_set = 0, binding = 1)] roughness: &f32, - #[spirv(descriptor_set = 0, binding = 2)] environment_cubemap: &Cubemap, - #[spirv(descriptor_set = 0, binding = 3)] sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 1)] environment_cubemap: &Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, in_pos: Vec3, + in_roughness: f32, frag_color: &mut Vec4, ) { let mut n = in_pos.alt_norm_or_zero(); // `wgpu` and vulkan's y coords are flipped from opengl n.y *= -1.0; - // These moves are redundant but the names have connections to the PBR - // equations. let r = n; let v = r; @@ -181,12 +197,12 @@ pub fn fragment_prefilter_environment_cubemap( for i in 0..SAMPLE_COUNT { let xi = hammersley(i, SAMPLE_COUNT); - let h = importance_sample_ggx(xi, n, *roughness); + let h = importance_sample_ggx(xi, n, in_roughness); let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); let n_dot_l = n.dot(l).max(0.0); if n_dot_l > 0.0 { - let mip_level = if *roughness == 0.0 { + let mip_level = if in_roughness == 0.0 { 0.0 } else { calc_lod(n_dot_l) @@ -273,15 +289,44 @@ pub fn fragment_bloom( *frag_color = result.extend(1.0); } +#[repr(C)] +#[derive(Clone, Copy)] +struct Vert { + pos: [f32; 3], + uv: [f32; 2], +} + +/// A screen-space quad. +const BRDF_VERTS: [Vert; 6] = { + let bl = Vert { + pos: [-1.0, -1.0, 0.0], + uv: [0.0, 1.0], + }; + let br = Vert { + pos: [1.0, -1.0, 0.0], + uv: [1.0, 1.0], + }; + let tl = Vert { + pos: [-1.0, 1.0, 0.0], + uv: [0.0, 0.0], + }; + let tr = Vert { + pos: [1.0, 1.0, 0.0], + uv: [1.0, 0.0], + }; + + [bl, br, tr, bl, tr, tl] +}; + #[spirv(vertex)] pub fn vertex_brdf_lut_convolution( - in_pos: glam::Vec3, - in_uv: glam::Vec2, + #[spirv(vertex_index)] vertex_id: u32, out_uv: &mut glam::Vec2, #[spirv(position)] gl_pos: &mut glam::Vec4, ) { - *out_uv = in_uv; - *gl_pos = in_pos.extend(1.0); + let Vert { pos, uv } = BRDF_VERTS[vertex_id as usize]; + *out_uv = Vec2::from(uv); + *gl_pos = Vec3::from(pos).extend(1.0); } #[spirv(fragment)] diff --git a/crates/renderling-shader/src/debug.rs b/crates/renderling-shader/src/debug.rs index 83fd6518..6868dcd5 100644 --- a/crates/renderling-shader/src/debug.rs +++ b/crates/renderling-shader/src/debug.rs @@ -1,9 +1,9 @@ //! Debugging helpers. -use crate as renderling_shader; -use crate::slab::Slabbed; +use crabslab::SlabItem; /// Used to debug shaders by early exiting the shader and attempting to display /// the value as shaded colors. +// TODO: Change DebugChannel to DebugMode and remove the previous DebugMode. #[repr(u32)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[derive(Clone, Copy, PartialEq, PartialOrd)] @@ -57,16 +57,19 @@ pub enum DebugChannel { /// Displays only the occlusion color for the fragment. Occlusion, - /// Displays only the calculated emissive effect (emissive_tex_color * emissive_factor * emissive_strength) of the fragment. + /// Displays only the calculated emissive effect (emissive_tex_color * + /// emissive_factor * emissive_strength) of the fragment. Emissive, - /// Displays only the emissive color (from the emissive map texture) of the fragment. + /// Displays only the emissive color (from the emissive map texture) of the + /// fragment. UvEmissive, /// Displays only teh emissive factor of the fragment. EmissiveFactor, - /// Displays only the emissive strength of the fragment (KHR_materials_emissive_strength). + /// Displays only the emissive strength of the fragment + /// (KHR_materials_emissive_strength). EmissiveStrength, } @@ -101,7 +104,7 @@ impl DebugChannel { /// /// Create one using `DebugChannel::into`. #[repr(transparent)] -#[derive(Default, Clone, Copy, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, Eq, SlabItem)] pub struct DebugMode(u32); impl core::fmt::Debug for DebugMode { diff --git a/crates/renderling-shader/src/gltf.rs b/crates/renderling-shader/src/gltf.rs index 86802f6b..c7d042ea 100644 --- a/crates/renderling-shader/src/gltf.rs +++ b/crates/renderling-shader/src/gltf.rs @@ -1,19 +1,28 @@ //! Gltf types that are used in shaders. +use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Vec2, Vec3, Vec4}; -use crate::{ - self as renderling_shader, - array::Array, - id::Id, - pbr::PbrMaterial, - slab::{Slab, Slabbed}, - texture::GpuTexture, -}; +use crate::{pbr::PbrMaterial, texture::GpuTexture}; #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfBuffer(pub Array); +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Default, Clone, Copy, SlabItem)] +pub struct GltfBufferView { + // Pointer to the parent buffer. + pub buffer: Id, + // The offset relative to the start of the parent buffer in bytes. + pub offset: u32, + // The length of the buffer view in bytes. + pub length: u32, + // The stride in bytes between vertex attributes or other interleavable data. + // + // When 0, data is assumed to be tightly packed. + pub stride: u32, +} + #[repr(u32)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[derive(Default, Clone, Copy, PartialEq)] @@ -27,7 +36,7 @@ pub enum DataType { F32, } -impl Slabbed for DataType { +impl SlabItem for DataType { fn slab_size() -> usize { // 1 u32::slab_size() @@ -69,7 +78,7 @@ pub enum Dimensions { Mat4, } -impl Slabbed for Dimensions { +impl SlabItem for Dimensions { fn slab_size() -> usize { 1 } @@ -106,20 +115,19 @@ impl Slabbed for Dimensions { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfAccessor { // The byte size of each element that this accessor describes. - // /// For example, if the accessor describes a `Vec3` of F32s, then // the size is 3 * 4 = 12. pub size: u32, - pub buffer: Id, - // Returns the offset relative to the start of the parent buffer view in bytes. + // A point to the parent view this accessor reads from. + /// This may be Id::NONE if the corresponding accessor is sparse. + pub view: Id, + // The offset relative to the start of the parent **buffer view** in bytes. // // This will be 0 if the corresponding accessor is sparse. - pub view_offset: u32, - // The stride in bytes between vertex attributes or other interleavable data. - pub view_stride: u32, + pub offset: u32, // The number of elements within the buffer view - not to be confused with the // number of bytes in the buffer view. pub count: u32, @@ -181,7 +189,6 @@ impl IncU16 { pub fn extract(self, slab: &[u32]) -> (u32, Self) { let (value, slab_index, byte_offset) = crate::bits::extract_u16(self.slab_index, self.byte_offset, slab); - crate::println!("value: {value:?}"); ( value, Self { @@ -214,28 +221,22 @@ impl IncI16 { impl GltfAccessor { fn slab_index_and_byte_offset(&self, element_index: usize, slab: &[u32]) -> (usize, usize) { - crate::println!("index: {element_index:?}"); - let buffer_id = self.buffer; - crate::println!("buffer_id: {buffer_id:?}"); - let buffer = slab.read(self.buffer); - crate::println!("buffer: {:?}", buffer); + let view = slab.read(self.view); + let buffer = slab.read(view.buffer); let buffer_start = buffer.0.starting_index(); - crate::println!("buffer_start: {buffer_start:?}"); let buffer_start_bytes = buffer_start * 4; - crate::println!("buffer_start_bytes: {buffer_start_bytes:?}"); + let stride = if self.size > view.stride { + self.size + } else { + view.stride + } as usize; let byte_offset = buffer_start_bytes - + self.view_offset as usize - + element_index as usize - * if self.size > self.view_stride { - self.size - } else { - self.view_stride - } as usize; - crate::println!("byte_offset: {byte_offset:?}"); + + view.offset as usize + + self.offset as usize + + element_index as usize * stride; let slab_index = byte_offset / 4; - crate::println!("slab_index: {slab_index:?}"); - let byte_offset = byte_offset % 4; - (slab_index, byte_offset) + let relative_byte_offset = byte_offset % 4; + (slab_index, relative_byte_offset) } pub fn inc_u8(&self, index: usize, slab: &[u32]) -> IncU8 { @@ -598,14 +599,16 @@ impl GltfAccessor { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfPrimitive { pub vertex_count: u32, pub material: Id, pub indices: Id, pub positions: Id, pub normals: Id, + pub normals_were_generated: bool, pub tangents: Id, + pub tangents_were_generated: bool, pub colors: Id, pub tex_coords0: Id, pub tex_coords1: Id, @@ -622,86 +625,78 @@ impl GltfPrimitive { } else { vertex_index }; - crate::println!("index: {index:?}"); let position = if self.positions.is_none() { Vec3::ZERO } else { let positions = slab.read(self.positions); - crate::println!("positions: {positions:?}"); positions.get_vec3(index, slab) }; - crate::println!("position: {position:?}"); let normal = if self.normals.is_none() { Vec3::Z } else { let normals = slab.read(self.normals); - crate::println!("normals: {normals:?}"); - normals.get_vec3(index, slab) + // If the normals were generated on the CPU, the index from + // `indices` won't be the same as the index from `normals`. + if self.normals_were_generated { + normals.get_vec3(vertex_index, slab) + } else { + normals.get_vec3(index, slab) + } }; - crate::println!("normal: {normal:?}"); let tangent = if self.tangents.is_none() { Vec4::Y } else { let tangents = slab.read(self.tangents); - crate::println!("tangents: {tangents:?}"); - tangents.get_vec4(index, slab) + // If the tangents were generated on the CPU, the index from + // `indices` won't be the same as the index from `tangents`. + if self.tangents_were_generated { + tangents.get_vec4(vertex_index, slab) + } else { + tangents.get_vec4(index, slab) + } }; - crate::println!("tangent: {tangent:?}"); let color = if self.colors.is_none() { Vec4::ONE } else { let colors = slab.read(self.colors); - crate::println!("colors: {colors:?}"); colors.get_vec4(index, slab) }; - crate::println!("color: {color:?}"); let tex_coords0 = if self.tex_coords0.is_none() { Vec2::ZERO } else { let tex_coords0 = slab.read(self.tex_coords0); - crate::println!("tex_coords0: {tex_coords0:?}"); tex_coords0.get_vec2(index, slab) }; - crate::println!("tex_coords0: {tex_coords0:?}"); let tex_coords1 = if self.tex_coords1.is_none() { Vec2::ZERO } else { let tex_coords1 = slab.read(self.tex_coords1); - crate::println!("tex_coords1: {tex_coords1:?}"); tex_coords1.get_vec2(index, slab) }; - crate::println!("tex_coords1: {tex_coords1:?}"); let uv = Vec4::new(tex_coords0.x, tex_coords0.y, tex_coords1.x, tex_coords1.y); - crate::println!("uv: {uv:?}"); let joints = if self.joints.is_none() { [0; 4] } else { let joints = slab.read(self.joints); - crate::println!("joints: {joints:?}"); let joints = joints.get_uvec4(index, slab); - crate::println!("joints: {joints:?}"); [joints.x, joints.y, joints.z, joints.w] }; - crate::println!("joints: {joints:?}"); let weights = if self.weights.is_none() { [0.0; 4] } else { let weights = slab.read(self.weights); - crate::println!("weights: {weights:?}"); let weights = weights.get_vec4(index, slab); - crate::println!("weights: {weights:?}"); [weights.x, weights.y, weights.z, weights.w] }; - crate::println!("weights: {weights:?}"); crate::stage::Vertex { position: position.extend(0.0), @@ -716,7 +711,7 @@ impl GltfPrimitive { } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfMesh { pub primitives: Array, pub weights: Array, @@ -755,7 +750,7 @@ impl Default for GltfCamera { } } -impl Slabbed for GltfCamera { +impl SlabItem for GltfCamera { fn slab_size() -> usize { 1 + 4 } @@ -844,7 +839,7 @@ pub enum GltfLightKind { }, } -impl Slabbed for GltfLightKind { +impl SlabItem for GltfLightKind { fn slab_size() -> usize { 1 // hash + 2 // inner_cone_angle, outer_cone_angle @@ -893,7 +888,7 @@ impl Slabbed for GltfLightKind { } } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfLight { pub color: glam::Vec3, pub intensity: f32, @@ -902,14 +897,14 @@ pub struct GltfLight { pub kind: GltfLightKind, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfSkin { pub joints: Array>, pub inverse_bind_matrices: Id, pub skeleton: Id, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Clone, Copy, SlabItem)] pub struct GltfNode { pub camera: Id, pub children: Array>, @@ -922,6 +917,22 @@ pub struct GltfNode { pub scale: glam::Vec3, } +impl Default for GltfNode { + fn default() -> Self { + Self { + camera: Default::default(), + children: Default::default(), + mesh: Default::default(), + light: Default::default(), + skin: Default::default(), + weights: Default::default(), + translation: Default::default(), + rotation: Default::default(), + scale: glam::Vec3::ONE, + } + } +} + #[repr(u32)] #[derive(Default, Clone, Copy, PartialEq)] pub enum GltfInterpolation { @@ -931,7 +942,7 @@ pub enum GltfInterpolation { CubicSpline, } -impl Slabbed for GltfInterpolation { +impl SlabItem for GltfInterpolation { fn slab_size() -> usize { 1 } @@ -959,7 +970,7 @@ impl Slabbed for GltfInterpolation { } } -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GltfAnimationSampler { pub input: Id, pub output: Id, @@ -976,7 +987,7 @@ pub enum GltfTargetProperty { MorphTargetWeights, } -impl Slabbed for GltfTargetProperty { +impl SlabItem for GltfTargetProperty { fn slab_size() -> usize { 1 } @@ -1006,42 +1017,34 @@ impl Slabbed for GltfTargetProperty { } } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfTarget { pub node: Id, pub property: GltfTargetProperty, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfChannel { pub sampler: Id, pub target: GltfTarget, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfAnimation { pub channels: Array, pub samplers: Array, } -#[derive(Default, Clone, Copy, Slabbed)] +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfScene { pub nodes: Array>, } -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, Slabbed)] -pub struct GltfBufferView { - pub buffer: Id, - pub offset: u32, - pub length: u32, - pub stride: u32, -} - /// A document of Gltf data. /// -/// This tells where certain parts of the Gltf document are stored in the [`Stage`]'s slab. -#[derive(Default, Clone, Copy, Slabbed)] +/// This tells where certain parts of the Gltf document are stored in the +/// [`Stage`]'s slab. +#[derive(Default, Clone, Copy, SlabItem)] pub struct GltfDocument { pub accessors: Array, pub animations: Array, @@ -1049,6 +1052,7 @@ pub struct GltfDocument { pub cameras: Array, // TODO: Think about making a `GltfMaterial` pub materials: Array, + pub default_material: Id, pub meshes: Array, pub nodes: Array, pub scenes: Array, @@ -1057,35 +1061,3 @@ pub struct GltfDocument { pub textures: Array, pub views: Array, } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn indices_accessor_sanity() { - // Taken from the indices accessor in the "simple meshes" gltf sample, - // but with the buffer changed to match where we write it here. - let buffer_id = Id::new(20); - let accessor = GltfAccessor { - size: 2, - buffer: buffer_id, - view_offset: 0, - view_stride: 0, - count: 3, - data_type: DataType::U16, - dimensions: Dimensions::Scalar, - normalized: false, - }; - let buffer = GltfBuffer(Array::new(0, 11)); - let mut slab: [u32; 22] = [ - 65536, 2, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216, 0, 0, 0, 1065353216, 0, 0, - 1065353216, 0, 0, 1065353216, 0, 0, - ]; - slab.write(&buffer, buffer_id.index()); - let i0 = accessor.get_u32(0, &slab); - let i1 = accessor.get_u32(1, &slab); - let i2 = accessor.get_u32(2, &slab); - assert_eq!([0, 1, 2], [i0, i1, i2]); - } -} diff --git a/crates/renderling-shader/src/lib.rs b/crates/renderling-shader/src/lib.rs index d15b73fa..77677fb3 100644 --- a/crates/renderling-shader/src/lib.rs +++ b/crates/renderling-shader/src/lib.rs @@ -7,18 +7,19 @@ use core::ops::Mul; use glam::{Quat, Vec3, Vec4Swizzles}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use spirv_std::num_traits::Zero; +use spirv_std::{ + image::{Cubemap, Image2d}, + num_traits::Zero, + Sampler, +}; -pub mod array; pub mod bits; pub mod convolution; pub mod debug; pub mod gltf; -pub mod id; pub mod math; pub mod pbr; pub mod skybox; -pub mod slab; pub mod stage; pub mod texture; pub mod tonemapping; @@ -65,9 +66,9 @@ pub trait IsMatrix { /// matrix is expected to be a 3D affine transformation matrix otherwise /// the output will be invalid. /// - /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant of - /// `self` is zero or if the resulting scale vector contains any zero elements - /// when `glam_assert` is enabled. + /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant + /// of `self` is zero or if the resulting scale vector contains any zero + /// elements when `glam_assert` is enabled. /// /// This is required instead of using /// [`glam::Mat4::to_scale_rotation_translation`], because that uses @@ -150,6 +151,7 @@ impl IsMatrix for glam::Mat4 { fn to_scale_rotation_translation_or_id(&self) -> (glam::Vec3, glam::Quat, glam::Vec3) { let det = self.determinant(); if det == 0.0 { + crate::println!("det == 0.0, returning identity"); return srt_id(); } @@ -178,3 +180,35 @@ impl IsMatrix for glam::Mat4 { (scale, rotation, translation) } } + +pub trait IsSampler: Copy + Clone {} + +impl IsSampler for Sampler {} + +pub trait Sample2d { + type Sampler: IsSampler; + + fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4; +} + +impl Sample2d for Image2d { + type Sampler = Sampler; + + fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4 { + self.sample_by_lod(sampler, uv, lod) + } +} + +pub trait SampleCube { + type Sampler: IsSampler; + + fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4; +} + +impl SampleCube for Cubemap { + type Sampler = Sampler; + + fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4 { + self.sample_by_lod(sampler, uv, lod) + } +} diff --git a/crates/renderling-shader/src/pbr.rs b/crates/renderling-shader/src/pbr.rs index 0eabb4ca..cc7b4b91 100644 --- a/crates/renderling-shader/src/pbr.rs +++ b/crates/renderling-shader/src/pbr.rs @@ -4,26 +4,17 @@ //! * https://learnopengl.com/PBR/Theory //! * https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/pbr.frag //! * https://github.khronos.org/glTF-Sample-Viewer-Release/ -use renderling_derive::Slabbed; +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use spirv_std::{ - image::{Cubemap, Image2d}, - Sampler, -}; - -use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; use crate::{ - self as renderling_shader, - array::Array, - id::Id, math, - slab::Slab, - stage::{GpuLight, LightType, LightingModel}, + stage::{light::LightStyle, GpuLight, LightType, LightingModel}, texture::GpuTexture, - IsVector, + IsSampler, IsVector, Sample2d, SampleCube, }; /// Represents a material on the GPU. @@ -33,7 +24,7 @@ use crate::{ /// [`SceneBuilder`](crate::SceneBuilder). #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct PbrMaterial { // x, y, z is emissive factor, default [0.0, 0.0, 0.0] // w is emissive strength multiplier (gltf's KHR_materials_emissive_strength extension), @@ -154,37 +145,58 @@ fn outgoing_radiance( metalness: f32, roughness: f32, ) -> Vec3 { + crate::println!("outgoing_radiance"); + crate::println!(" light_color: {light_color:?}"); + crate::println!(" albedo: {albedo:?}"); + crate::println!(" attenuation: {attenuation:?}"); + crate::println!(" v: {v:?}"); + crate::println!(" l: {l:?}"); + crate::println!(" n: {n:?}"); + crate::println!(" metalness: {metalness:?}"); + crate::println!(" roughness: {roughness:?}"); + let f0 = Vec3::splat(0.4).lerp(albedo, metalness); + crate::println!(" f0: {f0:?}"); let radiance = light_color.xyz() * attenuation; + crate::println!(" radiance: {radiance:?}"); let h = (v + l).alt_norm_or_zero(); + crate::println!(" h: {h:?}"); // cook-torrance brdf let ndf: f32 = normal_distribution_ggx(n, h, roughness); + crate::println!(" ndf: {ndf:?}"); let g: f32 = geometry_smith(n, v, l, roughness); + crate::println!(" g: {g:?}"); let f: Vec3 = fresnel_schlick(h.dot(v).max(0.0), f0); + crate::println!(" f: {f:?}"); let k_s = f; let k_d = (Vec3::splat(1.0) - k_s) * (1.0 - metalness); + crate::println!(" k_s: {k_s:?}"); let numerator: Vec3 = ndf * g * f; + crate::println!(" numerator: {numerator:?}"); let n_dot_l = n.dot(l).max(0.0); + crate::println!(" n_dot_l: {n_dot_l:?}"); let denominator: f32 = 4.0 * n.dot(v).max(0.0) * n_dot_l + 0.0001; + crate::println!(" denominator: {denominator:?}"); let specular: Vec3 = numerator / denominator; + crate::println!(" specular: {specular:?}"); (k_d * albedo / core::f32::consts::PI + specular) * radiance * n_dot_l } -pub fn sample_irradiance( - irradiance: &Cubemap, - irradiance_sampler: &Sampler, +pub fn sample_irradiance, S: IsSampler>( + irradiance: &T, + irradiance_sampler: &S, // Normal vector n: Vec3, ) -> Vec3 { irradiance.sample_by_lod(*irradiance_sampler, n, 0.0).xyz() } -pub fn sample_specular_reflection( - prefiltered: &Cubemap, - prefiltered_sampler: &Sampler, +pub fn sample_specular_reflection, S: IsSampler>( + prefiltered: &T, + prefiltered_sampler: &S, // camera position in world space camera_pos: Vec3, // fragment position in world space @@ -200,9 +212,9 @@ pub fn sample_specular_reflection( .xyz() } -pub fn sample_brdf( - brdf: &Image2d, - brdf_sampler: &Sampler, +pub fn sample_brdf, S: IsSampler>( + brdf: &T, + brdf_sampler: &S, // camera position in world space camera_pos: Vec3, // fragment position in world space @@ -344,12 +356,14 @@ pub fn stage_shade_fragment( prefiltered: Vec3, brdf: Vec2, - lights: Array, + lights: Array, slab: &[u32], ) -> Vec4 { let n = in_norm.alt_norm_or_zero(); let v = (camera_pos - in_pos).alt_norm_or_zero(); - + crate::println!("lights: {lights:?}"); + crate::println!("n: {n:?}"); + crate::println!("v: {v:?}"); // reflectance let mut lo = Vec3::ZERO; for i in 0..lights.len() { @@ -358,19 +372,17 @@ pub fn stage_shade_fragment( // determine the light ray and the radiance match light.light_type { - LightType::END_OF_LIGHTS => { - break; - } - LightType::POINT_LIGHT => { - let frag_to_light = light.position.xyz() - in_pos; + LightStyle::Point => { + let point_light = slab.read(light.into_point_id()); + let frag_to_light = point_light.position - in_pos; let distance = frag_to_light.length(); if distance == 0.0 { continue; } let l = frag_to_light.alt_norm_or_zero(); - let attenuation = light.intensity * 1.0 / (distance * distance); + let attenuation = point_light.intensity * 1.0 / (distance * distance); lo += outgoing_radiance( - light.color, + point_light.color, albedo, attenuation, v, @@ -381,19 +393,20 @@ pub fn stage_shade_fragment( ); } - LightType::SPOT_LIGHT => { - let frag_to_light = light.position.xyz() - in_pos; + LightStyle::Spot => { + let spot_light = slab.read(light.into_spot_id()); + let frag_to_light = spot_light.position - in_pos; let distance = frag_to_light.length(); if distance == 0.0 { continue; } let l = frag_to_light.alt_norm_or_zero(); - let theta: f32 = l.dot(light.direction.xyz().alt_norm_or_zero()); - let epsilon: f32 = light.inner_cutoff - light.outer_cutoff; - let attenuation: f32 = - light.intensity * ((theta - light.outer_cutoff) / epsilon).clamp(0.0, 1.0); + let theta: f32 = l.dot(spot_light.direction.alt_norm_or_zero()); + let epsilon: f32 = spot_light.inner_cutoff - spot_light.outer_cutoff; + let attenuation: f32 = spot_light.intensity + * ((theta - spot_light.outer_cutoff) / epsilon).clamp(0.0, 1.0); lo += outgoing_radiance( - light.color, + spot_light.color, albedo, attenuation, v, @@ -404,11 +417,15 @@ pub fn stage_shade_fragment( ); } - LightType::DIRECTIONAL_LIGHT => { - let l = -light.direction.xyz().alt_norm_or_zero(); - let attenuation = light.intensity; - lo += outgoing_radiance( - light.color, + LightStyle::Directional => { + let dir_light = slab.read(light.into_directional_id()); + let l = -dir_light.direction.alt_norm_or_zero(); + let attenuation = dir_light.intensity; + crate::println!("dir_light: {dir_light:?}"); + crate::println!("l: {l:?}"); + crate::println!("attenuation: {attenuation:?}"); + let radiance = outgoing_radiance( + dir_light.color, albedo, attenuation, v, @@ -417,11 +434,13 @@ pub fn stage_shade_fragment( metallic, roughness, ); + crate::println!("radiance: {radiance:?}"); + lo += radiance; } - _ => {} } } + crate::println!("lo: {lo:?}"); // calculate reflectance at normal incidence; if dia-electric (like plastic) use // F0 of 0.04 and if it's a metal, use the albedo color as F0 (metallic // workflow) diff --git a/crates/renderling-shader/src/skybox.rs b/crates/renderling-shader/src/skybox.rs index 36a69e0e..6adeda62 100644 --- a/crates/renderling-shader/src/skybox.rs +++ b/crates/renderling-shader/src/skybox.rs @@ -1,5 +1,6 @@ //! Skybox shader. +use crabslab::{Id, Slab}; use glam::{Mat3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -9,13 +10,7 @@ use spirv_std::{ #[cfg(target_arch = "spirv")] use spirv_std::num_traits::Float; -use crate::{ - id::Id, - math, - slab::Slab, - stage::{Camera, GpuConstants}, - IsVector, -}; +use crate::{math, stage::Camera, IsVector}; const INV_ATAN: Vec2 = Vec2::new(0.1591, core::f32::consts::FRAC_1_PI); @@ -28,23 +23,9 @@ pub fn direction_to_equirectangular_uv(dir: Vec3) -> Vec2 { uv } +/// Vertex shader for a skybox. #[spirv(vertex)] pub fn vertex( - #[spirv(vertex_index)] vertex_id: u32, - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - local_pos: &mut Vec3, - #[spirv(position)] gl_pos: &mut Vec4, -) { - let point = math::CUBE[vertex_id as usize]; - *local_pos = point; - let camera_view_without_translation = Mat3::from_mat4(constants.camera_view); - let rot_view = Mat4::from_mat3(camera_view_without_translation); - let clip_pos = constants.camera_projection * rot_view * point.extend(1.0); - *gl_pos = clip_pos.xyww(); -} - -#[spirv(vertex)] -pub fn slabbed_vertex( #[spirv(instance_index)] camera_index: u32, #[spirv(vertex_index)] vertex_index: u32, #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], @@ -61,18 +42,6 @@ pub fn slabbed_vertex( *clip_pos = position.xyww(); } -/// Colors a skybox using a cubemap texture. -#[spirv(fragment)] -pub fn stage_skybox_cubemap( - #[spirv(descriptor_set = 1, binding = 8)] texture: &Cubemap, - #[spirv(descriptor_set = 1, binding = 9)] sampler: &Sampler, - local_pos: Vec3, - out_color: &mut Vec4, -) { - let env_color: Vec3 = texture.sample(*sampler, local_pos.alt_norm_or_zero()).xyz(); - *out_color = env_color.extend(1.0); -} - /// Colors a skybox using a cubemap texture. #[spirv(fragment)] pub fn fragment_cubemap( @@ -85,19 +54,24 @@ pub fn fragment_cubemap( *out_color = env_color.extend(1.0); } -/// Passes the singular `Vec3` position attribute to the fragment shader unchanged, -/// while transforming `gl_pos` by the camera projection*view; +/// Draws a cubemap. +/// +/// Uses the `instance_index` as the [`Id`] for a [`Camera`]. /// -/// Used to create a cubemap from an equirectangular image as well as cubemap convolutions. +/// Used to create a cubemap from an equirectangular image as well as cubemap +/// convolutions. #[spirv(vertex)] -pub fn vertex_position_passthru( - #[spirv(uniform, descriptor_set = 0, binding = 0)] constants: &GpuConstants, - in_pos: Vec3, +pub fn vertex_cubemap( + #[spirv(instance_index)] camera_index: u32, + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], local_pos: &mut Vec3, #[spirv(position)] gl_pos: &mut Vec4, ) { - *local_pos = in_pos; - *gl_pos = constants.camera_projection * constants.camera_view * in_pos.extend(1.0); + let camera = slab.read(Id::::new(camera_index)); + let pos = crate::math::CUBE[vertex_index as usize]; + *local_pos = pos; + *gl_pos = camera.projection * camera.view * pos.extend(1.0); } /// Colors a skybox using an equirectangular texture. diff --git a/crates/renderling-shader/src/stage.rs b/crates/renderling-shader/src/stage.rs index 83817f4b..105afdaf 100644 --- a/crates/renderling-shader/src/stage.rs +++ b/crates/renderling-shader/src/stage.rs @@ -5,6 +5,7 @@ //! //! To read more about the technique, check out these resources: //! * https://stackoverflow.com/questions/59686151/what-is-gpu-driven-rendering +use crabslab::{Array, Id, Slab, SlabItem, ID_NONE}; use glam::{mat3, Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{ image::{Cubemap, Image2d}, @@ -15,22 +16,20 @@ use spirv_std::{ use spirv_std::num_traits::*; use crate::{ - self as renderling_shader, - array::Array, bits::{bits, extract, insert}, debug::*, gltf::{GltfMesh, GltfNode}, - id::{Id, ID_NONE}, pbr::{self, PbrMaterial}, - slab::{Slab, Slabbed}, texture::GpuTexture, - IsMatrix, IsVector, + IsMatrix, IsSampler, IsVector, Sample2d, SampleCube, }; +pub mod light; + /// A vertex in a mesh. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct Vertex { pub position: Vec4, pub color: Vec4, @@ -172,7 +171,7 @@ impl Vertex { #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Default, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Copy, Clone, Default, PartialEq, Eq, SlabItem)] pub struct LightType(u32); #[cfg(not(target_arch = "spirv"))] @@ -199,7 +198,7 @@ impl LightType { /// A light capable of representing a directional, point or spotlight. #[repr(C)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Copy, Clone, Default, SlabItem)] pub struct GpuLight { pub position: Vec4, pub direction: Vec4, @@ -213,19 +212,7 @@ pub struct GpuLight { /// Determines the lighting to use in an ubershader. #[repr(transparent)] -#[derive( - Clone, - Copy, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Debug, - bytemuck::Pod, - bytemuck::Zeroable, - Slabbed, -)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug, SlabItem)] pub struct LightingModel(u32); impl LightingModel { @@ -246,7 +233,7 @@ impl LightingModel { /// ### Provides info about if the entity is a skin. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct GpuEntityInfo(pub u32); impl GpuEntityInfo { @@ -309,7 +296,7 @@ impl GpuEntityInfo { /// A bundle of GPU components. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct GpuEntity { // The id of this entity. `Id::NONE` means this entity is not in use. pub id: Id, @@ -452,7 +439,7 @@ impl GpuEntity { /// Boolean toggles that cause the renderer to turn on/off certain features. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GpuToggles(pub u32); impl GpuToggles { @@ -485,7 +472,7 @@ impl GpuToggles { /// Unforms/constants for a scene's worth of rendering. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GpuConstants { pub camera_projection: Mat4, pub camera_view: Mat4, @@ -496,7 +483,7 @@ pub struct GpuConstants { } #[repr(C)] -#[derive(Default, Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Default, Debug, Clone, Copy, PartialEq, SlabItem)] pub struct DrawIndirect { pub vertex_count: u32, pub instance_count: u32, @@ -504,14 +491,18 @@ pub struct DrawIndirect { pub base_instance: u32, } -fn texture_color( +fn texture_color( texture_id: Id, uv: Vec2, - atlas: &Image2d, - sampler: &Sampler, + atlas: &T, + sampler: &S, atlas_size: UVec2, textures: &[GpuTexture], -) -> Vec4 { +) -> Vec4 +where + T: Sample2d, + S: IsSampler, +{ let texture = if texture_id.is_none() { GpuTexture::default() } else { @@ -526,11 +517,11 @@ fn texture_color( color } -fn stage_texture_color( +fn stage_texture_color, S: IsSampler>( texture_id: Id, uv: Vec2, - atlas: &Image2d, - sampler: &Sampler, + atlas: &T, + sampler: &S, atlas_size: UVec2, slab: &[u32], ) -> Vec4 { @@ -634,6 +625,65 @@ pub fn main_fragment_scene( output: &mut Vec4, brigtness: &mut Vec4, ) { + main_fragment_impl( + atlas, + atlas_sampler, + irradiance, + irradiance_sampler, + prefiltered, + prefiltered_sampler, + brdf, + brdf_sampler, + constants, + lights, + materials, + textures, + in_material, + in_color, + in_uv0, + in_uv1, + in_norm, + in_tangent, + in_bitangent, + in_pos, + output, + brigtness, + ); +} + +/// Scene fragment shader, callable from the CPU or GPU. +pub fn main_fragment_impl( + atlas: &T, + atlas_sampler: &S, + + irradiance: &C, + irradiance_sampler: &S, + prefiltered: &C, + prefiltered_sampler: &S, + brdf: &T, + brdf_sampler: &S, + + constants: &GpuConstants, + lights: &[GpuLight], + materials: &[pbr::PbrMaterial], + textures: &[GpuTexture], + + in_material: u32, + in_color: Vec4, + in_uv0: Vec2, + in_uv1: Vec2, + in_norm: Vec3, + in_tangent: Vec3, + in_bitangent: Vec3, + in_pos: Vec3, + + output: &mut Vec4, + brigtness: &mut Vec4, +) where + T: Sample2d, + C: SampleCube, + S: IsSampler, +{ let material = if in_material == ID_NONE || !constants.toggles.get_use_lighting() { // without an explicit material (or if the entire render has no lighting) // the entity will not participate in any lighting calculations @@ -869,42 +919,85 @@ pub fn main_fragment_scene( } /// A camera used for transforming the stage during rendering. +/// +/// Use `Camera::new(projection, view)` to create a new camera. +/// Or use `Camera::default` followed by `Camera::with_projection_and_view` +/// to set the projection and view matrices. Using the `with_*` or `set_*` +/// methods is preferred over setting the fields directly because they will +/// also update the camera's position. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct Camera { pub projection: Mat4, pub view: Mat4, pub position: Vec3, } +impl Camera { + pub fn new(projection: Mat4, view: Mat4) -> Self { + Camera::default().with_projection_and_view(projection, view) + } + + pub fn set_projection_and_view(&mut self, projection: Mat4, view: Mat4) { + self.projection = projection; + self.view = view; + self.position = view.inverse().transform_point3(Vec3::ZERO); + } + + pub fn with_projection_and_view(mut self, projection: Mat4, view: Mat4) -> Self { + self.set_projection_and_view(projection, view); + self + } + + pub fn set_projection(&mut self, projection: Mat4) { + self.set_projection_and_view(projection, self.view); + } + + pub fn with_projection(mut self, projection: Mat4) -> Self { + self.set_projection(projection); + self + } + + pub fn set_view(&mut self, view: Mat4) { + self.set_projection_and_view(self.projection, view); + } + + pub fn with_view(mut self, view: Mat4) -> Self { + self.set_view(view); + self + } +} + /// Holds important info about the stage. /// /// This should be the first struct in the stage's slab. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct StageLegend { pub atlas_size: UVec2, pub debug_mode: DebugMode, pub has_skybox: bool, pub has_lighting: bool, - pub light_array: Array, + pub light_array: Array, } -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] -pub struct NativeVertexData { - // Points to an array of `Vertex` in the stage's slab. - pub vertices: Array, - // Points to a `PbrMaterial` in the stage's slab. - pub material: Id, +impl Default for StageLegend { + fn default() -> Self { + Self { + atlas_size: Default::default(), + debug_mode: Default::default(), + has_skybox: Default::default(), + has_lighting: true, + light_array: Default::default(), + } + } } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct GltfVertexData { // A path of node ids that leads to the node that contains the mesh. pub parent_node_path: Array>, @@ -914,62 +1007,9 @@ pub struct GltfVertexData { pub primitive_index: u32, } -#[repr(u32)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq)] -pub enum VertexData { - Native(Id), - Gltf(Id), -} - -impl Default for VertexData { - fn default() -> Self { - VertexData::Native(Id::NONE) - } -} - -impl Slabbed for VertexData { - fn slab_size() -> usize { - 2 - } - - fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { - let mut proxy = 0u32; - let index = proxy.read_slab(index, slab); - match proxy { - 0 => { - let mut native = Id::default(); - let index = native.read_slab(index, slab); - *self = Self::Native(native); - index - } - 1 => { - let mut gltf = Id::default(); - let index = gltf.read_slab(index, slab); - *self = Self::Gltf(gltf); - index - } - _ => index, - } - } - - fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { - match self { - Self::Native(native) => { - let index = 0u32.write_slab(index, slab); - native.write_slab(index, slab) - } - Self::Gltf(gltf) => { - let index = 1u32.write_slab(index, slab); - gltf.write_slab(index, slab) - } - } - } -} - #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, Slabbed)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct Transform { pub translation: Vec3, pub rotation: Quat, @@ -986,18 +1026,27 @@ impl Default for Transform { } } -/// A fully-computed unit of rendering, roughly meaning a mesh with model matrix -/// transformations. +/// A rendering "command" that draws a single mesh from a top-level node. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Default, Clone, Copy, PartialEq, Slabbed)] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] pub struct RenderUnit { - pub vertex_data: VertexData, + // Which node are we rendering, and what is the path through its + // ancestors to get to it. + pub node_path: Array>, + // Index of the mesh within the child node that we're rendering. + pub mesh_index: u32, + // Index of the primitive within the mesh that we're rendering. + pub primitive_index: u32, // Points to a `Camera` in the stage's slab. pub camera: Id, - // Points to a `Transform` in the stage's slab. + // Points to a top-level `Transform` in the stage's slab. + // + // This is used to transform your GLTF models. pub transform: Id, // Number of vertices to draw for this unit. + // + // This is a cache for convenience on CPU. pub vertex_count: u32, } @@ -1007,32 +1056,42 @@ impl RenderUnit { vertex_index: u32, slab: &[u32], ) -> (Vertex, Transform, Id) { - let transform = slab.read(self.transform); - match self.vertex_data { - VertexData::Native(id) => { - let NativeVertexData { vertices, material } = slab.read(id); - let vertex = slab.read(vertices.at(vertex_index as usize)); - (vertex, transform, material) - } - VertexData::Gltf(id) => { - let GltfVertexData { - parent_node_path: _, - mesh, - primitive_index, - } = slab.read(id); - // TODO: check nodes for skinning - let mesh = slab.read(mesh); - let primitive = slab.read(mesh.primitives.at(primitive_index as usize)); - let material = primitive.material; - let vertex = primitive.get_vertex(vertex_index as usize, slab); - (vertex, transform, material) - } + let t = slab.read(self.transform); + crate::println!("t: {t:#?}"); + let mut model = Mat4::from_scale_rotation_translation(t.scale, t.rotation, t.translation); + crate::println!("model: {model:#?}"); + let mut node = GltfNode::default(); + for id_id in self.node_path.iter() { + let node_id = slab.read(id_id); + crate::println!(" node_id: {node_id:?}"); + node = slab.read(node_id); + crate::println!(" node.scale: {:?}", node.scale); + crate::println!(" node.rotation: {:?}", node.rotation); + crate::println!(" node.translation: {:?}", node.translation); + let node_transform = + Mat4::from_scale_rotation_translation(node.scale, node.rotation, node.translation); + model = model * node_transform; } + + crate::println!("model(after): {model:#?}"); + // TODO: check nodes for skinning + let mesh = slab.read(node.mesh); + let primitive_id = mesh.primitives.at(self.primitive_index as usize); + let primitive = slab.read(primitive_id); + let material = primitive.material; + let vertex = primitive.get_vertex(vertex_index as usize, slab); + let (s, r, t) = model.to_scale_rotation_translation_or_id(); + let transform = Transform { + translation: t, + rotation: r, + scale: s, + }; + (vertex, transform, material) } } #[spirv(vertex)] -pub fn new_stage_vertex( +pub fn gltf_vertex( // Which render unit are we rendering #[spirv(instance_index)] instance_index: u32, // Which vertex within the render unit are we rendering @@ -1079,10 +1138,35 @@ pub fn new_stage_vertex( *clip_pos = camera.projection * camera.view * view_pos; } +/// Returns the `StageLegend` from the stage's slab. +/// +/// The `StageLegend` should be the first struct in the slab, always. +pub fn get_stage_legend(slab: &[u32]) -> StageLegend { + slab.read(Id::new(0)) +} + +/// Returns the `PbrMaterial` from the stage's slab. +pub fn get_material(material_index: u32, has_lighting: bool, slab: &[u32]) -> pbr::PbrMaterial { + if material_index == ID_NONE { + // without an explicit material (or if the entire render has no lighting) + // the entity will not participate in any lighting calculations + pbr::PbrMaterial { + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + } + } else { + let mut material = slab.read(Id::::new(material_index)); + if !has_lighting { + material.lighting_model = LightingModel::NO_LIGHTING; + } + material + } +} + #[allow(clippy::too_many_arguments)] #[spirv(fragment)] /// Scene fragment shader. -pub fn stage_fragment( +pub fn gltf_fragment( #[spirv(descriptor_set = 1, binding = 0)] atlas: &Image2d, #[spirv(descriptor_set = 1, binding = 1)] atlas_sampler: &Sampler, @@ -1110,23 +1194,73 @@ pub fn stage_fragment( output: &mut Vec4, brigtness: &mut Vec4, ) { + gltf_fragment_impl( + atlas, + atlas_sampler, + irradiance, + irradiance_sampler, + prefiltered, + prefiltered_sampler, + brdf, + brdf_sampler, + slab, + in_camera, + in_material, + in_color, + in_uv0, + in_uv1, + in_norm, + in_tangent, + in_bitangent, + in_pos, + output, + brigtness, + ); +} + +#[allow(clippy::too_many_arguments)] +/// Scene fragment shader. +pub fn gltf_fragment_impl( + atlas: &T, + atlas_sampler: &S, + irradiance: &C, + irradiance_sampler: &S, + prefiltered: &C, + prefiltered_sampler: &S, + brdf: &T, + brdf_sampler: &S, + + slab: &[u32], + + in_camera: u32, + in_material: u32, + in_color: Vec4, + in_uv0: Vec2, + in_uv1: Vec2, + in_norm: Vec3, + in_tangent: Vec3, + in_bitangent: Vec3, + in_pos: Vec3, + + output: &mut Vec4, + brigtness: &mut Vec4, +) where + T: Sample2d, + C: SampleCube, + S: IsSampler, +{ + let legend = get_stage_legend(slab); + crate::println!("legend: {:?}", legend); let StageLegend { atlas_size, debug_mode, has_skybox: _, has_lighting, light_array, - } = slab.read(Id::new(0)); - let material = if in_material == ID_NONE || !has_lighting { - // without an explicit material (or if the entire render has no lighting) - // the entity will not participate in any lighting calculations - pbr::PbrMaterial { - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - } - } else { - slab.read(Id::::new(in_material)) - }; + } = legend; + + let material = get_material(in_material, has_lighting, slab); + crate::println!("material: {:?}", material); let albedo_tex_uv = if material.albedo_tex_coord == 0 { in_uv0 @@ -1141,6 +1275,7 @@ pub fn stage_fragment( atlas_size, slab, ); + crate::println!("albedo_tex_color: {:?}", albedo_tex_color); let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 { in_uv0 @@ -1155,6 +1290,10 @@ pub fn stage_fragment( atlas_size, slab, ); + crate::println!( + "metallic_roughness_tex_color: {:?}", + metallic_roughness_tex_color + ); let normal_tex_uv = if material.normal_tex_coord == 0 { in_uv0 @@ -1169,6 +1308,7 @@ pub fn stage_fragment( atlas_size, slab, ); + crate::println!("normal_tex_color: {:?}", normal_tex_color); let ao_tex_uv = if material.ao_tex_coord == 0 { in_uv0 @@ -1388,7 +1528,8 @@ pub fn compute_cull_entities( } #[spirv(compute(threads(32)))] -/// A shader to ensure that we can extract i8 and i16 values from a storage buffer. +/// A shader to ensure that we can extract i8 and i16 values from a storage +/// buffer. pub fn test_i8_i16_extraction( #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &mut [u32], #[spirv(global_invocation_id)] global_id: UVec3, @@ -1403,30 +1544,3 @@ pub fn test_i8_i16_extraction( slab[index] = value as u32; } } - -#[cfg(test)] -mod test { - use crate::{self as renderling_shader, id::Id, slab::Slab}; - use renderling_shader::slab::Slabbed; - - #[derive(Default, Debug, PartialEq, Slabbed)] - struct TheType { - a: glam::Vec3, - b: glam::Vec2, - c: glam::Vec4, - } - - #[test] - fn slabbed_writeread() { - let mut slab = [0u32; 100]; - let the = TheType { - a: glam::Vec3::new(0.0, 1.0, 2.0), - b: glam::Vec2::new(3.0, 4.0), - c: glam::Vec4::new(5.0, 6.0, 7.0, 8.0), - }; - let index = slab.write(&the, 0); - assert_eq!(9, index); - let the2 = slab.read(Id::::new(0)); - assert_eq!(the, the2); - } -} diff --git a/crates/renderling-shader/src/stage/id.rs b/crates/renderling-shader/src/stage/id.rs deleted file mode 100644 index fd801ac8..00000000 --- a/crates/renderling-shader/src/stage/id.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Typed identifiers that can also be used as indices. -use core::marker::PhantomData; - -pub const ID_NONE: u32 = u32::MAX; - -/// An identifier. -#[derive(Ord)] -#[repr(transparent)] -#[derive(bytemuck::Pod, bytemuck::Zeroable)] -pub struct Id(pub(crate) u32, PhantomData); - -impl PartialOrd for Id { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} - -impl Copy for Id {} - -impl Clone for Id { - fn clone(&self) -> Self { - Self(self.0.clone(), PhantomData) - } -} - -impl core::hash::Hash for Id { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } -} - -impl PartialEq for Id { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for Id {} - -impl From> for u32 { - fn from(value: Id) -> Self { - value.0 - } -} - -/// `Id::NONE` is the default. -impl Default for Id { - fn default() -> Self { - Id::NONE - } -} - -#[cfg(not(target_arch = "spirv"))] -impl std::fmt::Debug for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple(&format!("Id<{}>", std::any::type_name::())) - .field(&self.0) - .finish() - } -} - -impl Id { - pub const NONE: Self = Id::new(ID_NONE); - - pub const fn new(i: u32) -> Self { - Id(i, PhantomData) - } - - /// Convert this id into a usize for use as an index. - pub fn index(&self) -> usize { - self.0 as usize - } - - pub fn is_none(&self) -> bool { - // `u32` representing "null" or "none". - self == &Id::NONE - } - - pub fn is_some(&self) -> bool { - !self.is_none() - } -} - -#[cfg(test)] -mod test { - use crate::scene::GpuEntity; - - use super::*; - - #[test] - fn id_size() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>(), - "id is not u32" - ); - } -} diff --git a/crates/renderling-shader/src/stage/light.rs b/crates/renderling-shader/src/stage/light.rs new file mode 100644 index 00000000..7ac06ba6 --- /dev/null +++ b/crates/renderling-shader/src/stage/light.rs @@ -0,0 +1,204 @@ +//! Stage lighting. +use crabslab::{Id, SlabItem}; +use glam::{Vec3, Vec4}; + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct SpotLight { + pub position: Vec3, + pub direction: Vec3, + pub attenuation: Vec3, + pub inner_cutoff: f32, + pub outer_cutoff: f32, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for SpotLight { + fn default() -> Self { + let white = Vec4::splat(1.0); + let inner_cutoff = core::f32::consts::PI / 3.0; + let outer_cutoff = core::f32::consts::PI / 2.0; + let attenuation = Vec3::new(1.0, 0.014, 0.007); + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = white; + let intensity = 1.0; + + Self { + position: Default::default(), + direction, + attenuation, + inner_cutoff, + outer_cutoff, + color, + intensity, + } + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct DirectionalLight { + pub direction: Vec3, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for DirectionalLight { + fn default() -> Self { + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + direction, + color, + intensity, + } + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct PointLight { + pub position: Vec3, + pub attenuation: Vec3, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for PointLight { + fn default() -> Self { + let attenuation = Vec3::new(1.0, 0.14, 0.07); + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + position: Default::default(), + attenuation, + color, + intensity, + } + } +} + +#[cfg(feature = "gltf")] +pub fn from_gltf_light_kind(kind: gltf::khr_lights_punctual::Kind) -> LightStyle { + match kind { + gltf::khr_lights_punctual::Kind::Directional => LightStyle::Directional, + gltf::khr_lights_punctual::Kind::Point => LightStyle::Point, + gltf::khr_lights_punctual::Kind::Spot { .. } => LightStyle::Spot, + } +} + +#[cfg(feature = "gltf")] +pub fn gltf_light_intensity_units(kind: gltf::khr_lights_punctual::Kind) -> &'static str { + match kind { + gltf::khr_lights_punctual::Kind::Directional => "lux (lm/m^2)", + // sr is "steradian" + _ => "candelas (lm/sr)", + } +} + +#[repr(u32)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, PartialEq)] +pub enum LightStyle { + Directional = 0, + Point = 1, + Spot = 2, +} + +impl SlabItem for LightStyle { + fn slab_size() -> usize { + 1 + } + + fn read_slab(&mut self, index: usize, slab: &[u32]) -> usize { + let mut proxy = 0u32; + let index = proxy.read_slab(index, slab); + match proxy { + 0 => *self = LightStyle::Directional, + 1 => *self = LightStyle::Point, + 2 => *self = LightStyle::Spot, + _ => *self = LightStyle::Directional, + } + index + } + + fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { + let proxy = *self as u32; + proxy.write_slab(index, slab) + } +} + +/// A type-erased linked-list-of-lights that is used as a slab pointer to any +/// light type. +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, PartialEq, SlabItem)] +pub struct Light { + // The type of the light + pub light_type: LightStyle, + // The index of the light in the slab + pub index: u32, + //// The id of the next light + //pub next: Id, +} + +impl Default for Light { + fn default() -> Self { + Self { + light_type: LightStyle::Directional, + index: Id::<()>::NONE.inner(), + //next: Id::NONE, + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Directional, + index: id.inner(), + //next: Id::NONE, + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Spot, + index: id.inner(), + //next: Id::NONE, + } + } +} + +impl From> for Light { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Point, + index: id.inner(), + //next: Id::NONE, + } + } +} + +impl Light { + pub fn into_directional_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_spot_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_point_id(self) -> Id { + Id::from(self.index) + } +} diff --git a/crates/renderling-shader/src/texture.rs b/crates/renderling-shader/src/texture.rs index eef58a7f..e3a11be4 100644 --- a/crates/renderling-shader/src/texture.rs +++ b/crates/renderling-shader/src/texture.rs @@ -1,18 +1,16 @@ //! GPU textures. +use crabslab::SlabItem; use glam::{UVec2, Vec2}; -use renderling_derive::Slabbed; -use crate::{ - self as renderling_shader, - bits::{bits, extract, insert}, -}; +use crate::bits::{bits, extract, insert}; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::*; +// TODO: Completely rework the way we represent texture modes. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct TextureModes(u32); impl TextureModes { @@ -49,7 +47,7 @@ impl TextureModes { /// A GPU texture. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Slabbed)] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] pub struct GpuTexture { // The top left offset of texture in the atlas. pub offset_px: UVec2, @@ -113,7 +111,7 @@ pub fn clamp(input: f32) -> f32 { /// How edges should be handled in texture addressing/wrapping. #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, Eq, Default, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, PartialEq, Eq, Default)] pub struct TextureAddressMode(u32); impl core::fmt::Display for TextureAddressMode { @@ -127,6 +125,17 @@ impl core::fmt::Display for TextureAddressMode { } } +#[cfg(feature = "gltf")] +impl From for TextureAddressMode { + fn from(mode: gltf::texture::WrappingMode) -> Self { + match mode { + gltf::texture::WrappingMode::ClampToEdge => TextureAddressMode::CLAMP_TO_EDGE, + gltf::texture::WrappingMode::MirroredRepeat => TextureAddressMode::MIRRORED_REPEAT, + gltf::texture::WrappingMode::Repeat => TextureAddressMode::REPEAT, + } + } +} + impl TextureAddressMode { /// Clamp the value to the edge of the texture. pub const CLAMP_TO_EDGE: TextureAddressMode = TextureAddressMode(0); diff --git a/crates/renderling-shader/src/tonemapping.rs b/crates/renderling-shader/src/tonemapping.rs index 7c5bff14..6226dcbc 100644 --- a/crates/renderling-shader/src/tonemapping.rs +++ b/crates/renderling-shader/src/tonemapping.rs @@ -4,6 +4,7 @@ //! * https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/tonemapping.glsl //! * https://64.github.io/tonemapping +use crabslab::{Slab, SlabItem}; use glam::{mat3, Mat3, Vec2, Vec3, Vec4, Vec4Swizzles}; use spirv_std::{image::Image2d, spirv, Sampler}; @@ -75,7 +76,7 @@ fn tone_map_aces_hill(mut color: Vec3) -> Vec3 { #[repr(transparent)] #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, Eq, bytemuck::Zeroable, bytemuck::Pod)] +#[derive(Clone, Copy, Default, PartialEq, Eq, SlabItem)] pub struct Tonemap(u32); impl Tonemap { @@ -87,7 +88,7 @@ impl Tonemap { } #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Zeroable, bytemuck::Pod)] +#[derive(Clone, Copy, PartialEq, SlabItem)] pub struct TonemapConstants { pub tonemap: Tonemap, pub exposure: f32, @@ -102,7 +103,8 @@ impl Default for TonemapConstants { } } -pub fn tonemap(mut color: Vec4, constants: &TonemapConstants) -> Vec4 { +pub fn tonemap(mut color: Vec4, slab: &[u32]) -> Vec4 { + let constants = slab.read::(0u32.into()); color *= constants.exposure; match constants.tonemap { @@ -143,16 +145,13 @@ pub fn vertex( #[spirv(fragment)] pub fn fragment( - #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler, - #[spirv(uniform, descriptor_set = 1, binding = 0)] constants: &TonemapConstants, - #[spirv(descriptor_set = 2, binding = 0)] bloom_texture: &Image2d, - #[spirv(descriptor_set = 2, binding = 1)] bloom_sampler: &Sampler, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, in_uv: glam::Vec2, output: &mut glam::Vec4, ) { let color: Vec4 = texture.sample(*sampler, in_uv); - let bloom: Vec4 = bloom_texture.sample(*bloom_sampler, in_uv); - let color = tonemap(color + bloom, constants); + let color = tonemap(color, slab); *output = color; } diff --git a/crates/renderling-shader/src/tutorial.rs b/crates/renderling-shader/src/tutorial.rs index e3ddff8d..f13fcf4e 100644 --- a/crates/renderling-shader/src/tutorial.rs +++ b/crates/renderling-shader/src/tutorial.rs @@ -1,13 +1,9 @@ //! Shaders used in the intro tutorial. +use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Mat4, Vec4, Vec4Swizzles}; use spirv_std::spirv; -use crate::{ - array::Array, - id::Id, - slab::{Slab, Slabbed}, - stage::{RenderUnit, Vertex}, -}; +use crate::stage::{RenderUnit, Vertex}; /// Simple fragment shader that writes the input color to the output color. #[spirv(fragment)] diff --git a/crates/renderling-shader/src/ui.rs b/crates/renderling-shader/src/ui.rs index ccd57968..b52d5bb0 100644 --- a/crates/renderling-shader/src/ui.rs +++ b/crates/renderling-shader/src/ui.rs @@ -2,13 +2,14 @@ //! //! This is mostly for rendering text. +use crabslab::SlabItem; use glam::{Mat4, UVec2, Vec2, Vec4}; use spirv_std::{image::Image2d, spirv, Sampler}; /// A vertex in a mesh. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[repr(C)] -#[derive(Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, PartialEq)] pub struct UiVertex { pub position: Vec2, pub uv: Vec2, @@ -43,14 +44,14 @@ impl UiVertex { } #[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, SlabItem)] pub struct UiConstants { pub canvas_size: UVec2, pub camera_translation: Vec2, } #[repr(transparent)] -#[derive(Clone, Copy, Default, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] pub struct UiMode(pub u32); impl UiMode { @@ -59,7 +60,7 @@ impl UiMode { } #[repr(C)] -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Clone, Copy)] pub struct UiDrawParams { pub translation: Vec2, pub scale: Vec2, diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index 949a06e6..ac41fe54 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -19,33 +19,38 @@ wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] [dependencies] ab_glyph = { version = "0.2", optional = true } any_vec = "0.13" -async-channel = "1.8" +async-channel = {workspace=true} +bytemuck = { workspace = true } +crabslab = { version = "0.1.0", path = "../crabslab" } crunch = "0.5" futures-lite = {workspace=true} +glam = { workspace = true, default-features = false, features = ["bytemuck", "libm"] } +gltf = { workspace = true, optional = true } glyph_brush = { version = "0.7", optional = true } half = "2.3" +image = { workspace = true, features = ["hdr"] } +log = { workspace = true } moongraph = { version = "0.3.5", features = ["dot"] } raw-window-handle = { version = "0.5", optional = true } renderling-shader = { path = "../renderling-shader" } rustc-hash = "1.1" send_wrapper = "0.6" -snafu = "0.7" - -image = { workspace = true, features = ["hdr"] } -gltf = { workspace = true, optional = true } -bytemuck = { workspace = true } -log = { workspace = true } -glam = { workspace = true, default-features = false, features = ["bytemuck", "libm"] } -winit = { workspace = true, optional = true } +snafu = {workspace=true} wgpu = { workspace = true, features = ["spirv"] } +winit = { workspace = true, optional = true } [package.metadata.docs.rs] features = ["gltf", "text", "raw-window-handle", "winit"] [dev-dependencies] +criterion = { version = "0.4", features = ["html_reports"] } ctor = "0.2.2" env_logger = "0.10.0" icosahedron = "0.1" img-diff = { path = "../img-diff" } naga = { version = "0.13", features = ["spv-in", "wgsl-out", "wgsl-in", "msl-out"] } pretty_assertions = "1.4.0" + +[[bench]] +name = "benchmarks" +harness = false diff --git a/crates/renderling/benches/benchmarks.rs b/crates/renderling/benches/benchmarks.rs new file mode 100644 index 00000000..916b79c3 --- /dev/null +++ b/crates/renderling/benches/benchmarks.rs @@ -0,0 +1,51 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use renderling::{GltfLoader, Renderling}; + +fn load_damaged_helmet(c: &mut Criterion) { + //let _ = env_logger::builder() + // .is_test(true) + // .filter_level(log::LevelFilter::Trace) + // .filter_module("renderling", log::LevelFilter::Trace) + // .filter_module("dagga", log::LevelFilter::Warn) + // .filter_module("broomdog", log::LevelFilter::Warn) + // .filter_module("naga", log::LevelFilter::Warn) + // .filter_module("wgpu", log::LevelFilter::Warn) + // .filter_module("wgpu_hal", log::LevelFilter::Warn) + // .try_init(); + + let (document, buffers, images) = gltf::import("../../gltf/DamagedHelmet.glb").unwrap(); + + let mut group = c.benchmark_group("load_damaged_helmet"); + group.sample_size(20); + + println!("{}", std::env::current_dir().unwrap().display()); + + let r = Renderling::headless(100, 100); + group.bench_function("legacy", |b| { + b.iter(|| { + let mut builder = r.new_scene(); + let loader = GltfLoader::load( + &mut builder, + document.clone(), + buffers.clone(), + images.clone(), + ); + let scene = builder.build().unwrap(); + black_box((loader, scene)) + }) + }); + + let r = Renderling::headless(100, 100); + group.bench_function("gltf", |b| { + b.iter(|| { + let stage = r.new_stage(); + let gpu_doc = stage + .load_gltf_document(&document, buffers.clone(), images.clone()) + .unwrap(); + black_box(gpu_doc) + }) + }); +} + +criterion_group!(benches, load_damaged_helmet); +criterion_main!(benches); diff --git a/crates/renderling/src/atlas.rs b/crates/renderling/src/atlas.rs index 5d71a921..fc3fb2f1 100644 --- a/crates/renderling/src/atlas.rs +++ b/crates/renderling/src/atlas.rs @@ -1,17 +1,19 @@ //! Texture atlas. //! -//! All images are packed into an atlas at scene build time. +//! All images are packed into an atlas at staging time. //! Texture descriptors describing where in the atlas an image is, //! and how callsites should sample pixels is packed into a buffer //! on the GPU. This makes the number of texture binds _very_ low. //! -//! An atlas should be temporary until we can use bindless techniques +//! ## NOTE: +//! `Atlas` is a temporary work around until we can use bindless techniques //! on web. use glam::UVec2; use image::{EncodableLayout, RgbaImage}; use snafu::prelude::*; -use crate::SceneImage; +mod atlas_image; +pub use atlas_image::*; fn gpu_frame_from_rect(r: crunch::Rect) -> (UVec2, UVec2) { ( @@ -31,9 +33,9 @@ pub enum AtlasError { pub enum Packing { Img { index: usize, - image: SceneImage, + image: AtlasImage, }, - AtlasImg { + GpuImg { index: usize, offset_px: UVec2, size_px: UVec2, @@ -44,35 +46,42 @@ impl Packing { pub fn width(&self) -> u32 { match self { Packing::Img { image, .. } => image.width, - Packing::AtlasImg { size_px, .. } => size_px.x, + Packing::GpuImg { size_px, .. } => size_px.x, } } pub fn height(&self) -> u32 { match self { Packing::Img { image, .. } => image.height, - Packing::AtlasImg { size_px, .. } => size_px.y, + Packing::GpuImg { size_px, .. } => size_px.y, } } pub fn index(&self) -> usize { match self { Packing::Img { index, .. } => *index, - Packing::AtlasImg { index, .. } => *index, + Packing::GpuImg { index, .. } => *index, } } pub fn set_index(&mut self, index: usize) { match self { Packing::Img { index: i, .. } => *i = index, - Packing::AtlasImg { index: i, .. } => *i = index, + Packing::GpuImg { index: i, .. } => *i = index, } } - pub fn as_scene_img_mut(&mut self) -> Option<&mut SceneImage> { + pub fn as_scene_img_mut(&mut self) -> Option<&mut AtlasImage> { match self { Packing::Img { image, .. } => Some(image), - Packing::AtlasImg { .. } => None, + Packing::GpuImg { .. } => None, + } + } + + pub fn as_scene_img(&self) -> Option<&AtlasImage> { + match self { + Packing::Img { image, .. } => Some(image), + Packing::GpuImg { .. } => None, } } } @@ -99,6 +108,14 @@ impl RepackPreview { pub fn get_mut(&mut self, index: usize) -> Option<&mut Packing> { self.items.items.get_mut(index).map(|item| &mut item.data) } + + pub fn new_images_len(&self) -> usize { + self.items + .items + .iter() + .filter(|item| item.data.as_scene_img().is_some()) + .count() + } } /// A texture atlas, used to store all the textures in a scene. @@ -184,8 +201,8 @@ impl Atlas { /// but doesn't send any data to the GPU. pub fn pack_preview<'a>( device: &wgpu::Device, - images: impl IntoIterator, - ) -> Result, AtlasError> { + images: impl IntoIterator, + ) -> Result, AtlasError> { let images = images.into_iter().collect::>(); let len = images.len(); let limit = device.limits().max_texture_dimension_1d; @@ -205,7 +222,7 @@ impl Atlas { pub fn commit_preview( device: &wgpu::Device, queue: &wgpu::Queue, - crunch::PackedItems { w, h, items }: crunch::PackedItems, + crunch::PackedItems { w, h, items }: crunch::PackedItems, ) -> Result { let mut atlas = Atlas::new(device, queue, UVec2::new(w as u32, h as u32)); atlas.rects = items @@ -254,7 +271,7 @@ impl Atlas { pub fn repack_preview( &self, device: &wgpu::Device, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let mut images = images.into_iter().collect::>(); let len = images.len() + self.rects.len(); @@ -262,7 +279,7 @@ impl Atlas { device.limits().max_texture_dimension_1d as usize, self.rects .iter() - .map(|r| Packing::AtlasImg { + .map(|r| Packing::GpuImg { index: 0, offset_px: UVec2::new(r.x as u32, r.y as u32), size_px: UVec2::new(r.w as u32, r.y as u32), @@ -337,7 +354,7 @@ impl Atlas { ); atlas.rects[index] = rect; } - Packing::AtlasImg { + Packing::GpuImg { index, offset_px, size_px, @@ -381,12 +398,10 @@ impl Atlas { /// /// Returns a vector of ids that determine the locations of the given images /// within the atlas. - /// - /// This invalidates any pointers to previous textures in this atlas. pub fn pack( device: &wgpu::Device, queue: &wgpu::Queue, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let images = images.into_iter().collect::>(); let items = Self::pack_preview(device, images)?; @@ -397,7 +412,7 @@ impl Atlas { &self, device: &wgpu::Device, queue: &wgpu::Queue, - images: impl IntoIterator, + images: impl IntoIterator, ) -> Result { let images = images.into_iter().collect::>(); let items = self.repack_preview(device, images)?; @@ -479,41 +494,482 @@ impl Atlas { queue.submit(std::iter::once(encoder.finish())); Ok(atlas) } + + /// Read the atlas image from the GPU. + /// + /// This is primarily for testing. + /// + /// The resulting image will be in a **linear** color space. + /// + /// ## Panics + /// Panics if the pixels read from the GPU cannot be converted into an + /// `RgbaImage`. + pub fn atlas_img(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> RgbaImage { + let buffer = crate::Texture::read( + &self.texture.texture, + device, + queue, + self.size.x as usize, + self.size.y as usize, + 4, + 1, + ); + buffer.into_linear_rgba(device).unwrap() + } } #[cfg(test)] mod test { - use crate::Renderling; + use crate::{ + shader::{ + gltf::*, + pbr::PbrMaterial, + stage::{Camera, LightingModel, RenderUnit, Transform, Vertex}, + texture::{GpuTexture, TextureAddressMode, TextureModes}, + }, + Renderling, + }; + use crabslab::GrowableSlab; + use glam::{Vec2, Vec3, Vec4}; use super::*; - impl Atlas { - fn atlas_img(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> RgbaImage { - let buffer = crate::Texture::read( - &self.texture.texture, - device, - queue, - self.size.x as usize, - self.size.y as usize, - 4, - 1, - ); - buffer.into_rgba(device).unwrap() - } - } - #[test] fn can_merge_atlas() { let r = Renderling::headless(100, 100); let (device, queue) = r.get_device_and_queue_owned(); println!("{}", std::env::current_dir().unwrap().display()); - let cheetah = SceneImage::from_path("../../img/cheetah.jpg").unwrap(); - let dirt = SceneImage::from_path("../../img/dirt.jpg").unwrap(); - let happy_mac = SceneImage::from_path("../../img/happy_mac.png").unwrap(); - let sandstone = SceneImage::from_path("../../img/sandstone.png").unwrap(); + let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap(); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let happy_mac = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); let atlas1 = Atlas::pack(&device, &queue, vec![cheetah, dirt]).unwrap(); let atlas2 = Atlas::pack(&device, &queue, vec![happy_mac, sandstone]).unwrap(); let atlas3 = atlas1.merge(&device, &queue, &atlas2).unwrap(); img_diff::assert_img_eq("atlas/merge3.png", atlas3.atlas_img(&device, &queue)); } + + #[test] + // Ensures that textures are packed and rendered correctly. + fn atlas_uv_mapping() { + let mut r = + Renderling::headless(32, 32).with_background_color(Vec3::splat(0.0).extend(1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0); + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap(); + let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); + let mut texels_tex = textures[2]; + texels_tex + .modes + .set_wrap_s(TextureAddressMode::CLAMP_TO_EDGE); + texels_tex + .modes + .set_wrap_t(TextureAddressMode::CLAMP_TO_EDGE); + let texels_tex_id = stage.append(&texels_tex); + let material_id = stage.append(&PbrMaterial { + albedo_texture: texels_tex_id, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + let mesh = stage + .new_mesh() + .with_primitive( + { + let tl = Vertex::default() + .with_position(Vec3::ZERO) + .with_uv0(Vec2::ZERO); + let tr = Vertex::default() + .with_position(Vec3::new(1.0, 0.0, 0.0)) + .with_uv0(Vec2::new(1.0, 0.0)); + let bl = Vertex::default() + .with_position(Vec3::new(0.0, 1.0, 0.0)) + .with_uv0(Vec2::new(0.0, 1.0)); + let br = Vertex::default() + .with_position(Vec3::new(1.0, 1.0, 0.0)) + .with_uv0(Vec2::splat(1.0)); + vec![tl, bl, br, tl, br, tr] + }, + [], + material_id, + ) + .build(); + let mesh = stage.append(&mesh); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let transform = stage.append(&Transform { + scale: Vec3::new(32.0, 32.0, 1.0), + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let _unit = stage.draw_unit(&RenderUnit { + camera, + transform, + node_path, + vertex_count: 6, + ..Default::default() + }); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("atlas/uv_mapping.png", img); + } + + #[test] + // Ensures that textures with different wrapping modes are rendered correctly. + fn uv_wrapping() { + let icon_w = 32; + let icon_h = 41; + let sheet_w = icon_w * 3; + let sheet_h = icon_h * 3; + let w = sheet_w * 3 + 2; + let h = sheet_h; + let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + + let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); + let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); + let texel_tex = textures[2]; + let mut clamp_tex = texel_tex; + clamp_tex + .modes + .set_wrap_s(TextureAddressMode::CLAMP_TO_EDGE); + clamp_tex + .modes + .set_wrap_t(TextureAddressMode::CLAMP_TO_EDGE); + let mut repeat_tex = texel_tex; + repeat_tex.modes.set_wrap_s(TextureAddressMode::REPEAT); + repeat_tex.modes.set_wrap_t(TextureAddressMode::REPEAT); + let mut mirror_tex = texel_tex; + mirror_tex + .modes + .set_wrap_s(TextureAddressMode::MIRRORED_REPEAT); + mirror_tex + .modes + .set_wrap_t(TextureAddressMode::MIRRORED_REPEAT); + + let albedo_texture = stage.append(&clamp_tex); + let clamp_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + let albedo_texture = stage.append(&repeat_tex); + let repeat_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + let albedo_texture = stage.append(&mirror_tex); + let mirror_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + + let sheet_w = sheet_w as f32; + let sheet_h = sheet_h as f32; + let clamp_prim = stage.new_primitive( + { + let tl = Vertex::default() + .with_position(Vec3::ZERO) + .with_uv0(Vec2::ZERO); + let tr = Vertex::default() + .with_position(Vec3::new(sheet_w, 0.0, 0.0)) + .with_uv0(Vec2::new(3.0, 0.0)); + let bl = Vertex::default() + .with_position(Vec3::new(0.0, sheet_h, 0.0)) + .with_uv0(Vec2::new(0.0, 3.0)); + let br = Vertex::default() + .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) + .with_uv0(Vec2::splat(3.0)); + vec![tl, bl, br, tl, br, tr] + }, + [], + clamp_material_id, + ); + let repeat_prim = { + let mut p = clamp_prim; + p.material = repeat_material_id; + p + }; + let mirror_prim = { + let mut p = clamp_prim; + p.material = mirror_material_id; + p + }; + + let _clamp = { + let primitives = stage.append_array(&[clamp_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + ..Default::default() + }) + }; + let _repeat = { + let primitives = stage.append_array(&[repeat_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), + ..Default::default() + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, + ..Default::default() + }) + }; + let _mirror = { + let primitives = stage.append_array(&[mirror_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + translation: Vec3::new(sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0), + ..Default::default() + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, + ..Default::default() + }) + }; + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("atlas/uv_wrapping.png", img); + } + + #[test] + // Ensures that textures with negative uv coords wrap correctly + fn negative_uv_wrapping() { + let icon_w = 32; + let icon_h = 41; + let sheet_w = icon_w * 3; + let sheet_h = icon_h * 3; + let w = sheet_w * 3 + 2; + let h = sheet_h; + let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + + let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + + let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); + let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); + let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); + let textures = stage.set_images([dirt, sandstone, texels]).unwrap(); + + let texel_tex = textures[2]; + let mut clamp_tex = texel_tex; + clamp_tex + .modes + .set_wrap_s(TextureAddressMode::CLAMP_TO_EDGE); + clamp_tex + .modes + .set_wrap_t(TextureAddressMode::CLAMP_TO_EDGE); + let mut repeat_tex = texel_tex; + repeat_tex.modes.set_wrap_s(TextureAddressMode::REPEAT); + repeat_tex.modes.set_wrap_t(TextureAddressMode::REPEAT); + let mut mirror_tex = texel_tex; + mirror_tex + .modes + .set_wrap_s(TextureAddressMode::MIRRORED_REPEAT); + mirror_tex + .modes + .set_wrap_t(TextureAddressMode::MIRRORED_REPEAT); + + let albedo_texture = stage.append(&clamp_tex); + let clamp_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + + let albedo_texture = stage.append(&repeat_tex); + let repeat_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + + let albedo_texture = stage.append(&mirror_tex); + let mirror_material_id = stage.append(&PbrMaterial { + albedo_texture, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + + let sheet_w = sheet_w as f32; + let sheet_h = sheet_h as f32; + + let clamp_prim = stage.new_primitive( + { + let tl = Vertex::default() + .with_position(Vec3::ZERO) + .with_uv0(Vec2::ZERO); + let tr = Vertex::default() + .with_position(Vec3::new(sheet_w, 0.0, 0.0)) + .with_uv0(Vec2::new(-3.0, 0.0)); + let bl = Vertex::default() + .with_position(Vec3::new(0.0, sheet_h, 0.0)) + .with_uv0(Vec2::new(0.0, -3.0)); + let br = Vertex::default() + .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) + .with_uv0(Vec2::splat(-3.0)); + vec![tl, bl, br, tl, br, tr] + }, + [], + clamp_material_id, + ); + let repeat_prim = { + let mut p = clamp_prim; + p.material = repeat_material_id; + p + }; + let mirror_prim = { + let mut p = clamp_prim; + p.material = mirror_material_id; + p + }; + + let _clamp = { + let primitives = stage.append_array(&[clamp_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + ..Default::default() + }) + }; + let _repeat = { + let primitives = stage.append_array(&[repeat_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), + ..Default::default() + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + transform, + vertex_count: 6, + ..Default::default() + }) + }; + let _mirror = { + let primitives = stage.append_array(&[mirror_prim]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + translation: Vec3::new(sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0), + ..Default::default() + }); + stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 6, + transform, + ..Default::default() + }) + }; + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img); + } + + #[test] + fn transform_uvs_for_atlas() { + let mut tex = GpuTexture { + offset_px: UVec2::ZERO, + size_px: UVec2::ONE, + modes: { + let mut modes = TextureModes::default(); + modes.set_wrap_s(TextureAddressMode::CLAMP_TO_EDGE); + modes.set_wrap_t(TextureAddressMode::CLAMP_TO_EDGE); + modes + }, + ..Default::default() + }; + assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(100))); + assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(1))); + assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(256))); + tex.offset_px = UVec2::splat(10); + assert_eq!(Vec2::splat(0.1), tex.uv(Vec2::ZERO, UVec2::splat(100))); + } } diff --git a/crates/renderling/src/atlas/atlas_image.rs b/crates/renderling/src/atlas/atlas_image.rs new file mode 100644 index 00000000..ff18bd50 --- /dev/null +++ b/crates/renderling/src/atlas/atlas_image.rs @@ -0,0 +1,307 @@ +//! Images and texture formats. +//! +//! Used to represent textures before they are sent to the GPU, in the +//! [`AtlasBuilder`]. +use image::EncodableLayout; +use snafu::prelude::*; + +#[derive(Debug, Snafu)] +pub enum AtlasImageError { + #[snafu(display("Cannot load image '{}' from cwd '{:?}': {source}", path.display(), std::env::current_dir()))] + CannotLoad { + source: std::io::Error, + path: std::path::PathBuf, + }, + + #[snafu(display("Image error: {source}"))] + Image { source: image::error::ImageError }, +} + +#[derive(Clone, Copy, Debug)] +pub enum AtlasImageFormat { + R8, + R8G8, + R8G8B8, + R8G8B8A8, + R16, + R16G16, + R16G16B16, + R16G16B16A16, + R16G16B16A16FLOAT, + R32G32B32FLOAT, + R32G32B32A32FLOAT, +} + +impl AtlasImageFormat { + pub fn from_wgpu_texture_format(value: wgpu::TextureFormat) -> Option { + // TODO: implement more AtlasImageFormat conversions from wgpu::TetxureFormat + match value { + wgpu::TextureFormat::R8Uint => Some(AtlasImageFormat::R8), + wgpu::TextureFormat::R16Uint => Some(AtlasImageFormat::R16), + wgpu::TextureFormat::Rg8Uint => Some(AtlasImageFormat::R8G8), + wgpu::TextureFormat::Rg16Uint => Some(AtlasImageFormat::R16G16), + wgpu::TextureFormat::Rgba16Float => Some(AtlasImageFormat::R16G16B16A16FLOAT), + _ => None, + } + } +} + +/// Image data in transit from CPU to GPU. +#[derive(Clone, Debug)] +pub struct AtlasImage { + pub pixels: Vec, + pub width: u32, + pub height: u32, + pub format: AtlasImageFormat, + // Whether or not to convert from sRGB color space into linear color space. + pub apply_linear_transfer: bool, +} + +#[cfg(feature = "gltf")] +impl From for AtlasImage { + fn from(value: gltf::image::Data) -> Self { + let pixels = value.pixels; + let width = value.width; + let height = value.height; + let format = match value.format { + gltf::image::Format::R8 => AtlasImageFormat::R8, + gltf::image::Format::R8G8 => AtlasImageFormat::R8G8, + gltf::image::Format::R8G8B8 => AtlasImageFormat::R8G8B8, + gltf::image::Format::R8G8B8A8 => AtlasImageFormat::R8G8B8A8, + gltf::image::Format::R16 => AtlasImageFormat::R16, + gltf::image::Format::R16G16 => AtlasImageFormat::R16G16, + gltf::image::Format::R16G16B16 => AtlasImageFormat::R16G16B16, + gltf::image::Format::R16G16B16A16 => AtlasImageFormat::R16G16B16A16, + gltf::image::Format::R32G32B32FLOAT => AtlasImageFormat::R32G32B32FLOAT, + gltf::image::Format::R32G32B32A32FLOAT => AtlasImageFormat::R32G32B32A32FLOAT, + }; + + AtlasImage { + pixels, + format, + // Determining this gets deferred until material construction + apply_linear_transfer: false, + width, + height, + } + } +} + +impl From for AtlasImage { + fn from(value: image::DynamicImage) -> Self { + let width = value.width(); + let height = value.height(); + + use AtlasImageFormat::*; + let (pixels, format) = match value { + image::DynamicImage::ImageLuma8(img) => (img.into_vec(), R8), + i @ image::DynamicImage::ImageLumaA8(_) => (i.into_rgba8().into_vec(), R8G8B8A8), + image::DynamicImage::ImageRgb8(img) => (img.into_vec(), R8G8B8), + image::DynamicImage::ImageRgba8(img) => (img.into_vec(), R8G8B8A8), + image::DynamicImage::ImageLuma16(img) => (img.as_bytes().to_vec(), R16), + i @ image::DynamicImage::ImageLumaA16(_) => { + (i.into_rgba16().as_bytes().to_vec(), R16G16B16A16) + } + i @ image::DynamicImage::ImageRgb16(_) => (i.as_bytes().to_vec(), R16G16B16), + i @ image::DynamicImage::ImageRgba16(_) => (i.as_bytes().to_vec(), R16G16B16A16), + i @ image::DynamicImage::ImageRgb32F(_) => (i.as_bytes().to_vec(), R32G32B32FLOAT), + i @ image::DynamicImage::ImageRgba32F(_) => (i.as_bytes().to_vec(), R32G32B32A32FLOAT), + _ => todo!(), + }; + AtlasImage { + pixels, + format, + // Most of the time when people are using `image` to load images, those images + // have color data that was authored in sRGB space. + apply_linear_transfer: true, + width, + height, + } + } +} + +impl TryFrom for AtlasImage { + type Error = AtlasImageError; + + fn try_from(value: std::path::PathBuf) -> Result { + let img = image::open(value).context(ImageSnafu)?; + Ok(img.into()) + } +} + +impl AtlasImage { + pub fn from_hdr_path(p: impl AsRef) -> Result { + let bytes = std::fs::read(p.as_ref()).with_context(|_| CannotLoadSnafu { + path: std::path::PathBuf::from(p.as_ref()), + })?; + Self::from_hdr_bytes(&bytes) + } + + pub fn from_hdr_bytes(bytes: &[u8]) -> Result { + // Decode HDR data. + let decoder = image::codecs::hdr::HdrDecoder::new(bytes).context(ImageSnafu)?; + let width = decoder.metadata().width; + let height = decoder.metadata().height; + let pixels = decoder.read_image_hdr().unwrap(); + + // Add alpha data. + let mut pixel_data: Vec = Vec::new(); + for pixel in pixels { + pixel_data.push(pixel[0]); + pixel_data.push(pixel[1]); + pixel_data.push(pixel[2]); + pixel_data.push(1.0); + } + let mut pixels = vec![]; + pixels.extend_from_slice(bytemuck::cast_slice(pixel_data.as_slice())); + + Ok(Self { + pixels, + width, + height, + format: AtlasImageFormat::R32G32B32A32FLOAT, + apply_linear_transfer: false, + }) + } + + pub fn from_path(p: impl AsRef) -> Result { + Self::try_from(p.as_ref().to_path_buf()) + } + + pub fn into_rgba8(self) -> Option { + let pixels = convert_to_rgba8_bytes(self.pixels, self.format, self.apply_linear_transfer); + image::RgbaImage::from_vec(self.width, self.height, pixels) + } +} + +fn u16_to_u8(c: u16) -> u8 { + ((c as f32 / 65535.0) * 255.0) as u8 +} + +fn f32_to_u8(c: f32) -> u8 { + (c / 255.0) as u8 +} + +/// Interpret/convert the pixel data into rgba8 pixels. +/// +/// This applies the linear transfer function if `apply_linear_transfer` is +/// `true`. +pub fn convert_to_rgba8_bytes( + mut bytes: Vec, + format: AtlasImageFormat, + apply_linear_transfer: bool, +) -> Vec { + use crate::color::*; + log::trace!("converting image of format {format:?}"); + // Convert using linear transfer, if needed + if apply_linear_transfer { + log::trace!(" converting to linear color space (from sRGB)"); + match format { + AtlasImageFormat::R8 + | AtlasImageFormat::R8G8 + | AtlasImageFormat::R8G8B8 + | AtlasImageFormat::R8G8B8A8 => { + bytes.iter_mut().for_each(linear_xfer_u8); + } + AtlasImageFormat::R16 + | AtlasImageFormat::R16G16 + | AtlasImageFormat::R16G16B16 + | AtlasImageFormat::R16G16B16A16 => { + let bytes: &mut [u16] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_u16); + } + AtlasImageFormat::R16G16B16A16FLOAT => { + let bytes: &mut [u16] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_f16); + } + AtlasImageFormat::R32G32B32FLOAT | AtlasImageFormat::R32G32B32A32FLOAT => { + let bytes: &mut [f32] = bytemuck::cast_slice_mut(&mut bytes); + bytes.into_iter().for_each(linear_xfer_f32); + } + } + } + + // Convert to rgba8 + match format { + AtlasImageFormat::R8 => bytes.into_iter().flat_map(|r| [r, 0, 0, 255]).collect(), + AtlasImageFormat::R8G8 => bytes + .chunks_exact(2) + .flat_map(|p| { + if let [r, g] = p { + [*r, *g, 0, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R8G8B8 => bytes + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [*r, *g, *b, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R8G8B8A8 => bytes, + AtlasImageFormat::R16 => bytemuck::cast_slice::(&bytes) + .into_iter() + .flat_map(|r| [u16_to_u8(*r), 0, 0, 255]) + .collect(), + AtlasImageFormat::R16G16 => bytemuck::cast_slice::(&bytes) + .chunks_exact(2) + .flat_map(|p| { + if let [r, g] = p { + [u16_to_u8(*r), u16_to_u8(*g), 0, 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R16G16B16 => bytemuck::cast_slice::(&bytes) + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [u16_to_u8(*r), u16_to_u8(*g), u16_to_u8(*b), 255] + } else { + unreachable!() + } + }) + .collect(), + + AtlasImageFormat::R16G16B16A16 => bytemuck::cast_slice::(&bytes) + .into_iter() + .copied() + .map(u16_to_u8) + .collect(), + AtlasImageFormat::R16G16B16A16FLOAT => bytemuck::cast_slice::(&bytes) + .into_iter() + .map(|bits| half::f16::from_bits(*bits).to_f32()) + .collect::>() + .chunks_exact(4) + .flat_map(|p| { + if let [r, g, b, a] = p { + [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), f32_to_u8(*a)] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R32G32B32FLOAT => bytemuck::cast_slice::(&bytes) + .chunks_exact(3) + .flat_map(|p| { + if let [r, g, b] = p { + [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), 255] + } else { + unreachable!() + } + }) + .collect(), + AtlasImageFormat::R32G32B32A32FLOAT => bytemuck::cast_slice::(&bytes) + .into_iter() + .copied() + .map(f32_to_u8) + .collect(), + } +} diff --git a/crates/renderling/src/bloom.rs b/crates/renderling/src/bloom.rs deleted file mode 100644 index ca2014fb..00000000 --- a/crates/renderling/src/bloom.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! Resources for the bloom filter pass. - -use std::sync::Arc; - -use glam::UVec2; -use moongraph::{View, ViewMut}; - -use crate::{HdrSurface, Uniform}; - -fn create_bloom_texture( - device: &wgpu::Device, - queue: &wgpu::Queue, - width: u32, - height: u32, -) -> crate::Texture { - crate::Texture::new_with( - device, - queue, - Some("bloom pingpong tex"), - Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING), - Some(device.create_sampler(&wgpu::SamplerDescriptor { - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - })), - wgpu::TextureFormat::Rgba16Float, - 4, - 1, - width, - height, - 1, - &[], - ) -} - -fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("bloom"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - ], - }) -} - -fn create_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { - let bg_layout = create_bindgroup_layout(device); - let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("bloom"), - bind_group_layouts: &[&bg_layout], - push_constant_ranges: &[], - }); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("bloom filter"), - layout: Some(&pp_layout), - vertex: wgpu::VertexState { - module: &device.create_shader_module(wgpu::include_spirv!( - "linkage/convolution-vertex_generate_mipmap.spv" - )), - entry_point: "convolution::vertex_generate_mipmap", - buffers: &[], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - fragment: Some(wgpu::FragmentState { - module: &device.create_shader_module(wgpu::include_spirv!( - "linkage/convolution-fragment_bloom.spv" - )), - entry_point: "convolution::fragment_bloom", - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - multiview: None, - }) -} - -fn create_bindgroup( - device: &wgpu::Device, - horizontal_uniform: &Uniform, - size_uniform: &Uniform, - texture: &crate::Texture, -) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("bloom filter"), - layout: &create_bindgroup_layout(device), - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - horizontal_uniform.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer( - size_uniform.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&texture.view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&texture.sampler), - }, - ], - }) -} - -pub struct BloomFilter { - pub on: bool, - textures: [crate::Texture; 2], - tonemap_bindgroup: Arc, - pipeline: wgpu::RenderPipeline, - horizontal_uniform: Uniform, - size_uniform: Uniform, - initial_bindgroup: Option, - bindgroups: [wgpu::BindGroup; 2], -} - -impl BloomFilter { - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self { - let tonemap_bg_layout = crate::hdr::texture_and_sampler_layout(device, Some("bloom")); - let textures = [ - create_bloom_texture(device, queue, width, height), - create_bloom_texture(device, queue, width, height), - ]; - let tonemap_bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("tonemap-bloom"), - layout: &tonemap_bg_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&textures[1].view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&textures[1].sampler), - }, - ], - }); - let size_uniform = Uniform::new( - device, - UVec2::new(width, height), - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::FRAGMENT, - ); - let horizontal_uniform = Uniform::new( - device, - 1, - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::FRAGMENT, - ); - let bindgroups = [ - // bindgroup 'A' reads from pingpong 1 and writes to pingpong 0 (see `run`) - create_bindgroup(device, &horizontal_uniform, &size_uniform, &textures[1]), - // bindgroup 'B' reads from pingpong 0 and writes to pingpong 1 (see `run`) - create_bindgroup(device, &horizontal_uniform, &size_uniform, &textures[0]), - ]; - BloomFilter { - on: true, - pipeline: create_pipeline(device), - size_uniform, - horizontal_uniform, - textures, - initial_bindgroup: None, - bindgroups, - tonemap_bindgroup: tonemap_bindgroup.into(), - } - } - - pub fn run( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - hdr_surface: &crate::HdrSurface, - ) -> Arc { - let brightness_texture = &hdr_surface.brightness_texture; - // update the size if the size has changed - let size = brightness_texture.texture.size(); - let size = UVec2::new(size.width, size.height); - if size != *self.size_uniform { - *self.size_uniform = size; - self.size_uniform.update(queue); - } - - if brightness_texture.texture.size() != self.textures[0].texture.size() { - let width = size.x; - let height = size.y; - self.textures = [ - create_bloom_texture(device, queue, width, height), - create_bloom_texture(device, queue, width, height), - ]; - self.bindgroups = [ - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - &self.textures[1], - ), - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - &self.textures[0], - ), - ]; - } - - // if the brightness texture is not - if self.initial_bindgroup.is_none() { - self.initial_bindgroup = Some( - // initial bindgroup reads from brightness texture - create_bindgroup( - device, - &self.horizontal_uniform, - &self.size_uniform, - brightness_texture, - ), - ); - }; - // UNWRAP: safe because we just set it above - let initial_bindgroup = self.initial_bindgroup.as_ref().unwrap(); - - // first do a clear pass on the pingpong textures - crate::frame::conduct_clear_pass( - device, - queue, - Some("bloom filter clear"), - vec![&self.textures[0].view, &self.textures[1].view], - None, - wgpu::Color::TRANSPARENT, - ); - - for i in 0..10 { - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some(&format!("bloom-filter{i}")), - }); - - // index == 0 is group 'A', 1 is group 'B' - let index = i % 2; - - *self.horizontal_uniform = index as u32; - self.horizontal_uniform.update(queue); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&format!("bloomfilter{i}_index{index}")), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.textures[index].view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - render_pass.set_pipeline(&self.pipeline); - - // * if i == 0 we read from brightness_texture and write to textures[0] - // * if index == 1 we read from textures[0] and write to textures[1] - // * if index == 0 we read from textures[1] and write to textures[0] - let bindgroup = if i == 0 { - initial_bindgroup - } else { - &self.bindgroups[index] - }; - render_pass.set_bind_group(0, bindgroup, &[]); - render_pass.draw(0..6, 0..1); - } - - queue.submit([encoder.finish()]); - } - - self.tonemap_bindgroup.clone() - } -} - -pub struct BloomResult(pub Option>); - -pub fn bloom_filter( - (device, queue, mut bloom, hdr): ( - View, - View, - ViewMut, - View, - ), -) -> Result<(BloomResult,), crate::WgpuStateError> { - let may_bg = if bloom.on { - let bg = bloom.run(&device, &queue, &hdr); - Some(bg) - } else { - None - }; - Ok((BloomResult(may_bg),)) -} - -#[cfg(test)] -mod test { - use glam::{Mat4, Vec3}; - - use crate::Renderling; - - use super::BloomFilter; - - #[test] - fn bloom_on_off() { - let mut renderling = - Renderling::headless(100, 100).with_background_color(glam::Vec4::splat(1.0)); - let mut builder = renderling.new_scene(); - let loader = builder - .gltf_load("../../gltf/EmissiveStrengthTest.glb") - .unwrap(); - // find the bounding box of the model so we can display it correctly - let mut min = Vec3::splat(f32::INFINITY); - let mut max = Vec3::splat(f32::NEG_INFINITY); - for node in loader.nodes.iter() { - let entity = builder.entities.get(node.entity_id.index()).unwrap(); - let (translation, rotation, scale) = entity.get_world_transform(&builder.entities); - let tfrm = Mat4::from_scale_rotation_translation(scale, rotation, translation); - if let Some(mesh_index) = node.gltf_mesh_index { - for primitive in loader.meshes.get(mesh_index).unwrap().iter() { - let bbmin = tfrm.transform_point3(primitive.bounding_box.min); - let bbmax = tfrm.transform_point3(primitive.bounding_box.max); - min = min.min(bbmin); - max = max.max(bbmax); - } - } - } - - let length = min.distance(max); - let (projection, _) = crate::camera::default_perspective(100.0, 100.0); - let view = crate::camera::look_at(Vec3::new(0.0, 0.0, length), Vec3::ZERO, Vec3::Y); - builder.set_camera(projection, view); - let scene = builder.build().unwrap(); - - renderling.setup_render_graph(crate::RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = renderling.render_image().unwrap(); - img_diff::assert_img_eq("bloom/on.png", img); - - { - let bloom = renderling - .graph - .get_resource_mut::() - .unwrap() - .unwrap(); - bloom.on = false; - } - let img = renderling.render_image().unwrap(); - img_diff::assert_img_eq("bloom/off.png", img); - } -} diff --git a/crates/renderling/src/color.rs b/crates/renderling/src/color.rs index 40ca48cf..eeffa559 100644 --- a/crates/renderling/src/color.rs +++ b/crates/renderling/src/color.rs @@ -4,6 +4,10 @@ pub fn linear_xfer_u8(c: &mut u8) { *c = ((*c as f32 / 255.0).powf(2.2) * 255.0) as u8; } +pub fn opto_xfer_u8(c: &mut u8) { + *c = ((*c as f32 / 255.0).powf(1.0 / 2.2) * 255.0) as u8; +} + pub fn linear_xfer_u16(c: &mut u16) { *c = ((*c as f32 / 65535.0).powf(2.2) * 65535.0) as u16; } diff --git a/crates/renderling/src/cubemap.rs b/crates/renderling/src/cubemap.rs index 8eed46d7..f785778a 100644 --- a/crates/renderling/src/cubemap.rs +++ b/crates/renderling/src/cubemap.rs @@ -1,9 +1,5 @@ //! Render pipelines and layouts for creating cubemaps. -use renderling_shader::stage::GpuConstants; - -use crate::Uniform; - pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("cubemap-making bindgroup"), @@ -12,7 +8,7 @@ pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroup binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -41,7 +37,7 @@ pub fn cubemap_making_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroup pub fn cubemap_making_bindgroup( device: &wgpu::Device, label: Option<&str>, - constants: &Uniform, + buffer: &wgpu::Buffer, // The texture to sample the environment from texture: &crate::Texture, ) -> wgpu::BindGroup { @@ -51,9 +47,7 @@ pub fn cubemap_making_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, @@ -74,9 +68,8 @@ impl CubemapMakingRenderPipeline { /// images. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating cubemap-making render pipeline with format '{format:?}'"); - let vertex_shader = device.create_shader_module(wgpu::include_spirv!( - "linkage/skybox-vertex_position_passthru.spv" - )); + let vertex_shader = + device.create_shader_module(wgpu::include_spirv!("linkage/skybox-vertex_cubemap.spv")); let fragment_shader = device.create_shader_module(wgpu::include_spirv!( "linkage/skybox-fragment_equirectangular.spv" )); @@ -92,17 +85,8 @@ impl CubemapMakingRenderPipeline { layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/diffuse_irradiance.rs b/crates/renderling/src/diffuse_irradiance.rs index 9c83bed6..5e53a2fc 100644 --- a/crates/renderling/src/diffuse_irradiance.rs +++ b/crates/renderling/src/diffuse_irradiance.rs @@ -74,8 +74,8 @@ impl DiffuseIrradianceConvolutionRenderPipeline { /// Create the rendering pipeline that performs a convolution. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating convolution render pipeline with format '{format:?}'"); - let vertex_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/vertex_position_passthru.spv")); + let vertex_shader = + device.create_shader_module(wgpu::include_spirv!("linkage/vertex_cubemap.spv")); log::trace!("creating fragment shader"); let fragment_shader = device.create_shader_module(wgpu::include_wgsl!( "wgsl/diffuse_irradiance_convolution.wgsl" @@ -88,23 +88,15 @@ impl DiffuseIrradianceConvolutionRenderPipeline { bind_group_layouts: &[&bg_layout], push_constant_ranges: &[], }); + // TODO: merge irradiance pipeline with the pipeline in cubemap.rs let pipeline = DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( &wgpu::RenderPipelineDescriptor { label: Some("convolution pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/hdr.rs b/crates/renderling/src/hdr.rs index f8d5fda5..0a09668a 100644 --- a/crates/renderling/src/hdr.rs +++ b/crates/renderling/src/hdr.rs @@ -1,12 +1,13 @@ //! High definition rendering types and techniques. //! //! Also includes bloom effect. +use crabslab::{CpuSlab, Slab, SlabItem, WgpuBuffer}; use moongraph::*; use renderling_shader::tonemapping::TonemapConstants; use crate::{ frame::FrameTextureView, math::Vec4, BackgroundColor, DepthTexture, Device, Queue, - RenderTarget, ScreenSize, Uniform, WgpuStateError, + RenderTarget, ScreenSize, WgpuStateError, }; /// A texture, tonemapping pipeline and uniform used for high dynamic range @@ -15,12 +16,9 @@ use crate::{ /// See https://learnopengl.com/Advanced-Lighting/HDR. pub struct HdrSurface { pub hdr_texture: crate::Texture, - pub brightness_texture: crate::Texture, - pub texture_bindgroup: wgpu::BindGroup, - pub no_bloom_texture: crate::Texture, - pub no_bloom_bindgroup: wgpu::BindGroup, + pub bindgroup: wgpu::BindGroup, pub tonemapping_pipeline: wgpu::RenderPipeline, - pub constants: Uniform, + pub slab: CpuSlab, } impl HdrSurface { @@ -59,59 +57,66 @@ impl HdrSurface { ) } - pub fn create_texture_bindgroup( + pub fn create_bindgroup( device: &wgpu::Device, - texture: &crate::Texture, + hdr_texture: &crate::Texture, + slab_buffer: &wgpu::Buffer, ) -> wgpu::BindGroup { - let hdr_texture_layout = scene_hdr_surface_bindgroup_layout(&device); device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("HdrSurface texture bindgroup"), - layout: &hdr_texture_layout, + label: Some("HdrSurface bindgroup"), + layout: &bindgroup_layout(&device, Some("HdrSurface bindgroup")), entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&texture.view), + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: slab_buffer, + offset: 0, + size: None, + }), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Sampler(&texture.sampler), + resource: wgpu::BindingResource::TextureView(&hdr_texture.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler), }, ], }) } - pub fn color_attachments(&self) -> [Option; 2] { - [ - Some(wgpu::RenderPassColorAttachment { - view: &self.hdr_texture.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }), - Some(wgpu::RenderPassColorAttachment { - view: &self.brightness_texture.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }), - ] + pub fn color_attachments(&self) -> [Option; 1] { + [Some(wgpu::RenderPassColorAttachment { + view: &self.hdr_texture.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })] } } -pub fn texture_and_sampler_layout( - device: &wgpu::Device, - label: Option<&str>, -) -> wgpu::BindGroupLayout { +pub fn bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label, entries: &[ + // slab wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // hdr texture + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, @@ -119,8 +124,9 @@ pub fn texture_and_sampler_layout( }, count: None, }, + // hdr sampler wgpu::BindGroupLayoutEntry { - binding: 1, + binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, @@ -129,16 +135,6 @@ pub fn texture_and_sampler_layout( }) } -fn scene_hdr_surface_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - texture_and_sampler_layout(device, Some("hdr buffer bindgroup")) -} - -/// Layout for the bloom texture+sampler that get added to the color before -/// tonemapping. -fn blend_bloom_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - texture_and_sampler_layout(device, Some("blend bloom")) -} - pub fn create_hdr_render_surface( (device, queue, size, target): ( View, @@ -147,13 +143,10 @@ pub fn create_hdr_render_surface( View, ), ) -> Result<(HdrSurface,), WgpuStateError> { - let (constants, constants_layout) = Uniform::new_and_layout( - &device, - TonemapConstants::default(), - wgpu::BufferUsages::UNIFORM, - wgpu::ShaderStages::FRAGMENT, - ); - let bloom_layout = blend_bloom_layout(&device); + let buffer = WgpuBuffer::new(&*device, &*queue, TonemapConstants::slab_size()); + let mut slab = CpuSlab::new(buffer); + // TODO: make the tonemapping configurable + slab.write(0u32.into(), &TonemapConstants::default()); let size = wgpu::Extent3d { width: size.width, height: size.height, @@ -165,10 +158,10 @@ pub fn create_hdr_render_surface( device.create_shader_module(wgpu::include_spirv!("linkage/tonemapping-vertex.spv")); let fragment_shader = device.create_shader_module(wgpu::include_spirv!("linkage/tonemapping-fragment.spv")); - let hdr_texture_layout = scene_hdr_surface_bindgroup_layout(&device); + let hdr_layout = bindgroup_layout(&device, Some("hdr tonemapping")); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label, - bind_group_layouts: &[&hdr_texture_layout, &constants_layout, &bloom_layout], + bind_group_layouts: &[&hdr_layout], push_constant_ranges: &[], }); let tonemapping_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -202,28 +195,14 @@ pub fn create_hdr_render_surface( multiview: None, }); - let no_bloom_texture = HdrSurface::create_texture(&device, &queue, 1, 1); - let no_bloom_bindgroup = HdrSurface::create_texture_bindgroup(&device, &no_bloom_texture); - Ok((HdrSurface { - texture_bindgroup: HdrSurface::create_texture_bindgroup(&device, &hdr_texture), - brightness_texture: HdrSurface::create_texture(&device, &queue, size.width, size.height), - no_bloom_texture, - no_bloom_bindgroup, + bindgroup: HdrSurface::create_bindgroup(&device, &hdr_texture, slab.as_ref().get_buffer()), hdr_texture, tonemapping_pipeline, - constants, + slab, },)) } -/// Update the `HdrSurface` uniforms. -pub fn hdr_surface_update( - (queue, mut hdr_surface): (View, ViewMut), -) -> Result<(), WgpuStateError> { - hdr_surface.constants.update(&queue); - Ok(()) -} - /// Conduct a clear pass on the window surface, the hdr surface and the depth /// texture. pub fn clear_surface_hdr_and_depth( @@ -252,13 +231,25 @@ pub fn clear_surface_hdr_and_depth( &device, &queue, Some("clear_frame_and_depth"), - vec![ - &frame.view, - &hdr.hdr_texture.view, - &hdr.brightness_texture.view, - ], + vec![&frame.view, &hdr.hdr_texture.view], Some(&depth_view), color, ); Ok(()) } + +/// Resize the HDR surface to match [`ScreenSize`]. +pub fn resize_hdr_surface( + (device, queue, size, mut hdr): ( + View, + View, + View, + ViewMut, + ), +) -> Result<(), WgpuStateError> { + let ScreenSize { width, height } = *size; + hdr.hdr_texture = HdrSurface::create_texture(&device, &queue, width, height); + hdr.bindgroup = + HdrSurface::create_bindgroup(&device, &hdr.hdr_texture, hdr.slab.as_ref().get_buffer()); + Ok(()) +} diff --git a/crates/renderling/src/ibl/diffuse_irradiance.rs b/crates/renderling/src/ibl/diffuse_irradiance.rs index 010395bf..403c23a0 100644 --- a/crates/renderling/src/ibl/diffuse_irradiance.rs +++ b/crates/renderling/src/ibl/diffuse_irradiance.rs @@ -1,7 +1,4 @@ //! Pipeline and bindings for for diffuse irradiance convolution shaders. -use renderling_shader::stage::GpuConstants; - -use crate::Uniform; pub fn diffuse_irradiance_convolution_bindgroup_layout( device: &wgpu::Device, @@ -13,7 +10,7 @@ pub fn diffuse_irradiance_convolution_bindgroup_layout( binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -42,7 +39,7 @@ pub fn diffuse_irradiance_convolution_bindgroup_layout( pub fn diffuse_irradiance_convolution_bindgroup( device: &wgpu::Device, label: Option<&str>, - constants: &Uniform, + buffer: &wgpu::Buffer, // The texture to sample the environment from texture: &crate::Texture, ) -> wgpu::BindGroup { @@ -52,9 +49,7 @@ pub fn diffuse_irradiance_convolution_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, @@ -74,9 +69,8 @@ impl DiffuseIrradianceConvolutionRenderPipeline { /// Create the rendering pipeline that performs a convolution. pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { log::trace!("creating convolution render pipeline with format '{format:?}'"); - let vertex_shader = device.create_shader_module(wgpu::include_spirv!( - "../linkage/skybox-vertex_position_passthru.spv" - )); + let vertex_shader = device + .create_shader_module(wgpu::include_spirv!("../linkage/skybox-vertex_cubemap.spv")); log::trace!("creating fragment shader"); let fragment_shader = device.create_shader_module(wgpu::include_wgsl!( // TODO: rewrite this shader in Rust after atomics are added to naga spv @@ -90,23 +84,15 @@ impl DiffuseIrradianceConvolutionRenderPipeline { bind_group_layouts: &[&bg_layout], push_constant_ranges: &[], }); + // TODO: merge irradiance pipeline with cubemap let pipeline = DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( &wgpu::RenderPipelineDescriptor { label: Some("convolution pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "skybox::vertex_position_passthru", - buffers: &[wgpu::VertexBufferLayout { - array_stride: { - let position_size = std::mem::size_of::(); - position_size as wgpu::BufferAddress - }, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + entry_point: "skybox::vertex_cubemap", + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/ibl/prefiltered_environment.rs b/crates/renderling/src/ibl/prefiltered_environment.rs index 117c8651..b662c672 100644 --- a/crates/renderling/src/ibl/prefiltered_environment.rs +++ b/crates/renderling/src/ibl/prefiltered_environment.rs @@ -1,13 +1,9 @@ //! Pipeline for creating a prefiltered environment map from an existing //! environment cubemap. -use crate::Uniform; -use renderling_shader::stage::GpuConstants; - pub fn create_pipeline_and_bindgroup( device: &wgpu::Device, - constants: &Uniform, - roughness: &Uniform, + buffer: &wgpu::Buffer, environment_texture: &crate::Texture, ) -> (wgpu::RenderPipeline, wgpu::BindGroup) { let label = Some("prefiltered environment"); @@ -16,9 +12,9 @@ pub fn create_pipeline_and_bindgroup( entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -27,16 +23,6 @@ pub fn create_pipeline_and_bindgroup( wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::Cube, @@ -45,7 +31,7 @@ pub fn create_pipeline_and_bindgroup( count: None, }, wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, @@ -59,22 +45,14 @@ pub fn create_pipeline_and_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer( - constants.buffer().as_entire_buffer_binding(), - ), + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Buffer( - roughness.buffer().as_entire_buffer_binding(), - ), - }, - wgpu::BindGroupEntry { - binding: 2, resource: wgpu::BindingResource::TextureView(&environment_texture.view), }, wgpu::BindGroupEntry { - binding: 3, + binding: 2, resource: wgpu::BindingResource::Sampler(&environment_texture.sampler), }, ], @@ -96,13 +74,7 @@ pub fn create_pipeline_and_bindgroup( vertex: wgpu::VertexState { module: &vertex_shader, entry_point: "convolution::vertex_prefilter_environment_cubemap", - buffers: &[wgpu::VertexBufferLayout { - array_stride: 3 * std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3 - ], - }], + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index ae24a4ab..4282cabd 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -18,12 +18,9 @@ //! - forward+ style pipeline, configurable lighting model per material //! - [ ] light tiling //! - [ ] occlusion culling -//! - [x] physically based shading -//! - [x] user interface "colored text" shading (uses opacity glyphs in an -//! atlas) -//! - [x] no shading -//! - [ ] gltf support -//! - [ ] scenes, nodes +//! - [x] physically based shading atlas) +//! - [x] gltf support +//! - [x] scenes, nodes //! - [x] cameras //! - [x] meshes //! - [x] materials @@ -32,7 +29,7 @@ //! - [x] animations //! - [x] high definition rendering //! - [x] image based lighting -//! - [x] bloom +//! - [ ] bloom //! - [ ] ssao //! - [ ] depth of field //! @@ -40,8 +37,9 @@ //! You can also use the [shaders module](crate::shaders) without renderlings //! and manage your own resources for maximum flexibility. +// TODO: Audit the API and make it more ergonomic/predictable. + mod atlas; -pub mod bloom; mod buffer_array; mod camera; pub mod cubemap; @@ -52,32 +50,31 @@ mod linkage; pub mod math; pub mod mesh; mod renderer; -mod scene; mod skybox; -mod slab; mod stage; mod state; -#[cfg(feature = "text")] -mod text; +//#[cfg(feature = "text")] +//mod text; mod texture; -mod tutorial; -mod ui; +mod tonemapping; +//mod tutorial; +//mod ui; mod uniform; pub use atlas::*; pub use buffer_array::*; pub use camera::*; pub use hdr::*; +use image::GenericImageView; pub use renderer::*; -pub use scene::*; pub use skybox::*; -pub use slab::*; pub use stage::*; pub use state::*; -#[cfg(feature = "text")] -pub use text::*; +//#[cfg(feature = "text")] +//pub use text::*; pub use texture::*; -pub use ui::*; +pub use tonemapping::*; +//pub use ui::*; pub use uniform::*; pub mod color; @@ -95,102 +92,55 @@ pub mod graph { pub type RenderNode = Node; } +pub use crabslab::*; pub use graph::{graph, Graph, GraphError, Move, View, ViewMut}; -pub use renderling_shader::id::{Id, ID_NONE}; pub mod shader { //! Re-exports of [`renderling_shader`]. pub use renderling_shader::*; } -/// Set up the render graph, including: -/// * 3d scene objects -/// * skybox -/// * bloom filter -/// * hdr tonemapping -/// * UI +pub use renderling_shader::stage::{GpuEntityInfo, LightingModel}; + +/// A CPU-side texture sampler. /// -/// This is mostly for internal use. See [`Renderling::setup_render_graph`]. -pub fn setup_render_graph( - r: &mut Renderling, - scene: Scene, - ui_scene: UiScene, - ui_objects: impl IntoIterator, - with_screen_capture: bool, - with_bloom: bool, -) { - // add resources - let ui_objects = UiDrawObjects(ui_objects.into_iter().collect::>()); - r.graph.add_resource(ui_scene); - r.graph.add_resource(ui_objects); - r.graph.add_resource(scene); - let ui_pipeline = UiRenderPipeline( - r.graph - .visit(|(device, target): (View, View)| { - create_ui_pipeline(&device, target.format()) - }) - .unwrap(), - ); - r.graph.add_resource(ui_pipeline); - - let (hdr_surface,) = r - .graph - .visit(hdr::create_hdr_render_surface) - .unwrap() - .unwrap(); - let device = r.get_device(); - let queue = r.get_queue(); - let hdr_texture_format = hdr_surface.hdr_texture.texture.format(); - let size = hdr_surface.hdr_texture.texture.size(); - let scene_render_pipeline = - SceneRenderPipeline(create_scene_render_pipeline(device, hdr_texture_format)); - let compute_cull_pipeline = - SceneComputeCullPipeline(create_scene_compute_cull_pipeline(device)); - let skybox_pipeline = crate::skybox::create_skybox_render_pipeline(device, hdr_texture_format); - let mut bloom = crate::bloom::BloomFilter::new(device, queue, size.width, size.height); - bloom.on = with_bloom; - r.graph.add_resource(scene_render_pipeline); - r.graph.add_resource(hdr_surface); - r.graph.add_resource(compute_cull_pipeline); - r.graph.add_resource(skybox_pipeline); - r.graph.add_resource(bloom); - - // pre-render subgraph - use frame::{clear_depth, create_frame, present}; - - #[cfg(not(target_arch = "wasm32"))] - let scene_cull = scene_cull_gpu; - #[cfg(target_arch = "wasm32")] - let scene_cull = scene_cull_cpu; - r.graph - .add_subgraph(graph!( - create_frame, - clear_surface_hdr_and_depth, - hdr_surface_update, - scene_update < scene_cull, - ui_scene_update - )) - .add_barrier(); - - // render subgraph - use crate::bloom::bloom_filter; - r.graph - .add_subgraph(graph!( - scene_render - < skybox_render - < bloom_filter - < tonemapping - < clear_depth - < ui_scene_render - )) - .add_barrier(); - - // post-render subgraph - r.graph.add_subgraph(if with_screen_capture { - use crate::frame::copy_frame_to_post; - graph!(copy_frame_to_post < present) - } else { - graph!(present) - }); +/// Provided primarily for testing purposes. +#[derive(Debug, Clone, Copy)] +pub struct CpuSampler; + +impl crate::shader::IsSampler for CpuSampler {} + +#[derive(Debug)] +pub struct CpuTexture2d { + pub image: image::DynamicImage, +} + +impl crate::shader::Sample2d for CpuTexture2d { + type Sampler = CpuSampler; + + fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec2, _lod: f32) -> glam::Vec4 { + // TODO: lerp the CPU texture sampling + let image::Rgba([r, g, b, a]) = self.image.get_pixel(uv.x as u32, uv.y as u32); + glam::Vec4::new( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) + } +} + +/// A CPU-side cubemap texture. +/// +/// Provided primarily for testing purposes. +pub struct CpuCubemap; + +impl crate::shader::SampleCube for CpuCubemap { + type Sampler = CpuSampler; + + fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: glam::Vec3, _lod: f32) -> glam::Vec4 { + // TODO: implement CPU-side cubemap sampling + glam::Vec4::ONE + } } #[cfg(test)] @@ -210,10 +160,13 @@ fn init_logging() { #[cfg(test)] mod test { use super::*; - use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4}; - use moongraph::View; + use glam::{Mat3, Mat4, Quat, Vec2, Vec3, Vec4}; use pretty_assertions::assert_eq; - use renderling_shader::stage::{DrawIndirect, GpuEntity, Vertex}; + use renderling_shader::{ + gltf as gl, + pbr::PbrMaterial, + stage::{light::*, Camera, RenderUnit, Transform, Vertex}, + }; #[test] fn sanity_transmute() { @@ -247,53 +200,80 @@ mod test { ] } - struct CmyTri { - ui: Renderling, - tri: GpuEntity, - } - - fn cmy_triangle_setup() -> CmyTri { + #[test] + // This tests our ability to draw a CMYK triangle in the top left corner. + fn cmy_triangle_sanity() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let (projection, view) = default_ortho2d(100.0, 100.0); - let mut builder = r.new_scene().with_camera(projection, view); - let tri = builder - .new_entity() - .with_meshlet(right_tri_vertices()) + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + let mesh = stage + .new_mesh() + .with_primitive(right_tri_vertices(), [], Id::NONE) .build(); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let _tri = stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 3, ..Default::default() }); - CmyTri { ui: r, tri } - } - - #[test] - fn cmy_triangle_sanity() { - let mut c = cmy_triangle_setup(); - let img = c.ui.render_image().unwrap(); + let img = r.render_image().unwrap(); img_diff::assert_img_eq("cmy_triangle.png", img); } #[test] + // This tests our ability to update the transform of a `RenderUnit` after it + // has already been sent to the GPU. + // We do this by writing over the previous transform in the stage. fn cmy_triangle_update_transform() { - let mut c = cmy_triangle_setup(); - let _ = c.ui.render_image().unwrap(); - - let mut tri = c.tri; - tri.position = Vec4::new(100.0, 0.0, 0.0, 0.0); - tri.rotation = Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2); - tri.scale = Vec4::new(0.5, 0.5, 1.0, 0.0); - c.ui.graph - .visit(|mut scene: ViewMut| { - scene.update_entity(tri).unwrap(); - }) - .unwrap(); + let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + let (projection, view) = default_ortho2d(100.0, 100.0); + let camera = stage.append(&Camera::new(projection, view)); + let mesh = stage + .new_mesh() + .with_primitive(right_tri_vertices(), [], Id::NONE) + .build(); + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let transform = stage.append(&Transform::default()); + let node_path = stage.append_array(&[node]); + let _tri = stage.draw_unit(&RenderUnit { + camera, + node_path, + vertex_count: 3, + transform, + ..Default::default() + }); + + let _ = r.render_image().unwrap(); - let img = c.ui.render_image().unwrap(); + stage.write( + transform, + &Transform { + translation: Vec3::new(100.0, 0.0, 0.0), + rotation: Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2), + scale: Vec3::new(0.5, 0.5, 1.0), + }, + ); + + let img = r.render_image().unwrap(); img_diff::assert_img_eq("cmy_triangle_update_transform.png", img); } @@ -336,143 +316,174 @@ mod test { .collect() } - fn gpu_pyramid_vertices() -> Vec { - let vertices = pyramid_points(); - let indices = pyramid_indices(); - indices - .into_iter() - .map(|i| cmy_gpu_vertex(vertices[i as usize])) - .collect() - } - #[test] + // Tests our ability to draw a CMYK cube. fn cmy_cube_sanity() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let mut builder = r.new_scene().with_camera( - Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), - Mat4::look_at_rh(Vec3::new(0.0, 12.0, 20.0), Vec3::ZERO, Vec3::Y), - ); - - let _cube = builder - .new_entity() - .with_meshlet(gpu_cube_vertices()) - .with_scale(Vec3::new(6.0, 6.0, 6.0)) - .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)) + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + let camera_position = Vec3::new(0.0, 12.0, 20.0); + let camera = stage.append(&Camera { + projection: Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), + view: Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), + position: camera_position, + }); + let vertices = gpu_cube_vertices(); + let vertex_count = vertices.len() as u32; + let mesh = stage + .new_mesh() + .with_primitive(vertices, [], Id::NONE) .build(); - let scene = builder.build().unwrap(); - - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = Transform { + scale: Vec3::new(6.0, 6.0, 6.0), + rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), + ..Default::default() + }; + let transform = stage.append(&transform); + let _cube = stage.draw_unit(&RenderUnit { + camera, + vertex_count, + node_path, + transform, ..Default::default() }); let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube.png", img); + img_diff::assert_img_eq("cmy_cube/sanity.png", img); } #[test] + // Test our ability to create two cubes and toggle the visibility of one of + // them. fn cmy_cube_visible() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let (projection, view) = camera::default_perspective(100.0, 100.0); - let mut builder = r.new_scene().with_camera(projection, view); - - let _cube_one = builder - .new_entity() - .with_meshlet(gpu_cube_vertices()) - .with_position(Vec3::new(-4.5, 0.0, 0.0)) - .with_scale(Vec3::new(6.0, 6.0, 6.0)) - .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)) - .build(); - - let mut cube_two = builder - .new_entity() - .with_meshlet(gpu_cube_vertices()) - .with_position(Vec3::new(4.5, 0.0, 0.0)) - .with_scale(Vec3::new(6.0, 6.0, 6.0)) - .with_rotation(Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4)) + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + let vertices = gpu_cube_vertices(); + let vertex_count = vertices.len() as u32; + let mesh = stage + .new_mesh() + .with_primitive(vertices, [], Id::NONE) .build(); - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, ..Default::default() }); + let mut render_unit = RenderUnit { + camera, + vertex_count, + node_path: stage.append_array(&[node]), + transform: stage.append(&Transform { + translation: Vec3::new(-4.5, 0.0, 0.0), + scale: Vec3::new(6.0, 6.0, 6.0), + rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), + }), + ..Default::default() + }; + let _cube_one = stage.draw_unit(&render_unit); + render_unit.transform = stage.append(&Transform { + translation: Vec3::new(4.5, 0.0, 0.0), + scale: Vec3::new(6.0, 6.0, 6.0), + rotation: Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4), + }); + let cube_two = stage.draw_unit(&render_unit); // we should see two colored cubes let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_visible_before.png", img.clone()); + img_diff::assert_img_eq("cmy_cube/visible_before.png", img.clone()); let img_before = img; - // update cube two making in invisible - r.graph - .visit(|mut scene: ViewMut| { - cube_two.visible = 0; - scene.update_entity(cube_two).unwrap(); - }) - .unwrap(); + // update cube two making it invisible + stage.hide_unit(cube_two); - // we should see one colored cube + // we should see only one colored cube let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_visible_after.png", img); + img_diff::assert_img_eq("cmy_cube/visible_after.png", img); // update cube two making in visible again - r.graph - .visit(|mut scene: ViewMut| { - cube_two.visible = 1; - scene.update_entity(cube_two).unwrap(); - }) - .unwrap(); + stage.show_unit(cube_two); // we should see two colored cubes again let img = r.render_image().unwrap(); - img_diff::assert_eq("cmy_cube_visible_before_again.png", img_before, img); + img_diff::assert_eq("cmy_cube/visible_before_again.png", img_before, img); } #[test] + // Tests the ability to specify indexed vertices, as well as the ability to + // update a field within a struct stored on the slab by offset. fn cmy_cube_remesh() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); + let mut stage = r.new_stage().with_lighting(false); + stage.configure_graph(&mut r, true); let (projection, view) = camera::default_perspective(100.0, 100.0); - let mut builder = r - .new_scene() - .with_camera(projection, view) - .with_lighting(false); - - let (pyramid_start, pyramid_count) = builder.add_meshlet(gpu_pyramid_vertices()); - - let mut cube = builder - .new_entity() - .with_meshlet(gpu_cube_vertices()) - .with_scale(Vec3::new(10.0, 10.0, 10.0)) + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + let pyramid_vertices = pyramid_points().map(cmy_gpu_vertex); + let pyramid_indices = pyramid_indices().map(|i| i as u32); + let _pyramid_vertex_count = pyramid_indices.len(); + let pyramid_mesh = stage + .new_mesh() + .with_primitive(pyramid_vertices, pyramid_indices, Id::NONE) .build(); + let mesh = stage.append(&pyramid_mesh); + let pyramid_node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let pyramid_node_path = stage.append_array(&[pyramid_node]); + + let cube_vertices = renderling_shader::math::UNIT_POINTS.map(cmy_gpu_vertex); + let cube_indices = renderling_shader::math::UNIT_INDICES.map(|i| i as u32); + let cube_vertex_count = cube_indices.len(); + let cube_mesh = stage + .new_mesh() + .with_primitive(cube_vertices, cube_indices, Id::NONE) + .build(); + let mesh = stage.append(&cube_mesh); + let cube_node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[cube_node]); + let transform = stage.append(&Transform { + scale: Vec3::new(10.0, 10.0, 10.0), + ..Default::default() + }); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + let cube = stage.draw_unit(&RenderUnit { + camera, + vertex_count: cube_vertex_count as u32, + node_path, + transform, ..Default::default() }); - // we should see a cube + // we should see a cube (in sRGB color space) let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_remesh_before.png", img); - - // update the cube mesh to a pyramid - r.graph - .visit(|mut scene: ViewMut| { - cube.mesh_first_vertex = pyramid_start; - cube.mesh_vertex_count = pyramid_count; - scene.update_entity(cube).unwrap(); - }) - .unwrap(); + img_diff::assert_img_eq("cmy_cube/remesh_before.png", img); + + // Update the cube mesh to a pyramid by overwriting the `.node_path` field + // of `RenderUnit` + stage.write(cube + RenderUnit::offset_of_node_path(), &pyramid_node_path); - // we should see a pyramid + // we should see a pyramid (in sRGB color space) let img = r.render_image().unwrap(); - img_diff::assert_img_eq("cmy_cube_remesh_after.png", img); + img_diff::assert_img_eq("cmy_cube/remesh_after.png", img); } fn gpu_uv_unit_cube() -> Vec { @@ -522,50 +533,60 @@ mod test { } #[test] - // tests that updating the material actually updates the rendering of an unlit + // Tests that updating the material actually updates the rendering of an unlit // mesh fn unlit_textured_cube_material() { let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(0.0)); - let (proj, view) = camera::default_perspective(100.0, 100.0); - let mut builder = r.new_scene().with_camera(proj, view); - let sandstone = SceneImage::from(image::open("../../img/sandstone.png").unwrap()); - let sandstone_id = builder.add_image_texture(sandstone); - let dirt = SceneImage::from(image::open("../../img/dirt.jpg").unwrap()); - let dirt_id = builder.add_image_texture(dirt); - - let material_id = builder.add_material(PbrMaterial { - albedo_texture: sandstone_id, + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + let (projection, view) = camera::default_perspective(100.0, 100.0); + let camera = stage.append(&Camera { + projection, + view, + ..Default::default() + }); + let sandstone = AtlasImage::from(image::open("../../img/sandstone.png").unwrap()); + let dirt = AtlasImage::from(image::open("../../img/dirt.jpg").unwrap()); + let textures = stage.set_images([sandstone, dirt]).unwrap(); + let sandstone_tex = textures[0]; + let dirt_tex = textures[1]; + let sandstone_tex_id = stage.append(&sandstone_tex); + let material_id = stage.append(&PbrMaterial { + albedo_texture: sandstone_tex_id, lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); - let mut material = builder.materials.get(material_id.index()).copied().unwrap(); - let _cube = builder - .new_entity() - .with_material(material_id) - .with_meshlet(gpu_uv_unit_cube()) - .with_scale(Vec3::new(10.0, 10.0, 10.0)) + let vertices = gpu_uv_unit_cube(); + let vertex_count = vertices.len() as u32; + let mesh = stage + .new_mesh() + .with_primitive(vertices, [], material_id) .build(); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, ..Default::default() }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + scale: Vec3::new(10.0, 10.0, 10.0), + ..Default::default() + }); + let cube = stage.draw_unit(&RenderUnit { + camera, + node_path, + transform, + vertex_count, + ..Default::default() + }); + println!("cube: {cube:?}"); + // we should see a cube with a stoney texture let img = r.render_image().unwrap(); img_diff::assert_img_eq("unlit_textured_cube_material_before.png", img); // update the material's texture on the GPU - r.graph - .visit(|mut scene: ViewMut| { - material.albedo_texture = dirt_id; - let _ = scene - .materials - .overwrite(material_id.index(), vec![material]) - .unwrap(); - }) - .unwrap(); + stage.write(sandstone_tex_id, &dirt_tex); // we should see a cube with a dirty texture let img = r.render_image().unwrap(); @@ -573,611 +594,116 @@ mod test { } #[test] - fn gpu_array_update() { - let (_, device, queue, _) = - futures_lite::future::block_on(crate::state::new_adapter_device_queue_and_target( - 100, - 100, - None as Option, - )) - .unwrap(); - - let points = vec![ - Vec4::new(0.0, 0.0, 0.0, 0.0), - Vec4::new(1.0, 0.0, 0.0, 0.0), - Vec4::new(1.0, 1.0, 0.0, 0.0), - ]; - let mut array = BufferArray::new_gpu( - &device, - &points, - 6, - wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::COPY_SRC, - ); - - // send them to the GPU - array.update(&queue); - // read them back - let verts = futures_lite::future::block_on(array.read_gpu(&device, &queue, 0, 3)).unwrap(); - - println!("{verts:#?}"); - assert_eq!(points, verts); - - let additions = vec![Vec4::splat(1.0), Vec4::splat(2.0)]; - let (start_index, len) = array.overwrite(2, additions.clone()).unwrap(); - assert_eq!((2, 2), (start_index, len)); - - array.update(&queue); - let verts = futures_lite::future::block_on(array.read_gpu(&device, &queue, 0, 4)).unwrap(); - let all_points = points[0..2] - .into_iter() - .copied() - .chain(additions) - .collect::>(); - assert_eq!(all_points, verts); - - let (start, len) = array.extend(vec![Vec4::Y, Vec4::Z]).unwrap(); - assert_eq!((4, 2), (start, len)); - } - - #[test] - fn gpu_scene_sanity1() { + // Ensures that we can render multiple nodes with mesh primitives + // that share the same buffer and geometry but have different materials. + fn multi_node_scene() { let mut r = Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); - let mut builder = r.new_scene(); - - let verts = vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 1.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(100.0, 100.0, 0.0, 1.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(100.0, 0.0, 0.0, 1.0), - color: Vec4::new(1.0, 0.0, 1.0, 1.0), - ..Default::default() - }, - ]; - - let ent = builder.new_entity().with_meshlet(verts.clone()).build(); - - let mut scene = builder.build().unwrap(); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let (projection, view) = camera::default_ortho2d(100.0, 100.0); - scene.set_camera(projection, view); - - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, + let camera = stage.append(&Camera { + projection, + view, ..Default::default() }); - r.graph.visit(scene::scene_update).unwrap().unwrap(); - r.graph.visit(scene::scene_cull_gpu).unwrap().unwrap(); - - let (constants, gpu_verts, ents, indirect) = r - .graph - .visit( - |(scene, device, queue): (View, View, View)| { - let constants = - futures_lite::future::block_on(crate::read_buffer::( - &device, - &queue, - &scene.constants.buffer(), - 0, - 1, - )) - .unwrap(); - let vertices = futures_lite::future::block_on( - scene.vertices.read_gpu(&device, &queue, 0, 3), - ) - .unwrap(); - let entities = futures_lite::future::block_on(scene.entities.read_gpu( - &device, - &queue, - 0, - scene.entities.capacity(), - )) - .unwrap(); - let indirect = if scene.entities.capacity() > 0 { - futures_lite::future::block_on(scene.indirect_draws.read_gpu( - &device, - &queue, - 0, - scene.entities.capacity(), - )) - .unwrap() - } else { - vec![] - }; - (constants[0], vertices, entities, indirect) - }, - ) - .unwrap(); - assert_eq!(constants.camera_projection, projection); - assert_eq!(constants.camera_view, view); - assert_eq!(verts, gpu_verts); - assert_eq!(vec![ent], ents); - assert_eq!( - vec![DrawIndirect { - vertex_count: 3, - instance_count: 1, - base_vertex: 0, - base_instance: 0 - },], - indirect - ); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gpu_scene_sanity.png", img); - } - - #[test] - fn gpu_scene_sanity2() { - let mut r = - Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); - let (projection, view) = camera::default_ortho2d(100.0, 100.0); - let mut builder = r.new_scene().with_camera(projection, view); // now test the textures functionality - let img = image::io::Reader::open("../../img/cheetah.jpg") - .unwrap() - .decode() - .unwrap(); - let tex_id = builder.add_image_texture(img); - assert_eq!(Id::new(0), tex_id); - let material = builder.add_material(PbrMaterial { - albedo_texture: tex_id, + let img = AtlasImage::from_path("../../img/cheetah.jpg").unwrap(); + let textures = stage.set_images([img]).unwrap(); + let textures = stage.append_array(&textures); + let cheetah_material = stage.append(&PbrMaterial { + albedo_texture: textures.at(0), lighting_model: LightingModel::NO_LIGHTING, ..Default::default() }); - let verts = vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - uv: Vec4::new(0.0, 0.0, 0.0, 0.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(100.0, 100.0, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - uv: Vec4::new(1.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(100.0, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 0.0, 1.0, 1.0), - uv: Vec4::new(1.0, 0.0, 1.0, 0.0), - ..Default::default() - }, - ]; - let ent = builder - .new_entity() - .with_meshlet(verts.clone()) - .with_material(material) - .with_position(Vec3::new(15.0, 35.0, 0.5)) - .with_scale(Vec3::new(0.5, 0.5, 1.0)) - .build(); - - assert_eq!(Id::new(0), ent.id); - assert_eq!( - GpuEntity { - id: Id::new(0), - mesh_first_vertex: 0, - mesh_vertex_count: 3, - material: Id::new(0), - position: Vec4::new(15.0, 35.0, 0.5, 0.0), - scale: Vec4::new(0.5, 0.5, 1.0, 1.0), - ..Default::default() - }, - ent - ); - - let ent = builder.new_entity().with_meshlet(verts.clone()).build(); - assert_eq!(Id::new(1), ent.id); - - let scene = builder.build().unwrap(); - assert_eq!(2, scene.entities.len()); - - let textures = scene.atlas.frames().collect::>(); - assert_eq!(1, textures.len()); - assert_eq!(0, textures[0].0); - assert_eq!(UVec2::splat(170), textures[0].1 .1); - - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - - let scene = r.graph.get_resource::().unwrap().unwrap(); - let draws = futures_lite::future::block_on(scene.indirect_draws.read_gpu( - r.get_device(), - r.get_queue(), - 0, - 2, - )) - .unwrap(); - assert_eq!( + let cheetah_primitive = stage.new_primitive( vec![ - DrawIndirect { - vertex_count: 3, - instance_count: 1, - base_vertex: 0, - base_instance: 0 + Vertex { + position: Vec4::new(0.0, 0.0, 0.0, 0.0), + color: Vec4::new(1.0, 1.0, 0.0, 1.0), + uv: Vec4::new(0.0, 0.0, 0.0, 0.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(100.0, 100.0, 0.0, 0.0), + color: Vec4::new(0.0, 1.0, 1.0, 1.0), + uv: Vec4::new(1.0, 1.0, 1.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec4::new(100.0, 0.0, 0.0, 0.0), + color: Vec4::new(1.0, 0.0, 1.0, 1.0), + uv: Vec4::new(1.0, 0.0, 1.0, 0.0), + ..Default::default() }, - DrawIndirect { - vertex_count: 3, - instance_count: 1, - base_vertex: 3, - base_instance: 1 - } ], - draws + [], + cheetah_material, ); - let constants: GpuConstants = futures_lite::future::block_on(read_buffer( - r.get_device(), - r.get_queue(), - &scene.constants.buffer(), - 0, - 1, - )) - .unwrap()[0]; - assert_eq!(UVec2::splat(256), constants.atlas_size); - - let materials = futures_lite::future::block_on(scene.materials.read_gpu( - r.get_device(), - r.get_queue(), - 0, - 1, - )) - .unwrap(); - assert_eq!( - vec![PbrMaterial { - albedo_texture: Id::new(0), - lighting_model: LightingModel::NO_LIGHTING, + let color_primitive = { + let mut prim = cheetah_primitive; + prim.material = Id::NONE; + prim + }; + let _unit = { + let primitives = stage.append_array(&[color_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, ..Default::default() - },], - materials - ); - - img_diff::assert_img_eq("gpu_scene_sanity2.png", img); - } + }); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); - #[test] - fn atlas_uv_mapping() { - let mut r = - Renderling::headless(32, 32).with_background_color(Vec3::splat(0.0).extend(1.0)); - let (projection, view) = camera::default_ortho2d(32.0, 32.0); - let mut builder = r.new_scene().with_camera(projection, view); - let dirt = image::open("../../img/dirt.jpg").unwrap(); - let dirt = builder.add_image(dirt); - println!("dirt: {dirt}"); - let sandstone = image::open("../../img/sandstone.png").unwrap(); - let sandstone = builder.add_image(sandstone); - println!("sandstone: {sandstone}"); - let texels = image::open("../../test_img/atlas_uv_mapping.png").unwrap(); - let texels_index = builder.add_image(texels); - println!("atlas_uv_mapping: {texels_index}"); - let texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::CLAMP_TO_EDGE, - mode_t: TextureAddressMode::CLAMP_TO_EDGE, - }); - let material_id = builder.add_material(PbrMaterial { - albedo_texture: texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let _ = builder - .new_entity() - .with_material(material_id) - .with_meshlet({ - let tl = Vertex::default() - .with_position(Vec3::ZERO) - .with_uv0(Vec2::ZERO); - let tr = Vertex::default() - .with_position(Vec3::new(1.0, 0.0, 0.0)) - .with_uv0(Vec2::new(1.0, 0.0)); - let bl = Vertex::default() - .with_position(Vec3::new(0.0, 1.0, 0.0)) - .with_uv0(Vec2::new(0.0, 1.0)); - let br = Vertex::default() - .with_position(Vec3::new(1.0, 1.0, 0.0)) - .with_uv0(Vec2::splat(1.0)); - vec![tl, bl, br, tl, br, tr] + stage.draw_unit(&RenderUnit { + camera, + vertex_count: cheetah_primitive.vertex_count, + node_path, + ..Default::default() }) - .with_scale([32.0, 32.0, 1.0]) - .build(); - let scene = builder.build().unwrap(); - // let atlas_img = scene.atlas.texture.read( - // r.get_device(), - // r.get_queue(), - // scene.atlas.size.x as usize, - // scene.atlas.size.y as usize, - // 4, - // 1, - //); - // let atlas_img = atlas_img.into_rgba(r.get_device()).unwrap(); - // img_diff::save("atlas.png", atlas_img); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("atlas_uv_mapping.png", img); - } - - #[test] - fn uv_wrapping() { - let icon_w = 32; - let icon_h = 41; - let sheet_w = icon_w * 3; - let sheet_h = icon_h * 3; - let w = sheet_w * 3 + 2; - let h = sheet_h; - let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); - let (projection, view) = camera::default_ortho2d(w as f32, h as f32); - let mut builder = r.new_scene().with_camera(projection, view); - let dirt = image::open("../../img/dirt.jpg").unwrap(); - builder.add_image(dirt); - let sandstone = image::open("../../img/sandstone.png").unwrap(); - builder.add_image(sandstone); - let texels = image::open("../../img/happy_mac.png").unwrap(); - let texels_index = builder.add_image(texels); - let clamp_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::CLAMP_TO_EDGE, - mode_t: TextureAddressMode::CLAMP_TO_EDGE, - }); - let repeat_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::REPEAT, - mode_t: TextureAddressMode::REPEAT, - }); - let mirror_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::MIRRORED_REPEAT, - mode_t: TextureAddressMode::MIRRORED_REPEAT, - }); - - let clamp_material_id = builder.add_material(PbrMaterial { - albedo_texture: clamp_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let repeat_material_id = builder.add_material(PbrMaterial { - albedo_texture: repeat_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let mirror_material_id = builder.add_material(PbrMaterial { - albedo_texture: mirror_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - - let sheet_w = sheet_w as f32; - let sheet_h = sheet_h as f32; - - let (start, count) = builder.add_meshlet({ - let tl = Vertex::default() - .with_position(Vec3::ZERO) - .with_uv0(Vec2::ZERO); - let tr = Vertex::default() - .with_position(Vec3::new(sheet_w, 0.0, 0.0)) - .with_uv0(Vec2::new(3.0, 0.0)); - let bl = Vertex::default() - .with_position(Vec3::new(0.0, sheet_h, 0.0)) - .with_uv0(Vec2::new(0.0, 3.0)); - let br = Vertex::default() - .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) - .with_uv0(Vec2::splat(3.0)); - vec![tl, bl, br, tl, br, tr] - }); - - let _clamp = builder - .new_entity() - .with_material(clamp_material_id) - .with_starting_vertex_and_count(start, count) - .build(); - let _repeat = builder - .new_entity() - .with_material(repeat_material_id) - .with_starting_vertex_and_count(start, count) - .with_position([sheet_w as f32 + 1.0, 0.0, 0.0]) - .build(); - let _mirror = builder - .new_entity() - .with_material(mirror_material_id) - .with_starting_vertex_and_count(start, count) - .with_position([sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0]) - .build(); - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("uv_wrapping.png", img); - } - - #[test] - fn negative_uv_wrapping() { - let icon_w = 32; - let icon_h = 41; - let sheet_w = icon_w * 3; - let sheet_h = icon_h * 3; - let w = sheet_w * 3 + 2; - let h = sheet_h; - let mut r = Renderling::headless(w, h).with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); - let (projection, view) = camera::default_ortho2d(w as f32, h as f32); - let mut builder = r.new_scene().with_camera(projection, view); - let dirt = image::open("../../img/dirt.jpg").unwrap(); - builder.add_image(dirt); - let sandstone = image::open("../../img/sandstone.png").unwrap(); - builder.add_image(sandstone); - let texels = image::open("../../img/happy_mac.png").unwrap(); - let texels_index = builder.add_image(texels); - let clamp_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::CLAMP_TO_EDGE, - mode_t: TextureAddressMode::CLAMP_TO_EDGE, - }); - let repeat_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::REPEAT, - mode_t: TextureAddressMode::REPEAT, - }); - let mirror_texture_id = builder.add_texture(TextureParams { - image_index: texels_index, - mode_s: TextureAddressMode::MIRRORED_REPEAT, - mode_t: TextureAddressMode::MIRRORED_REPEAT, - }); - - let clamp_material_id = builder.add_material(PbrMaterial { - albedo_texture: clamp_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let repeat_material_id = builder.add_material(PbrMaterial { - albedo_texture: repeat_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let mirror_material_id = builder.add_material(PbrMaterial { - albedo_texture: mirror_texture_id, - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - - let sheet_w = sheet_w as f32; - let sheet_h = sheet_h as f32; - - let (start, count) = builder.add_meshlet({ - let tl = Vertex::default() - .with_position(Vec3::ZERO) - .with_uv0(Vec2::ZERO); - let tr = Vertex::default() - .with_position(Vec3::new(sheet_w, 0.0, 0.0)) - .with_uv0(Vec2::new(-3.0, 0.0)); - let bl = Vertex::default() - .with_position(Vec3::new(0.0, sheet_h, 0.0)) - .with_uv0(Vec2::new(0.0, -3.0)); - let br = Vertex::default() - .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) - .with_uv0(Vec2::splat(-3.0)); - vec![tl, bl, br, tl, br, tr] - }); - - let _clamp = builder - .new_entity() - .with_material(clamp_material_id) - .with_starting_vertex_and_count(start, count) - .build(); - let _repeat = builder - .new_entity() - .with_material(repeat_material_id) - .with_starting_vertex_and_count(start, count) - .with_position([sheet_w as f32 + 1.0, 0.0, 0.0]) - .build(); - let _mirror = builder - .new_entity() - .with_material(mirror_material_id) - .with_starting_vertex_and_count(start, count) - .with_position([sheet_w as f32 * 2.0 + 2.0, 0.0, 0.0]) - .build(); - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); + }; + let _cheetah_unit = { + let primitives = stage.append_array(&[cheetah_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + translation: Vec3::new(15.0, 35.0, 0.5), + scale: Vec3::new(0.5, 0.5, 1.0), + ..Default::default() + }); + stage.draw_unit(&RenderUnit { + camera, + transform, + vertex_count: cheetah_primitive.vertex_count, + node_path, + ..Default::default() + }) + }; let img = r.render_image().unwrap(); - img_diff::assert_img_eq("negative_uv_wrapping.png", img); - } - #[test] - fn transform_uvs_for_atlas() { - let mut tex = GpuTexture { - offset_px: UVec2::ZERO, - size_px: UVec2::ONE, - modes: { - let mut modes = TextureModes::default(); - modes.set_wrap_s(TextureAddressMode::CLAMP_TO_EDGE); - modes.set_wrap_t(TextureAddressMode::CLAMP_TO_EDGE); - modes - }, - ..Default::default() - }; - assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(100))); - assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(1))); - assert_eq!(Vec2::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(256))); - tex.offset_px = UVec2::splat(10); - assert_eq!(Vec2::splat(0.1), tex.uv(Vec2::ZERO, UVec2::splat(100))); + img_diff::assert_img_eq("gpu_scene_sanity2.png", img); } #[test] - /// Ensures that the directional light coloring works. + /// Tests shading with directional light. fn scene_cube_directional() { let mut r = Renderling::headless(100, 100).with_background_color(Vec3::splat(0.0).extend(1.0)); - - let mut builder = r.new_scene(); - let red = Vec3::X.extend(1.0); - let green = Vec3::Y.extend(1.0); - let blue = Vec3::Z.extend(1.0); - let _dir_red = builder - .new_directional_light() - .with_direction(Vec3::NEG_Y) - .with_color(red) - .with_intensity(10.0) - .build(); - let _dir_green = builder - .new_directional_light() - .with_direction(Vec3::NEG_X) - .with_color(green) - .with_intensity(10.0) - .build(); - let _dir_blue = builder - .new_directional_light() - .with_direction(Vec3::NEG_Z) - .with_color(blue) - .with_intensity(10.0) - .build(); - - let material = builder.add_material(PbrMaterial::default()); - - let _cube = builder - .new_entity() - .with_meshlet( - renderling_shader::math::unit_cube() - .into_iter() - .map(|(p, n)| Vertex { - position: p.extend(1.0), - normal: n.extend(0.0), - ..Default::default() - }), - ) - .with_material(material) - .build(); - - let mut scene = builder.build().unwrap(); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let (projection, _) = camera::default_perspective(100.0, 100.0); let view = Mat4::look_at_rh( @@ -1185,14 +711,59 @@ mod test { Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), ); - scene.set_camera(projection, view); + let camera = stage.append(&Camera::default().with_projection_and_view(projection, view)); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() + let red = Vec3::X.extend(1.0); + let green = Vec3::Y.extend(1.0); + let blue = Vec3::Z.extend(1.0); + let dir_red = stage.append(&DirectionalLight { + direction: Vec3::NEG_Y, + color: red, + intensity: 10.0, + }); + let dir_green = stage.append(&DirectionalLight { + direction: Vec3::NEG_X, + color: green, + intensity: 10.0, }); + let dir_blue = stage.append(&DirectionalLight { + direction: Vec3::NEG_Z, + color: blue, + intensity: 10.0, + }); + assert_eq!( + Light { + light_type: LightStyle::Directional, + index: dir_red.inner() + }, + dir_red.into() + ); + let lights = stage.append_array(&[dir_red.into(), dir_green.into(), dir_blue.into()]); + stage.set_lights(lights); + let material = stage.append(&PbrMaterial::default()); + let verts = renderling_shader::math::unit_cube() + .into_iter() + .map(|(p, n)| Vertex { + position: p.extend(1.0), + normal: n.extend(0.0), + ..Default::default() + }) + .collect::>(); + let vertex_count = verts.len() as u32; + let mesh = stage.new_mesh().with_primitive(verts, [], material).build(); + let mesh = stage.append(&mesh); + let node = stage.append(&gl::GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let _cube = stage.draw_unit(&RenderUnit { + camera, + vertex_count, + node_path, + ..Default::default() + }); let img = r.render_image().unwrap(); img_diff::assert_img_eq("scene_cube_directional.png", img); } @@ -1239,149 +810,132 @@ mod test { fn scene_parent_sanity() { let mut r = Renderling::headless(100, 100); r.set_background_color(Vec4::splat(0.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let (projection, view) = camera::default_ortho2d(100.0, 100.0); - let mut builder = r.new_scene().with_camera(projection, view); + let camera = stage.append(&Camera::new(projection, view)); let size = 1.0; - let root = builder.new_entity().with_scale([25.0, 25.0, 1.0]).build(); - let cyan_tri = builder - .new_entity() - .with_meshlet(vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, size, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, 0.0, 0.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - ..Default::default() - }, - ]) - .with_position([1.0, 1.0, 0.0]) - .with_parent(root) - .build(); - let yellow_tri = builder // - .new_entity() - .with_meshlet(vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, size, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - ..Default::default() - }, - ]) - .with_position([1.0, 1.0, 0.0]) - .with_parent(&cyan_tri) - .build(); - let _red_tri = builder - .new_entity() - .with_meshlet(vec![ - Vertex { - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 0.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, size, 0.0, 0.0), - color: Vec4::new(1.0, 0.0, 0.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec4::new(size, 0.0, 0.0, 0.0), - color: Vec4::new(1.0, 0.0, 0.0, 1.0), - ..Default::default() - }, - ]) - .with_position([1.0, 1.0, 0.0]) - .with_parent(&yellow_tri) - .build(); + let cyan_material = stage.append(&PbrMaterial { + albedo_factor: Vec4::new(0.0, 1.0, 1.0, 1.0), + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + let yellow_material = stage.append(&PbrMaterial { + albedo_factor: Vec4::new(1.0, 1.0, 0.0, 1.0), + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + let red_material = stage.append(&PbrMaterial { + albedo_factor: Vec4::new(1.0, 0.0, 0.0, 1.0), + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); - assert_eq!( - vec![ - GpuEntity { - id: Id::new(0), - position: Vec4::new(0.0, 0.0, 0.0, 0.0), - scale: Vec4::new(25.0, 25.0, 1.0, 1.0), - ..Default::default() - }, - GpuEntity { - id: Id::new(1), - parent: Id::new(0), - position: Vec4::new(1.0, 1.0, 0.0, 0.0), - scale: Vec4::new(1.0, 1.0, 1.0, 1.0), - mesh_first_vertex: 0, - mesh_vertex_count: 3, - ..Default::default() - }, - GpuEntity { - id: Id::new(2), - parent: Id::new(1), - position: Vec4::new(1.0, 1.0, 0.0, 0.0), - scale: Vec4::new(1.0, 1.0, 1.0, 1.0), - mesh_first_vertex: 3, - mesh_vertex_count: 3, - ..Default::default() - }, - GpuEntity { - id: Id::new(3), - parent: Id::new(2), - position: Vec4::new(1.0, 1.0, 0.0, 0.0), - scale: Vec4::new(1.0, 1.0, 1.0, 1.0), - mesh_first_vertex: 6, - mesh_vertex_count: 3, - ..Default::default() - } + let cyan_primitive = stage.new_primitive( + [ + Vertex::default().with_position([0.0, 0.0, 0.0]), + Vertex::default().with_position([size, size, 0.0]), + Vertex::default().with_position([size, 0.0, 0.0]), ], - builder.entities + [], + cyan_material, ); - let tfrm = yellow_tri.get_world_transform(&builder.entities); - assert_eq!( - ( - Vec3::new(50.0, 50.0, 0.0), - Quat::IDENTITY, - Vec3::new(25.0, 25.0, 1.0), - ), - tfrm + let yellow_primitive = { + let mut p = cyan_primitive; + p.material = yellow_material; + p + }; + let red_primitive = { + let mut p = cyan_primitive; + p.material = red_material; + p + }; + + let root_node = stage.allocate::(); + let cyan_node = stage.allocate::(); + let yellow_node = stage.allocate::(); + let red_node = stage.allocate::(); + + // Write the nodes now that we have references to them all + let children = stage.append_array(&[cyan_node]); + stage.write( + root_node, + &gl::GltfNode { + children, + scale: Vec3::new(25.0, 25.0, 1.0), + ..Default::default() + }, ); - // while we're at it let's also check that skinning doesn't affect - // entities/vertices that aren't skins - let vertex = &builder.vertices[yellow_tri.mesh_first_vertex as usize]; - let skin_matrix = vertex.get_skin_matrix(&yellow_tri.skin_joint_ids, &builder.entities); - assert_eq!(Mat4::IDENTITY, skin_matrix); - - let entities = builder.entities.clone(); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, + + let primitives = stage.append_array(&[cyan_primitive]); + let children = stage.append_array(&[yellow_node]); + let mesh = stage.append(&gl::GltfMesh { + primitives, ..Default::default() }); + stage.write( + cyan_node, + &gl::GltfNode { + mesh, + children, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); - let gpu_entities = futures_lite::future::block_on( - r.graph - .get_resource::() - .unwrap() - .unwrap() - .entities - .read_gpu(r.get_device(), r.get_queue(), 0, entities.len()), - ) - .unwrap(); - assert_eq!(entities, gpu_entities); + let primitives = stage.append_array(&[yellow_primitive]); + let children = stage.append_array(&[red_node]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + stage.write( + yellow_node, + &gl::GltfNode { + mesh, + children, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); + + let primitives = stage.append_array(&[red_primitive]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + stage.write( + red_node, + &gl::GltfNode { + mesh, + translation: Vec3::new(1.0, 1.0, 0.0), + ..Default::default() + }, + ); + + let node_path = stage.append_array(&[root_node, cyan_node]); + let _cyan_unit = stage.draw_unit(&RenderUnit { + camera, + vertex_count: 3, + node_path, + ..Default::default() + }); + + let node_path = stage.append_array(&[root_node, cyan_node, yellow_node]); + let _yellow_unit = stage.draw_unit(&RenderUnit { + camera, + vertex_count: 3, + node_path, + ..Default::default() + }); + + let node_path = stage.append_array(&[root_node, cyan_node, yellow_node, red_node]); + let _red_unit = stage.draw_unit(&RenderUnit { + camera, + vertex_count: 3, + node_path, + ..Default::default() + }); let img = r.render_image().unwrap(); img_diff::assert_img_eq("scene_parent_sanity.png", img); @@ -1406,8 +960,24 @@ mod test { let ss = 600; let mut r = Renderling::headless(ss, ss).with_background_color(Vec3::splat(0.0).extend(1.0)); + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); let radius = 0.5; + let ss = ss as f32; + let projection = camera::perspective(ss, ss); + let k = 7; + let diameter = 2.0 * radius; + let spacing = radius * 0.25; + let len = (k - 1) as f32 * (diameter + spacing); + let half = len / 2.0; + let view = camera::look_at( + Vec3::new(half, half, 1.6 * len), + Vec3::new(half, half, 0.0), + Vec3::Y, + ); + let camera = stage.append(&Camera::new(projection, view)); + let mut icosphere = icosahedron::Polyhedron::new_isocahedron(radius, 5); icosphere.compute_triangle_normals(); let icosahedron::Polyhedron { @@ -1416,7 +986,7 @@ mod test { cells, .. } = icosphere; - log::info!("icosphere created"); + log::info!("icosphere created on CPU"); let to_vertex = |ndx: &usize| -> Vertex { let p: [f32; 3] = positions[*ndx].0.into(); @@ -1429,56 +999,47 @@ mod test { let p2 = to_vertex(&c); vec![p0, p1, p2] }); - - let ss = ss as f32; - let projection = camera::perspective(ss, ss); - let k = 7; - let diameter = 2.0 * radius; - let spacing = radius * 0.25; - let len = (k - 1) as f32 * (diameter + spacing); - let half = len / 2.0; - let view = camera::look_at( - Vec3::new(half, half, 1.6 * len), - Vec3::new(half, half, 0.0), - Vec3::Y, - ); - - let mut builder = r - .new_scene() - .with_camera(projection, view) - .with_skybox_image_from_path("../../img/hdr/resting_place.hdr"); - let (start, count) = builder.add_meshlet(sphere_vertices); - + let sphere_primitive = stage.new_primitive(sphere_vertices, [], Id::NONE); for i in 0..k { let roughness = i as f32 / (k - 1) as f32; let x = (diameter + spacing) * i as f32; for j in 0..k { let metallic = j as f32 / (k - 1) as f32; let y = (diameter + spacing) * j as f32; - let material_id = builder.add_material(PbrMaterial { + let mut prim = sphere_primitive; + prim.material = stage.append(&PbrMaterial { albedo_factor: Vec4::new(1.0, 1.0, 1.0, 1.0), metallic_factor: metallic, roughness_factor: roughness, ..Default::default() }); - let _entity = builder - .new_entity() - .with_starting_vertex_and_count(start, count) - .with_material(material_id) - .with_position([x, y, 0.0]) - .build(); + let primitives = stage.append_array(&[prim]); + let mesh = stage.append(&gl::GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&gl::GltfNode { + mesh, + translation: Vec3::new(x, y, 0.0), + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let _entity = stage.draw_unit(&RenderUnit { + camera, + vertex_count: prim.vertex_count, + node_path, + ..Default::default() + }); } } - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); + let (device, queue) = r.get_device_and_queue_owned(); + let hdr_image = AtlasImage::from_hdr_path("../../img/hdr/resting_place.hdr").unwrap(); + let skybox = crate::skybox::Skybox::new(device, queue, hdr_image, camera); + stage.set_skybox(skybox); - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("pbr_metallic_roughness_spheres.png", img); + let img = r.render_linear_image().unwrap(); + img_diff::assert_img_eq("pbr/metallic_roughness_spheres.png", img); } #[test] diff --git a/crates/renderling/src/linkage/convolution-fragment_bloom.spv b/crates/renderling/src/linkage/convolution-fragment_bloom.spv index 2efb0137..a9709123 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_bloom.spv and b/crates/renderling/src/linkage/convolution-fragment_bloom.spv differ diff --git a/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv b/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv index c085089e..a9c0d3ff 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv and b/crates/renderling/src/linkage/convolution-fragment_brdf_lut_convolution.spv differ diff --git a/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv b/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv index 37fc0096..16dadac5 100644 Binary files a/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv and b/crates/renderling/src/linkage/convolution-fragment_prefilter_environment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv b/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv index 6b37843c..062f70b4 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv and b/crates/renderling/src/linkage/convolution-vertex_brdf_lut_convolution.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv b/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv index 70f72ada..0f6af9d7 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv and b/crates/renderling/src/linkage/convolution-vertex_generate_mipmap.spv differ diff --git a/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv b/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv index eb663fbf..2c24629b 100644 Binary files a/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv and b/crates/renderling/src/linkage/convolution-vertex_prefilter_environment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-fragment_cubemap.spv b/crates/renderling/src/linkage/skybox-fragment_cubemap.spv index c6945d99..b985be73 100644 Binary files a/crates/renderling/src/linkage/skybox-fragment_cubemap.spv and b/crates/renderling/src/linkage/skybox-fragment_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv b/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv index 3f80ad1e..19ffa5e7 100644 Binary files a/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv and b/crates/renderling/src/linkage/skybox-fragment_equirectangular.spv differ diff --git a/crates/renderling/src/linkage/skybox-slabbed_vertex.spv b/crates/renderling/src/linkage/skybox-slabbed_vertex.spv deleted file mode 100644 index 47ea414c..00000000 Binary files a/crates/renderling/src/linkage/skybox-slabbed_vertex.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv b/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv deleted file mode 100644 index c025bede..00000000 Binary files a/crates/renderling/src/linkage/skybox-stage_skybox_cubemap.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/skybox-vertex.spv b/crates/renderling/src/linkage/skybox-vertex.spv index 779c8041..1283523f 100644 Binary files a/crates/renderling/src/linkage/skybox-vertex.spv and b/crates/renderling/src/linkage/skybox-vertex.spv differ diff --git a/crates/renderling/src/linkage/skybox-vertex_cubemap.spv b/crates/renderling/src/linkage/skybox-vertex_cubemap.spv new file mode 100644 index 00000000..f7054025 Binary files /dev/null and b/crates/renderling/src/linkage/skybox-vertex_cubemap.spv differ diff --git a/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv b/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv deleted file mode 100644 index 94e7cdd3..00000000 Binary files a/crates/renderling/src/linkage/skybox-vertex_position_passthru.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/stage-compute_cull_entities.spv b/crates/renderling/src/linkage/stage-compute_cull_entities.spv index 70a1934e..902bca54 100644 Binary files a/crates/renderling/src/linkage/stage-compute_cull_entities.spv and b/crates/renderling/src/linkage/stage-compute_cull_entities.spv differ diff --git a/crates/renderling/src/linkage/stage-gltf_fragment.spv b/crates/renderling/src/linkage/stage-gltf_fragment.spv new file mode 100644 index 00000000..84ab130f Binary files /dev/null and b/crates/renderling/src/linkage/stage-gltf_fragment.spv differ diff --git a/crates/renderling/src/linkage/stage-gltf_vertex.spv b/crates/renderling/src/linkage/stage-gltf_vertex.spv new file mode 100644 index 00000000..3ddefd49 Binary files /dev/null and b/crates/renderling/src/linkage/stage-gltf_vertex.spv differ diff --git a/crates/renderling/src/linkage/stage-main_fragment_scene.spv b/crates/renderling/src/linkage/stage-main_fragment_scene.spv index 74e08400..0a63c106 100644 Binary files a/crates/renderling/src/linkage/stage-main_fragment_scene.spv and b/crates/renderling/src/linkage/stage-main_fragment_scene.spv differ diff --git a/crates/renderling/src/linkage/stage-main_vertex_scene.spv b/crates/renderling/src/linkage/stage-main_vertex_scene.spv index b65d5bdf..df2b7b0b 100644 Binary files a/crates/renderling/src/linkage/stage-main_vertex_scene.spv and b/crates/renderling/src/linkage/stage-main_vertex_scene.spv differ diff --git a/crates/renderling/src/linkage/stage-new_stage_vertex.spv b/crates/renderling/src/linkage/stage-new_stage_vertex.spv deleted file mode 100644 index 7b194d66..00000000 Binary files a/crates/renderling/src/linkage/stage-new_stage_vertex.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/stage-stage_fragment.spv b/crates/renderling/src/linkage/stage-stage_fragment.spv deleted file mode 100644 index e9c5b00e..00000000 Binary files a/crates/renderling/src/linkage/stage-stage_fragment.spv and /dev/null differ diff --git a/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv b/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv index d685467f..1fad7572 100644 Binary files a/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv and b/crates/renderling/src/linkage/stage-test_i8_i16_extraction.spv differ diff --git a/crates/renderling/src/linkage/tonemapping-fragment.spv b/crates/renderling/src/linkage/tonemapping-fragment.spv index ac674150..4a8c61aa 100644 Binary files a/crates/renderling/src/linkage/tonemapping-fragment.spv and b/crates/renderling/src/linkage/tonemapping-fragment.spv differ diff --git a/crates/renderling/src/linkage/tonemapping-vertex.spv b/crates/renderling/src/linkage/tonemapping-vertex.spv index 9b866b93..35d63094 100644 Binary files a/crates/renderling/src/linkage/tonemapping-vertex.spv and b/crates/renderling/src/linkage/tonemapping-vertex.spv differ diff --git a/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv b/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv index d9155e82..1b706e6c 100644 Binary files a/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv and b/crates/renderling/src/linkage/tutorial-implicit_isosceles_vertex.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv b/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv index e904b51a..8ce4c700 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv and b/crates/renderling/src/linkage/tutorial-slabbed_render_unit.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv index 960ffb21..9001018d 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv and b/crates/renderling/src/linkage/tutorial-slabbed_vertices.spv differ diff --git a/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv b/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv index a5d9b952..f6ab41b9 100644 Binary files a/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv and b/crates/renderling/src/linkage/tutorial-slabbed_vertices_no_instance.spv differ diff --git a/crates/renderling/src/linkage/ui-fragment.spv b/crates/renderling/src/linkage/ui-fragment.spv index 8a70212e..1d1880bd 100644 Binary files a/crates/renderling/src/linkage/ui-fragment.spv and b/crates/renderling/src/linkage/ui-fragment.spv differ diff --git a/crates/renderling/src/linkage/ui-vertex.spv b/crates/renderling/src/linkage/ui-vertex.spv index c191daa7..e6374f92 100644 Binary files a/crates/renderling/src/linkage/ui-vertex.spv and b/crates/renderling/src/linkage/ui-vertex.spv differ diff --git a/crates/renderling/src/renderer.rs b/crates/renderling/src/renderer.rs index 7f973d58..07ef6887 100644 --- a/crates/renderling/src/renderer.rs +++ b/crates/renderling/src/renderer.rs @@ -5,8 +5,8 @@ use snafu::prelude::*; use std::{ops::Deref, sync::Arc}; use crate::{ - hdr::HdrSurface, CreateSurfaceFn, Graph, RenderTarget, Scene, SceneBuilder, TextureError, - UiDrawObject, UiScene, UiSceneBuilder, View, ViewMut, WgpuStateError, + CreateSurfaceFn, Graph, RenderTarget, Stage, TextureError, + /* UiScene, UiSceneBuilder, */ View, ViewMut, WgpuStateError, }; #[derive(Debug, Snafu)] @@ -38,19 +38,12 @@ pub enum RenderlingError { #[snafu(display("gltf import failed: {}", source))] GltfImport { source: gltf::Error }, - //#[snafu(display("could not create scene: {}", source))] - // Scene { source: crate::GltfError }, #[snafu(display("missing resource"))] Resource { key: TypeKey }, #[snafu(display("{source}"))] Graph { source: moongraph::GraphError }, - //#[snafu(display("{source}"))] - // Lights { source: crate::light::LightsError }, - - //#[snafu(display("{source}"))] - // Object { source: crate::object::ObjectError }, #[snafu(display( "Missing PostRenderBuffer resource. Ensure a node that creates PostRenderBuffer (like \ PostRenderbufferCreate) is present in the graph: {source}" @@ -82,6 +75,12 @@ impl Deref for Device { } } +impl Into> for &Device { + fn into(self) -> Arc { + self.0.clone() + } +} + /// A thread-safe, clonable wrapper around `wgpu::Queue`. #[derive(Clone)] pub struct Queue(pub Arc); @@ -94,6 +93,12 @@ impl Deref for Queue { } } +impl Into> for &Queue { + fn into(self) -> Arc { + self.0.clone() + } +} + /// A thread-safe, clonable wrapper around `wgpu::Adapter`. #[derive(Clone)] pub struct Adapter(pub Arc); @@ -127,30 +132,6 @@ impl Deref for DepthTexture { /// The global background color. pub struct BackgroundColor(pub Vec4); -/// A helper struct for configuring calls to `Renderling::setup_render_graph`. -pub struct RenderGraphConfig { - pub scene: Option, - pub ui: Option, - pub objs: Vec, - // Whether or not to use the screen capture node. - // You probably want this to be `true` if you are rendering headless. - pub with_screen_capture: bool, - // Whether or not to use bloom filter in post-processing. - pub with_bloom: bool, -} - -impl Default for RenderGraphConfig { - fn default() -> Self { - Self { - scene: Default::default(), - ui: Default::default(), - objs: Default::default(), - with_screen_capture: false, - with_bloom: true, - } - } -} - /// A graph-based renderer that manages GPU resources for cameras, materials and /// meshes. pub struct Renderling { @@ -260,8 +241,8 @@ impl Renderling { /// Create a new headless renderer. /// /// ## Panics - /// This function will panic if an adapter cannot be found. For example this would - /// happen on machines without a GPU. + /// This function will panic if an adapter cannot be found. For example this + /// would happen on machines without a GPU. pub fn headless(width: u32, height: u32) -> Self { futures_lite::future::block_on(Self::try_new_headless(width, height)).unwrap() } @@ -300,13 +281,7 @@ impl Renderling { .unwrap(); // The renderer doesn't _always_ have an HrdSurface, so we don't unwrap this // one. - let _ = self.graph.visit( - |(device, queue, mut hdr): (View, View, ViewMut)| { - hdr.hdr_texture = HdrSurface::create_texture(&device, &queue, width, height); - hdr.texture_bindgroup = - HdrSurface::create_texture_bindgroup(&device, &hdr.hdr_texture); - }, - ); + let _ = self.graph.visit(crate::hdr::resize_hdr_surface); } pub fn create_texture

( @@ -400,45 +375,59 @@ impl Renderling { self.graph.get_resource().unwrap().unwrap() } - pub fn new_scene(&self) -> SceneBuilder { + pub fn new_stage(&self) -> Stage { let (device, queue) = self.get_device_and_queue_owned(); - SceneBuilder::new(device.0, queue.0) + Stage::new(device, queue) } - pub fn empty_scene(&self) -> Scene { - self.new_scene().build().unwrap() - } + //pub fn new_ui_scene(&self) -> UiSceneBuilder<'_> { + // let (device, _) = self.get_device_and_queue_owned(); + // let queue = self.get_queue(); + // UiSceneBuilder::new(device.0.clone(), queue) + //} - pub fn new_ui_scene(&self) -> UiSceneBuilder<'_> { - let (device, _) = self.get_device_and_queue_owned(); - let queue = self.get_queue(); - UiSceneBuilder::new(device.0.clone(), queue) - } + //pub fn empty_ui_scene(&self) -> UiScene { + // self.new_ui_scene().build() + //} - pub fn empty_ui_scene(&self) -> UiScene { - self.new_ui_scene().build() - } + //#[cfg(feature = "text")] + ///// Create a new `GlyphCache` used to cache text rendering info. + //pub fn new_glyph_cache(&self, fonts: Vec) -> + // crate::GlyphCache { crate::GlyphCache::new(fonts) + //} - #[cfg(feature = "text")] - /// Create a new `GlyphCache` used to cache text rendering info. - pub fn new_glyph_cache(&self, fonts: Vec) -> crate::GlyphCache { - crate::GlyphCache::new(fonts) - } - - /// Sets up the render graph with the given scenes and objects. + /// Render into an image. + /// + /// This should be called after rendering, before presentation. + /// Good for getting headless screen grabs. + /// + /// For this call to succeed, the `PostRenderBufferCreate::create` node must + /// be present in the graph. + /// + /// The resulting image will be in the color space of the internal + /// [`RenderTarget`]. /// - /// The scenes and objects may be "visited" later, or even retrieved. - pub fn setup_render_graph(&mut self, config: RenderGraphConfig) { - let RenderGraphConfig { - scene, - ui, - objs, - with_screen_capture, - with_bloom, - } = config; - let scene = scene.unwrap_or_else(|| self.empty_scene()); - let ui = ui.unwrap_or_else(|| self.empty_ui_scene()); - crate::setup_render_graph(self, scene, ui, objs, with_screen_capture, with_bloom) + /// ## Note + /// This operation can take a long time, depending on how big the screen is. + pub fn render_image(&mut self) -> Result { + use crate::frame::PostRenderBuffer; + + self.render()?; + let buffer = self + .graph + .remove_resource::() + .context(MissingPostRenderBufferSnafu)? + .context(ResourceSnafu { + key: TypeKey::new::(), + })?; + let device = self.get_device(); + let is_srgb = self.get_render_target().format().is_srgb(); + let img = if is_srgb { + buffer.0.into_srgba(device).context(TextureSnafu)? + } else { + buffer.0.into_linear_rgba(device).context(TextureSnafu)? + }; + Ok(img) } /// Render into an image. @@ -449,9 +438,11 @@ impl Renderling { /// For this call to succeed, the `PostRenderBufferCreate::create` node must /// be present in the graph. /// + /// The resulting image will be in a linear color space. + /// /// ## Note /// This operation can take a long time, depending on how big the screen is. - pub fn render_image(&mut self) -> Result { + pub fn render_linear_image(&mut self) -> Result { use crate::frame::PostRenderBuffer; self.render()?; @@ -463,7 +454,7 @@ impl Renderling { key: TypeKey::new::(), })?; let device = self.get_device(); - let img = buffer.0.into_rgba(device).context(TextureSnafu)?; + let img = buffer.0.into_linear_rgba(device).context(TextureSnafu)?; Ok(img) } diff --git a/crates/renderling/src/scene.rs b/crates/renderling/src/scene.rs index 54abc872..7ad43448 100644 --- a/crates/renderling/src/scene.rs +++ b/crates/renderling/src/scene.rs @@ -6,7 +6,11 @@ use moongraph::{Move, View, ViewMut}; use renderling_shader::debug::DebugChannel; use snafu::prelude::*; -pub use renderling_shader::{pbr::PbrMaterial, stage::*, texture::*}; +pub use renderling_shader::{ + pbr::PbrMaterial, + stage::{DrawIndirect, GpuConstants, GpuEntity, GpuLight, GpuToggles, Vertex}, + texture::*, +}; use crate::{ bloom::BloomResult, frame::FrameTextureView, hdr::HdrSurface, Atlas, BufferArray, DepthTexture, @@ -475,7 +479,7 @@ impl Scene { skybox } else if let Some(skybox_img) = skybox_image { log::trace!("scene will build a skybox"); - Skybox::new(&device, &queue, skybox_img) + Skybox::new(&device, &queue, skybox_img, Id::NONE) } else { log::trace!("skybox is empty"); Skybox::empty(&device, &queue) @@ -582,7 +586,7 @@ impl Scene { // skybox should change image log::trace!("skybox changed"); constants.toggles.set_has_skybox(true); - *skybox = Skybox::new(device, queue, img); + *skybox = Skybox::new(device, queue, img, Id::NONE); *environment_bindgroup = crate::skybox::create_skybox_bindgroup( device, constants, @@ -1112,43 +1116,3 @@ pub fn scene_render( queue.submit(std::iter::once(encoder.finish())); Ok(()) } - -/// Conducts the HDR tone mapping, writing the HDR surface texture to the (most -/// likely) sRGB window surface. -pub fn tonemapping( - (device, queue, frame, hdr_frame, bloom_result): ( - View, - View, - View, - View, - Move, - ), -) -> Result<(), SceneError> { - let label = Some("tonemapping"); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - render_pass.set_pipeline(&hdr_frame.tonemapping_pipeline); - render_pass.set_bind_group(0, &hdr_frame.texture_bindgroup, &[]); - render_pass.set_bind_group(1, hdr_frame.constants.bindgroup(), &[]); - let bloom_bg = bloom_result - .0 - .as_deref() - .unwrap_or(&hdr_frame.no_bloom_bindgroup); - render_pass.set_bind_group(2, bloom_bg, &[]); - render_pass.draw(0..6, 0..1); - drop(render_pass); - - queue.submit(std::iter::once(encoder.finish())); - Ok(()) -} diff --git a/crates/renderling/src/scene/gltf_support.rs b/crates/renderling/src/scene/gltf_support.rs deleted file mode 100644 index 0c072f3a..00000000 --- a/crates/renderling/src/scene/gltf_support.rs +++ /dev/null @@ -1,1499 +0,0 @@ -//! GLTF support for renderling GPU scenes. -use glam::{Mat4, Quat, Vec2, Vec3, Vec4, Vec4Swizzles}; -use gltf::khr_lights_punctual::Kind; -use rustc_hash::FxHashMap; -use snafu::prelude::*; - -use super::*; -use crate::{ - shader::texture::{GpuTexture, TextureAddressMode}, - Id, -}; - -mod anime; -pub use anime::*; - -#[derive(Debug, Snafu)] -pub enum GltfLoaderError { - #[snafu(display("{source}"))] - Gltf { source: gltf::Error, cwd: String }, - - #[snafu(display("Unsupported gltf image format: {:?}", format))] - UnsupportedImageFormat { format: gltf::image::Format }, - - #[snafu(display("Missing image {}", index))] - MissingImage { index: usize }, - - #[snafu(display("Missing image for texture {tex_id:?}"))] - MissingTextureImage { tex_id: Id }, - - #[snafu(display("Missing node {}", index))] - MissingNode { index: usize }, - - #[snafu(display("Invalid image"))] - InvalidImage, - - #[snafu(display("Error during scene building phase: {source}"))] - Scene { source: crate::SceneError }, - - #[snafu(display("{what} is missing texture={index}"))] - MissingTexture { what: &'static str, index: usize }, - - #[snafu(display("Unsupported primitive mode: {:?}", mode))] - PrimitiveMode { mode: gltf::mesh::Mode }, - - #[snafu(display("No {} attribute for mesh", attribute.to_string()))] - MissingAttribute { attribute: gltf::Semantic }, - - #[snafu(display("{what} is missing material {:?} {:?}", index, name))] - MissingMaterial { - what: &'static str, - index: Option, - name: Option, - }, - - #[snafu(display("Missing mesh {:?} {:?}", index, name))] - MissingMesh { - index: Option, - name: Option, - }, - - #[snafu(display("Missing entity {id:?}"))] - MissingEntity { id: Id }, - - #[snafu(display("Missing animation channel inputs"))] - MissingInputs, - - #[snafu(display("Missing animation channel outputs"))] - MissingOutputs, -} - -pub struct GltfStore { - dense: Vec>, - names: FxHashMap>, -} - -impl Default for GltfStore { - fn default() -> Self { - Self { - dense: Default::default(), - names: Default::default(), - } - } -} - -impl GltfStore { - pub fn iter(&self) -> impl Iterator { - self.dense.iter().flatten() - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.dense.iter_mut().flatten() - } - - pub fn remove(&mut self, index: usize, name: Option) -> Option { - if let Some(name) = name { - if let Some(indices) = self.names.get_mut(&name) { - indices.retain(|i| *i != index); - } - } - self.dense.get_mut(index)?.take() - } - - pub fn insert(&mut self, index: usize, name: Option, item: T) -> Option { - if self.dense.len() <= index { - self.dense.resize_with(index + 1, || None); - } - while self.dense.len() <= index { - self.dense.push(None); - } - let existing = self.remove(index, name.clone()); - self.dense[index] = Some(item); - if let Some(name) = name { - let indices = self.names.entry(name).or_default(); - indices.push(index); - } - existing - } - - pub fn get_name(&self, index: usize) -> Option<&String> { - self.names.iter().find_map(|(name, indices)| { - if indices.contains(&index) { - Some(name) - } else { - None - } - }) - } - - pub fn get(&self, index: usize) -> Option<&T> { - self.dense.get(index)?.as_ref() - } - - pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { - self.dense.get_mut(index)?.as_mut() - } - - pub fn get_by_name(&self, name: &str) -> impl Iterator + '_ { - if let Some(indices) = self.names.get(name) { - Box::new(indices.iter().flat_map(|index| self.get(*index))) - as Box> - } else { - Box::new(std::iter::empty()) as Box> - } - } - - pub fn len(&self) -> usize { - self.dense.len() - } -} - -#[derive(Clone, Copy)] -pub struct GltfBoundingBox { - pub min: Vec3, - pub max: Vec3, -} - -impl From> for GltfBoundingBox { - fn from(gltf::mesh::Bounds { min, max }: gltf::mesh::Bounds<[f32; 3]>) -> Self { - GltfBoundingBox { - min: min.into(), - max: max.into(), - } - } -} - -#[derive(Clone, Default)] -pub struct GltfNode { - pub entity_id: Id, - // Contains an index into the GltfLoader.cameras, lights or meshes fields. - pub gltf_camera_index: Option, - pub gltf_light_index: Option, - pub gltf_mesh_index: Option, - pub gltf_skin_index: Option, - pub child_ids: Vec>, -} - -#[derive(Clone)] -pub struct GltfMeshPrim { - pub vertex_start: u32, - pub vertex_count: u32, - pub material_id: Id, - pub bounding_box: GltfBoundingBox, - pub num_morph_targets: u32, - pub morph_targets_have_positions: bool, - pub morph_targets_have_normals: bool, - pub morph_targets_have_tangents: bool, - pub weights: Vec, -} - -/// The result of loading a gltf file into a [`SceneBuilder`]. -/// -/// Contains indexed and named lookups for resources contained within the loaded -/// gltf file. -/// -/// To load a gltf file into a scene thereby creating a `GltfLoader` you can use -/// [`SceneBuilder::gltf_load`]. -#[derive(Default)] -pub struct GltfLoader { - // Contains the indices of SceneBuilder images loaded - pub images: Vec, - pub cameras: GltfStore<(Mat4, Mat4)>, - pub lights: GltfStore>, - pub textures: GltfStore>, - pub default_material: Id, - pub materials: GltfStore>, - pub meshes: GltfStore>, - pub nodes: GltfStore, - pub animations: GltfStore, -} - -impl GltfLoader { - /// Load everything into a scene builder and return the loader. - pub fn load( - builder: &mut SceneBuilder, - document: gltf::Document, - buffers: Vec, - images: Vec, - ) -> Result { - let mut loader = GltfLoader::default(); - - log::trace!("node hierarchy:"); - for node in document.nodes() { - // This associates the node with a GpuEntity and transform, which - // we need in order to load meshes, because mesh vertices may reference - // GpuEntity ids in the 'joints' field. - let _ = loader.load_shallow_node(node, builder)?; - } - for node in document.nodes() { - let index = node.index(); - let name = node.name().map(String::from); - // UNWRAP: safe because we already created and stored all the nodes - let parent_id = loader.nodes.get(index).unwrap().entity_id; - log::trace!("node {index} {name:?}"); - let mut printed = false; - for child in node.children() { - let child_index = child.index(); - let child_name = child.name().map(String::from); - if index == child_index { - continue; - } - if !printed { - printed = true; - log::trace!("contains children:"); - } - log::trace!(" node {child_index} {child_name:?}"); - // UNWRAP: safe because we already created and stored all the nodes - let child_id = loader.nodes.get(child_index).unwrap().entity_id; - let child_entity = builder.entities.get_mut(child_id.index()).unwrap(); - child_entity.parent = parent_id; - } - } - - if !builder.materials.is_empty() { - loader.default_material = Id::new(0); - } - - for (i, image) in images.into_iter().enumerate() { - // let format = image_data_format_to_wgpu(image.format)?; - // let num_channels = image_data_format_num_channels(image.format); - log::trace!("adding image {} with format {:?}", i, image.format); - let scene_img = SceneImage::from(image); - let image_index = builder.add_image(scene_img); - loader.images.push(image_index); - log::trace!(" with index={image_index} in the scene builder"); - } - - loader.load_textures(builder, &document)?; - loader.load_materials(builder, &document)?; - - log::debug!("adding meshlets"); - for mesh in document.meshes() { - loader.load_mesh(mesh, builder, &document, &buffers)?; - } - - for node in document.nodes() { - // We don't call GltfLoader::load_node here because that function will - // also load any children of this node, which will lead to doubles when - // we encounter those children in this loop. - let _ = loader.load_node(node, builder, &buffers)?; - } - - log::debug!("adding animations"); - loader.load_animations(&document, &buffers)?; - - Ok(loader) - } - - fn load_textures( - &mut self, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result<(), GltfLoaderError> { - log::debug!("loading textures"); - for texture in document.textures() { - self.load_texture(texture, builder)?; - } - Ok(()) - } - - fn load_texture( - &mut self, - texture: gltf::Texture<'_>, - builder: &mut SceneBuilder, - ) -> Result, GltfLoaderError> { - let index = texture.index(); - let name = texture.name().map(String::from); - let image_loader_index = texture.source().index(); - let image_index = - self.images - .get(image_loader_index) - .copied() - .context(MissingImageSnafu { - index: image_loader_index, - })?; - fn mode(mode: gltf::texture::WrappingMode) -> TextureAddressMode { - match mode { - gltf::texture::WrappingMode::ClampToEdge => TextureAddressMode::CLAMP_TO_EDGE, - gltf::texture::WrappingMode::MirroredRepeat => TextureAddressMode::MIRRORED_REPEAT, - gltf::texture::WrappingMode::Repeat => TextureAddressMode::REPEAT, - } - } - let mode_s = mode(texture.sampler().wrap_s()); - let mode_t = mode(texture.sampler().wrap_t()); - let params = TextureParams { - image_index, - mode_s, - mode_t, - }; - - let texture_id = builder.add_texture(params); - log::trace!( - "adding texture index:{index} name:{name:?} id:{texture_id:?} with wrapping \ - s:{mode_s} t:{mode_t}" - ); - let _ = self.textures.insert(index, name, texture_id); - Ok(texture_id) - } - - /// Return the scene `Id` for the gltf texture at the given - /// index, if possible. - /// - /// If the texture at the given index has not been loaded into the - /// [`SceneBuilder`], it will be. - pub fn texture_at( - &mut self, - index: usize, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - if let Some(id) = self.textures.get(index) { - Ok(*id) - } else { - let texture = - document - .textures() - .find(|t| t.index() == index) - .context(MissingTextureSnafu { - what: "document", - index, - })?; - self.load_texture(texture, builder) - } - } - - fn load_materials( - &mut self, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result<(), GltfLoaderError> { - log::debug!("loading materials"); - let mut total = 0; - for material in document.materials() { - self.load_material(material, builder, document)?; - total += 1; - } - log::debug!(" {total} materials"); - Ok(()) - } - - fn load_material( - &mut self, - material: gltf::Material<'_>, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - let index = material.index(); - let name = material.name().map(String::from); - log::trace!("loading material {index:?} {name:?}"); - let pbr = material.pbr_metallic_roughness(); - let material = if material.unlit() { - log::trace!(" is unlit"); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; - builder - .get_image_for_texture_id_mut(&albedo_texture) - .context(MissingTextureImageSnafu { - tex_id: albedo_texture, - })? - .apply_linear_transfer = true; - PbrMaterial { - albedo_texture, - albedo_tex_coord, - albedo_factor: pbr.base_color_factor().into(), - ..Default::default() - } - } else { - log::trace!(" is pbr"); - let albedo_factor: Vec4 = pbr.base_color_factor().into(); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - builder - .get_image_for_texture_id_mut(&tex_id) - .context(MissingTextureImageSnafu { tex_id })? - .apply_linear_transfer = true; - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; - - let ( - metallic_factor, - roughness_factor, - metallic_roughness_texture, - metallic_roughness_tex_coord, - ) = if let Some(info) = pbr.metallic_roughness_texture() { - let index = info.texture().index(); - let tex_id = self.texture_at(index, builder, document)?; - (1.0, 1.0, tex_id, info.tex_coord()) - } else { - (pbr.metallic_factor(), pbr.roughness_factor(), Id::NONE, 0) - }; - - let (normal_texture, normal_tex_coord) = - if let Some(norm_tex) = material.normal_texture() { - let tex_id = self.texture_at(norm_tex.texture().index(), builder, document)?; - (tex_id, norm_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; - - let (ao_strength, ao_texture, ao_tex_coord) = if let Some(occlusion_tex) = - material.occlusion_texture() - { - let tex_id = self.texture_at(occlusion_tex.texture().index(), builder, document)?; - (occlusion_tex.strength(), tex_id, occlusion_tex.tex_coord()) - } else { - (0.0, Id::NONE, 0) - }; - - let (emissive_texture, emissive_tex_coord) = if let Some(emissive_tex) = - material.emissive_texture() - { - let tex_id = self.texture_at(emissive_tex.texture().index(), builder, document)?; - builder - .get_image_for_texture_id_mut(&tex_id) - .context(MissingTextureImageSnafu { tex_id })? - .apply_linear_transfer = true; - (tex_id, emissive_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; - let emissive_factor = Vec3::from(material.emissive_factor()) - .extend(material.emissive_strength().unwrap_or(1.0)); - - PbrMaterial { - albedo_factor, - metallic_factor, - roughness_factor, - albedo_texture, - metallic_roughness_texture, - normal_texture, - ao_texture, - albedo_tex_coord, - metallic_roughness_tex_coord, - normal_tex_coord, - ao_tex_coord, - ao_strength, - emissive_factor, - emissive_texture, - emissive_tex_coord, - lighting_model: LightingModel::PBR_LIGHTING, - ..Default::default() - } - }; - - let material_id = builder.add_material(material); - - // If this material doesn't have an index it's because it's the default material - // for this gltf file. - if let Some(index) = index { - let _ = self.materials.insert(index, name, material_id); - } else { - self.default_material = material_id; - } - Ok(material_id) - } - - /// Return the scene `Id` for the gltf material at the given - /// index, if possible. - /// - /// If the material at the given index has not been loaded into the - /// [`SceneBuilder`], it will be. - /// - /// Providing `None` returns the id of the default material, or `Id::NONE` - /// if there is none. - pub fn material_at( - &mut self, - may_index: Option, - builder: &mut SceneBuilder, - document: &gltf::Document, - ) -> Result, GltfLoaderError> { - if let Some(index) = may_index { - if let Some(material_id) = self.materials.get(index) { - Ok(*material_id) - } else { - let material = document - .materials() - .find(|material| material.index() == Some(index)) - .context(MissingMaterialSnafu { - what: "document", - name: None, - index: Some(index), - })?; - self.load_material(material, builder, document) - } - } else { - Ok(self.default_material) - } - } - - fn load_mesh( - &mut self, - mesh: gltf::Mesh<'_>, - builder: &mut SceneBuilder, - document: &gltf::Document, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let mesh_index = mesh.index(); - let mesh_name = mesh.name().map(String::from); - log::trace!("loading mesh {mesh_index} {mesh_name:?}"); - - let mut mesh_primitives = vec![]; - for primitive in mesh.primitives() { - log::trace!(" reading primitive {}", primitive.index()); - log::trace!(" bounds: {:?}", primitive.bounding_box()); - snafu::ensure!( - primitive.mode() == gltf::mesh::Mode::Triangles, - PrimitiveModeSnafu { - mode: primitive.mode() - } - ); - let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); - let positions = reader - .read_positions() - .context(MissingAttributeSnafu { - attribute: gltf::Semantic::Positions, - })? - .map(Vec3::from) - .collect::>(); - log::trace!(" {} vertices", positions.len()); - if positions.len() <= 10 { - log::trace!(" positions:"); - for (i, p) in positions.iter().enumerate() { - log::trace!(" {i}: {p:?}"); - } - } - let mut gen_normals = false; - let normals: Box> = - if let Some(normals) = reader.read_normals() { - log::trace!(" with normals"); - Box::new(normals.map(Vec3::from)) - } else { - log::trace!(" no normals (will generate)"); - gen_normals = true; - Box::new(std::iter::repeat(Vec3::ZERO)) - }; - let mut gen_tangents = false; - let tangents: Box> = - if let Some(tangents) = reader.read_tangents() { - log::trace!(" with tangents"); - Box::new(tangents.map(Vec4::from)) - } else { - log::trace!(" no tangents (will generate)"); - gen_tangents = true; - Box::new(std::iter::repeat(Vec4::ZERO)) - }; - let colors: Box> = if let Some(colors) = reader.read_colors(0) - { - log::trace!(" colored"); - let colors = colors.into_rgba_f32(); - Box::new(colors.map(Vec4::from)) - } else { - log::trace!(" not colored"); - Box::new(std::iter::repeat(Vec4::splat(1.0))) - }; - let uv0: Box> = if let Some(uvs) = reader.read_tex_coords(0) { - let uvs = uvs.into_f32().map(Vec2::from); - log::trace!(" uv0: {} vertices", uvs.len()); - Box::new(uvs) - } else { - log::trace!(" uv0: none"); - Box::new(std::iter::repeat(Vec2::ZERO)) - }; - let uv1: Box> = if let Some(uvs) = reader.read_tex_coords(1) { - let uvs = uvs.into_f32().map(Vec2::from); - log::trace!(" uv1: {} vertices", uvs.len()); - Box::new(uvs) - } else { - log::trace!(" uv1: none"); - Box::new(std::iter::repeat(Vec2::ZERO)) - }; - let uvs = uv0 - .zip(uv1) - .map(|(uv0, uv1)| Vec4::new(uv0.x, uv0.y, uv1.x, uv1.y)); - - // See the GLTF spec on morph targets - // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets - let mut num_morph_targets_positions = 0; - let mut num_morph_targets_normals = 0; - let mut num_morph_targets_tangents = 0; - let morph_targets = reader - .read_morph_targets() - .map(|(may_ps, may_ns, may_ts)| { - let may_ps = may_ps.map(|ps| ps.collect::>()); - let may_ns = may_ns.map(|ns| ns.collect::>()); - let may_ts = may_ts.map(|ts| ts.collect::>()); - num_morph_targets_positions = num_morph_targets_positions - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - num_morph_targets_normals = num_morph_targets_normals - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - num_morph_targets_tangents = num_morph_targets_tangents - .max(may_ps.as_ref().map(Vec::len).unwrap_or_default()); - (may_ps, may_ns, may_ts) - }) - .collect::>(); - let num_morph_targets = morph_targets.len(); - log::trace!(" {num_morph_targets} morph targets"); - let has_morph_targets = num_morph_targets_positions > 0 - || num_morph_targets_normals > 0 - || num_morph_targets_tangents > 0; - - let joint_weights_normalized = primitive - .attributes() - .find_map(|att| match att { - (gltf::Semantic::Weights(_), acc) => { - let n = acc.normalized(); - let ns = if n { "normalized" } else { "unnormalized" }; - let dt = acc.data_type(); - log::trace!(" joint weights {ns} {dt:?}"); - Some(n) - } - _ => None, - }) - .unwrap_or_default(); - - let joints = reader - .read_joints(0) - .map(|joints| { - let joints: Box> = Box::new( - joints - .into_u16() - .map(|[a, b, c, d]| [a as u32, b as u32, c as u32, d as u32]), - ); - joints - }) - .unwrap_or_else(|| Box::new(std::iter::repeat([0; 4]))); - - fn normalize_weights([a, b, c, d]: [f32; 4]) -> [f32; 4] { - let v = Vec4::new(a, b, c, d); - let manhatten = v.x.abs() + v.y.abs() + v.z.abs() + v.w.abs(); - let v = if manhatten <= f32::EPSILON { - Vec4::X - } else { - v / manhatten - }; - v.to_array() - } - - let joint_weights = reader - .read_weights(0) - .map(|weights| { - let weights: Box> = - Box::new(weights.into_f32().map(|w| { - if joint_weights_normalized { - w - } else { - normalize_weights(w) - } - })); - weights - }) - .unwrap_or_else(|| Box::new(std::iter::repeat([0.0, 0.0, 0.0, 0.0]))); - - let vertices = positions - .iter() - .zip(colors.zip(uvs.zip(normals.zip(tangents.zip(joints.zip(joint_weights)))))) - .enumerate() - .map( - |(i, (position, (color, (uv, (normal, (tangent, (joints, weights)))))))| { - ( - Vertex { - position: position.extend(0.0), - color, - uv, - normal: normal.extend(0.0), - tangent, - joints, - weights, - }, - morph_targets - .iter() - .map(|(mps, mns, mts)| { - ( - mps.as_ref().map(|ps| ps[i]), - mns.as_ref().map(|ns| ns[i]), - mts.as_ref().map(|ts| ts[i]), - ) - }) - .collect::>(), - ) - }, - ) - .collect::>(); - drop(morph_targets); - - // We don't yet support indices, so we'll just repeat vertices - let mut vertices = if let Some(indices) = reader.read_indices() { - let indices = indices.into_u32(); - indices - .map(|i| vertices[i as usize].clone()) - .collect::>() - } else { - vertices - }; - if gen_normals || gen_tangents { - vertices.chunks_mut(3).for_each(|t| match t { - [(a, _), (b, _), (c, _)] => { - if gen_normals { - let n = Vertex::generate_normal( - a.position.xyz(), - b.position.xyz(), - c.position.xyz(), - ); - a.normal = n.extend(a.normal.w); - b.normal = n.extend(b.normal.w); - c.normal = n.extend(c.normal.w); - } - if gen_tangents { - let tangent = Vertex::generate_tangent( - a.position.xyz(), - a.uv.xy(), - b.position.xyz(), - b.uv.xy(), - c.position.xyz(), - c.uv.xy(), - ); - debug_assert!(!tangent.w.is_nan(), "tangent is NaN"); - a.tangent = tangent; - b.tangent = tangent; - c.tangent = tangent; - } - } - _ => unreachable!("safe because we know these are triangles"), - }); - } - - // If we have morph targets we'll represent them by creating separate meshlets - // after the first "original" meshlet. That way we can index into them by using - // the vertex count, which MUST be the same according to the spec. - let (original_meshlet, morph_target_meshlets) = if has_morph_targets { - if num_morph_targets_positions > 0 { - log::trace!(" {num_morph_targets_positions} positions"); - } - if num_morph_targets_normals > 0 { - log::trace!(" {num_morph_targets_normals} normals"); - } - if num_morph_targets_tangents > 0 { - log::trace!(" {num_morph_targets_tangents} tangents"); - } - - // TODO: Optimization - preset capacity on these arrays. - let mut morph_target_meshlets = vec![vec![]; num_morph_targets]; - let mut original_meshlet = vec![]; - for (vert, targets) in vertices.into_iter() { - original_meshlet.push(vert); - for (i, (may_ps, may_ns, may_ts)) in targets.into_iter().enumerate() { - let p = may_ps.map(Vec3::from).unwrap_or_default().extend(0.0); - let n = may_ns.map(Vec3::from).unwrap_or_default().extend(0.0); - let t = may_ts.map(Vec3::from).unwrap_or_default().extend(0.0); - let mut v = Vertex::default(); - v.position = p; - v.normal = n; - v.tangent = t; - morph_target_meshlets[i].push(v); - } - } - (original_meshlet, morph_target_meshlets) - } else { - (vertices.into_iter().map(|(v, _)| v).collect(), vec![]) - }; - - // Here we add the morph targets as contiguous meshlets occurring directly after - // the original. This is because the GpuEntity has an array of 8 possible morph - // target weights, where each weight's index in the array maps to - // this contiguous meshlet. See GpuEntity::get_vertex - // for that indexing operation. - let (vertex_start, vertex_count) = builder.add_meshlet(original_meshlet); - for morph_target_meshlet in morph_target_meshlets.into_iter() { - let _ = builder.add_meshlet(morph_target_meshlet); - } - let weights = mesh.weights().map(|ws| ws.to_vec()).unwrap_or_default(); - if !weights.is_empty() { - log::trace!(" weights:"); - } - for (i, w) in weights.iter().enumerate() { - log::trace!(" {i}: {w}"); - } - let material_index = primitive.material().index(); - let material_id = self.material_at(material_index, builder, document)?; - let bounding_box = primitive.bounding_box().into(); - mesh_primitives.push(GltfMeshPrim { - weights, - vertex_start, - vertex_count, - material_id, - bounding_box, - num_morph_targets: num_morph_targets as u32, - morph_targets_have_positions: num_morph_targets_positions > 0, - morph_targets_have_normals: num_morph_targets_normals > 0, - morph_targets_have_tangents: num_morph_targets_tangents > 0, - }); - } - let _ = self - .meshes - .insert(mesh.index(), mesh.name().map(String::from), mesh_primitives); - Ok(()) - } - - /// Load a node, setting its transform and type. - fn load_shallow_node( - &mut self, - node: gltf::Node<'_>, - builder: &mut SceneBuilder, - ) -> Result, GltfLoaderError> { - let index = node.index(); - let name = node.name().map(String::from); - log::trace!("loading node {index} {name:?}",); - let (position, rotation, scale) = node.transform().decomposed(); - let position = Vec3::from(position); - let rotation = Quat::from_array(rotation); - let scale = Vec3::from(scale); - log::trace!(" position: {position:?}"); - log::trace!(" rotation: {rotation:?}"); - log::trace!(" scale: {scale:?}"); - - let mut gltf_node = GltfNode::default(); - - if let Some(camera) = node.camera() { - log::trace!(" is camera"); - gltf_node.gltf_camera_index = Some(camera.index()); - } - if let Some(mesh) = node.mesh() { - log::trace!(" is mesh"); - gltf_node.gltf_mesh_index = Some(mesh.index()); - } - if let Some(light) = node.light() { - log::trace!(" is light"); - gltf_node.gltf_light_index = Some(light.index()); - } - if let Some(skin) = node.skin() { - log::trace!(" is skin"); - gltf_node.gltf_skin_index = Some(skin.index()); - } - let mut printed = false; - for child in node.children() { - if !printed { - log::trace!(" is parent"); - printed = true; - } - log::trace!( - " child {} {:?}", - child.index(), - child.name().map(String::from) - ); - } - - let entity = builder - .new_entity() - .with_position(position) - .with_rotation(rotation) - .with_scale(scale) - .build(); - gltf_node.entity_id = entity.id; - let _ = self.nodes.insert(index, name, gltf_node); - - Ok(entity.id) - } - - fn load_node( - &mut self, - node: gltf::Node<'_>, - builder: &mut SceneBuilder, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let index = node.index(); - let name = node.name().map(String::from); - let entity_id = self.nodes.get(node.index()).unwrap().entity_id; - log::trace!("fleshing out node {index} {name:?} {entity_id:?}"); - let (position, rotation, _scale) = { - let entity = builder.entities.get_mut(entity_id.index()).unwrap(); - (entity.position, entity.rotation, entity.scale) - }; - - if let Some(camera) = node.camera() { - let projection = match camera.projection() { - gltf::camera::Projection::Orthographic(o) => Mat4::orthographic_rh( - -o.xmag(), - o.xmag(), - -o.ymag(), - o.ymag(), - o.znear(), - o.zfar(), - ), - gltf::camera::Projection::Perspective(p) => { - let fovy = p.yfov(); - let aspect = p.aspect_ratio().unwrap_or(1.0); - if let Some(zfar) = p.zfar() { - Mat4::perspective_rh(fovy, aspect, p.znear(), zfar) - } else { - Mat4::perspective_infinite_rh( - p.yfov(), - p.aspect_ratio().unwrap_or(1.0), - p.znear(), - ) - } - } - }; - let view: Mat4 = Mat4::from_cols_array_2d(&node.transform().matrix()).inverse(); - - let _ = self.cameras.insert( - camera.index(), - camera.name().map(String::from), - (projection, view), - ); - } - - if let Some(mesh) = node.mesh() { - let index = mesh.index(); - log::trace!(" node is mesh {index}"); - let prims = self.meshes.get(index).context(MissingMeshSnafu { - index, - name: mesh - .name() - .map(String::from) - .unwrap_or("unknown".to_string()), - })?; - - let node_weights = node.weights().map(|ws| ws.to_vec()); - if let Some(ws) = node_weights.as_ref() { - log::trace!(" node weights:"); - for (i, w) in ws.iter().enumerate() { - log::trace!(" {i}: {w}"); - } - } - let children = if prims.len() == 1 { - log::trace!(" with only 1 primitive, so no children needed"); - let GltfMeshPrim { - vertex_start, - vertex_count, - material_id, - bounding_box: _, - weights, - num_morph_targets, - morph_targets_have_positions, - morph_targets_have_normals, - morph_targets_have_tangents, - } = &prims[0]; - - let entity = builder.entities.get_mut(entity_id.index()).unwrap(); - entity.mesh_first_vertex = *vertex_start; - entity.mesh_vertex_count = *vertex_count; - entity.material = *material_id; - entity.info.set_num_morph_targets(*num_morph_targets as u8); - entity - .info - .set_morph_targets_have_positions(*morph_targets_have_positions); - entity - .info - .set_morph_targets_have_normals(*morph_targets_have_normals); - entity - .info - .set_morph_targets_have_tangents(*morph_targets_have_tangents); - entity.set_morph_target_weights(weights.iter().copied()); - vec![] - } else { - log::trace!(" with {} child primitives:", prims.len()); - prims - .iter() - .map( - |GltfMeshPrim { - vertex_start, - vertex_count, - material_id, - bounding_box: _, - weights, - num_morph_targets, - morph_targets_have_positions, - morph_targets_have_normals, - morph_targets_have_tangents, - }| { - let child = builder - .new_entity() - .with_starting_vertex_and_count(*vertex_start, *vertex_count) - .with_material(*material_id) - .with_num_morph_targets(*num_morph_targets as u8) - .with_morph_targets_have_positions(*morph_targets_have_positions) - .with_morph_targets_have_normals(*morph_targets_have_normals) - .with_morph_targets_have_tangents(*morph_targets_have_tangents) - .with_morph_target_weights(weights.clone()) - .with_parent(entity_id) - .build() - .id; - log::trace!(" child {child:?}"); - log::trace!(" weights {weights:?}"); - log::trace!(" num_morph_targets {}", num_morph_targets); - child - }, - ) - .collect::>() - }; - self.nodes.get_mut(index).unwrap().child_ids = children; - } - - if let Some(light) = node.light() { - let color = Vec3::from(light.color()).extend(1.0); - let direction = Mat4::from_quat(rotation).transform_vector3(Vec3::NEG_Z); - let intensity = light.intensity(); - let gpu_light = match light.kind() { - Kind::Directional => builder - .new_directional_light() - .with_direction(direction) - .with_color(color) - .with_intensity(intensity) - .build(), - Kind::Point => builder - .new_point_light() - .with_position(position.xyz()) - .with_color(color) - .with_intensity(intensity) - .build(), - Kind::Spot { - inner_cone_angle, - outer_cone_angle, - } => builder - .new_spot_light() - .with_position(position.xyz()) - .with_direction(direction) - .with_color(color) - .with_intensity(intensity) - .with_cutoff(inner_cone_angle, outer_cone_angle) - .build(), - }; - log::trace!(" node is {}", from_gltf_light_kind(light.kind())); - log::trace!(" with color : {color:?}"); - log::trace!(" with direction: {direction:?}"); - log::trace!( - " with intensity: {intensity:?} {}", - gltf_light_intensity_units(light.kind()) - ); - let _ = self.lights.insert(light.index(), None, gpu_light); - } - - if let Some(skin) = node.skin() { - log::trace!(" node is a skin"); - if let Some(matrices) = skin - .reader(|buffer| Some(&buffers[buffer.index()])) - .read_inverse_bind_matrices() - { - let mut joint_ids = vec![]; - for (matrix, joint_node) in matrices.zip(skin.joints()) { - let index = joint_node.index(); - let name = joint_node.name().map(String::from); - let gltf_node = self.nodes.get(index).unwrap(); - let id = gltf_node.entity_id; - joint_ids.push(id); - log::trace!(" with joint {index} {name:?} {id:?}"); - let joint_entity = builder.entities.get_mut(id.index()).unwrap(); - joint_entity.inverse_bind_matrix = Mat4::from_cols_array_2d(&matrix); - } - let skin_entity = builder.entities.get_mut(entity_id.index()).unwrap(); - skin_entity.info.set_is_skin(true); - assert!( - joint_ids.len() <= skin_entity.skin_joint_ids.len(), - "renderling only supports {} joints for skinning, which is less than the \ - required {} for this model", - skin_entity.skin_joint_ids.len(), - joint_ids.len() - ); - for (i, id) in joint_ids - .into_iter() - .take(skin_entity.skin_joint_ids.len()) - .enumerate() - { - skin_entity.skin_joint_ids[i] = id; - } - } - } - - Ok(()) - } - - pub fn load_animation( - &mut self, - animation: gltf::Animation, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - let index = animation.index(); - let name = animation.name().map(String::from); - log::trace!("loading animation {index} {name:?}"); - let mut r_animation = GltfAnimation::default(); - r_animation.name = name; - for (i, channel) in animation.channels().enumerate() { - log::trace!(" channel {i}"); - let reader = channel.reader(|buffer| Some(&buffers[buffer.index()])); - let inputs = reader.read_inputs().context(MissingInputsSnafu)?; - let outputs = reader.read_outputs().context(MissingOutputsSnafu)?; - let keyframes = inputs.map(|t| Keyframe(t)).collect::>(); - log::trace!(" with {} keyframes", keyframes.len()); - let interpolation = channel.sampler().interpolation().into(); - log::trace!(" using {interpolation} interpolation"); - let index = channel.target().node().index(); - let name = channel.target().node().name(); - log::trace!(" of node {index} {name:?}"); - let tween = Tween { - properties: match outputs { - gltf::animation::util::ReadOutputs::Translations(ts) => { - log::trace!(" tweens translations"); - TweenProperties::Translations(ts.map(Vec3::from).collect()) - } - gltf::animation::util::ReadOutputs::Rotations(rs) => { - log::trace!(" tweens rotations"); - TweenProperties::Rotations(rs.into_f32().map(Quat::from_array).collect()) - } - gltf::animation::util::ReadOutputs::Scales(ss) => { - log::trace!(" tweens scales"); - TweenProperties::Scales(ss.map(Vec3::from).collect()) - } - gltf::animation::util::ReadOutputs::MorphTargetWeights(ws) => { - log::trace!(" tweens morph target weights"); - let ws = ws.into_f32().collect::>(); - let num_morph_targets = ws.len() / keyframes.len(); - log::trace!(" weights length : {}", ws.len()); - log::trace!(" keyframes length: {}", keyframes.len()); - log::trace!(" morph targets : {}", num_morph_targets); - TweenProperties::MorphTargetWeights( - ws.chunks_exact(num_morph_targets) - .map(|chunk| chunk.iter().copied().collect::>()) - .collect(), - ) - } - }, - keyframes, - interpolation, - target_node_index: index, - target_entity_id: { - let node = self.nodes.get(index).context(MissingNodeSnafu { index })?; - node.entity_id - }, - }; - r_animation.tweens.push(tween); - } - - let total_time = r_animation.length_in_seconds(); - log::trace!(" taking {total_time} seconds in total"); - - self.animations.insert( - animation.index(), - animation.name().map(String::from), - r_animation, - ); - Ok(()) - } - - pub fn load_animations( - &mut self, - document: &gltf::Document, - buffers: &[gltf::buffer::Data], - ) -> Result<(), GltfLoaderError> { - for animation in document.animations() { - self.load_animation(animation, buffers)?; - } - - Ok(()) - } -} - -#[cfg(all(test, feature = "gltf"))] -mod test { - use glam::{Vec3, Vec4}; - use renderling_shader::pbr::PbrMaterial; - - use crate::{camera, Id, LightingModel, RenderGraphConfig, Renderling, Vertex}; - - #[test] - // tests importing a gltf file and rendering the first image as a 2d object - // ensures we are decoding images correctly - fn images() { - let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); - let mut builder = r.new_scene(); - let _loader = builder.gltf_load("../../gltf/cheetah_cone.glb").unwrap(); - let (projection, view) = camera::default_ortho2d(100.0, 100.0); - builder.set_camera(projection, view); - let material_id = builder.add_material(PbrMaterial { - albedo_texture: Id::new(0), - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - let _img = builder - .new_entity() - .with_meshlet({ - let vs = vec![ - Vertex::default() - .with_position([0.0, 0.0, 0.0]) - .with_uv0([0.0, 0.0]), - Vertex::default() - .with_position([1.0, 0.0, 0.0]) - .with_uv0([1.0, 0.0]), - Vertex::default() - .with_position([1.0, 1.0, 0.0]) - .with_uv0([1.0, 1.0]), - Vertex::default() - .with_position([0.0, 1.0, 0.0]) - .with_uv0([0.0, 1.0]), - ]; - [0, 3, 2, 0, 2, 1].map(|i| vs[i]) - }) - .with_material(material_id) - .with_scale(Vec3::new(100.0, 100.0, 1.0)) - .build(); - let scene = builder.build().unwrap(); - let (device, queue) = r.get_device_and_queue_owned(); - let texture = - futures_lite::future::block_on(scene.textures.read_gpu(&device, &queue, 0, 1)).unwrap() - [0]; - println!("{texture:?}"); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gltf_images.png", img); - } - - #[test] - fn simple_texture() { - let size = 100; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); - let mut builder = r.new_scene(); - let _loader = builder - .gltf_load("../../gltf/gltfTutorial_013_SimpleTexture.gltf") - .unwrap(); - - let projection = camera::perspective(size as f32, size as f32); - let view = camera::look_at(Vec3::new(0.5, 0.5, 1.25), Vec3::new(0.5, 0.5, 0.0), Vec3::Y); - builder.set_camera(projection, view); - - // there are no lights in the scene and the material isn't marked as "unlit", so - // let's force it to be unlit. - let mut material = builder.materials.get(0).copied().unwrap(); - material.lighting_model = LightingModel::NO_LIGHTING; - builder.materials[0] = material; - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gltf_simple_texture.png", img); - } - - #[test] - fn normal_mapping_brick_sphere() { - let size = 600; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(1.0).extend(1.0)); - let mut builder = r.new_scene(); - let loader = builder.gltf_load("../../gltf/red_brick_03_1k.glb").unwrap(); - let (projection, view) = loader.cameras.get(0).copied().unwrap(); - builder.set_camera(projection, view); - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - println!("saving frame"); - img_diff::assert_img_eq("gltf_normal_mapping_brick_sphere.png", img); - } - - #[test] - // Tests that we can reuse the same builder for multiple loaders, building - // up a scene of multiple gltf documents. - fn can_load_multiple_gltfs_into_one_builder() { - let size = 600; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(1.0).extend(1.0)); - let mut builder = r.new_scene(); - let brick_loader = builder.gltf_load("../../gltf/red_brick_03_1k.glb").unwrap(); - let (projection, view) = brick_loader.cameras.get(0).copied().unwrap(); - builder.set_camera(projection, view); - - let _another_sun = builder - .new_directional_light() - .with_color(Vec4::ONE) - .with_direction(Vec3::NEG_Z) - .build(); - - let brick_sphere_id = brick_loader - .nodes - .get_by_name("Sphere") - .next() - .unwrap() - .entity_id; - { - // move the sphere over so we can see both models - let brick_sphere = builder.entities.get_mut(brick_sphere_id.index()).unwrap(); - brick_sphere.position = Vec4::new(-0.2, 0.0, 0.0, 0.0); - } - - let bust_loader = builder.gltf_load("../../gltf/marble_bust_1k.glb").unwrap(); - let bust_id = bust_loader - .nodes - .get_by_name("marble_bust_01") - .next() - .unwrap() - .entity_id; - { - // move the bust over too - let bust = builder.entities.get_mut(bust_id.index()).unwrap(); - bust.position = Vec4::new(0.2, -0.1, 0.2, 0.0); - } - - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - with_bloom: false, - ..Default::default() - }); - - let img = r.render_image().unwrap(); - println!("saving frame"); - img_diff::assert_img_eq("gltf_can_load_multiple.png", img.clone()); - } - - #[cfg(feature = "gltf")] - #[test] - fn simple_animation() { - let mut r = Renderling::headless(50, 50).with_background_color(Vec4::ONE); - - let projection = camera::perspective(50.0, 50.0); - let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(projection, view); - let default_material = builder.add_material(PbrMaterial { - albedo_factor: Vec4::new(0.0, 0.0, 0.0, 0.5), - lighting_model: LightingModel::NO_LIGHTING, - ..Default::default() - }); - - let loader = builder - .gltf_load("../../gltf/animated_triangle.gltf") - .unwrap(); - let tri_id = loader.nodes.get(0).unwrap().entity_id; - { - let entity = builder.entities.get_mut(tri_id.index()).unwrap(); - entity.material = default_material; - } - let mut entities = builder.entities.clone(); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("gltf_simple_animation.png", img); - - assert_eq!(1, loader.animations.len()); - - let anime = loader.animations.get(0).unwrap(); - println!("anime: {:?}", anime); - assert_eq!(1.0, anime.tweens[0].length_in_seconds()); - - let num = 8; - for i in 0..8 { - let t = i as f32 / num as f32; - let transforms = anime.get_properties_at_time(t).unwrap(); - let scene = r.graph.get_resource_mut::().unwrap().unwrap(); - for (id, tween_prop) in transforms.into_iter() { - let entity = entities.get_mut(id.index()).unwrap(); - match tween_prop { - crate::TweenProperty::Translation(t) => { - entity.position = t.extend(0.0); - } - crate::TweenProperty::Rotation(r) => { - entity.rotation = r; - } - crate::TweenProperty::Scale(s) => { - entity.scale = s.extend(0.0); - } - crate::TweenProperty::MorphTargetWeights(ws) => { - entity.set_morph_target_weights(ws); - } - } - scene.update_entity(*entity).unwrap(); - } - let img = r.render_image().unwrap(); - img_diff::assert_img_eq(&format!("gltf_simple_animation_after/{i}.png"), img); - } - } - - #[cfg(feature = "gltf")] - #[test] - fn simple_skin() { - use crate::{Scene, TweenProperty, ViewMut}; - - let size = 100; - let mut r = - Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); - let projection = camera::perspective(50.0, 50.0); - let view = camera::look_at(Vec3::Z * 4.0, Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(projection, view); - let loader = builder - .gltf_load("../../gltf/gltfTutorial_019_SimpleSkin.gltf") - .unwrap(); - let skin_animation = loader.animations.get(0).unwrap(); - let skin_animation_duration = skin_animation.length_in_seconds(); - let mut entities = builder.entities.clone(); - assert!(entities[0].info.is_skin()); - let scene = builder.build().unwrap(); - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - - let frames = 4; - for i in 0..=frames { - let time = if i == 0 { - 0.0 - } else { - i as f32 / frames as f32 * skin_animation_duration - }; - r.graph - .visit(|mut scene: ViewMut| { - for (id, tween_prop) in skin_animation.get_properties_at_time(time).unwrap() { - let ent = entities.get_mut(id.index()).unwrap(); - match tween_prop { - TweenProperty::Translation(t) => { - ent.position = t.extend(ent.position.w); - } - TweenProperty::Rotation(r) => { - ent.rotation = r; - } - TweenProperty::Scale(s) => { - if s == Vec3::ZERO { - log::trace!("scale is zero at time: {time:?}"); - panic!("animation: {skin_animation:#?}"); - } - ent.scale = s.extend(ent.scale.w); - } - TweenProperty::MorphTargetWeights(ws) => { - ent.set_morph_target_weights(ws); - } - } - scene.update_entity(*ent).unwrap(); - } - }) - .unwrap(); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq(&format!("gltf_simple_skin/{i}.png"), img); - } - } -} diff --git a/crates/renderling/src/skybox.rs b/crates/renderling/src/skybox.rs index d05b3b8f..89f119ad 100644 --- a/crates/renderling/src/skybox.rs +++ b/crates/renderling/src/skybox.rs @@ -1,8 +1,13 @@ //! An HDR skybox. +use std::sync::Arc; + +use crabslab::{CpuSlab, GrowableSlab, Slab, SlabItem, WgpuBuffer}; use glam::{Mat4, Vec3}; -use renderling_shader::stage::GpuConstants; -use crate::{SceneImage, Uniform}; +use crate::{ + atlas::AtlasImage, + shader::{convolution::VertexPrefilterEnvironmentCubemapIds, stage::Camera}, +}; /// Render pipeline used to draw a skybox. pub struct SkyboxRenderPipeline(pub wgpu::RenderPipeline); @@ -15,7 +20,7 @@ pub fn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -43,7 +48,7 @@ pub fn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { pub fn create_skybox_bindgroup( device: &wgpu::Device, - constants: &Uniform, + slab_buffer: &wgpu::Buffer, texture: &crate::Texture, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -52,7 +57,7 @@ pub fn create_skybox_bindgroup( entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: constants.buffer().as_entire_binding(), + resource: slab_buffer.as_entire_binding(), }, wgpu::BindGroupEntry { binding: 1, @@ -84,7 +89,7 @@ pub fn create_skybox_render_pipeline( }); SkyboxRenderPipeline( device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("skybox pipeline"), + label: Some("skybox render pipeline"), layout: Some(&pp_layout), vertex: wgpu::VertexState { module: &vertex_shader, @@ -140,26 +145,36 @@ pub struct Skybox { pub prefiltered_environment_cubemap: crate::Texture, // Texture of the pre-computed brdf integration pub brdf_lut: crate::Texture, + // `Id` of the camera to use for rendering the skybox. + // + // The camera is used to determine the orientation of the skybox. + pub camera: crabslab::Id, } impl Skybox { /// Create an empty, transparent skybox. - pub fn empty(device: &wgpu::Device, queue: &wgpu::Queue) -> Self { + pub fn empty(device: crate::Device, queue: crate::Queue) -> Self { log::trace!("creating empty skybox"); - let hdr_img = SceneImage { + let hdr_img = AtlasImage { pixels: vec![0u8; 4 * 4], width: 1, height: 1, - format: crate::SceneImageFormat::R32G32B32A32FLOAT, + format: crate::AtlasImageFormat::R32G32B32A32FLOAT, apply_linear_transfer: false, }; - Self::new(device, queue, hdr_img) + Self::new(device, queue, hdr_img, crabslab::Id::::NONE) } /// Create a new `Skybox`. - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, hdr_img: SceneImage) -> Self { + pub fn new( + device: crate::Device, + queue: crate::Queue, + hdr_img: AtlasImage, + camera: crabslab::Id, + ) -> Self { log::trace!("creating skybox"); - let equirectangular_texture = Skybox::hdr_texture_from_scene_image(device, queue, hdr_img); + let equirectangular_texture = + Skybox::hdr_texture_from_atlas_image(&device, &queue, hdr_img); let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); let views = [ Mat4::look_at_rh( @@ -193,81 +208,45 @@ impl Skybox { Vec3::new(0.0, -1.0, 0.0), ), ]; - // Create unit cube for projections. - let cube_vertices: [[f32; 3]; 8] = [ - // front - [-1.0, -1.0, 1.0], - [1.0, -1.0, 1.0], - [1.0, 1.0, 1.0], - [-1.0, 1.0, 1.0], - // back - [-1.0, -1.0, -1.0], - [1.0, -1.0, -1.0], - [1.0, 1.0, -1.0], - [-1.0, 1.0, -1.0], - ]; - let cube_elements: [u16; 36] = [ - // front - 0, 1, 2, 2, 3, 0, // right - 1, 5, 6, 6, 2, 1, // back - 7, 6, 5, 5, 4, 7, // left - 4, 0, 3, 3, 7, 4, // bottom - 4, 5, 1, 1, 0, 4, // top - 3, 2, 6, 6, 7, 3, - ]; - - let unit_cube_mesh = crate::mesh::Mesh::new( - device, - Some("unit cube"), - cube_vertices, - Some(cube_elements), - ); // Create environment map. let environment_cubemap = Skybox::create_environment_map_from_hdr( - device, - queue, + device.clone(), + queue.clone(), &equirectangular_texture, - &unit_cube_mesh, proj, views, ); // Convolve the environment map. - let irradiance_cubemap = Skybox::create_irradiance_map( - device, - queue, - &environment_cubemap, - &unit_cube_mesh, - proj, - views, - ); + let irradiance_cubemap = + Skybox::create_irradiance_map(&device, &queue, &environment_cubemap, proj, views); // Generate specular IBL pre-filtered environment map. let prefiltered_environment_cubemap = Skybox::create_prefiltered_environment_map( - device, - queue, + &device, + &queue, &environment_cubemap, - &unit_cube_mesh, proj, views, ); - let brdf_lut = Skybox::create_precomputed_brdf_texture(device, queue); + let brdf_lut = Skybox::create_precomputed_brdf_texture(&device, &queue); Skybox { environment_cubemap, irradiance_cubemap, prefiltered_environment_cubemap, brdf_lut, + camera, } } - /// Convert an HDR [`SceneImage`] into a texture. - pub fn hdr_texture_from_scene_image( + /// Convert an HDR [`AtlasImage`] into a texture. + pub fn hdr_texture_from_atlas_image( device: &wgpu::Device, queue: &wgpu::Queue, - img: SceneImage, + img: AtlasImage, ) -> crate::Texture { crate::Texture::new_with( device, @@ -296,48 +275,50 @@ impl Skybox { queue: &wgpu::Queue, hdr_data: &[u8], ) -> crate::Texture { - let img = SceneImage::from_hdr_bytes(hdr_data).unwrap(); - Self::hdr_texture_from_scene_image(device, queue, img) + let img = AtlasImage::from_hdr_bytes(hdr_data).unwrap(); + Self::hdr_texture_from_atlas_image(device, queue, img) } fn create_environment_map_from_hdr( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: crate::Device, + queue: crate::Queue, hdr_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { // Create the cubemap-making pipeline. let pipeline = crate::cubemap::CubemapMakingRenderPipeline::new( - device, + &device, wgpu::TextureFormat::Rgba16Float, ); - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + + let buffer = WgpuBuffer::new_usage( + device.0.clone(), + queue.0.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, + ); + let mut slab = CpuSlab::new(buffer); + slab.write( + crabslab::Id::new(0), + &Camera::default().with_projection(proj), ); let bindgroup = crate::cubemap::cubemap_making_bindgroup( - device, + &device, Some("environment cubemap"), - &constants, + &slab.as_ref().get_buffer(), hdr_texture, ); Self::render_cubemap( - device, - queue, + &device, + &queue, "environment", &pipeline.0, - &mut constants, + &mut slab, + proj, &bindgroup, - unit_cube_mesh, views, 512, Some(9), @@ -349,9 +330,9 @@ impl Skybox { queue: &wgpu::Queue, label_prefix: &str, pipeline: &wgpu::RenderPipeline, - constants: &mut Uniform, + slab: &mut CpuSlab, + projection: Mat4, bindgroup: &wgpu::BindGroup, - unit_cube_mesh: &crate::mesh::Mesh, views: [Mat4; 6], texture_size: u32, mip_levels: Option, @@ -386,8 +367,7 @@ impl Skybox { ); // update the view to point at one of the cube faces - constants.camera_view = views[i]; - constants.update(queue); + slab.write(0u32.into(), &Camera::new(projection, views[i])); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -405,7 +385,7 @@ impl Skybox { render_pass.set_pipeline(pipeline); render_pass.set_bind_group(0, &bindgroup, &[]); - unit_cube_mesh.draw(&mut render_pass); + render_pass.draw(0..36, 0..1); } queue.submit([encoder.finish()]); @@ -431,42 +411,43 @@ impl Skybox { } fn create_irradiance_map( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: impl Into>, + queue: impl Into>, environment_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { + let device = device.into(); + let queue = queue.into(); let pipeline = crate::ibl::diffuse_irradiance::DiffuseIrradianceConvolutionRenderPipeline::new( - device, + &device, wgpu::TextureFormat::Rgba16Float, ); - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + let buffer = WgpuBuffer::new_usage( + device.clone(), + queue.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, ); + let mut slab = CpuSlab::new(buffer); + slab.write(0u32.into(), &Camera::default().with_projection(proj)); + let bindgroup = crate::ibl::diffuse_irradiance::diffuse_irradiance_convolution_bindgroup( - device, + &device, Some("irradiance"), - &constants, + &slab.as_ref().get_buffer(), environment_texture, ); Self::render_cubemap( - device, - queue, + &device, + &queue, "irradiance", &pipeline.0, - &mut constants, + &mut slab, + proj, &bindgroup, - unit_cube_mesh, views, 32, None, @@ -474,33 +455,29 @@ impl Skybox { } fn create_prefiltered_environment_map( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: impl Into>, + queue: impl Into>, environment_texture: &crate::Texture, - unit_cube_mesh: &crate::mesh::Mesh, proj: Mat4, views: [Mat4; 6], ) -> crate::Texture { - let mut constants = crate::uniform::Uniform::new( - device, - GpuConstants { - camera_projection: proj, - ..Default::default() - }, + let device = device.into(); + let queue = queue.into(); + let buffer = WgpuBuffer::new_usage( + device.clone(), + queue.clone(), + Camera::slab_size(), wgpu::BufferUsages::VERTEX, - wgpu::ShaderStages::VERTEX, - ); - let mut roughness = Uniform::::new( - device, - 0.0, - wgpu::BufferUsages::empty(), - wgpu::ShaderStages::VERTEX_FRAGMENT, ); + let mut slab = CpuSlab::new(buffer); + // Write the camera at 0 and then the roughness + let camera = slab.append(&Camera::default().with_projection(proj)); + let roughness = slab.append(&0.0f32); + let id = slab.append(&VertexPrefilterEnvironmentCubemapIds { camera, roughness }); let (pipeline, bindgroup) = crate::ibl::prefiltered_environment::create_pipeline_and_bindgroup( - device, - &constants, - &roughness, + &device, + &slab.as_ref().get_buffer(), environment_texture, ); @@ -512,16 +489,15 @@ impl Skybox { let mip_height: u32 = 128 >> mip_level; // update the roughness for these mips - *roughness = mip_level as f32 / 4.0; - roughness.update(queue); + slab.write(roughness, &(mip_level as f32 / 4.0)); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("specular convolution"), }); let cubemap_face = crate::Texture::new_with( - device, - queue, + &device, + &queue, Some(&format!("cubemap{i}{mip_level}prefiltered_environment")), Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC), None, @@ -535,8 +511,7 @@ impl Skybox { ); // update the view to point at one of the cube faces - constants.camera_view = views[i]; - constants.update(queue); + slab.write(camera, &Camera::new(proj, views[i])); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -554,7 +529,7 @@ impl Skybox { render_pass.set_pipeline(&pipeline); render_pass.set_bind_group(0, &bindgroup, &[]); - unit_cube_mesh.draw(&mut render_pass); + render_pass.draw(0..36, id.inner()..id.inner() + 1); } queue.submit([encoder.finish()]); @@ -563,8 +538,8 @@ impl Skybox { } crate::Texture::new_cubemap_texture( - device, - queue, + &device, + &queue, Some(&format!("prefiltered environment cubemap")), 128, cubemap_faces.as_slice(), @@ -577,35 +552,6 @@ impl Skybox { device: &wgpu::Device, queue: &wgpu::Queue, ) -> crate::Texture { - #[repr(C)] - #[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] - struct Vert { - pos: [f32; 3], - uv: [f32; 2], - } - - let bl = Vert { - pos: [-1.0, -1.0, 0.0], - uv: [0.0, 1.0], - }; - let br = Vert { - pos: [1.0, -1.0, 0.0], - uv: [1.0, 1.0], - }; - let tl = Vert { - pos: [-1.0, 1.0, 0.0], - uv: [0.0, 0.0], - }; - let tr = Vert { - pos: [1.0, 1.0, 0.0], - uv: [1.0, 0.0], - }; - - let vertices = [bl, br, tr, bl, tr, tl]; - - let screen_space_quad_mesh = - crate::mesh::Mesh::from_vertices(device, Some("brdf_lut"), vertices); - let vertex_module = device.create_shader_module(wgpu::include_spirv!( "linkage/convolution-vertex_brdf_lut_convolution.spv" )); @@ -618,14 +564,7 @@ impl Skybox { vertex: wgpu::VertexState { module: &vertex_module, entry_point: "convolution::vertex_brdf_lut_convolution", - buffers: &[wgpu::VertexBufferLayout { - array_stride: (3 + 2) * std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array![ - 0 => Float32x3, - 1 => Float32x2 - ], - }], + buffers: &[], }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -692,7 +631,7 @@ impl Skybox { }); render_pass.set_pipeline(&pipeline); - screen_space_quad_mesh.draw(&mut render_pass); + render_pass.draw(0..6, 0..1); } queue.submit([encoder.finish()]); framebuffer @@ -701,37 +640,39 @@ impl Skybox { #[cfg(test)] mod test { + use crabslab::GrowableSlab; use glam::Vec3; use super::*; - use crate::{RenderGraphConfig, Renderling}; + use crate::Renderling; #[test] fn hdr_skybox_scene() { let mut r = Renderling::headless(600, 400); let proj = crate::camera::perspective(600.0, 400.0); let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y); - let mut builder = r.new_scene().with_camera(proj, view); - builder.add_skybox_image_from_path("../../img/hdr/resting_place.hdr"); - let scene = builder.build().unwrap(); + + let mut stage = r.new_stage(); + stage.configure_graph(&mut r, true); + + let camera = stage.append(&Camera::new(proj, view)); + let skybox = stage + .new_skybox_from_path("../../img/hdr/resting_place.hdr", camera) + .unwrap(); assert_eq!( wgpu::TextureFormat::Rgba16Float, - scene.skybox.irradiance_cubemap.texture.format() + skybox.irradiance_cubemap.texture.format() ); assert_eq!( wgpu::TextureFormat::Rgba16Float, - scene - .skybox - .prefiltered_environment_cubemap - .texture - .format() + skybox.prefiltered_environment_cubemap.texture.format() ); for i in 0..6 { // save out the irradiance face let copied_buffer = crate::Texture::read_from( - &scene.skybox.irradiance_cubemap.texture, + &skybox.irradiance_cubemap.texture, r.get_device(), r.get_queue(), 32, @@ -755,7 +696,7 @@ mod test { let mip_size = 128u32 >> mip_level; // save out the prefiltered environment faces' mips let copied_buffer = crate::Texture::read_from( - &scene.skybox.prefiltered_environment_cubemap.texture, + &skybox.prefiltered_environment_cubemap.texture, r.get_device(), r.get_queue(), mip_size as usize, @@ -782,12 +723,9 @@ mod test { } } - r.setup_render_graph(RenderGraphConfig { - scene: Some(scene), - with_screen_capture: true, - ..Default::default() - }); - let img = r.render_image().unwrap(); + stage.set_skybox(skybox); + + let img = r.render_linear_image().unwrap(); img_diff::assert_img_eq("skybox/hdr.png", img); } diff --git a/crates/renderling/src/slab.rs b/crates/renderling/src/slab.rs deleted file mode 100644 index 1a6aaa28..00000000 --- a/crates/renderling/src/slab.rs +++ /dev/null @@ -1,463 +0,0 @@ -//! CPU side of slab storage. -use std::{ - ops::Deref, - sync::{atomic::AtomicUsize, Arc, RwLock}, -}; - -use renderling_shader::{array::Array, id::Id}; -use snafu::{ResultExt, Snafu}; - -pub use renderling_shader::slab::{Slab, Slabbed}; - -#[derive(Debug, Snafu)] -pub enum SlabError { - #[snafu(display( - "Out of capacity. Tried to write {type_is}(slab size={slab_size}) \ - at {index} but capacity is {capacity}", - ))] - Capacity { - type_is: &'static str, - slab_size: usize, - index: usize, - capacity: usize, - }, - - #[snafu(display( - "Out of capacity. Tried to write an array of {elements} {type_is}\ - (each of slab size={slab_size}) \ - at {index} but capacity is {capacity}", - ))] - ArrayCapacity { - type_is: &'static str, - elements: usize, - slab_size: usize, - index: usize, - capacity: usize, - }, - - #[snafu(display( - "Array({type_is}) length mismatch. Tried to write {data_len} elements \ - into array of length {array_len}", - ))] - ArrayLen { - type_is: &'static str, - array_len: usize, - data_len: usize, - }, - - #[snafu(display("Async recv error: {source}"))] - AsyncRecv { source: async_channel::RecvError }, - - #[snafu(display("Async error: {source}"))] - Async { source: wgpu::BufferAsyncError }, -} - -/// A slab buffer used by the stage to store heterogeneous objects. -/// -/// A clone of a buffer is a reference to the same buffer. -#[derive(Clone)] -pub struct SlabBuffer { - pub(crate) buffer: Arc>, - // The number of u32 elements currently stored in the buffer. - // - // This is the next index to write into. - len: Arc, - // The total number of u32 elements that can be stored in the buffer. - capacity: Arc, -} - -impl SlabBuffer { - fn new_buffer( - device: &wgpu::Device, - capacity: usize, - usage: wgpu::BufferUsages, - ) -> wgpu::Buffer { - device.create_buffer(&wgpu::BufferDescriptor { - label: Some("SlabBuffer"), - size: (capacity * std::mem::size_of::()) as u64, - usage: wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::COPY_SRC - | usage, - mapped_at_creation: false, - }) - } - - /// Create a new slab buffer with a capacity of `capacity` u32 elements. - pub fn new(device: &wgpu::Device, capacity: usize) -> Self { - Self::new_usage(device, capacity, wgpu::BufferUsages::empty()) - } - - /// Create a new slab buffer with a capacity of `capacity` u32 elements. - pub fn new_usage(device: &wgpu::Device, capacity: usize, usage: wgpu::BufferUsages) -> Self { - Self { - buffer: RwLock::new(Self::new_buffer(device, capacity, usage)).into(), - len: AtomicUsize::new(0).into(), - capacity: AtomicUsize::new(capacity).into(), - } - } - - /// The number of u32 elements currently stored in the buffer. - pub fn len(&self) -> usize { - self.len.load(std::sync::atomic::Ordering::Relaxed) - } - - /// The total number of u32 elements that can be stored in the buffer. - pub fn capacity(&self) -> usize { - self.capacity.load(std::sync::atomic::Ordering::Relaxed) - } - - fn maybe_expand_to_fit( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - len: usize, - ) { - let size = T::slab_size(); - let capacity = self.capacity(); - //log::trace!( - // "append_slice: {size} * {ts_len} + {len} ({}) >= {capacity}", - // size * ts_len + len - //); - let capacity_needed = self.len() + size * len; - if capacity_needed > capacity { - let mut new_capacity = capacity * 2; - while new_capacity < capacity_needed { - new_capacity *= 2; - } - self.resize(device, queue, new_capacity); - } - } - - /// Preallocate space for one `T` element, but don't write anything to the buffer. - /// - /// This can be used to write later with [`Self::write`]. - /// - /// NOTE: This changes the next available buffer index and may change the buffer capacity. - pub fn allocate(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> Id { - self.maybe_expand_to_fit::(device, queue, 1); - let index = self - .len - .fetch_add(T::slab_size(), std::sync::atomic::Ordering::Relaxed); - Id::from(index) - } - - /// Preallocate space for `len` `T` elements, but don't write to - /// the buffer. - /// - /// This can be used to allocate space for a bunch of elements that get written - /// later with [`Self::write_array`]. - /// - /// NOTE: This changes the length of the buffer and may change the capacity. - pub fn allocate_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - len: usize, - ) -> Array { - if len == 0 { - return Array::default(); - } - self.maybe_expand_to_fit::(device, queue, len); - let index = self - .len - .fetch_add(T::slab_size() * len, std::sync::atomic::Ordering::Relaxed); - Array::new(index as u32, len as u32) - } - - /// Write into the slab buffer, modifying in place. - /// - /// NOTE: This has no effect on the length of the buffer. - pub fn write( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - id: Id, - data: &T, - ) -> Result<(), SlabError> { - let byte_offset = id.index() * std::mem::size_of::(); - let size = T::slab_size(); - let mut bytes = vec![0u32; size]; - let _ = bytes.write(data, 0); - let capacity = self.capacity(); - snafu::ensure!( - id.index() + size <= capacity, - CapacitySnafu { - type_is: std::any::type_name::(), - slab_size: T::slab_size(), - index: id.index(), - capacity - } - ); - let encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - queue.write_buffer( - // UNWRAP: if we can't lock we want to panic - &self.buffer.read().unwrap(), - byte_offset as u64, - bytemuck::cast_slice(bytes.as_slice()), - ); - queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } - - /// Write elements into the slab buffer, modifying in place. - /// - /// NOTE: This has no effect on the length of the buffer. - /// - /// ## Errors - /// Errors if the capacity is exceeded. - pub fn write_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - array: Array, - data: &[T], - ) -> Result<(), SlabError> { - snafu::ensure!( - array.len() == data.len(), - ArrayLenSnafu { - type_is: std::any::type_name::(), - array_len: array.len(), - data_len: data.len() - } - ); - let capacity = self.capacity(); - let size = T::slab_size() * array.len(); - snafu::ensure!( - array.starting_index() + size <= capacity, - ArrayCapacitySnafu { - capacity, - type_is: std::any::type_name::(), - elements: array.len(), - slab_size: T::slab_size(), - index: array.at(0).index() - } - ); - let encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - let mut u32_data = vec![0u32; size]; - let _ = u32_data.write_slice(data, 0); - let byte_offset = array.starting_index() * std::mem::size_of::(); - queue.write_buffer( - // UNWRAP: if we can't lock we want to panic - &self.buffer.read().unwrap(), - byte_offset as u64, - bytemuck::cast_slice(&u32_data), - ); - queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } - - /// Read from the slab buffer. - /// - /// `T` is only for the error message. - pub async fn read_raw( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - start: usize, - len: usize, - ) -> Result, SlabError> { - let byte_offset = start * std::mem::size_of::(); - let length = len * std::mem::size_of::(); - let output_buffer_size = length as u64; - let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("SlabBuffer::read_raw"), - size: output_buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - log::trace!( - "copy_buffer_to_buffer byte_offset:{byte_offset}, \ - output_buffer_size:{output_buffer_size}", - ); - encoder.copy_buffer_to_buffer( - // UNWRAP: if we can't lock we want to panic - &self.buffer.read().unwrap(), - byte_offset as u64, - &output_buffer, - 0, - output_buffer_size, - ); - queue.submit(std::iter::once(encoder.finish())); - - let buffer_slice = output_buffer.slice(..); - let (tx, rx) = async_channel::bounded(1); - buffer_slice.map_async(wgpu::MapMode::Read, move |res| tx.try_send(res).unwrap()); - device.poll(wgpu::Maintain::Wait); - rx.recv() - .await - .context(AsyncRecvSnafu)? - .context(AsyncSnafu)?; - let bytes = buffer_slice.get_mapped_range(); - Ok(bytemuck::cast_slice(bytes.deref()).to_vec()) - } - - /// Read from the slab buffer. - pub async fn read( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - id: Id, - ) -> Result { - let vec = self - .read_raw(device, queue, id.index(), T::slab_size()) - .await?; - let t = Slab::read(vec.as_slice(), Id::::new(0)); - Ok(t) - } - - /// Append to the end of the buffer. - pub fn append( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - t: &T, - ) -> Id { - let id = self.allocate::(device, queue); - // IGNORED: safe because we just allocated the id - let _ = self.write(device, queue, id, t); - id - } - - /// Append a slice to the end of the buffer, resizing if necessary - /// and returning a slabbed array. - pub fn append_array( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - ts: &[T], - ) -> Array { - let array = self.allocate_array::(device, queue, ts.len()); - // IGNORED: safe because we just allocated the array - let _ = self.write_array(device, queue, array, ts); - array - } - - /// Resize the slab buffer. - /// - /// This creates a new buffer and writes the data from the old into the new. - pub fn resize(&self, device: &wgpu::Device, queue: &wgpu::Queue, new_capacity: usize) { - let capacity = self.capacity(); - if new_capacity > capacity { - log::trace!("resizing buffer from {capacity} to {new_capacity}"); - let len = self.len(); - let mut buffer = self.buffer.write().unwrap(); - let new_buffer = Self::new_buffer(device, new_capacity, buffer.usage()); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - // UNWRAP: if we can't lock we want to panic - encoder.copy_buffer_to_buffer( - &buffer, - 0, - &new_buffer, - 0, - (len * std::mem::size_of::()) as u64, - ); - queue.submit(std::iter::once(encoder.finish())); - *buffer = new_buffer; - self.capacity - .store(new_capacity, std::sync::atomic::Ordering::Relaxed); - } - } - - /// Get the underlying buffer. - pub fn get_buffer(&self) -> impl Deref + '_ { - // UNWRAP: if we can't lock we want to panic - self.buffer.read().unwrap() - } -} - -#[cfg(test)] -mod test { - use renderling_shader::stage::{NativeVertexData, RenderUnit, Vertex, VertexData}; - - use crate::Renderling; - - use super::*; - - #[test] - fn slab_buffer_roundtrip() { - println!("write"); - let _ = env_logger::builder().is_test(true).try_init(); - let r = Renderling::headless(10, 10); - let device = r.get_device(); - let queue = r.get_queue(); - let slab = SlabBuffer::new(device, 2); - slab.append(device, queue, &42); - slab.append(device, queue, &1); - let id = Id::<[u32; 2]>::new(0); - let t = futures_lite::future::block_on(slab.read(device, queue, id)).unwrap(); - assert_eq!([42, 1], t, "read back what we wrote"); - - println!("overflow"); - let id = Id::::new(2); - let err = slab.write(device, queue, id, &666).unwrap_err(); - assert_eq!( - "Out of capacity. Tried to write u32(slab size=1) at 2 but capacity is 2", - err.to_string() - ); - assert_eq!(2, slab.len()); - - println!("append"); - slab.append(device, queue, &666); - let id = Id::<[u32; 3]>::new(0); - let t = futures_lite::future::block_on(slab.read(device, queue, id)).unwrap(); - assert_eq!([42, 1, 666], t); - - println!("append slice"); - let a = glam::Vec3::new(0.0, 0.0, 0.0); - let b = glam::Vec3::new(1.0, 1.0, 1.0); - let c = glam::Vec3::new(2.0, 2.0, 2.0); - let points = vec![a, b, c]; - let array = slab.append_array(device, queue, &points); - let slab_u32 = - futures_lite::future::block_on(slab.read_raw(device, queue, 0, slab.len())).unwrap(); - let points_out = slab_u32.read_vec::(array); - assert_eq!(points, points_out); - - println!("append slice 2"); - let points = vec![a, a, a, a, b, b, b, c, c]; - let array = slab.append_array(device, queue, &points); - let slab_u32 = - futures_lite::future::block_on(slab.read_raw(device, queue, 0, slab.len())).unwrap(); - let points_out = slab_u32.read_vec::(array); - assert_eq!(points, points_out); - } - - #[test] - fn slab_buffer_unit_roundtrip() { - let _ = env_logger::builder().is_test(true).try_init(); - let r = Renderling::headless(10, 10); - let device = r.get_device(); - let queue = r.get_queue(); - let slab = SlabBuffer::new(device, 2); - let vertices = vec![ - Vertex::default().with_position([0.0, 0.0, 0.0]), - Vertex::default().with_position([1.0, 1.0, 1.0]), - Vertex::default().with_position([2.0, 2.0, 2.0]), - ]; - let vertices = slab.append_array(device, queue, &vertices); - let data_id = slab.append( - device, - queue, - &NativeVertexData { - vertices, - material: Id::new(666), - }, - ); - let unit = RenderUnit { - vertex_data: VertexData::Native(data_id), - camera: Id::new(42), - transform: Id::new(1337), - vertex_count: vertices.len() as u32, - }; - let unit_id = slab.append(device, queue, &unit); - let t = futures_lite::future::block_on(slab.read(device, queue, unit_id)).unwrap(); - assert_eq!(unit, t, "read back what we wrote"); - } -} diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index 8348ddc7..faea6e4b 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -1,44 +1,65 @@ -//! Rendering objects in the scene graph. +//! GPU staging area. //! -//! Provides a `Stage` object that can be used to render a scene graph. +//! The `Stage` object contains a slab buffer and a render pipeline. +//! It is used to stage objects for rendering. use std::{ ops::{Deref, DerefMut}, sync::{atomic::AtomicBool, Arc, Mutex, RwLock}, }; +use crabslab::{Array, CpuSlab, GrowableSlab, Id, Slab, SlabItem, WgpuBuffer}; use moongraph::{View, ViewMut}; use renderling_shader::{ - array::Array, debug::DebugMode, - id::Id, - slab::Slabbed, - stage::{GpuLight, RenderUnit, StageLegend}, + stage::{light::Light, Camera, RenderUnit, StageLegend}, + texture::GpuTexture, }; +use snafu::Snafu; use crate::{ - bloom::{BloomFilter, BloomResult}, - Atlas, DepthTexture, Device, HdrSurface, Queue, Skybox, SlabBuffer, SlabError, + Atlas, AtlasError, AtlasImage, AtlasImageError, DepthTexture, Device, HdrSurface, Queue, + Skybox, SlabError, }; #[cfg(feature = "gltf")] mod gltf_support; -pub mod light; #[cfg(feature = "gltf")] pub use gltf_support::*; +#[derive(Debug, Snafu)] +pub enum StageError { + #[snafu(display("{source}"))] + Atlas { source: AtlasError }, + + #[snafu(display("{source}"))] + Slab { source: SlabError }, +} + +impl From for StageError { + fn from(source: AtlasError) -> Self { + Self::Atlas { source } + } +} + +impl From for StageError { + fn from(source: SlabError) -> Self { + Self::Slab { source } + } +} + /// Represents an entire scene worth of rendering data. /// /// A clone of a stage is a reference to the same stage. #[derive(Clone)] pub struct Stage { - pub(crate) slab: SlabBuffer, + pub(crate) slab: Arc>>, pub(crate) atlas: Arc>, - pub(crate) skybox: Arc>, + pub(crate) skybox: Arc>, + pub(crate) skybox_bindgroup: Arc>>>, pub(crate) pipeline: Arc>>>, pub(crate) skybox_pipeline: Arc>>>, pub(crate) has_skybox: Arc, - pub(crate) bloom: Arc>, pub(crate) has_bloom: Arc, pub(crate) buffers_bindgroup: Arc>>>, pub(crate) textures_bindgroup: Arc>>>, @@ -47,17 +68,65 @@ pub struct Stage { pub(crate) queue: Queue, } +impl Slab for Stage { + fn len(&self) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.read().unwrap().len() + } + + fn read(&self, id: Id) -> T { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.read().unwrap().read(id) + } + + fn write_indexed(&mut self, t: &T, index: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().write_indexed(t, index) + } + + fn write_indexed_slice(&mut self, t: &[T], index: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().write_indexed_slice(t, index) + } +} + +impl GrowableSlab for Stage { + fn capacity(&self) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().capacity() + } + + fn reserve_capacity(&mut self, capacity: usize) { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().reserve_capacity(capacity) + } + + fn increment_len(&mut self, n: usize) -> usize { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab.write().unwrap().increment_len(n) + } +} + impl Stage { /// Create a new stage. pub fn new(device: Device, queue: Queue) -> Self { - let s = Self { - slab: SlabBuffer::new(&device, 256), + let atlas = Atlas::empty(&device, &queue); + let legend = StageLegend { + atlas_size: atlas.size, + ..Default::default() + }; + let mut s = Self { + slab: Arc::new(RwLock::new(CpuSlab::new(WgpuBuffer::new( + device.0.clone(), + queue.0.clone(), + 256, + )))), pipeline: Default::default(), - atlas: Arc::new(RwLock::new(Atlas::empty(&device, &queue))), - skybox: Arc::new(Mutex::new(Skybox::empty(&device, &queue))), + atlas: Arc::new(RwLock::new(atlas)), + skybox: Arc::new(RwLock::new(Skybox::empty(device.clone(), queue.clone()))), + skybox_bindgroup: Default::default(), skybox_pipeline: Default::default(), has_skybox: Arc::new(AtomicBool::new(false)), - bloom: Arc::new(RwLock::new(BloomFilter::new(&device, &queue, 1, 1))), has_bloom: Arc::new(AtomicBool::new(false)), buffers_bindgroup: Default::default(), textures_bindgroup: Default::default(), @@ -65,82 +134,78 @@ impl Stage { device, queue, }; - let _ = s.append(&StageLegend::default()); + s.append(&legend); s } - /// Allocate some storage for a type on the slab, but don't write it. - pub fn allocate(&self) -> Id { - self.slab.allocate(&self.device, &self.queue) - } - - /// Allocate contiguous storage for `len` elements of a type on the slab, but don't write them. - pub fn allocate_array(&self, len: usize) -> Array { - self.slab.allocate_array(&self.device, &self.queue, len) - } - - /// Write an object to the slab. - pub fn write(&self, id: Id, object: &T) -> Result<(), SlabError> { - self.slab.write(&self.device, &self.queue, id, object)?; - Ok(()) - } - - /// Write many objects to the slab. - pub fn write_array( - &self, - array: Array, - objects: &[T], - ) -> Result<(), SlabError> { - let () = self - .slab - .write_array(&self.device, &self.queue, array, objects)?; - Ok(()) - } - - /// Add an object to the slab and return its ID. - pub fn append(&self, object: &T) -> Id { - self.slab.append(&self.device, &self.queue, object) - } - - /// Add a slice of objects to the slab and return an [`Array`]. - pub fn append_array(&self, objects: &[T]) -> Array { - self.slab.append_array(&self.device, &self.queue, objects) - } - /// Set the debug mode. - pub fn set_debug_mode(&self, debug_mode: DebugMode) { + pub fn set_debug_mode(&mut self, debug_mode: DebugMode) { let id = Id::::from(StageLegend::offset_of_debug_mode()); - // UNWRAP: safe because the debug mode offset is guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &debug_mode) - .unwrap(); + self.write(id, &debug_mode); } /// Set the debug mode. - pub fn with_debug_mode(self, debug_mode: DebugMode) -> Self { + pub fn with_debug_mode(mut self, debug_mode: DebugMode) -> Self { self.set_debug_mode(debug_mode); self } /// Set whether the stage uses lighting. - pub fn set_has_lighting(&self, use_lighting: bool) { + pub fn set_has_lighting(&mut self, use_lighting: bool) { let id = Id::::from(StageLegend::offset_of_has_lighting()); - // UNWRAP: safe because the has lighting offset is guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &use_lighting) - .unwrap(); + self.write(id, &use_lighting); } /// Set whether the stage uses lighting. - pub fn with_lighting(self, use_lighting: bool) -> Self { + pub fn with_lighting(mut self, use_lighting: bool) -> Self { self.set_has_lighting(use_lighting); self } + /// Set the lights to use for shading. + pub fn set_lights(&mut self, lights: Array) { + let id = Id::>::from(StageLegend::offset_of_light_array()); + self.write(id, &lights); + } + + /// Set the images to use for the atlas. + /// + /// Resets the atlas, packing it with the given images and returning a + /// vector of the textures ready to be staged. + /// + /// ## WARNING + /// This invalidates any currently staged `GpuTextures`. + pub fn set_images( + &mut self, + images: impl IntoIterator, + ) -> Result, StageError> { + // UNWRAP: if we can't write the atlas we want to panic + let mut atlas = self.atlas.write().unwrap(); + *atlas = Atlas::pack(&self.device, &self.queue, images)?; + + // The textures bindgroup will have to be remade + let _ = self.textures_bindgroup.lock().unwrap().take(); + // The atlas size must be reset + let size_id = Id::::from(StageLegend::offset_of_atlas_size()); + // UNWRAP: if we can't write to the stage legend we want to panic + self.slab.write().unwrap().write(size_id, &atlas.size); + + let textures = atlas + .frames() + .map(|(i, (offset_px, size_px))| GpuTexture { + offset_px, + size_px, + atlas_index: i, + ..Default::default() + }) + .collect(); + Ok(textures) + } + /// Set the skybox. pub fn set_skybox(&self, skybox: Skybox) { // UNWRAP: if we can't acquire the lock we want to panic. - let mut guard = self.skybox.lock().unwrap(); + let mut guard = self.skybox.write().unwrap(); *guard = skybox; self.has_skybox .store(true, std::sync::atomic::Ordering::Relaxed); @@ -158,47 +223,6 @@ impl Stage { self } - /// Create a new spot light and return its builder. - pub fn new_spot_light(&self) -> light::GpuSpotLightBuilder { - light::GpuSpotLightBuilder::new(self) - } - - /// Create a new directional light and return its builder. - pub fn new_directional_light(&self) -> light::GpuDirectionalLightBuilder { - light::GpuDirectionalLightBuilder::new(self) - } - - /// Create a new point light and return its builder. - pub fn new_point_light(&self) -> light::GpuPointLightBuilder { - light::GpuPointLightBuilder::new(self) - } - - /// Set the light array. - /// - /// This should be an iterator over the ids of all the lights on the stage. - pub fn set_light_array( - &self, - lights: impl IntoIterator>, - ) -> Array> { - let lights = lights.into_iter().collect::>(); - let light_array = self.append_array(&lights); - let id = Id::>>::from(StageLegend::offset_of_light_array()); - // UNWRAP: safe because we just appended the array, and the light array offset is - // guaranteed to be valid. - self.slab - .write(&self.device, &self.queue, id, &light_array) - .unwrap(); - light_array - } - - /// Set the light array. - /// - /// This should be an iterator over the ids of all the lights on the stage. - pub fn with_light_array(self, lights: impl IntoIterator>) -> Self { - self.set_light_array(lights); - self - } - fn buffers_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { let visibility = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::COMPUTE; @@ -283,74 +307,39 @@ impl Stage { } /// Return the skybox render pipeline, creating it if necessary. - pub fn get_skybox_pipeline(&self) -> Arc { - fn create_skybox_render_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { - log::trace!("creating stage's skybox render pipeline"); - let vertex_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/skybox-slabbed_vertex.spv")); - let fragment_shader = device.create_shader_module(wgpu::include_spirv!( - "linkage/skybox-stage_skybox_cubemap.spv" - )); - let stage_slab_buffers_layout = Stage::buffers_bindgroup_layout(&device); - let textures_layout = Stage::textures_bindgroup_layout(&device); - let label = Some("stage skybox"); - let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label, - bind_group_layouts: &[&stage_slab_buffers_layout, &textures_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("skybox pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &vertex_shader, - entry_point: "skybox::vertex", - buffers: &[], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::LessEqual, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_shader, - entry_point: "skybox::fragment_cubemap", - targets: &[Some(wgpu::ColorTargetState { - format: crate::hdr::HdrSurface::TEXTURE_FORMAT, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }) - } - + pub fn get_skybox_pipeline_and_bindgroup( + &self, + ) -> (Arc, Arc) { // UNWRAP: safe because we're only ever called from the render thread. let mut pipeline = self.skybox_pipeline.write().unwrap(); - if let Some(pipeline) = pipeline.as_ref() { + let pipeline = if let Some(pipeline) = pipeline.as_ref() { pipeline.clone() } else { - let p = Arc::new(create_skybox_render_pipeline(&self.device)); + let p = Arc::new( + crate::skybox::create_skybox_render_pipeline( + &self.device, + crate::hdr::HdrSurface::TEXTURE_FORMAT, + ) + .0, + ); *pipeline = Some(p.clone()); p - } + }; + // UNWRAP: safe because we're only ever called from the render thread. + let mut bindgroup = self.skybox_bindgroup.lock().unwrap(); + let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() { + bindgroup.clone() + } else { + let slab = self.slab.read().unwrap(); + let bg = Arc::new(crate::skybox::create_skybox_bindgroup( + &self.device, + slab.as_ref().get_buffer(), + &self.skybox.read().unwrap().environment_cubemap, + )); + *bindgroup = Some(bg.clone()); + bg + }; + (pipeline, bindgroup) } /// Return the main render pipeline, creating it if necessary. @@ -358,10 +347,10 @@ impl Stage { fn create_stage_render_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { log::trace!("creating stage render pipeline"); let label = Some("stage render pipeline"); - let vertex_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/stage-new_stage_vertex.spv")); + let vertex_shader = + device.create_shader_module(wgpu::include_spirv!("linkage/stage-gltf_vertex.spv")); let fragment_shader = device - .create_shader_module(wgpu::include_spirv!("linkage/stage-stage_fragment.spv")); + .create_shader_module(wgpu::include_spirv!("linkage/stage-gltf_fragment.spv")); let stage_slab_buffers_layout = Stage::buffers_bindgroup_layout(device); let textures_layout = Stage::textures_bindgroup_layout(device); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { @@ -374,7 +363,7 @@ impl Stage { layout: Some(&layout), vertex: wgpu::VertexState { module: &vertex_shader, - entry_point: "stage::new_stage_vertex", + entry_point: "stage::gltf_vertex", buffers: &[], }, primitive: wgpu::PrimitiveState { @@ -400,19 +389,12 @@ impl Stage { }, fragment: Some(wgpu::FragmentState { module: &fragment_shader, - entry_point: "stage::stage_fragment", - targets: &[ - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - }), - ], + entry_point: "stage::gltf_fragment", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba16Float, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], }), multiview: None, }); @@ -431,34 +413,32 @@ impl Stage { } pub fn get_slab_buffers_bindgroup(&self) -> Arc { - fn create_slab_buffers_bindgroup( - device: &wgpu::Device, - pipeline: &wgpu::RenderPipeline, - stage_slab: &SlabBuffer, - ) -> wgpu::BindGroup { - let label = Some("stage slab buffer"); - let stage_slab_buffers_bindgroup = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label, - layout: &pipeline.get_bind_group_layout(0), - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: stage_slab.get_buffer().as_entire_binding(), - }], - }); - stage_slab_buffers_bindgroup - } - // UNWRAP: safe because we're only ever called from the render thread. let mut bindgroup = self.buffers_bindgroup.lock().unwrap(); if let Some(bindgroup) = bindgroup.as_ref() { bindgroup.clone() } else { - let b = Arc::new(create_slab_buffers_bindgroup( - &self.device, - &self.get_pipeline(), - &self.slab, - )); + let b = Arc::new({ + let device: &wgpu::Device = &self.device; + let pipeline: &wgpu::RenderPipeline = &self.get_pipeline(); + let label = Some("stage slab buffer"); + let stage_slab_buffers_bindgroup = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: self + .slab + .read() + .unwrap() + .as_ref() + .get_buffer() + .as_entire_binding(), + }], + }); + stage_slab_buffers_bindgroup + }); *bindgroup = Some(b.clone()); b } @@ -543,7 +523,7 @@ impl Stage { &self.get_pipeline(), // UNWRAP: if we can't acquire locks we want to panic &self.atlas.read().unwrap(), - &self.skybox.lock().unwrap(), + &self.skybox.read().unwrap(), )); *bindgroup = Some(b.clone()); b @@ -551,11 +531,12 @@ impl Stage { } /// Draw the [`RenderUnit`] each frame, and immediately return its `Id`. - pub fn draw_unit(&self, unit: &RenderUnit) -> Id { - let id = self.slab.append(&self.device, &self.queue, unit); + pub fn draw_unit(&mut self, unit: &RenderUnit) -> Id { + let id = self.append(unit); let draw = DrawUnit { id, vertex_count: unit.vertex_count, + visible: true, }; // UNWRAP: if we can't acquire the lock we want to panic. let mut draws = self.draws.write().unwrap(); @@ -567,8 +548,18 @@ impl Stage { id } + /// Erase the [`RenderUnit`] with the given `Id` from the stage. + pub fn erase_unit(&self, id: Id) { + let mut draws = self.draws.write().unwrap(); + match draws.deref_mut() { + StageDrawStrategy::Direct(units) => { + units.retain(|unit| unit.id != id); + } + } + } + /// Returns all the draw operations on the stage. - pub(crate) fn get_draws(&self) -> Vec { + pub fn get_draws(&self) -> Vec { // UNWRAP: if we can't acquire the lock we want to panic. let draws = self.draws.read().unwrap(); match draws.deref() { @@ -576,12 +567,30 @@ impl Stage { } } - /// Erase the [`RenderUnit`] with the given `Id` from the stage. - pub fn erase_unit(&self, id: Id) { + /// Show the [`RenderUnit`] with the given `Id` for rendering. + pub fn show_unit(&self, id: Id) { let mut draws = self.draws.write().unwrap(); match draws.deref_mut() { StageDrawStrategy::Direct(units) => { - units.retain(|unit| unit.id != id); + for unit in units.iter_mut() { + if unit.id == id { + unit.visible = true; + } + } + } + } + } + + /// Hide the [`RenderUnit`] with the given `Id` from rendering. + pub fn hide_unit(&self, id: Id) { + let mut draws = self.draws.write().unwrap(); + match draws.deref_mut() { + StageDrawStrategy::Direct(units) => { + for unit in units.iter_mut() { + if unit.id == id { + unit.visible = false; + } + } } } } @@ -592,8 +601,8 @@ impl Stage { use crate::{ frame::{copy_frame_to_post, create_frame, present}, graph::{graph, Graph}, - hdr::{clear_surface_hdr_and_depth, create_hdr_render_surface, hdr_surface_update}, - scene::tonemapping, + hdr::{clear_surface_hdr_and_depth, create_hdr_render_surface}, + tonemapping, }; let (hdr_surface,) = r.graph.visit(create_hdr_render_surface).unwrap().unwrap(); @@ -602,11 +611,7 @@ impl Stage { // pre-render r.graph - .add_subgraph(graph!( - create_frame, - clear_surface_hdr_and_depth, - hdr_surface_update - )) + .add_subgraph(graph!(create_frame, clear_surface_hdr_and_depth)) .add_barrier(); // render @@ -625,16 +630,63 @@ impl Stage { )); } } + + /// Read the atlas image from the GPU. + /// + /// This is primarily used for debugging. + /// + /// ## Panics + /// Panics if the pixels read from the GPU cannot be converted into an + /// `RgbaImage`. + pub fn read_atlas_image(&self) -> image::RgbaImage { + // UNWRAP: if we can't acquire the lock we want to panic. + self.atlas + .read() + .unwrap() + .atlas_img(&self.device, &self.queue) + } + + /// Read all the data from the stage. + /// + /// This blocks until the GPU buffer is mappable, and then copies the data + /// into a vector. + /// + /// This is primarily used for debugging. + pub fn read_slab(&self) -> Result, SlabError> { + // UNWRAP: if we can't acquire the lock we want to panic. + self.slab + .read() + .unwrap() + .as_ref() + .block_on_read_raw(0, self.len()) + } + + + pub fn new_skybox_from_path( + &self, + path: impl AsRef, + camera: Id, + ) -> Result { + let hdr = AtlasImage::from_hdr_path(path)?; + Ok(Skybox::new( + self.device.clone(), + self.queue.clone(), + hdr, + camera, + )) + } } /// A unit of work to be drawn. #[derive(Clone, Copy, Debug, Default)] -pub(crate) struct DrawUnit { +pub struct DrawUnit { pub id: Id, pub vertex_count: u32, + pub visible: bool, } -/// Provides a way to communicate with the stage about how you'd like your objects drawn. +/// Provides a way to communicate with the stage about how you'd like your +/// objects drawn. pub(crate) enum StageDrawStrategy { Direct(Vec), } @@ -642,22 +694,24 @@ pub(crate) enum StageDrawStrategy { /// Render the stage. pub fn stage_render( (stage, hdr_frame, depth): (ViewMut, View, View), -) -> Result<(BloomResult,), SlabError> { +) -> Result<(), SlabError> { let label = Some("stage render"); let pipeline = stage.get_pipeline(); let slab_buffers_bindgroup = stage.get_slab_buffers_bindgroup(); let textures_bindgroup = stage.get_textures_bindgroup(); - let may_skybox_pipeline = if stage.has_skybox.load(std::sync::atomic::Ordering::Relaxed) { - Some(stage.get_skybox_pipeline()) - } else { - None - }; - let mut may_bloom_filter = if stage.has_bloom.load(std::sync::atomic::Ordering::Relaxed) { - // UNWRAP: if we can't acquire the lock we want to panic. - Some(stage.bloom.write().unwrap()) + let has_skybox = stage.has_skybox.load(std::sync::atomic::Ordering::Relaxed); + let may_skybox_pipeline_and_bindgroup = if has_skybox { + Some(stage.get_skybox_pipeline_and_bindgroup()) } else { None }; + //let mut may_bloom_filter = if + // stage.has_bloom.load(std::sync::atomic::Ordering::Relaxed) { // UNWRAP: + // if we can't acquire the lock we want to panic. Some(stage.bloom. + // write().unwrap()) + //} else { + // None + //}; // UNWRAP: if we can't read we want to panic. let draws = stage.draws.read().unwrap(); @@ -683,93 +737,25 @@ pub fn stage_render( match draws.deref() { StageDrawStrategy::Direct(units) => { for unit in units { - render_pass.draw(0..unit.vertex_count, unit.id.inner()..unit.id.inner() + 1); + if unit.visible { + render_pass + .draw(0..unit.vertex_count, unit.id.inner()..unit.id.inner() + 1); + } } - } //render_pass.multi_draw_indirect(&indirect_buffer, 0, stage.number_of_indirect_draws()); + } /* render_pass.multi_draw_indirect(&indirect_buffer, 0, + * stage.number_of_indirect_draws()); */ } - if let Some(pipeline) = may_skybox_pipeline.as_ref() { + if let Some((pipeline, bindgroup)) = may_skybox_pipeline_and_bindgroup.as_ref() { + log::trace!("rendering skybox"); + // UNWRAP: if we can't acquire the lock we want to panic. + let skybox = stage.skybox.read().unwrap(); render_pass.set_pipeline(pipeline); - render_pass.set_bind_group(0, &textures_bindgroup, &[]); - render_pass.draw(0..36, 0..1); + render_pass.set_bind_group(0, bindgroup, &[]); + render_pass.draw(0..36, skybox.camera.inner()..skybox.camera.inner() + 1); } } stage.queue.submit(std::iter::once(encoder.finish())); - let bloom_result = BloomResult( - may_bloom_filter - .as_mut() - .map(|bloom| bloom.run(&stage.device, &stage.queue, &hdr_frame)), - ); - Ok((bloom_result,)) -} - -#[cfg(test)] -mod test { - use glam::Vec3; - - use crate::{ - default_ortho2d, - shader::stage::{Camera, NativeVertexData, RenderUnit, Vertex, VertexData}, - slab::Slab, - Renderling, - }; - - use super::*; - - fn right_tri_vertices() -> Vec { - vec![ - Vertex::default() - .with_position([0.0, 0.0, 0.5]) - .with_color([0.0, 1.0, 1.0, 1.0]), - Vertex::default() - .with_position([0.0, 100.0, 0.5]) - .with_color([1.0, 1.0, 0.0, 1.0]), - Vertex::default() - .with_position([100.0, 0.0, 0.5]) - .with_color([1.0, 0.0, 1.0, 1.0]), - ] - } - - #[test] - fn stage_new() { - let mut r = Renderling::headless(100, 100).with_background_color(glam::Vec4::splat(1.0)); - let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device.clone(), queue.clone()) - .with_lighting(true) - .with_bloom(true); - let (projection, view) = default_ortho2d(100.0, 100.0); - let camera = Camera { - projection, - view, - position: Vec3::ZERO, - }; - let camera_id = stage.append(&camera); - let vertices = stage.append_array(&right_tri_vertices()); - println!("vertices: {vertices:?}"); - let vertex_data_id = stage.append(&NativeVertexData { - vertices, - ..Default::default() - }); - let _ = stage.draw_unit(&RenderUnit { - camera: camera_id, - vertex_data: VertexData::Native(vertex_data_id), - vertex_count: 3, - ..Default::default() - }); - let stage_slab = futures_lite::future::block_on(stage.slab.read_raw( - &stage.device, - &stage.queue, - 0, - stage.slab.len(), - )) - .unwrap(); - assert_eq!(camera, stage_slab.read(camera_id)); - assert_eq!(right_tri_vertices(), stage_slab.read_vec(vertices)); - - stage.configure_graph(&mut r, true); - - let img = r.render_image().unwrap(); - img_diff::assert_img_eq("stage/stage_cmyk_tri.png", img); - } + Ok(()) } diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index a908e0b9..740a0f7a 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/stage/gltf_support.rs @@ -4,17 +4,20 @@ use crate::{ shader::{ gltf::*, pbr::PbrMaterial, - stage::{Camera, GltfVertexData, LightingModel, VertexData}, + stage::{Camera, LightingModel}, texture::{GpuTexture, TextureAddressMode, TextureModes}, }, - SceneImage, + AtlasImage, }; use glam::{Quat, Vec2, Vec3, Vec4}; -use renderling_shader::stage::{Transform, Vertex}; +use renderling_shader::stage::Vertex; use snafu::{OptionExt, ResultExt, Snafu}; #[derive(Debug, Snafu)] pub enum StageGltfError { + #[snafu(display("{source}"))] + Gltf { source: gltf::Error }, + #[snafu(display("{source}"))] Atlas { source: crate::atlas::AtlasError }, @@ -42,16 +45,25 @@ pub enum StageGltfError { #[snafu(display("Missing sampler"))] MissingSampler, + #[snafu(display("Missing gltf camera at index {index}"))] + MissingCamera { index: usize }, + #[snafu(display("{source}"))] - Slab { source: crate::slab::SlabError }, + Slab { source: crabslab::SlabError }, } -impl From for StageGltfError { - fn from(source: crate::slab::SlabError) -> Self { +impl From for StageGltfError { + fn from(source: crabslab::SlabError) -> Self { Self::Slab { source } } } +impl From for StageGltfError { + fn from(source: gltf::Error) -> Self { + Self::Gltf { source } + } +} + pub fn get_vertex_count(primitive: &gltf::Primitive<'_>) -> u32 { if let Some(indices) = primitive.indices() { let count = indices.count() as u32; @@ -69,15 +81,12 @@ pub fn get_vertex_count(primitive: &gltf::Primitive<'_>) -> u32 { } } -pub fn make_accessor(accessor: gltf::Accessor<'_>, buffers: &Array) -> GltfAccessor { +pub fn make_accessor(accessor: gltf::Accessor<'_>, views: &Array) -> GltfAccessor { let size = accessor.size() as u32; let buffer_view = accessor.view().unwrap(); - let view_buffer = buffer_view.buffer(); - let buffer_index = view_buffer.index(); - let buffer = buffers.at(buffer_index); + let offset = accessor.offset() as u32; let count = accessor.count() as u32; - let view_offset = buffer_view.offset() as u32; - let view_stride = buffer_view.stride().unwrap_or(0) as u32; + let view = views.at(buffer_view.index()); let component_type = match accessor.data_type() { gltf::accessor::DataType::I8 => DataType::I8, gltf::accessor::DataType::U8 => DataType::U8, @@ -98,10 +107,9 @@ pub fn make_accessor(accessor: gltf::Accessor<'_>, buffers: &Array) let normalized = accessor.normalized(); GltfAccessor { size, + view, count, - buffer, - view_offset, - view_stride, + offset, data_type: component_type, dimensions, normalized, @@ -109,8 +117,17 @@ pub fn make_accessor(accessor: gltf::Accessor<'_>, buffers: &Array) } impl Stage { + pub fn load_gltf_document_from_path( + &mut self, + path: impl AsRef, + ) -> Result<(gltf::Document, GltfDocument), StageGltfError> { + let (document, buffers, images) = gltf::import(path)?; + let gpu_doc = self.load_gltf_document(&document, buffers, images)?; + Ok((document, gpu_doc)) + } + pub fn load_gltf_document( - &self, + &mut self, document: &gltf::Document, buffer_data: Vec, images: Vec, @@ -118,16 +135,14 @@ impl Stage { log::trace!("Loading buffers into the GPU"); let buffers = self.allocate_array::(buffer_data.len()); for (i, buffer) in buffer_data.iter().enumerate() { - log::trace!(" Loading buffer {i} size: {} bytes", buffer.len()); let slice: &[u32] = bytemuck::cast_slice(&buffer); let buffer = self.append_array(slice); - self.write(buffers.at(i), &GltfBuffer(buffer))?; + self.write(buffers.at(i), &GltfBuffer(buffer)); } log::trace!("Loading views into the GPU"); let views = self.allocate_array(document.views().len()); - log::trace!(" reserved array: {views:?}"); - for (i, view) in document.views().enumerate() { + for view in document.views() { let buffer = buffers.at(view.buffer().index()); let offset = view.offset() as u32; let length = view.length() as u32; @@ -139,15 +154,18 @@ impl Stage { length, stride, }; - log::trace!(" view {i} id: {id:#?}"); - log::trace!(" writing view: {gltf_view:#?}"); - self.write(id, &gltf_view)?; + self.write(id, &gltf_view); } log::trace!("Loading accessors into the GPU"); let accessors = document .accessors() - .map(|accessor| make_accessor(accessor, &buffers)) + .enumerate() + .map(|(i, accessor)| { + let a = make_accessor(accessor, &views); + log::trace!(" accessor {i}: {a:#?}",); + a + }) .collect::>(); let accessors = self.append_array(&accessors); @@ -185,25 +203,26 @@ impl Stage { .collect::>(); let cameras = self.append_array(&cameras); - // We need the (re)packing of the atlas before we marshal the images into the GPU - // because we need their frames for textures and materials, but we need to know - // if the materials are require us to apply a linear transfer. So we'll get the - // preview repacking first, then update the frames in the textures. + // We need the (re)packing of the atlas before we marshal the images into the + // GPU because we need their frames for textures and materials, but we + // need to know if the materials require us to apply a linear transfer. + // So we'll get the preview repacking first, then update the frames in + // the textures. let (mut repacking, atlas_offset) = { // UNWRAP: if we can't lock the atlas, we want to panic. let atlas = self.atlas.read().unwrap(); let atlas_offset = atlas.rects.len(); ( atlas - .repack_preview(&self.device, images.into_iter().map(SceneImage::from)) + .repack_preview(&self.device, images.into_iter().map(AtlasImage::from)) .context(AtlasSnafu)?, atlas_offset, ) }; log::trace!("Creating GPU textures"); - let mut gpu_textures = vec![]; - for texture in document.textures() { + let textures = self.allocate_array::(document.textures().len()); + for (i, texture) in document.textures().enumerate() { let image_index = texture.source().index(); fn mode(mode: gltf::texture::WrappingMode) -> TextureAddressMode { @@ -225,24 +244,32 @@ impl Stage { index: image_index, offset: atlas_offset, })?; - gpu_textures.push(GpuTexture { + let texture = GpuTexture { offset_px, size_px, modes: TextureModes::default() .with_wrap_s(mode_s) .with_wrap_t(mode_t), atlas_index: (image_index + atlas_offset) as u32, - }); + }; + let texture_id = textures.at(i); + log::trace!(" texture {i} {texture_id:?}: {texture:#?}"); + self.write(texture_id, &texture); } - let gpu_textures = gpu_textures; - let textures = self.append_array(&gpu_textures); log::trace!("Creating materials"); - let mut gpu_materials = vec![]; + let mut default_material = Id::::NONE; + let materials = self.allocate_array::(document.materials().len()); for material in document.materials() { - let index = material.index(); + let material_id = if let Some(index) = material.index() { + materials.at(index) + } else { + // Allocate some extra space for this default material + default_material = self.allocate::(); + default_material + }; let name = material.name().map(String::from); - log::trace!("loading material {index:?} {name:?}"); + log::trace!("loading material {:?} {name:?}", material.index()); let pbr = material.pbr_metallic_roughness(); let material = if material.unlit() { log::trace!(" is unlit"); @@ -253,6 +280,7 @@ impl Stage { let tex_id = textures.at(index); // The index of the image in the original gltf document let image_index = texture.source().index(); + // Update the image to ensure it gets transferred correctly let image = repacking .get_mut(atlas_offset + image_index) .context(MissingImageSnafu { @@ -285,6 +313,7 @@ impl Stage { let index = texture.index(); let tex_id = textures.at(index); let image_index = texture.source().index(); + // Update the image to ensure it gets transferred correctly let image = repacking .get_mut(image_index + atlas_offset) .context(MissingImageSnafu { @@ -337,6 +366,7 @@ impl Stage { let index = texture.index(); let tex_id = textures.at(index); let image_index = texture.source().index(); + // Update the image to ensure it gets transferred correctly let image = repacking .get_mut(image_index + atlas_offset) .context(MissingImageSnafu { @@ -376,19 +406,26 @@ impl Stage { ..Default::default() } }; - gpu_materials.push(material); + log::trace!(" material {material_id:?}: {material:#?}",); + self.write(material_id, &material); } - let gpu_materials = gpu_materials; - let materials = self.append_array(&gpu_materials); - log::trace!("Packing the atlas"); - { + let number_of_new_images = repacking.new_images_len(); + if number_of_new_images > 0 { + log::trace!("Packing the atlas"); + log::trace!(" adding {number_of_new_images} new images",); // UNWRAP: if we can't lock the atlas, we want to panic. let mut atlas = self.atlas.write().unwrap(); let new_atlas = atlas .commit_repack_preview(&self.device, &self.queue, repacking) .context(AtlasSnafu)?; + let size = new_atlas.size; *atlas = new_atlas; + // The bindgroup will have to be remade + let _ = self.textures_bindgroup.lock().unwrap().take(); + // The atlas size must be reset + let size_id = StageLegend::offset_of_atlas_size().into(); + self.slab.write().unwrap().write(size_id, &size); } fn log_accessor(gltf_accessor: gltf::Accessor<'_>) { @@ -431,6 +468,35 @@ impl Stage { }) .unwrap_or_default(); + let mut indices_vec: Option> = None; + fn get_indices<'a>( + buffer_data: &[gltf::buffer::Data], + primitive: &gltf::Primitive<'_>, + indicies_vec: &'a mut Option>, + ) -> &'a Vec { + if indicies_vec.is_none() { + let reader = primitive.reader(|buffer| { + let data = buffer_data.get(buffer.index())?; + Some(data.0.as_slice()) + }); + let indices = reader + .read_indices() + .map(|is| is.into_u32().collect::>()) + .unwrap_or_else(|| { + let count = primitive + .get(&gltf::Semantic::Positions) + .map(|ps| ps.count()) + .unwrap_or_default() + as u32; + (0u32..count).collect::>() + }); + assert_eq!(indices.len() % 3, 0, "indices do not form triangles"); + *indicies_vec = Some(indices); + } + // UNWRAP: safe because we just set it to `Some` if previously `None` + indicies_vec.as_ref().unwrap() + } + // We may need the positions and uvs in-memory if we need // to generate normals or tangents, so we'll keep them in // a vec, if necessary, and access them through a function. @@ -438,6 +504,7 @@ impl Stage { fn get_positions<'a>( buffer_data: &[gltf::buffer::Data], primitive: &gltf::Primitive<'_>, + indices_vec: &'a mut Option>, position_vec: &'a mut Option>, ) -> &'a Vec { if position_vec.is_none() { @@ -445,52 +512,62 @@ impl Stage { let data = buffer_data.get(buffer.index())?; Some(data.0.as_slice()) }); - let positions = reader + let indices = get_indices(buffer_data, primitive, indices_vec); + let mut positions = reader .read_positions() .map(|ps| ps.map(Vec3::from).collect::>()) - .unwrap_or_default(); + .unwrap_or_else(|| vec![Vec3::ZERO; indices.len()]); + if positions.len() != indices.len() { + let mut new_positions = Vec::with_capacity(indices.len()); + for index in indices { + new_positions.push(positions[*index as usize]); + } + positions = new_positions; + } + assert_eq!( + positions.len() % 3, + 0, + "{} positions do not form triangles", + positions.len() + ); *position_vec = Some(positions); } // UNWRAP: safe because we just set it to `Some` if previously `None` position_vec.as_ref().unwrap() } - let mut positions_and_uv_vec: Option> = None; + let mut uv_vec: Option> = None; fn get_uvs<'a>( buffer_data: &[gltf::buffer::Data], primitive: &gltf::Primitive<'_>, - positions: &'a mut Option>, - positions_and_uv_vec: &'a mut Option>, - ) -> &'a Vec<(Vec3, Vec2)> { + indices: &'a mut Option>, + uv_vec: &'a mut Option>, + ) -> &'a Vec { // ensures we have position - if positions_and_uv_vec.is_none() { - let positions = get_positions(buffer_data, primitive, positions); + if uv_vec.is_none() { let reader = primitive.reader(|buffer| { let data = buffer_data.get(buffer.index())?; Some(data.0.as_slice()) }); - let puvs: Vec<(Vec3, Vec2)> = reader + let indices = get_indices(buffer_data, primitive, indices); + let mut uvs: Vec = reader .read_tex_coords(0) - .map(|uvs| { - positions - .iter() - .copied() - .zip(uvs.into_f32().map(Vec2::from)) - .collect() - }) - .unwrap_or_else(|| { - positions - .iter() - .copied() - .zip(std::iter::repeat(Vec2::ZERO)) - .collect() - }); - *positions_and_uv_vec = Some(puvs); + .map(|coords| coords.into_f32().map(Vec2::from).collect::>()) + .unwrap_or_else(|| vec![Vec2::ZERO; indices.len()]); + if uvs.len() != indices.len() { + let mut new_uvs = Vec::with_capacity(indices.len()); + for index in indices { + new_uvs.push(uvs[*index as usize]); + } + uvs = new_uvs; + } + *uv_vec = Some(uvs); } // UNWRAP: safe because we just set it to `Some` - positions_and_uv_vec.as_ref().unwrap() + uv_vec.as_ref().unwrap() } + let mut normals_were_generated = false; let normals = primitive .get(&gltf::Semantic::Normals) .map(|acc| { @@ -502,24 +579,35 @@ impl Stage { .unwrap_or_else(|| { log::trace!(" generating normals"); // Generate the normals - let normals = get_positions(&buffer_data, &primitive, &mut position_vec) - .chunks(3) - .flat_map(|chunk| match chunk { - [a, b, c] => { - let n = Vertex::generate_normal(*a, *b, *c); - [n, n, n] - } - _ => panic!("not triangles!"), - }) - .collect::>(); + normals_were_generated = true; + let normals = get_positions( + &buffer_data, + &primitive, + &mut indices_vec, + &mut position_vec, + ) + .chunks(3) + .flat_map(|chunk| match chunk { + [a, b, c] => { + let n = Vertex::generate_normal(*a, *b, *c); + [n, n, n] + } + _ => panic!("not triangles!"), + }) + .collect::>(); let normals_array = self.append_array(&normals); let buffer = GltfBuffer(normals_array.into_u32_array()); - let buffer_id = self.append(&buffer); + let buffer = self.append(&buffer); + let view = self.append(&GltfBufferView { + buffer, + offset: 0, + length: normals.len() as u32 * 3 * 4, // 3 components * 4 bytes each + stride: 12, + }); let accessor = GltfAccessor { size: 12, - buffer: buffer_id, - view_offset: 0, - view_stride: 12, + view, + offset: 0, count: normals.len() as u32, data_type: DataType::F32, dimensions: Dimensions::Vec3, @@ -527,6 +615,7 @@ impl Stage { }; self.append(&accessor) }); + let mut tangents_were_generated = false; let tangents = primitive .get(&gltf::Semantic::Tangents) .map(|acc| { @@ -537,12 +626,20 @@ impl Stage { }) .unwrap_or_else(|| { log::trace!(" generating tangents"); - let p_uvs = get_uvs( + tangents_were_generated = true; + let positions = get_positions( &buffer_data, &primitive, + &mut indices_vec, &mut position_vec, - &mut positions_and_uv_vec, - ); + ) + .clone(); + let uvs = get_uvs(&buffer_data, &primitive, &mut indices_vec, &mut uv_vec) + .clone(); + let p_uvs = positions + .into_iter() + .zip(uvs.into_iter().chain(std::iter::repeat(Vec2::ZERO).cycle())) + .collect::>(); let tangents = p_uvs .chunks(3) .flat_map(|chunk| match chunk { @@ -556,15 +653,20 @@ impl Stage { .collect::>(); let tangents_array = self.append_array(&tangents); let buffer = GltfBuffer(tangents_array.into_u32_array()); - let buffer_id = self.append(&buffer); + let buffer = self.append(&buffer); + let view = self.append(&GltfBufferView { + buffer, + offset: 0, + length: tangents.len() as u32 * 4 * 4, // 4 components * 4 bytes each + stride: 16, + }); let accessor = GltfAccessor { - size: 12, - buffer: buffer_id, - view_offset: 0, - view_stride: 12, + size: 16, + view, + offset: 0, count: tangents.len() as u32, data_type: DataType::F32, - dimensions: Dimensions::Vec3, + dimensions: Dimensions::Vec4, normalized: true, }; self.append(&accessor) @@ -622,7 +724,9 @@ impl Stage { indices, positions, normals, + normals_were_generated, tangents, + tangents_were_generated, colors, tex_coords0, tex_coords1, @@ -630,7 +734,7 @@ impl Stage { weights, }; log::trace!(" writing primitive {id:?}:\n{prim:#?}"); - self.write(id, &prim)?; + self.write(id, &prim); } let weights = mesh.weights().unwrap_or(&[]); let weights = self.append_array(weights); @@ -640,7 +744,7 @@ impl Stage { primitives, weights, }, - )?; + ); } log::trace!("Loading lights"); let lights_array = self.allocate_array::( @@ -674,7 +778,7 @@ impl Stage { intensity, kind, }, - )?; + ); } } let lights = lights_array; @@ -731,7 +835,7 @@ impl Stage { light, skin, }, - )?; + ); } log::trace!("Loading skins"); @@ -785,9 +889,12 @@ impl Stage { let mut stored_samplers = vec![]; for (i, sampler) in animation.samplers().enumerate() { let sampler = create_sampler(accessors, sampler); - self.write(samplers.at(i), &sampler)?; + self.write(samplers.at(i), &sampler); // Store it later so we can figure out the index of the sampler // used by the channel. + // + // TODO: Remove `stored_samplers` once `gltf` provides `.index()` + // @see https://github.com/gltf-rs/gltf/issues/398 stored_samplers.push(sampler); } let channels = self.allocate_array::(animation.channels().count()); @@ -809,12 +916,12 @@ impl Stage { .position(|s| s == &sampler) .context(MissingSamplerSnafu)?; let sampler = samplers.at(index); - self.write(channels.at(i), &GltfChannel { target, sampler })?; + self.write(channels.at(i), &GltfChannel { target, sampler }); } self.write( animations.at(animation.index()), &GltfAnimation { channels, samplers }, - )?; + ); } log::trace!("Loading scenes"); @@ -825,7 +932,7 @@ impl Stage { .map(|node| nodes.at(node.index())) .collect::>(); let nodes = self.append_array(&nodes); - self.write(scenes.at(scene.index()), &GltfScene { nodes })?; + self.write(scenes.at(scene.index()), &GltfScene { nodes }); } log::trace!("Done loading gltf"); @@ -836,6 +943,7 @@ impl Stage { buffers, cameras, materials, + default_material, meshes, nodes, scenes, @@ -845,37 +953,113 @@ impl Stage { }) } - // For now we have to keep the original document around to figure out - // what to draw. - fn draw_gltf_node_with<'a>( + /// Create a native camera for the gltf camera with the given index. + pub fn create_camera_from_gltf( &self, + cpu_doc: &gltf::Document, + index: usize, + ) -> Result { + let gltf_camera = cpu_doc + .cameras() + .nth(index) + .context(MissingCameraSnafu { index })?; + let projection = match gltf_camera.projection() { + gltf::camera::Projection::Orthographic(o) => glam::Mat4::orthographic_rh( + -o.xmag(), + o.xmag(), + -o.ymag(), + o.ymag(), + o.znear(), + o.zfar(), + ), + gltf::camera::Projection::Perspective(p) => { + let fovy = p.yfov(); + let aspect = p.aspect_ratio().unwrap_or(1.0); + if let Some(zfar) = p.zfar() { + glam::Mat4::perspective_rh(fovy, aspect, p.znear(), zfar) + } else { + glam::Mat4::perspective_infinite_rh( + p.yfov(), + p.aspect_ratio().unwrap_or(1.0), + p.znear(), + ) + } + } + }; + let view = cpu_doc + .nodes() + .find_map(|node| { + if node.camera().map(|c| c.index()) == Some(index) { + Some(glam::Mat4::from_cols_array_2d(&node.transform().matrix()).inverse()) + } else { + None + } + }) + .unwrap_or_default(); + Ok(Camera { + projection, + view, + ..Default::default() + }) + } + + /// Draw the given `gltf::Node` with the given `Camera`. + /// `parents` is a list of the parent nodes of the given node. + fn draw_gltf_node_with<'a>( + &mut self, gpu_doc: &GltfDocument, camera_id: Id, node: gltf::Node<'a>, parents: Vec>, ) -> Vec> { + if let Some(_light) = node.light() { + // TODO: Support transforming lights based on node transforms + ////let light = gpu_doc.lights.at(light.index()); + //let t = Mat4::from_cols_array_2d(&node.transform().matrix()); + //let position = t.transform_point3(Vec3::ZERO); + + //let light_index = light.index(); + //let color = Vec3::from(light.color()); + //let range = light.range().unwrap_or(f32::MAX); + //let intensity = light.intensity(); + //match light.kind() { + // gltf::khr_lights_punctual::Kind::Directional => + // GltfLightKind::Directional, + // gltf::khr_lights_punctual::Kind::Point => + // GltfLightKind::Point, + // gltf::khr_lights_punctual::Kind::Spot { + // inner_cone_angle, + // outer_cone_angle, + // } => GltfLightKind::Spot { + // inner_cone_angle, + // outer_cone_angle, + // }, + //}; + + //let transform = self.append(&transform); + //let render_unit = RenderUnit { + // light, + // transform, + // ..Default::default() + //}; + //return vec![self.draw_unit(&render_unit)]; + } let mut units = if let Some(mesh) = node.mesh() { + log::trace!("drawing mesh {}", mesh.index()); let primitives = mesh.primitives(); let mesh = gpu_doc.meshes.at(mesh.index()); + let mut node_path = parents.clone(); + node_path.push(gpu_doc.nodes.at(node.index())); primitives .map(|primitive| { - let parent_node_path = self.append_array(&parents); - let vertex_data_id = self.append(&GltfVertexData { - parent_node_path, - mesh, - primitive_index: primitive.index() as u32, - }); - let (t, r, s) = node.transform().decomposed(); - let transform = self.append(&Transform { - translation: Vec3::from(t), - rotation: Quat::from_array(r), - scale: Vec3::from(s), - }); + let node_path = self.append_array(&node_path); let render_unit = RenderUnit { - vertex_data: VertexData::Gltf(vertex_data_id), + node_path, + mesh_index: mesh.index() as u32, + primitive_index: primitive.index() as u32, vertex_count: super::get_vertex_count(&primitive), - transform, camera: camera_id, + ..Default::default() }; self.draw_unit(&render_unit) }) @@ -891,10 +1075,10 @@ impl Stage { units } - /// Draw the given [`gltf::Node`] using the given [`Camera`] and return the ids of the - /// render units that were created. + /// Draw the given [`gltf::Node`] using the given [`Camera`] and return the + /// ids of the render units that were created. pub fn draw_gltf_node( - &self, + &mut self, gpu_doc: &GltfDocument, camera_id: Id, node: gltf::Node<'_>, @@ -902,10 +1086,10 @@ impl Stage { self.draw_gltf_node_with(gpu_doc, camera_id, node, vec![]) } - /// Draw the given [`gltf::Scene`] using the given [`Camera`] and return the ids of the - /// render units that were created. + /// Draw the given [`gltf::Scene`] using the given [`Camera`] and return the + /// ids of the render units that were created. pub fn draw_gltf_scene( - &self, + &mut self, gpu_doc: &GltfDocument, camera_id: Id, scene: gltf::Scene<'_>, @@ -915,21 +1099,272 @@ impl Stage { .flat_map(|node| self.draw_gltf_node(gpu_doc, camera_id, node)) .collect() } + + /// Convenience method for creating a `GltfPrimitive` along with all its + /// `GltfAccessor`s, `GltfBufferView`s and a `GltfBuffer`. + /// + /// ## Note + /// This does **not** generate tangents or normals. + pub fn new_primitive( + &mut self, + vertices: impl IntoIterator, + indices: impl IntoIterator, + material: Id, + ) -> GltfPrimitive { + let vertices: Vec = vertices.into_iter().collect(); + let indices: Vec = indices.into_iter().collect(); + let vertex_count = vertices.len().max(indices.len()) as u32; + let indices = if indices.is_empty() { + Id::NONE + } else { + let buffer = GltfBuffer(self.append_array(&indices).into_u32_array()); + let buffer = self.append(&buffer); + let view = self.append(&GltfBufferView { + buffer, + offset: 0, + length: indices.len() as u32 * 4, // 4 bytes per u32 + stride: 4, // 4 bytes in a u32, + }); + let accessor = self.append(&GltfAccessor { + size: 4, + view, + offset: 0, + count: indices.len() as u32, + data_type: DataType::U32, + dimensions: Dimensions::Scalar, + normalized: false, + }); + accessor + }; + + let vertex_buffer = GltfBuffer(self.append_array(&vertices).into_u32_array()); + let buffer = self.append(&vertex_buffer); + let u32_stride = 4 // 4 position components, + + 4 // 4 color components, + + 4 // 4 uv components, + + 4 // 4 normal components, + + 4 // 4 tangent components, + + 4 // 4 joint components, + + 4; // 4 weight components + let stride = u32_stride * 4; // 4 bytes in a u32 + + let view = self.append(&GltfBufferView { + buffer, + offset: 0, + length: vertices.len() as u32 * u32_stride * 4, // stride as u32s * 4 bytes each + stride, + }); + + let positions = self.append(&GltfAccessor { + size: 3 * 4, // 3 position components * 4 bytes each + view, + offset: 0, + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec3, + normalized: false, + }); + + let colors = self.append(&GltfAccessor { + size: 4 * 4, // 4 color components * 4 bytes each + view, + offset: 4 * 4, // 3 + 1 position components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec4, + normalized: false, + }); + + let tex_coords0 = self.append(&GltfAccessor { + size: 2 * 4, // 2 uv components * 4 bytes each + view, + offset: 8 * 4, // (3 + 1) position + 4 color components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec2, + normalized: false, + }); + + let tex_coords1 = self.append(&GltfAccessor { + size: 2 * 4, // 2 uv components * 4 bytes each + view, + offset: 10 * 4, // (3 + 1) position + 4 color + 2 uv components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec2, + normalized: false, + }); + + let normals = self.append(&GltfAccessor { + size: 3 * 4, // 3 normal components * 4 bytes each + view, + offset: 12 * 4, // (3 + 1) position + 4 color + 4 uv components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec3, + normalized: false, + }); + + let tangents = self.append(&GltfAccessor { + size: 4 * 4, // 4 tangent components * 4 bytes each + view, + offset: 16 * 4, /* (3 + 1) position + 4 color + 4 uv + (3 + 1) normal components * 4 + * bytes each */ + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec4, + normalized: false, + }); + + let joints = self.append(&GltfAccessor { + size: 4 * 4, // 4 joint components * 4 bytes each + view, + offset: 20 * 4, // (3 + 1) position + 4 color + 4 uv + (3 + 1) normal + 4 tangent components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec4, + normalized: false, + }); + + let weights = self.append(&GltfAccessor { + size: 4 * 4, // 4 weight components * 4 bytes each + view, + offset: 24 * 4, // (3 + 1) position + 4 color + 4 uv + (3 + 1) normal + 4 tangent + 4 joint components * 4 bytes each + count: vertex_count as u32, + data_type: DataType::F32, + dimensions: Dimensions::Vec4, + normalized: false, + }); + + GltfPrimitive { + vertex_count, + material, + indices, + positions, + normals, + normals_were_generated: false, + tangents, + tangents_were_generated: false, + colors, + tex_coords0, + tex_coords1, + joints, + weights, + } + } + + /// Convenience method for creating a [`GltfMesh`] without having a + /// [`gltf::Document`]. + /// + /// This is useful if you have non-GLTF assets that you want to render. + pub fn new_mesh(&mut self) -> GltfMeshBuilder { + GltfMeshBuilder::new(self) + } +} + +/// Convenience builder for creating a [`GltfMesh`] without having a +/// [`gltf::Document`]. +/// +/// This is useful if you have non-GLTF assets that you want to render. +pub struct GltfMeshBuilder<'a> { + stage: &'a mut Stage, + primitives: Vec, +} + +impl<'a> GltfMeshBuilder<'a> { + pub fn new(stage: &'a mut Stage) -> Self { + Self { + stage, + primitives: vec![], + } + } + + pub fn add_primitive( + &mut self, + vertices: impl IntoIterator, + indices: impl IntoIterator, + material_id: Id, + ) { + let primitive = self.stage.new_primitive(vertices, indices, material_id); + self.primitives.push(primitive); + } + + pub fn with_primitive( + mut self, + vertices: impl IntoIterator, + indices: impl IntoIterator, + material_id: Id, + ) -> Self { + self.add_primitive(vertices, indices, material_id); + self + } + + pub fn build(self) -> GltfMesh { + let weights = Array::default(); + let primitives = self.stage.append_array(&self.primitives); + GltfMesh { + primitives, + weights, + } + } +} + +/// Convenience builder for creating a [`GltfDocument`] without having a +/// [`gltf::Document`]. +/// +/// This is useful if you have non-GLTF assets that you want to render. +pub struct GltfDocumentBuilder<'a> { + stage: &'a mut Stage, +} + +impl<'a> GltfDocumentBuilder<'a> { + pub fn new(stage: &'a mut Stage) -> Self { + Self { stage } + } + + pub fn build(self) -> GltfDocument { + let accessors = Array::default(); + let animations = Array::default(); + let buffers = Array::default(); + let cameras = Array::default(); + let materials = Array::default(); + let default_material = Id::NONE; + let meshes = Array::default(); + let nodes = Array::default(); + let scenes = Array::default(); + let skins = Array::default(); + let textures = Array::default(); + let views = Array::default(); + GltfDocument { + accessors, + animations, + buffers, + cameras, + materials, + default_material, + meshes, + nodes, + scenes, + skins, + textures, + views, + } + } } #[cfg(test)] mod test { - use glam::{Vec2, Vec3, Vec4}; - use crate::{ shader::{ - array::Array, gltf::*, - slab::Slab, - stage::{Camera, RenderUnit}, + pbr::PbrMaterial, + stage::{Camera, LightingModel, RenderUnit, Transform, Vertex}, }, - DrawUnit, Id, Renderling, Stage, + Renderling, Stage, }; + use crabslab::{Array, GrowableSlab, Id, Slab}; + use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; + use renderling_shader::stage::{GpuConstants, GpuEntity}; #[test] fn get_vertex_count_primitive_sanity() { @@ -961,23 +1396,31 @@ mod test { _ => panic!("bad chunk"), } } - let u32buffer = bytemuck::cast_slice::(&u16buffer).to_vec(); // + let u32buffer = bytemuck::cast_slice::(&u16buffer).to_vec(); for u in u32buffer.iter() { println!("{u:032b}"); } println!("u32buffer: {u32buffer:?}"); assert_eq!(2, u32buffer.len()); let mut data = [0u32; 256]; - let buffer_index = data.write_slice(&u32buffer, 0); + let buffer_index = data.write_indexed_slice(&u32buffer, 0); assert_eq!(2, buffer_index); let buffer = GltfBuffer(Array::new(0, buffer_index as u32)); - let _ = data.write(&buffer, buffer_index); + let view_index = data.write_indexed(&buffer, buffer_index); + let _ = data.write_indexed( + &GltfBufferView { + buffer: Id::from(buffer_index), + offset: 0, + length: 4 * 2, // 4 elements * 2 bytes each + stride: 2, + }, + view_index, + ); let accessor = GltfAccessor { size: 2, count: 3, - buffer: Id::from(buffer_index), - view_offset: 0, - view_stride: 0, + view: Id::from(view_index), + offset: 0, data_type: DataType::U16, dimensions: Dimensions::Scalar, normalized: false, @@ -1005,7 +1448,7 @@ mod test { let projection = crate::camera::perspective(100.0, 50.0); let position = Vec3::new(1.0, 0.5, 1.5); let view = crate::camera::look_at(position, Vec3::new(1.0, 0.5, 0.0), Vec3::Y); - let stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); + let mut stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); stage.configure_graph(&mut r, true); let gpu_doc = stage .load_gltf_document(&document, buffers.clone(), images) @@ -1021,125 +1464,6 @@ mod test { let unit_ids = stage.draw_gltf_scene(&gpu_doc, camera_id, default_scene); assert_eq!(2, unit_ids.len()); - let data = futures_lite::future::block_on(stage.slab.read_raw( - &device, - &queue, - 0, - stage.slab.len(), - )) - .unwrap(); - - #[allow(unused)] - #[derive(Debug, Default)] - struct VertexInvocation { - draw: DrawUnit, - instance_index: u32, - vertex_index: u32, - render_unit_id: Id, - render_unit: RenderUnit, - out_camera: u32, - out_material: u32, - out_color: Vec4, - out_uv0: Vec2, - out_uv1: Vec2, - out_norm: Vec3, - out_tangent: Vec3, - out_bitangent: Vec3, - out_pos: Vec3, - clip_pos: Vec4, - } - - let draws = stage.get_draws(); - let slab = &data; - - for i in 0..gpu_doc.accessors.len() { - let accessor = slab.read(gpu_doc.accessors.at(i)); - println!("accessor {i}: {accessor:#?}", i = i, accessor = accessor); - let buffer = slab.read(accessor.buffer); - println!("buffer: {buffer:#?}"); - let buffer_data = slab.read_vec(buffer.0); - println!("buffer_data: {buffer_data:#?}"); - } - - let indices = draws - .iter() - .map(|draw| { - let unit_id = draw.id; - let unit = slab.read(unit_id); - let vertex_data_id = match unit.vertex_data { - renderling_shader::stage::VertexData::Native(_) => panic!("should be gltf"), - renderling_shader::stage::VertexData::Gltf(id) => id, - }; - let vertex_data = slab.read(vertex_data_id); - let mesh = slab.read(vertex_data.mesh); - let primitive_id = mesh.primitives.at(vertex_data.primitive_index as usize); - let primitive = slab.read(primitive_id); - if primitive.indices.is_some() { - let indices_accessor = slab.read(primitive.indices); - (0..draw.vertex_count) - .map(|i| { - let index = indices_accessor.get_u32(i as usize, slab); - index - }) - .collect::>() - } else { - (0..draw.vertex_count).collect::>() - } - }) - .collect::>(); - assert_eq!([0, 1, 2], indices[0].as_slice()); - assert_eq!([0, 1, 2], indices[1].as_slice()); - - let invocations = draws - .into_iter() - .flat_map(|draw| { - let render_unit_id = draw.id; - let instance_index = render_unit_id.inner(); - let render_unit = data.read(render_unit_id); - let data = &data; - (0..draw.vertex_count).map(move |vertex_index| { - let mut invocation = VertexInvocation { - draw, - render_unit_id, - render_unit, - instance_index, - vertex_index, - ..Default::default() - }; - renderling_shader::stage::new_stage_vertex( - instance_index, - vertex_index, - data, - &mut invocation.out_camera, - &mut invocation.out_material, - &mut invocation.out_color, - &mut invocation.out_uv0, - &mut invocation.out_uv1, - &mut invocation.out_norm, - &mut invocation.out_tangent, - &mut invocation.out_bitangent, - &mut invocation.out_pos, - &mut invocation.clip_pos, - ); - invocation - }) - }) - .collect::>(); - let seen_positions = invocations - .iter() - .map(|inv| inv.out_pos) - .take(3) - .collect::>(); - let mesh = document.meshes().next().unwrap(); - let prim = mesh.primitives().next().unwrap(); - let expected_positions_reader = prim.reader(|buffer| Some(&buffers[buffer.index()])); - let expected_positions = expected_positions_reader - .read_positions() - .unwrap() - .map(|pos| Vec3::from(pos)) - .collect::>(); - assert_eq!(expected_positions, seen_positions); - let img = r.render_image().unwrap(); img_diff::assert_img_eq("gltf_simple_meshes.png", img); } @@ -1150,7 +1474,7 @@ mod test { let mut r = Renderling::headless(20, 20).with_background_color(Vec3::splat(0.0).extend(1.0)); let (device, queue) = r.get_device_and_queue_owned(); - let stage = Stage::new(device, queue).with_lighting(false); + let mut stage = Stage::new(device, queue).with_lighting(false); stage.configure_graph(&mut r, true); let (document, buffers, images) = gltf::import("../../gltf/gltfTutorial_003_MinimalGltfFile.gltf").unwrap(); @@ -1172,4 +1496,373 @@ mod test { let img = r.render_image().unwrap(); img_diff::assert_img_eq("gltf_minimal_mesh.png", img); } + + #[test] + // Test that the top-level transform on `RenderUnit` transforms their + // child primitive's geometry correctly. + fn render_unit_transforms_primitive_geometry() { + let mut r = Renderling::headless(50, 50).with_background_color(Vec4::splat(1.0)); + let mut stage = r.new_stage().with_lighting(false); + stage.configure_graph(&mut r, true); + let (projection, view) = crate::camera::default_ortho2d(50.0, 50.0); + let camera = stage.append(&Camera::new(projection, view)); + let cyan = [0.0, 1.0, 1.0, 1.0]; + let magenta = [1.0, 0.0, 1.0, 1.0]; + let yellow = [1.0, 1.0, 0.0, 1.0]; + let white = [1.0, 1.0, 1.0, 1.0]; + let vertices = [ + Vertex::default() + .with_position([0.0, 0.0, 0.0]) + .with_color(cyan), + Vertex::default() + .with_position([1.0, 0.0, 0.0]) + .with_color(magenta), + Vertex::default() + .with_position([1.0, 1.0, 0.0]) + .with_color(yellow), + Vertex::default() + .with_position([0.0, 1.0, 0.0]) + .with_color(white), + ]; + let primitive = stage.new_primitive(vertices, [0, 3, 2, 0, 2, 1], Id::NONE); + let primitives = stage.append_array(&[primitive]); + let mesh = stage.append(&GltfMesh { + primitives, + ..Default::default() + }); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + let transform = stage.append(&Transform { + scale: Vec3::new(50.0, 50.0, 1.0), + ..Default::default() + }); + let _unit_id = stage.draw_unit(&RenderUnit { + camera, + transform, + vertex_count: primitive.vertex_count, + node_path, + ..Default::default() + }); + let img = r.render_linear_image().unwrap(); + img_diff::assert_img_eq("gltf/render_unit_transforms_primitive_geometry.png", img); + } + + #[test] + // Tests importing a gltf file and rendering the first image as a 2d object. + // + // This ensures we are decoding images correctly. + fn gltf_images() { + let mut r = Renderling::headless(100, 100).with_background_color(Vec4::splat(1.0)); + let (device, queue) = r.get_device_and_queue_owned(); + let mut stage = Stage::new(device.clone(), queue.clone()).with_lighting(false); + stage.configure_graph(&mut r, true); + let (document, buffers, images) = gltf::import("../../gltf/cheetah_cone.glb").unwrap(); + let gpu_doc = stage + .load_gltf_document(&document, buffers, images) + .unwrap(); + let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0); + let camera_id = stage.append(&Camera::new(projection, view)); + assert!(!gpu_doc.textures.is_empty()); + let albedo_texture_id = gpu_doc.textures.at(0); + assert!(albedo_texture_id.is_some()); + let material_id = stage.append(&PbrMaterial { + albedo_texture: albedo_texture_id, + lighting_model: LightingModel::NO_LIGHTING, + ..Default::default() + }); + println!("material_id: {:#?}", material_id); + let mesh = stage + .new_mesh() + .with_primitive( + [ + Vertex::default() + .with_position([0.0, 0.0, 0.0]) + .with_uv0([0.0, 0.0]), + Vertex::default() + .with_position([1.0, 0.0, 0.0]) + .with_uv0([1.0, 0.0]), + Vertex::default() + .with_position([1.0, 1.0, 0.0]) + .with_uv0([1.0, 1.0]), + Vertex::default() + .with_position([0.0, 1.0, 0.0]) + .with_uv0([0.0, 1.0]), + ], + [0, 3, 2, 0, 2, 1], + material_id, + ) + .build(); + let mesh = stage.append(&mesh); + let node = stage.append(&GltfNode { + mesh, + ..Default::default() + }); + let node_path = stage.append_array(&[node]); + + let transform = stage.append(&Transform { + scale: Vec3::new(100.0, 100.0, 1.0), + ..Default::default() + }); + + let _unit_id = stage.draw_unit(&RenderUnit { + camera: camera_id, + transform, + vertex_count: 6, + node_path, + mesh_index: 0, + primitive_index: 0, + }); + + let img = r.render_linear_image().unwrap(); + img_diff::assert_img_eq("gltf_images.png", img); + } + + #[test] + fn simple_texture() { + let size = 100; + let mut r = + Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); + let (device, queue) = r.get_device_and_queue_owned(); + let mut stage = Stage::new(device.clone(), queue.clone()) + // There are no lights in the scene and the material isn't marked as "unlit", so + // let's force it to be unlit. + .with_lighting(false); + stage.configure_graph(&mut r, true); + let (cpu_doc, gpu_doc) = stage + .load_gltf_document_from_path("../../gltf/gltfTutorial_013_SimpleTexture.gltf") + .unwrap(); + + let projection = crate::camera::perspective(size as f32, size as f32); + let view = + crate::camera::look_at(Vec3::new(0.5, 0.5, 1.25), Vec3::new(0.5, 0.5, 0.0), Vec3::Y); + let camera = stage.append(&Camera::new(projection, view)); + let _unit_ids = stage.draw_gltf_scene(&gpu_doc, camera, cpu_doc.default_scene().unwrap()); + + let img = r.render_image().unwrap(); + img_diff::assert_img_eq("gltf_simple_texture.png", img); + } + + // This can be uncommented when we support lighting from GLTF files + //#[test] + //// Demonstrates how to load and render a gltf file containing lighting and a + //// normal map. + //fn normal_mapping_brick_sphere() { + // let size = 600; + // let mut r = + // Renderling::headless(size, + // size).with_background_color(Vec3::splat(1.0).extend(1.0)); let mut + // stage = r.new_stage().with_lighting(true).with_bloom(true); + // stage.configure_graph(&mut r, true); + // let (cpu_doc, gpu_doc) = stage + // .load_gltf_document_from_path("../../gltf/red_brick_03_1k.glb") + // .unwrap(); + // let camera = stage.create_camera_from_gltf(&cpu_doc, 0).unwrap(); + // let camera_id = stage.append(&camera); + // let _unit_ids = + // stage.draw_gltf_scene(&gpu_doc, camera_id, + // cpu_doc.default_scene().unwrap()); + + // let img = r.render_image().unwrap(); + // img_diff::assert_img_eq("gltf_normal_mapping_brick_sphere.png", img); + //} + + #[test] + // Demonstrates how to generate a mesh primitive on the CPU, long hand. + fn gltf_cmy_tri() { + let size = 100; + let mut r = + Renderling::headless(size, size).with_background_color(Vec3::splat(0.0).extend(1.0)); + let mut stage = r + .new_stage() + // There are no lights in the scene and the material isn't marked as "unlit", so + // let's force it to be unlit. + .with_lighting(false); + stage.configure_graph(&mut r, true); + + // buffers + let positions = + stage.append_array(&[[0.0, 0.0, 0.5], [0.0, 100.0, 0.5], [100.0, 0.0, 0.5]]); + let positions_buffer = GltfBuffer(positions.into_u32_array()); + let cyan = [0.0, 1.0, 1.0, 1.0]; + let magenta = [1.0, 0.0, 1.0, 1.0]; + let yellow = [1.0, 1.0, 0.0, 1.0]; + let colors = stage.append_array(&[cyan, magenta, yellow]); + let colors_buffer = GltfBuffer(colors.into_u32_array()); + let buffers = stage.append_array(&[positions_buffer, colors_buffer]); + + // views + let positions_view = GltfBufferView { + buffer: buffers.at(0), + offset: 0, + length: 3 * 3 * 4, // 3 vertices * 3 components * 4 bytes each + stride: 3 * 4, + }; + let colors_view = GltfBufferView { + buffer: buffers.at(1), + offset: 0, + length: 3 * 4 * 4, // 3 vertices * 4 components * 4 bytes each + stride: 4 * 4, + }; + let views = stage.append_array(&[positions_view, colors_view]); + + // accessors + let positions_accessor = GltfAccessor { + size: 3 * 4, // 3 components * 4 bytes each + view: views.at(0), + offset: 0, + count: 3, + data_type: DataType::F32, + dimensions: Dimensions::Vec3, + normalized: false, + }; + let colors_accessor = GltfAccessor { + size: 4 * 4, + view: views.at(1), + offset: 0, + count: 3, + data_type: DataType::F32, + dimensions: Dimensions::Vec4, + normalized: false, + }; + let accessors = stage.append_array(&[positions_accessor, colors_accessor]); + + // meshes + let primitive = GltfPrimitive { + vertex_count: 3, + positions: accessors.at(0), + colors: accessors.at(1), + ..Default::default() + }; + let primitives = stage.append_array(&[primitive]); + let mesh = GltfMesh { + primitives, + ..Default::default() + }; + let meshes = stage.append_array(&[mesh]); + + // nodes + let node = GltfNode { + mesh: meshes.at(0), + ..Default::default() + }; + let nodes = stage.append_array(&[node]); + + // doc + let _doc = stage.append(&GltfDocument { + accessors, + buffers, + meshes, + nodes, + ..Default::default() + }); + + // render unit + let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0); + let camera = stage.append(&Camera::new(projection, view)); + let node_path = stage.append_array(&[nodes.at(0)]); + let unit_id = stage.draw_unit(&RenderUnit { + camera, + node_path, + mesh_index: 0, + primitive_index: 0, + vertex_count: 3, + ..Default::default() + }); + + let data = stage.read_slab().unwrap(); + let invocation = VertexInvocation::invoke(unit_id.inner(), 0, &data); + println!("invoctaion: {invocation:#?}"); + + let img = r.render_linear_image().unwrap(); + img_diff::assert_img_eq("gltf/cmy_tri.png", img); + } + + /// A helper struct that contains all outputs of the vertex shader. + #[allow(unused)] + #[derive(Clone, Debug, Default, PartialEq)] + pub struct VertexInvocation { + pub instance_index: u32, + pub vertex_index: u32, + pub render_unit_id: Id, + pub render_unit: RenderUnit, + pub out_camera: u32, + pub out_material: u32, + pub out_color: Vec4, + pub out_uv0: Vec2, + pub out_uv1: Vec2, + pub out_norm: Vec3, + pub out_tangent: Vec3, + pub out_bitangent: Vec3, + pub out_pos: Vec3, + // output clip coordinates + pub clip_pos: Vec4, + // output normalized device coordinates + pub ndc_pos: Vec3, + } + + impl VertexInvocation { + #[allow(dead_code)] + pub fn invoke(instance_index: u32, vertex_index: u32, slab: &[u32]) -> Self { + let mut v = Self { + instance_index, + vertex_index, + ..Default::default() + }; + v.render_unit_id = Id::from(v.instance_index); + v.render_unit = slab.read(v.render_unit_id); + renderling_shader::stage::gltf_vertex( + v.instance_index, + v.vertex_index, + slab, + &mut v.out_camera, + &mut v.out_material, + &mut v.out_color, + &mut v.out_uv0, + &mut v.out_uv1, + &mut v.out_norm, + &mut v.out_tangent, + &mut v.out_bitangent, + &mut v.out_pos, + &mut v.clip_pos, + ); + v.ndc_pos = v.clip_pos.xyz() / v.clip_pos.w; + v + } + + #[allow(dead_code)] + pub fn invoke_legacy( + instance_index: u32, + vertex_index: u32, + constants: &GpuConstants, + vertices: &[Vertex], + entities: &[GpuEntity], + ) -> Self { + let mut v = Self { + instance_index, + vertex_index, + ..Default::default() + }; + renderling_shader::stage::main_vertex_scene( + v.instance_index, + v.vertex_index, + &constants, + &vertices, + &entities, + &mut v.out_material, + &mut v.out_color, + &mut v.out_uv0, + &mut v.out_uv1, + &mut v.out_norm, + &mut v.out_tangent, + &mut v.out_bitangent, + &mut v.out_pos, + &mut v.clip_pos, + ); + v.ndc_pos = v.clip_pos.xyz() / v.clip_pos.w; + v + } + } } diff --git a/crates/renderling/src/stage/light.rs b/crates/renderling/src/stage/light.rs deleted file mode 100644 index 5a6f57cc..00000000 --- a/crates/renderling/src/stage/light.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Light builders for the stage. -use glam::{Vec3, Vec4}; -use renderling_shader::id::Id; -use renderling_shader::stage::{GpuLight, LightType}; - -use crate::Stage; - -#[cfg(feature = "gltf")] -pub fn from_gltf_light_kind(kind: gltf::khr_lights_punctual::Kind) -> LightType { - match kind { - gltf::khr_lights_punctual::Kind::Directional => LightType::DIRECTIONAL_LIGHT, - gltf::khr_lights_punctual::Kind::Point => LightType::POINT_LIGHT, - gltf::khr_lights_punctual::Kind::Spot { .. } => LightType::SPOT_LIGHT, - } -} - -#[cfg(feature = "gltf")] -pub fn gltf_light_intensity_units(kind: gltf::khr_lights_punctual::Kind) -> &'static str { - match kind { - gltf::khr_lights_punctual::Kind::Directional => "lux (lm/m^2)", - // sr is "steradian" - _ => "candelas (lm/sr)", - } -} - -/// A builder for a spot light. -pub struct GpuSpotLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuSpotLightBuilder<'a> { - pub fn new(stage: &'a Stage) -> GpuSpotLightBuilder<'a> { - let inner = GpuLight { - light_type: LightType::SPOT_LIGHT, - ..Default::default() - }; - let white = Vec4::splat(1.0); - Self { inner, stage } - .with_cutoff(std::f32::consts::PI / 3.0, std::f32::consts::PI / 2.0) - .with_attenuation(1.0, 0.014, 0.007) - .with_direction(Vec3::new(0.0, -1.0, 0.0)) - .with_color(white) - .with_intensity(1.0) - } - - pub fn with_position(mut self, position: impl Into) -> Self { - self.inner.position = position.into().extend(1.0); - self - } - - pub fn with_direction(mut self, direction: impl Into) -> Self { - self.inner.direction = direction.into().extend(1.0); - self - } - - pub fn with_attenuation(mut self, constant: f32, linear: f32, quadratic: f32) -> Self { - self.inner.attenuation = Vec4::new(constant, linear, quadratic, 0.0); - self - } - - pub fn with_cutoff(mut self, inner: f32, outer: f32) -> Self { - self.inner.inner_cutoff = inner; - self.inner.outer_cutoff = outer; - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, i: f32) -> Self { - self.inner.intensity = i; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -} - -/// A builder for a directional light. -/// -/// Directional lights illuminate all geometry from a certain direction, -/// without attenuation. -/// -/// This is like the sun, or the moon. -pub struct GpuDirectionalLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuDirectionalLightBuilder<'a> { - pub fn new(stage: &'a Stage) -> GpuDirectionalLightBuilder<'a> { - let inner = GpuLight { - light_type: LightType::DIRECTIONAL_LIGHT, - ..Default::default() - }; - Self { inner, stage } - .with_direction(Vec3::new(0.0, -1.0, 0.0)) - .with_color(Vec4::splat(1.0)) - .with_intensity(1.0) - } - - pub fn with_direction(mut self, direction: impl Into) -> Self { - self.inner.direction = direction.into().extend(1.0); - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, intensity: f32) -> Self { - self.inner.intensity = intensity; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -} - -pub struct GpuPointLightBuilder<'a> { - inner: GpuLight, - stage: &'a Stage, -} - -impl<'a> GpuPointLightBuilder<'a> { - pub fn new(stage: &Stage) -> GpuPointLightBuilder<'_> { - let inner = GpuLight { - light_type: LightType::POINT_LIGHT, - ..Default::default() - }; - let white = Vec4::splat(1.0); - GpuPointLightBuilder { stage, inner } - .with_attenuation(1.0, 0.14, 0.07) - .with_color(white) - .with_intensity(1.0) - } - - pub fn with_position(mut self, position: impl Into) -> Self { - self.inner.position = position.into().extend(0.0); - self - } - - pub fn with_attenuation(mut self, constant: f32, linear: f32, quadratic: f32) -> Self { - self.inner.attenuation = Vec4::new(constant, linear, quadratic, 0.0); - self - } - - pub fn with_color(mut self, color: impl Into) -> Self { - self.inner.color = color.into(); - self - } - - pub fn with_intensity(mut self, i: f32) -> Self { - self.inner.intensity = i; - self - } - - pub fn build(self) -> Id { - self.stage.append(&self.inner) - } -} diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index 4be840df..785fe8a0 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -552,8 +552,8 @@ impl Texture { /// Generate `mipmap_levels - 1` mipmaps for the given texture. /// /// ## Note - /// Ensure that `self` only has one mip level. If not it will try to sample from - /// an empty mip. + /// Ensure that `self` only has one mip level. If not it will try to sample + /// from an empty mip. pub fn generate_mips( &mut self, device: &wgpu::Device, @@ -805,17 +805,17 @@ impl CopiedTextureBuffer { Ok(image::DynamicImage::from(img_buffer)) } - /// Convert the post render buffer into an internal-format [`SceneImage`]. + /// Convert the post render buffer into an internal-format [`AtlasImage`]. pub fn into_scene_image( self, device: &wgpu::Device, - ) -> Result { + ) -> Result { let pixels = self.pixels(device); - let img = crate::SceneImage { + let img = crate::AtlasImage { pixels, width: self.dimensions.width as u32, height: self.dimensions.height as u32, - format: crate::SceneImageFormat::from_wgpu_texture_format(self.format) + format: crate::AtlasImageFormat::from_wgpu_texture_format(self.format) .context(UnsupportedFormatSnafu)?, apply_linear_transfer: false, }; @@ -826,11 +826,13 @@ impl CopiedTextureBuffer { /// /// Ensures that the pixels are in a linear color space by applying the /// linear transfer if the texture this buffer was copied from was sRGB. - pub fn into_rgba(self, device: &wgpu::Device) -> Result { + pub fn into_linear_rgba(self, device: &wgpu::Device) -> Result { let format = self.format; let mut img_buffer = self.into_image::>(device)?.into_rgba8(); if format.is_srgb() { - log::trace!("converting applying linear transfer to srgb pixels"); + log::trace!( + "converting by applying linear transfer fn to srgb pixels (sRGB -> linear)" + ); // Convert back to linear img_buffer.pixels_mut().for_each(|p| { crate::color::linear_xfer_u8(&mut p.0[0]); @@ -842,6 +844,29 @@ impl CopiedTextureBuffer { Ok(img_buffer) } + + /// Convert the post render buffer into an RgbaImage. + /// + /// Ensures that the pixels are in a linear color space by applying the + /// linear transfer if the texture this buffer was copied from was sRGB. + pub fn into_srgba(self, device: &wgpu::Device) -> Result { + let format = self.format; + let mut img_buffer = self.into_image::>(device)?.into_rgba8(); + if !format.is_srgb() { + log::trace!( + "converting by applying opto transfer fn to linear pixels (linear -> sRGB)" + ); + // Convert back to linear + img_buffer.pixels_mut().for_each(|p| { + crate::color::opto_xfer_u8(&mut p.0[0]); + crate::color::opto_xfer_u8(&mut p.0[1]); + crate::color::opto_xfer_u8(&mut p.0[2]); + crate::color::opto_xfer_u8(&mut p.0[3]); + }); + } + + Ok(img_buffer) + } } #[cfg(test)] diff --git a/crates/renderling/src/tonemapping.rs b/crates/renderling/src/tonemapping.rs new file mode 100644 index 00000000..f246a16c --- /dev/null +++ b/crates/renderling/src/tonemapping.rs @@ -0,0 +1,37 @@ +use moongraph::{GraphError, Move, View}; + +use crate::{frame::FrameTextureView, Device, HdrSurface, Queue}; + +/// Conducts the HDR tone mapping, writing the HDR surface texture to the (most +/// likely) sRGB window surface. +pub fn tonemapping( + (device, queue, frame, hdr_frame): ( + View, + View, + View, + View, + ), +) -> Result<(), GraphError> { + log::trace!("tonemapping"); + let label = Some("tonemapping"); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + render_pass.set_pipeline(&hdr_frame.tonemapping_pipeline); + render_pass.set_bind_group(0, &hdr_frame.bindgroup, &[]); + render_pass.draw(0..6, 0..1); + drop(render_pass); + + queue.submit(std::iter::once(encoder.finish())); + Ok(()) +} diff --git a/crates/renderling/src/tutorial.rs b/crates/renderling/src/tutorial.rs index 61bbfb73..7e4a980f 100644 --- a/crates/renderling/src/tutorial.rs +++ b/crates/renderling/src/tutorial.rs @@ -530,7 +530,7 @@ mod test { }, ); let unit = RenderUnit { - vertex_data: VertexData::Native(vertex_data_id), + vertex_data: VertexData::new_native(vertex_data_id), ..Default::default() }; let unit_id = slab.append(&device, &queue, &unit); @@ -760,7 +760,7 @@ mod test { }, ); let unit = RenderUnit { - vertex_data: renderling_shader::stage::VertexData::Native(vertex_data_id), + vertex_data: renderling_shader::stage::VertexData::new_native(vertex_data_id), camera: camera_id, transform: transform_id, vertex_count: vertices.len() as u32, diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index f1d94df8..3039179f 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -7,6 +7,7 @@ use std::{ sync::Arc, }; +use crabslab::{CpuSlab, Slab, SlabItem, WgpuBuffer}; use glam::{UVec2, Vec2, Vec4}; use snafu::prelude::*; @@ -345,7 +346,8 @@ impl<'a> UiDrawObjectBuilder<'a> { pub struct UiScene { device: Arc, - constants: Uniform, + /// Slab containing `UiConstants`. + slab: CpuSlab, _default_texture: Texture, default_texture_bindgroup: wgpu::BindGroup, } @@ -353,22 +355,26 @@ pub struct UiScene { impl UiScene { pub fn new( device: Arc, - queue: &wgpu::Queue, + queue: Arc, canvas_size: UVec2, camera_translation: Vec2, ) -> Self { - let constants = Uniform::new( - &device, - UiConstants { + let buffer = WgpuBuffer::new( + device.clone(), + UiConstants::slab_size(), + wgpu::BufferUsages::empty(), + ); + let mut slab = CpuSlab::new(buffer); + slab.write( + 0u32.into(), + &UiConstants { canvas_size, camera_translation, }, - wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - wgpu::ShaderStages::VERTEX, ); let texture = Texture::new( &device, - queue, + &queue, Some("UiScene.default_texture"), None, 4, diff --git a/shaders/Cargo.toml b/shaders/Cargo.toml index 551b2b4e..498ec4d8 100644 --- a/shaders/Cargo.toml +++ b/shaders/Cargo.toml @@ -12,10 +12,17 @@ env_logger = "^0.10" log = "^0.4" spirv-builder = "^0.9" -# Compile build-dependencies in release mode with -# the same settings as regular dependencies. +# Enable incremental by default in release mode. +[profile.release] +incremental = true +# HACK(eddyb) this is the default but without explicitly specifying it, Cargo +# will treat the identical settings in `[profile.release.build-override]` below +# as different sets of `rustc` flags and will not reuse artifacts between them. +codegen-units = 256 + +# Compile build-dependencies in release mode with the same settings +# as regular dependencies (including the incremental enabled above). [profile.release.build-override] opt-level = 3 -codegen-units = 16 -[profile.dev.build-override] -opt-level = 3 \ No newline at end of file +incremental = true +codegen-units = 256 \ No newline at end of file diff --git a/shaders/shader-crate/Cargo.toml b/shaders/shader-crate/Cargo.toml deleted file mode 100644 index 5173cb9a..00000000 --- a/shaders/shader-crate/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "shader-crate" -version = "0.1.0" -authors = ["Schell Carl Scivally "] -edition = "2021" -license = "MIT OR Apache-2.0" -keywords = ["game", "graphics", "shader", "rendering"] -categories = ["rendering", "game-development", "graphics"] -description = "Shaders for the renderling project" - -[lib] -crate-type = ["dylib"] - -[dependencies] -renderling-shader = { path = "../../crates/renderling-shader" } -spirv-std = "0.9" -glam = { version = "0.24.2", default-features = false, features = ["libm"]} diff --git a/shaders/shader-crate/src/lib.rs b/shaders/shader-crate/src/lib.rs deleted file mode 100644 index 0fb9a7a2..00000000 --- a/shaders/shader-crate/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Shader entry points. -#![no_std] -#![feature(lang_items)] -pub use renderling_shader::*; diff --git a/shaders/src/main.rs b/shaders/src/main.rs index e30416ee..aaea227d 100644 --- a/shaders/src/main.rs +++ b/shaders/src/main.rs @@ -10,6 +10,10 @@ struct Cli { #[clap(short, action = clap::ArgAction::Count)] verbosity: u8, + /// Directory containing the shader crate to compile. + #[clap(long, short, default_value = "renderling-shader")] + shader_crate: std::path::PathBuf, + /// Path to the output directory for the compiled shaders. #[clap(long, short, default_value = "../crates/renderling/src/linkage")] output_dir: std::path::PathBuf, @@ -18,6 +22,7 @@ struct Cli { fn main() -> Result<(), Box> { let Cli { verbosity, + shader_crate, output_dir, } = Cli::parse(); let level = match verbosity { @@ -33,10 +38,18 @@ fn main() -> Result<(), Box> { std::fs::create_dir_all(&output_dir).unwrap(); + let shader_crate = std::path::Path::new("../crates/").join(shader_crate); + assert!( + shader_crate.exists(), + "shader crate '{}' does not exist. Current dir is {}", + shader_crate.display(), + std::env::current_dir().unwrap().display() + ); + let CompileResult { entry_points, module, - } = SpirvBuilder::new("shader-crate", "spirv-unknown-vulkan1.2") + } = SpirvBuilder::new(shader_crate, "spirv-unknown-vulkan1.2") .print_metadata(MetadataPrintout::None) .multimodule(true) .build()?; diff --git a/test_img/negative_uv_wrapping.png b/test_img/atlas/negative_uv_wrapping.png similarity index 100% rename from test_img/negative_uv_wrapping.png rename to test_img/atlas/negative_uv_wrapping.png diff --git a/test_img/atlas_uv_mapping.png b/test_img/atlas/uv_mapping.png similarity index 100% rename from test_img/atlas_uv_mapping.png rename to test_img/atlas/uv_mapping.png diff --git a/test_img/uv_wrapping.png b/test_img/atlas/uv_wrapping.png similarity index 100% rename from test_img/uv_wrapping.png rename to test_img/atlas/uv_wrapping.png diff --git a/test_img/cmy_cube/remesh_after.png b/test_img/cmy_cube/remesh_after.png new file mode 100644 index 00000000..a79ecaae Binary files /dev/null and b/test_img/cmy_cube/remesh_after.png differ diff --git a/test_img/cmy_cube/remesh_before.png b/test_img/cmy_cube/remesh_before.png new file mode 100644 index 00000000..3e43585c Binary files /dev/null and b/test_img/cmy_cube/remesh_before.png differ diff --git a/test_img/cmy_cube.png b/test_img/cmy_cube/sanity.png similarity index 100% rename from test_img/cmy_cube.png rename to test_img/cmy_cube/sanity.png diff --git a/test_img/cmy_cube_visible_after.png b/test_img/cmy_cube/visible_after.png similarity index 100% rename from test_img/cmy_cube_visible_after.png rename to test_img/cmy_cube/visible_after.png diff --git a/test_img/cmy_cube_visible_before.png b/test_img/cmy_cube/visible_before.png similarity index 100% rename from test_img/cmy_cube_visible_before.png rename to test_img/cmy_cube/visible_before.png diff --git a/test_img/cmy_cube_remesh_after.png b/test_img/cmy_cube_remesh_after.png deleted file mode 100644 index 7ee6d1cc..00000000 Binary files a/test_img/cmy_cube_remesh_after.png and /dev/null differ diff --git a/test_img/cmy_cube_remesh_before.png b/test_img/cmy_cube_remesh_before.png deleted file mode 100644 index c4c23d03..00000000 Binary files a/test_img/cmy_cube_remesh_before.png and /dev/null differ diff --git a/test_img/gltf/cmy_tri.png b/test_img/gltf/cmy_tri.png new file mode 100644 index 00000000..52fa9cd9 Binary files /dev/null and b/test_img/gltf/cmy_tri.png differ diff --git a/test_img/gltf/render_unit_transforms_primitive_geometry.png b/test_img/gltf/render_unit_transforms_primitive_geometry.png new file mode 100644 index 00000000..8e9217b8 Binary files /dev/null and b/test_img/gltf/render_unit_transforms_primitive_geometry.png differ diff --git a/test_img/gltf_images.png b/test_img/gltf_images.png index f70bae09..6795f425 100644 Binary files a/test_img/gltf_images.png and b/test_img/gltf_images.png differ diff --git a/test_img/gpu_scene_sanity.png b/test_img/gpu_scene_sanity.png deleted file mode 100644 index 3fb5a730..00000000 Binary files a/test_img/gpu_scene_sanity.png and /dev/null differ diff --git a/test_img/pbr/metallic_roughness_spheres.png b/test_img/pbr/metallic_roughness_spheres.png new file mode 100644 index 00000000..b2fa23b3 Binary files /dev/null and b/test_img/pbr/metallic_roughness_spheres.png differ diff --git a/test_img/pbr_metallic_roughness_spheres.png b/test_img/pbr_metallic_roughness_spheres.png deleted file mode 100644 index 7949927e..00000000 Binary files a/test_img/pbr_metallic_roughness_spheres.png and /dev/null differ