├── Justfile ├── _readme ├── demo-v11.jpg ├── demo-v16.jpg ├── demo-v17.jpg └── demo-v9.jpg ├── bevy-strolle ├── assets │ ├── demo.zip │ └── cornell.zip ├── src │ ├── event.rs │ ├── utils.rs │ ├── camera.rs │ ├── sun.rs │ ├── rendering_node.rs │ ├── graph.rs │ ├── stages.rs │ ├── lib.rs │ └── state.rs ├── Cargo.toml └── examples │ └── cornell.rs ├── rustfmt.toml ├── strolle ├── assets │ ├── blue-noise.txt │ └── blue-noise.png ├── src │ ├── buffers │ │ ├── utils.rs │ │ ├── bindable.rs │ │ ├── bufferable.rs │ │ ├── storage_buffer.rs │ │ ├── double_buffered.rs │ │ ├── mapped_uniform_buffer.rs │ │ └── bind_group.rs │ ├── utils.rs │ ├── sun.rs │ ├── mesh.rs │ ├── buffers.rs │ ├── instance.rs │ ├── meshes.rs │ ├── utils │ │ ├── metrics.rs │ │ ├── axis.rs │ │ ├── bounding_box.rs │ │ └── allocator.rs │ ├── triangle.rs │ ├── image.rs │ ├── camera_controllers.rs │ ├── camera_controller │ │ ├── passes │ │ │ ├── bvh_heatmap.rs │ │ │ ├── frame_reprojection.rs │ │ │ ├── gi_reprojection.rs │ │ │ ├── gi_temporal_resampling.rs │ │ │ ├── di_sampling.rs │ │ │ ├── ref_tracing.rs │ │ │ ├── di_temporal_resampling.rs │ │ │ ├── gi_resolving.rs │ │ │ ├── di_resolving.rs │ │ │ ├── ref_shading.rs │ │ │ ├── gi_preview_resampling.rs │ │ │ ├── gi_sampling.rs │ │ │ ├── gi_spatial_resampling.rs │ │ │ ├── di_spatial_resampling.rs │ │ │ └── atmosphere.rs │ │ ├── passes.rs │ │ └── pass.rs │ ├── bvh │ │ ├── nodes.rs │ │ ├── primitives.rs │ │ ├── node.rs │ │ ├── primitive.rs │ │ └── serializer.rs │ ├── noise.rs │ ├── light.rs │ ├── bvh.rs │ ├── shaders.rs │ ├── mesh_triangle.rs │ ├── material.rs │ └── materials.rs ├── Cargo.toml └── build.rs ├── strolle-gpu ├── src │ ├── noise.rs │ ├── bvh_view.rs │ ├── materials.rs │ ├── triangles.rs │ ├── frame.rs │ ├── utils │ │ ├── f32_ext.rs │ │ ├── u32_ext.rs │ │ ├── vec2_ext.rs │ │ ├── vec3_ext.rs │ │ └── bilinear_filter.rs │ ├── lights.rs │ ├── noise │ │ ├── blue.rs │ │ └── white.rs │ ├── world.rs │ ├── normal.rs │ ├── utils.rs │ ├── surface.rs │ ├── reservoir.rs │ ├── reservoir │ │ └── ephemeral.rs │ ├── lib.rs │ ├── reprojection.rs │ ├── triangle.rs │ ├── hit.rs │ ├── passes.rs │ └── material.rs └── Cargo.toml ├── rust-toolchain.toml ├── .gitignore ├── strolle-shader-builder ├── Cargo.toml └── src │ └── main.rs ├── strolle-shaders ├── Cargo.toml └── src │ ├── lib.rs │ ├── atmosphere │ ├── generate_transmittance_lut.rs │ └── utils.rs │ ├── gi_reprojection.rs │ ├── ref_tracing.rs │ ├── gi_resolving.rs │ ├── bvh_heatmap.rs │ ├── frame_composition.rs │ ├── di_sampling.rs │ ├── frame_reprojection.rs │ ├── atmosphere.rs │ ├── di_temporal_resampling.rs │ ├── gi_sampling_a.rs │ ├── di_resolving.rs │ └── prim_raster.rs ├── .envrc ├── shell.nix ├── Cargo.toml ├── LICENSE └── README.md /Justfile: -------------------------------------------------------------------------------- 1 | run: 2 | cargo run --release --example demo 3 | -------------------------------------------------------------------------------- /_readme/demo-v11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/_readme/demo-v11.jpg -------------------------------------------------------------------------------- /_readme/demo-v16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/_readme/demo-v16.jpg -------------------------------------------------------------------------------- /_readme/demo-v17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/_readme/demo-v17.jpg -------------------------------------------------------------------------------- /_readme/demo-v9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/_readme/demo-v9.jpg -------------------------------------------------------------------------------- /bevy-strolle/assets/demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/bevy-strolle/assets/demo.zip -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | imports_granularity = "Module" 3 | group_imports = "StdExternalCrate" 4 | -------------------------------------------------------------------------------- /strolle/assets/blue-noise.txt: -------------------------------------------------------------------------------- 1 | Thanks to http://momentsingraphics.de/BlueNoise.html (by Christoph Peters) 2 | -------------------------------------------------------------------------------- /strolle/src/buffers/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn pad_size(size: usize) -> usize { 2 | (size + 31) & !31 3 | } 4 | -------------------------------------------------------------------------------- /strolle-gpu/src/noise.rs: -------------------------------------------------------------------------------- 1 | mod blue; 2 | mod white; 3 | 4 | pub use self::blue::*; 5 | pub use self::white::*; 6 | -------------------------------------------------------------------------------- /strolle/assets/blue-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/strolle/assets/blue-noise.png -------------------------------------------------------------------------------- /bevy-strolle/assets/cornell.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patryk27/strolle/HEAD/bevy-strolle/assets/cornell.zip -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-11-22" 3 | components = ["rust-src", "rustc-dev", "llvm-tools"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /bevy-strolle/assets/cornell 3 | /bevy-strolle/assets/demo 4 | /bevy-strolle/examples/bench-* 5 | /bevy-strolle/examples/scene* 6 | -------------------------------------------------------------------------------- /bevy-strolle/src/event.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | #[derive(Debug, Event)] 4 | pub enum StrolleEvent { 5 | MarkImageAsDynamic { id: AssetId }, 6 | } 7 | -------------------------------------------------------------------------------- /strolle-shader-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strolle-shader-builder" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | spirv-builder.workspace = true 8 | -------------------------------------------------------------------------------- /strolle/src/utils.rs: -------------------------------------------------------------------------------- 1 | mod allocator; 2 | mod axis; 3 | mod bounding_box; 4 | mod metrics; 5 | 6 | pub use self::allocator::*; 7 | pub use self::axis::*; 8 | pub use self::bounding_box::*; 9 | pub use self::metrics::*; 10 | -------------------------------------------------------------------------------- /strolle-shaders/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strolle-shaders" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [lib] 10 | crate-type = ["dylib"] 11 | 12 | [dependencies] 13 | spirv-std.workspace = true 14 | strolle-gpu = { path = "../strolle-gpu" } 15 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # the shebang is ignored, but nice for editors 3 | 4 | if type -P lorri &>/dev/null; then 5 | eval "$(lorri direnv)" 6 | else 7 | echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]' 8 | use nix 9 | fi 10 | -------------------------------------------------------------------------------- /strolle/src/sun.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq)] 2 | pub struct Sun { 3 | pub azimuth: f32, 4 | pub altitude: f32, 5 | } 6 | 7 | impl Default for Sun { 8 | fn default() -> Self { 9 | Self { 10 | azimuth: 0.0, 11 | altitude: 0.35, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bevy-strolle/src/utils.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::{vec3, vec4}; 2 | use bevy::prelude::*; 3 | 4 | pub fn color_to_vec3(color: Color) -> Vec3 { 5 | let [r, g, b, _] = color.as_linear_rgba_f32(); 6 | 7 | vec3(r, g, b) 8 | } 9 | 10 | pub fn color_to_vec4(color: Color) -> Vec4 { 11 | let [r, g, b, a] = color.as_linear_rgba_f32(); 12 | 13 | vec4(r, g, b, a) 14 | } 15 | -------------------------------------------------------------------------------- /strolle/src/mesh.rs: -------------------------------------------------------------------------------- 1 | use crate::MeshTriangle; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Mesh { 5 | triangles: Vec, 6 | } 7 | 8 | impl Mesh { 9 | pub fn new(triangles: Vec) -> Self { 10 | Self { triangles } 11 | } 12 | 13 | pub(crate) fn triangles(&self) -> &[MeshTriangle] { 14 | &self.triangles 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /strolle-gpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strolle-gpu" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [dependencies] 10 | bytemuck = { workspace = true, features = ["derive", "min_const_generics"] } 11 | glam = { workspace = true, default-features = false, features = ["bytemuck"] } 12 | spirv-std.workspace = true 13 | 14 | [dev-dependencies] 15 | approx = "0.5.1" 16 | -------------------------------------------------------------------------------- /bevy-strolle/src/camera.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::Component; 2 | use strolle as st; 3 | 4 | /// Extends Bevy's camera with extra features supported by Strolle. 5 | /// 6 | /// This is a component that can be attached into Bevy's `Camera`; when not 7 | /// attached, the default configuration is used. 8 | #[derive(Clone, Debug, Default, Component)] 9 | pub struct StrolleCamera { 10 | pub mode: st::CameraMode, 11 | } 12 | -------------------------------------------------------------------------------- /strolle-gpu/src/bvh_view.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec4; 2 | use spirv_std::arch::IndexUnchecked; 3 | 4 | #[derive(Clone, Copy)] 5 | pub struct BvhView<'a> { 6 | buffer: &'a [Vec4], 7 | } 8 | 9 | impl<'a> BvhView<'a> { 10 | pub fn new(buffer: &'a [Vec4]) -> Self { 11 | Self { buffer } 12 | } 13 | 14 | pub fn get(self, ptr: u32) -> Vec4 { 15 | unsafe { *self.buffer.index_unchecked(ptr as usize) } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | 5 | with pkgs; 6 | 7 | mkShell rec { 8 | nativeBuildInputs = [ 9 | pkg-config 10 | ]; 11 | 12 | buildInputs = [ 13 | alsa-lib 14 | libxkbcommon 15 | udev 16 | vulkan-loader 17 | wayland 18 | xorg.libX11 19 | xorg.libXcursor 20 | xorg.libXi 21 | xorg.libXrandr 22 | ]; 23 | 24 | LD_LIBRARY_PATH = lib.makeLibraryPath (buildInputs ++ [ stdenv.cc.cc.lib ]); 25 | } 26 | -------------------------------------------------------------------------------- /strolle-gpu/src/materials.rs: -------------------------------------------------------------------------------- 1 | use spirv_std::arch::IndexUnchecked; 2 | 3 | use crate::{Material, MaterialId}; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct MaterialsView<'a> { 7 | items: &'a [Material], 8 | } 9 | 10 | impl<'a> MaterialsView<'a> { 11 | pub fn new(items: &'a [Material]) -> Self { 12 | Self { items } 13 | } 14 | 15 | pub fn get(self, id: MaterialId) -> Material { 16 | unsafe { *self.items.index_unchecked(id.get() as usize) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /strolle-gpu/src/triangles.rs: -------------------------------------------------------------------------------- 1 | use spirv_std::arch::IndexUnchecked; 2 | 3 | use crate::{Triangle, TriangleId}; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct TrianglesView<'a> { 7 | buffer: &'a [Triangle], 8 | } 9 | 10 | impl<'a> TrianglesView<'a> { 11 | pub fn new(buffer: &'a [Triangle]) -> Self { 12 | Self { buffer } 13 | } 14 | 15 | pub fn get(self, id: TriangleId) -> Triangle { 16 | unsafe { *self.buffer.index_unchecked(id.get() as usize) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bevy-strolle/src/sun.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use bevy::prelude::Resource; 4 | use strolle as st; 5 | 6 | #[derive(Clone, Debug, Default, Resource)] 7 | pub struct StrolleSun { 8 | sun: st::Sun, 9 | } 10 | 11 | impl Deref for StrolleSun { 12 | type Target = st::Sun; 13 | 14 | fn deref(&self) -> &Self::Target { 15 | &self.sun 16 | } 17 | } 18 | 19 | impl DerefMut for StrolleSun { 20 | fn deref_mut(&mut self) -> &mut Self::Target { 21 | &mut self.sun 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bevy-strolle/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy-strolle" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bevy.workspace = true 8 | bevy_egui.workspace = true 9 | log.workspace = true 10 | strolle = { path = "../strolle", features = ["metrics"] } 11 | wgpu.workspace = true 12 | 13 | [dev-dependencies] 14 | bevy = { workspace = true, features = ["jpeg"] } 15 | bevy_mod_raycast.workspace = true 16 | bevy_rapier3d.workspace = true 17 | smooth-bevy-cameras.workspace = true 18 | zip = { workspace = true, default-features = false, features = ["deflate"] } 19 | -------------------------------------------------------------------------------- /strolle-gpu/src/frame.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | 3 | #[repr(C)] 4 | #[derive( 5 | Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Pod, Zeroable, 6 | )] 7 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 8 | pub struct Frame(u32); 9 | 10 | impl Frame { 11 | pub fn new(id: u32) -> Self { 12 | Self(id) 13 | } 14 | 15 | pub fn get(self) -> u32 { 16 | self.0 17 | } 18 | 19 | pub fn is_gi_tracing(self) -> bool { 20 | self.0 % 6 < 4 21 | } 22 | 23 | pub fn is_gi_validation(self) -> bool { 24 | !self.is_gi_tracing() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /strolle-shaders/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_arch = "spirv", no_std)] 2 | #![allow(clippy::too_many_arguments)] 3 | 4 | pub mod atmosphere; 5 | pub mod bvh_heatmap; 6 | pub mod di_resolving; 7 | pub mod di_sampling; 8 | pub mod di_spatial_resampling; 9 | pub mod di_temporal_resampling; 10 | pub mod frame_composition; 11 | pub mod frame_denoising; 12 | pub mod frame_reprojection; 13 | pub mod gi_preview_resampling; 14 | pub mod gi_reprojection; 15 | pub mod gi_resolving; 16 | pub mod gi_sampling_a; 17 | pub mod gi_sampling_b; 18 | pub mod gi_spatial_resampling; 19 | pub mod gi_temporal_resampling; 20 | pub mod prim_raster; 21 | pub mod ref_shading; 22 | pub mod ref_tracing; 23 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils/f32_ext.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_arch = "spirv")] 2 | use spirv_std::num_traits::Float; 3 | 4 | pub trait F32Ext 5 | where 6 | Self: Sized, 7 | { 8 | fn sqr(self) -> Self; 9 | fn saturate(self) -> Self; 10 | fn inverse_sqrt(self) -> Self; 11 | fn acos_approx(self) -> Self; 12 | } 13 | 14 | impl F32Ext for f32 { 15 | fn sqr(self) -> Self { 16 | self * self 17 | } 18 | 19 | fn saturate(self) -> Self { 20 | self.clamp(0.0, 1.0) 21 | } 22 | 23 | fn inverse_sqrt(self) -> Self { 24 | 1.0 / self.sqrt() 25 | } 26 | 27 | fn acos_approx(self) -> Self { 28 | 2.0f32.sqrt() * (1.0 - self).saturate().sqrt() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /strolle-gpu/src/lights.rs: -------------------------------------------------------------------------------- 1 | use spirv_std::arch::IndexUnchecked; 2 | 3 | use crate::{Light, LightId}; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct LightsView<'a> { 7 | items: &'a [Light], 8 | } 9 | 10 | impl<'a> LightsView<'a> { 11 | pub fn new(items: &'a [Light]) -> Self { 12 | Self { items } 13 | } 14 | 15 | pub fn get(self, id: LightId) -> Light { 16 | unsafe { *self.items.index_unchecked(id.get() as usize) } 17 | } 18 | 19 | pub fn get_prev(self, id: LightId) -> Light { 20 | let mut light = self.get(id); 21 | 22 | light.rollback(); 23 | light 24 | } 25 | 26 | pub fn len(self) -> usize { 27 | self.items.len() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /strolle/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "strolle" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bytemuck.workspace = true 8 | derivative.workspace = true 9 | fxhash.workspace = true 10 | glam.workspace = true 11 | guillotiere.workspace = true 12 | humantime = { workspace = true, optional = true } 13 | image = { workspace = true, default-features = false, features = ["png"] } 14 | log.workspace = true 15 | rand.workspace = true 16 | spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu" } 17 | strolle-gpu = { path = "../strolle-gpu" } 18 | strolle-shaders = { path = "../strolle-shaders" } 19 | wgpu = { workspace = true, features = ["spirv"] } 20 | 21 | [features] 22 | metrics = ["humantime"] 23 | -------------------------------------------------------------------------------- /strolle/src/buffers.rs: -------------------------------------------------------------------------------- 1 | mod bind_group; 2 | mod bindable; 3 | mod bufferable; 4 | mod double_buffered; 5 | mod mapped_storage_buffer; 6 | mod mapped_uniform_buffer; 7 | mod storage_buffer; 8 | mod texture; 9 | mod utils; 10 | 11 | pub use self::bind_group::*; 12 | pub use self::bindable::*; 13 | pub use self::bufferable::*; 14 | pub use self::double_buffered::*; 15 | pub use self::mapped_storage_buffer::*; 16 | pub use self::mapped_uniform_buffer::*; 17 | pub use self::storage_buffer::*; 18 | pub use self::texture::*; 19 | 20 | #[must_use = "buffer might have gotten reallocated which you should probably react upon"] 21 | #[derive(Clone, Copy, Debug, Default)] 22 | pub struct BufferFlushOutcome { 23 | pub reallocated: bool, 24 | } 25 | -------------------------------------------------------------------------------- /strolle-gpu/src/noise/blue.rs: -------------------------------------------------------------------------------- 1 | use glam::{uvec2, UVec2, Vec2, Vec4Swizzles}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | use crate::{Frame, TexRgba8}; 6 | 7 | pub struct BlueNoise<'a> { 8 | tex: TexRgba8<'a>, 9 | uv: UVec2, 10 | } 11 | 12 | impl<'a> BlueNoise<'a> { 13 | pub const SIZE: UVec2 = uvec2(256, 256); 14 | 15 | pub fn new(tex: TexRgba8<'a>, id: UVec2, frame: Frame) -> Self { 16 | let uv = (id + uvec2(71, 11) * frame.get()) % Self::SIZE; 17 | 18 | Self { tex, uv } 19 | } 20 | 21 | pub fn first_sample(self) -> Vec2 { 22 | self.tex.read(self.uv).xy() 23 | } 24 | 25 | pub fn second_sample(self) -> Vec2 { 26 | self.tex.read(self.uv).zw() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /strolle/src/instance.rs: -------------------------------------------------------------------------------- 1 | use glam::Affine3A; 2 | 3 | use crate::Params; 4 | 5 | #[derive(Debug)] 6 | pub struct Instance

7 | where 8 | P: Params, 9 | { 10 | pub(crate) mesh_handle: P::MeshHandle, 11 | pub(crate) material_handle: P::MaterialHandle, 12 | pub(crate) transform: Affine3A, 13 | pub(crate) transform_inverse: Affine3A, 14 | } 15 | 16 | impl

Instance

17 | where 18 | P: Params, 19 | { 20 | pub fn new( 21 | mesh_handle: P::MeshHandle, 22 | material_handle: P::MaterialHandle, 23 | transform: Affine3A, 24 | ) -> Self { 25 | Self { 26 | mesh_handle, 27 | material_handle, 28 | transform, 29 | transform_inverse: transform.inverse(), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /strolle/src/meshes.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use derivative::Derivative; 4 | 5 | use crate::{Mesh, Params}; 6 | 7 | #[derive(Debug, Derivative)] 8 | #[derivative(Default)] 9 | pub struct Meshes

10 | where 11 | P: Params, 12 | { 13 | meshes: HashMap, 14 | } 15 | 16 | impl

Meshes

17 | where 18 | P: Params, 19 | { 20 | pub fn insert(&mut self, handle: P::MeshHandle, item: Mesh) { 21 | self.meshes.insert(handle, item); 22 | } 23 | 24 | pub fn get(&self, handle: P::MeshHandle) -> Option<&Mesh> { 25 | self.meshes.get(&handle) 26 | } 27 | 28 | pub fn remove(&mut self, handle: P::MeshHandle) { 29 | self.meshes.remove(&handle); 30 | } 31 | 32 | pub fn len(&self) -> usize { 33 | self.meshes.len() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils/u32_ext.rs: -------------------------------------------------------------------------------- 1 | pub trait U32Ext 2 | where 3 | Self: Sized, 4 | { 5 | fn from_bytes(bytes: [u32; 4]) -> Self; 6 | fn to_bytes(self) -> [u32; 4]; 7 | } 8 | 9 | impl U32Ext for u32 { 10 | fn from_bytes([a, b, c, d]: [u32; 4]) -> Self { 11 | a | (b << 8) | (c << 16) | (d << 24) 12 | } 13 | 14 | fn to_bytes(mut self) -> [u32; 4] { 15 | let a = self & 0xff; 16 | self >>= 8; 17 | let b = self & 0xff; 18 | self >>= 8; 19 | let c = self & 0xff; 20 | self >>= 8; 21 | let d = self & 0xff; 22 | 23 | [a, b, c, d] 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | #[test] 32 | fn from_to_bytes() { 33 | assert_eq!(0xcafebabe, u32::from_bytes(u32::to_bytes(0xcafebabe))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils/vec2_ext.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec2; 2 | 3 | pub trait Vec2Ext 4 | where 5 | Self: Sized, 6 | { 7 | /// Clips this color-vector into given bounding box. 8 | /// 9 | /// See: 10 | /// - https://s3.amazonaws.com/arena-attachments/655504/c5c71c5507f0f8bf344252958254fb7d.pdf?1468341463 11 | fn clip(self, aabb_min: Self, aabb_max: Self) -> Self; 12 | } 13 | 14 | impl Vec2Ext for Vec2 { 15 | fn clip(self, aabb_min: Self, aabb_max: Self) -> Self { 16 | let p_clip = 0.5 * (aabb_max + aabb_min); 17 | let e_clip = 0.5 * (aabb_max - aabb_min); 18 | let v_clip = self - p_clip; 19 | let v_unit = v_clip / e_clip; 20 | let a_unit = v_unit.abs(); 21 | let ma_unit = a_unit.max_element(); 22 | 23 | if ma_unit > 1.0 { 24 | p_clip + v_clip / ma_unit 25 | } else { 26 | self 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /strolle-gpu/src/world.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::{vec3, Vec3}; 3 | #[cfg(target_arch = "spirv")] 4 | use spirv_std::num_traits::Float; 5 | 6 | #[repr(C)] 7 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 8 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 9 | pub struct World { 10 | pub light_count: u32, 11 | pub sun_azimuth: f32, 12 | pub sun_altitude: f32, 13 | } 14 | 15 | impl World { 16 | // TODO recalculate the distance factor using sun's solid angle 17 | pub const SUN_DISTANCE: f32 = 1000.0; 18 | 19 | pub fn sun_dir(self) -> Vec3 { 20 | vec3( 21 | self.sun_altitude.cos() * self.sun_azimuth.sin(), 22 | self.sun_altitude.sin(), 23 | -self.sun_altitude.cos() * self.sun_azimuth.cos(), 24 | ) 25 | } 26 | 27 | pub fn sun_pos(self) -> Vec3 { 28 | self.sun_dir() * Self::SUN_DISTANCE 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /strolle-gpu/src/normal.rs: -------------------------------------------------------------------------------- 1 | use glam::{vec3, Vec2, Vec3, Vec3Swizzles}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | pub struct Normal; 6 | 7 | impl Normal { 8 | /// Compresses normal from Vec3 into Vec2 using octahedron-normal mapping. 9 | pub fn encode(n: Vec3) -> Vec2 { 10 | let n = n / (n.x.abs() + n.y.abs() + n.z.abs()); 11 | 12 | let n = if n.z >= 0.0 { 13 | n.xy() 14 | } else { 15 | let mut t = 1.0 - n.yx().abs(); 16 | 17 | t.x = t.x.copysign(n.x); 18 | t.y = t.y.copysign(n.y); 19 | t 20 | }; 21 | 22 | n * 0.5 + 0.5 23 | } 24 | 25 | /// See: [`Self::encode()`]. 26 | pub fn decode(n: Vec2) -> Vec3 { 27 | let n = n * 2.0 - 1.0; 28 | let mut n = vec3(n.x, n.y, 1.0 - n.x.abs() - n.y.abs()); 29 | let t = (-n.z).max(0.0); 30 | 31 | n.x -= t.copysign(n.x); 32 | n.y -= t.copysign(n.y); 33 | n.normalize() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /strolle/src/utils/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | pub fn measure(name: &str, f: impl FnOnce() -> T) -> T { 4 | let tt = Instant::now(); 5 | let result = f(); 6 | 7 | metric(name, tt); 8 | 9 | result 10 | } 11 | 12 | #[cfg(feature = "metrics")] 13 | pub fn metric(metric: &str, tt: Instant) { 14 | use std::env; 15 | use std::sync::OnceLock; 16 | use std::time::Duration; 17 | 18 | use log::trace; 19 | 20 | static METRIC_THRESHOLD: OnceLock = OnceLock::new(); 21 | 22 | let threshold = METRIC_THRESHOLD.get_or_init(|| { 23 | env::var("STROLLE_METRIC_THRESHOLD") 24 | .ok() 25 | .map(|threshold| humantime::parse_duration(&threshold).unwrap()) 26 | .unwrap_or_else(|| Duration::from_millis(0)) 27 | }); 28 | 29 | let tt = tt.elapsed(); 30 | 31 | if tt > *threshold { 32 | trace!("metric({metric})={tt:?}"); 33 | } 34 | } 35 | 36 | #[cfg(not(feature = "metrics"))] 37 | pub fn metric(_metric: &str, _tt: Instant) { 38 | // 39 | } 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "bevy-strolle", 6 | "strolle", 7 | "strolle-gpu", 8 | "strolle-shader-builder", 9 | "strolle-shaders", 10 | ] 11 | 12 | [workspace.lints.rust] 13 | unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } 14 | 15 | [workspace.dependencies] 16 | bevy = "0.12.1" 17 | bevy_egui = "0.24" 18 | bevy_mod_raycast = "0.16.0" 19 | bevy_rapier3d = "0.23.0" 20 | bytemuck = "1.13.1" 21 | derivative = "2.2.0" 22 | fxhash = "0.2.1" 23 | glam = { version = "0.24", default-features = false } 24 | guillotiere = "0.6.2" 25 | humantime = "2.1.0" 26 | image = { version = "0.24.6", default-features = false } 27 | log = "0.4.18" 28 | rand = "0.8.5" 29 | smooth-bevy-cameras = "0.10.0" 30 | spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu" } 31 | spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu" } 32 | wgpu = "0.17.2" 33 | zip = { version = "0.6.6", default-features = false } 34 | 35 | [patch."crates-io"] 36 | # TODO https://github.com/gfx-rs/naga/issues/2373 37 | naga = { git = "https://github.com/Patryk27/naga", branch = "v0.13.0-strolle" } 38 | -------------------------------------------------------------------------------- /bevy-strolle/src/rendering_node.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::render::render_graph::{NodeRunError, RenderGraphContext, ViewNode}; 3 | use bevy::render::renderer::RenderContext; 4 | use bevy::render::view::ViewTarget; 5 | 6 | use crate::{EngineResource, SyncedState}; 7 | 8 | #[derive(Default)] 9 | pub struct RenderingNode; 10 | 11 | impl ViewNode for RenderingNode { 12 | type ViewQuery = &'static ViewTarget; 13 | 14 | fn run( 15 | &self, 16 | graph: &mut RenderGraphContext, 17 | render_context: &mut RenderContext, 18 | target: &ViewTarget, 19 | world: &World, 20 | ) -> Result<(), NodeRunError> { 21 | let entity = graph.view_entity(); 22 | let engine = world.resource::(); 23 | let state = world.resource::(); 24 | 25 | let Some(camera) = state.cameras.get(&entity) else { 26 | return Ok(()); 27 | }; 28 | 29 | engine.render_camera( 30 | camera.handle, 31 | render_context.command_encoder(), 32 | target.main_texture_view(), 33 | ); 34 | 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Patryk Wychowaniec & Jakub Trąd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /strolle/src/utils/axis.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | 3 | use glam::Vec3; 4 | 5 | #[derive(Clone, Copy, Debug)] 6 | pub enum Axis { 7 | X, 8 | Y, 9 | Z, 10 | } 11 | 12 | impl Axis { 13 | pub fn all() -> impl Iterator { 14 | [Self::X, Self::Y, Self::Z].into_iter() 15 | } 16 | } 17 | 18 | impl From for Axis { 19 | fn from(value: usize) -> Self { 20 | match value { 21 | 0 => Self::X, 22 | 1 => Self::Y, 23 | 2 => Self::Z, 24 | _ => panic!(), 25 | } 26 | } 27 | } 28 | 29 | impl Index for Vec3 { 30 | type Output = f32; 31 | 32 | fn index(&self, index: Axis) -> &Self::Output { 33 | match index { 34 | Axis::X => &self.x, 35 | Axis::Y => &self.y, 36 | Axis::Z => &self.z, 37 | } 38 | } 39 | } 40 | 41 | impl IndexMut for Vec3 { 42 | fn index_mut(&mut self, index: Axis) -> &mut Self::Output { 43 | match index { 44 | Axis::X => &mut self.x, 45 | Axis::Y => &mut self.y, 46 | Axis::Z => &mut self.z, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /strolle/src/buffers/bindable.rs: -------------------------------------------------------------------------------- 1 | /// Object that can be attached to a pipeline, e.g. a buffer or a texture 2 | pub trait Bindable { 3 | fn bind( 4 | &self, 5 | binding: u32, 6 | ) -> Vec<(wgpu::BindGroupLayoutEntry, wgpu::BindingResource)>; 7 | } 8 | 9 | /// Object that can be attached to a pipeline, e.g. a buffer or a texture, and 10 | /// it's double-buffered (i.e. exists in two similar versions swapped after each 11 | /// frame) 12 | pub trait DoubleBufferedBindable { 13 | fn bind( 14 | &self, 15 | binding: u32, 16 | ) -> Vec<(wgpu::BindGroupLayoutEntry, [wgpu::BindingResource; 2])>; 17 | } 18 | 19 | impl DoubleBufferedBindable for T 20 | where 21 | T: Bindable, 22 | { 23 | fn bind( 24 | &self, 25 | binding: u32, 26 | ) -> Vec<(wgpu::BindGroupLayoutEntry, [wgpu::BindingResource; 2])> { 27 | T::bind(self, binding) 28 | .into_iter() 29 | .map(|(layout, resource)| { 30 | let resource_a = resource.clone(); 31 | let resource_b = resource; 32 | 33 | (layout, [resource_a, resource_b]) 34 | }) 35 | .collect() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /strolle/src/triangle.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec3Swizzles; 2 | use spirv_std::glam::{Vec2, Vec3, Vec4}; 3 | 4 | use crate::gpu; 5 | use crate::utils::BoundingBox; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Triangle { 9 | pub positions: [Vec3; 3], 10 | pub normals: [Vec3; 3], 11 | pub uvs: [Vec2; 3], 12 | pub tangents: [Vec4; 3], 13 | } 14 | 15 | impl Triangle { 16 | pub fn center(&self) -> Vec3 { 17 | self.positions.iter().sum::() / 3.0 18 | } 19 | 20 | pub fn bounds(&self) -> BoundingBox { 21 | self.positions.iter().copied().collect() 22 | } 23 | 24 | pub fn serialize(&self) -> gpu::Triangle { 25 | gpu::Triangle { 26 | d0: self.positions[0].xyz().extend(self.uvs[0].x), 27 | d1: self.normals[0].xyz().extend(self.uvs[0].y), 28 | d2: self.tangents[0], 29 | 30 | d3: self.positions[1].xyz().extend(self.uvs[1].x), 31 | d4: self.normals[1].xyz().extend(self.uvs[1].y), 32 | d5: self.tangents[1], 33 | 34 | d6: self.positions[2].xyz().extend(self.uvs[2].x), 35 | d7: self.normals[2].xyz().extend(self.uvs[2].y), 36 | d8: self.tangents[2], 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /strolle/src/image.rs: -------------------------------------------------------------------------------- 1 | use crate::Params; 2 | 3 | #[derive(Debug)] 4 | pub struct Image

5 | where 6 | P: Params, 7 | { 8 | pub(crate) data: ImageData

, 9 | pub(crate) texture_descriptor: wgpu::TextureDescriptor<'static>, 10 | 11 | // TODO propagate sampler's addressing modes to the shader so that we know 12 | // whether the texture should be repeated, etc. 13 | pub(crate) _sampler_descriptor: wgpu::SamplerDescriptor<'static>, 14 | } 15 | 16 | impl

Image

17 | where 18 | P: Params, 19 | { 20 | pub fn new( 21 | data: ImageData

, 22 | texture_descriptor: wgpu::TextureDescriptor<'static>, 23 | sampler_descriptor: wgpu::SamplerDescriptor<'static>, 24 | ) -> Self { 25 | assert_eq!(texture_descriptor.dimension, wgpu::TextureDimension::D2); 26 | 27 | Self { 28 | data, 29 | texture_descriptor, 30 | _sampler_descriptor: sampler_descriptor, 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | pub enum ImageData

37 | where 38 | P: Params, 39 | { 40 | Raw { 41 | data: Vec, 42 | }, 43 | Texture { 44 | texture: P::ImageTexture, 45 | is_dynamic: bool, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /strolle-shader-builder/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::path::Path; 4 | 5 | use spirv_builder::{Capability, MetadataPrintout, SpirvBuilder}; 6 | 7 | fn main() -> Result<(), Box> { 8 | let crate_path = Path::new(env!("CARGO_MANIFEST_DIR")) 9 | .parent() 10 | .unwrap() 11 | .join("strolle-shaders"); 12 | 13 | let result = SpirvBuilder::new(crate_path, "spirv-unknown-spv1.3") 14 | .multimodule(true) 15 | .print_metadata(MetadataPrintout::DependencyOnly) 16 | .capability(Capability::Int8) 17 | .extra_arg("--spirt-passes=reduce,fuse_selects") 18 | .build()?; 19 | 20 | for (shader_name, shader_path) in result.module.unwrap_multi() { 21 | let shader_id = shader_name.replace("::", "_"); 22 | let shader_id = shader_id.strip_suffix("_main").unwrap_or(&shader_id); 23 | 24 | println!( 25 | "cargo:rustc-env=strolle_shaders::{}.path={}", 26 | shader_id, 27 | shader_path.display() 28 | ); 29 | 30 | println!( 31 | "cargo:rustc-env=strolle_shaders::{}.entry_point={}", 32 | shader_id, shader_name, 33 | ); 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils.rs: -------------------------------------------------------------------------------- 1 | mod bilinear_filter; 2 | mod f32_ext; 3 | mod u32_ext; 4 | mod vec2_ext; 5 | mod vec3_ext; 6 | 7 | use core::ops; 8 | 9 | use glam::{uvec2, UVec2}; 10 | use spirv_std::Image; 11 | 12 | pub use self::bilinear_filter::*; 13 | pub use self::f32_ext::*; 14 | pub use self::u32_ext::*; 15 | pub use self::vec2_ext::*; 16 | pub use self::vec3_ext::*; 17 | 18 | pub type Tex<'a> = &'a Image!(2D, type = f32, sampled); 19 | pub type TexRgba8<'a> = &'a Image!(2D, format = rgba8, sampled = false); 20 | pub type TexRgba16<'a> = &'a Image!(2D, format = rgba16f, sampled = false); 21 | pub type TexRgba32<'a> = &'a Image!(2D, format = rgba32f, sampled = false); 22 | 23 | pub fn lerp(a: T, b: T, t: f32) -> T 24 | where 25 | T: ops::Add, 26 | T: ops::Sub, 27 | T: ops::Mul, 28 | T: Copy, 29 | { 30 | a + (b - a) * t.clamp(0.0, 1.0) 31 | } 32 | 33 | pub fn resolve_checkerboard(global_id: UVec2, frame: u32) -> UVec2 { 34 | global_id * uvec2(2, 1) + uvec2((frame + global_id.y) % 2, 0) 35 | } 36 | 37 | pub fn resolve_checkerboard_alt(global_id: UVec2, frame: u32) -> UVec2 { 38 | resolve_checkerboard(global_id, frame + 1) 39 | } 40 | 41 | pub fn got_checkerboard_at(screen_pos: UVec2, frame: u32) -> bool { 42 | screen_pos == resolve_checkerboard(screen_pos / uvec2(2, 1), frame) 43 | } 44 | -------------------------------------------------------------------------------- /strolle/src/buffers/bufferable.rs: -------------------------------------------------------------------------------- 1 | use std::slice; 2 | 3 | use bytemuck::Pod; 4 | 5 | use crate::gpu; 6 | 7 | /// Object that can be sent into the GPU 8 | pub trait Bufferable { 9 | fn data(&self) -> &[u8]; 10 | 11 | fn size(&self) -> usize { 12 | self.data().len() 13 | } 14 | } 15 | 16 | impl Bufferable for u32 { 17 | fn data(&self) -> &[u8] { 18 | bytemuck::cast_slice(slice::from_ref(self)) 19 | } 20 | } 21 | 22 | impl Bufferable for u64 { 23 | fn data(&self) -> &[u8] { 24 | bytemuck::cast_slice(slice::from_ref(self)) 25 | } 26 | } 27 | 28 | impl Bufferable for f32 { 29 | fn data(&self) -> &[u8] { 30 | bytemuck::cast_slice(slice::from_ref(self)) 31 | } 32 | } 33 | 34 | impl Bufferable for gpu::Camera { 35 | fn data(&self) -> &[u8] { 36 | bytemuck::cast_slice(slice::from_ref(self)) 37 | } 38 | } 39 | 40 | impl Bufferable for gpu::World { 41 | fn data(&self) -> &[u8] { 42 | bytemuck::cast_slice(slice::from_ref(self)) 43 | } 44 | } 45 | 46 | impl Bufferable for &[T] 47 | where 48 | T: Pod, 49 | { 50 | fn data(&self) -> &[u8] { 51 | bytemuck::cast_slice(self) 52 | } 53 | } 54 | 55 | impl Bufferable for Vec 56 | where 57 | T: Pod, 58 | { 59 | fn data(&self) -> &[u8] { 60 | bytemuck::cast_slice(self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /strolle/src/camera_controllers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{CameraController, CameraHandle}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct CameraControllers { 7 | cameras: HashMap, 8 | next_id: usize, 9 | } 10 | 11 | impl CameraControllers { 12 | pub fn add(&mut self, camera: CameraController) -> CameraHandle { 13 | let handle = CameraHandle::new(self.next_id); 14 | 15 | self.cameras.insert(handle, camera); 16 | self.next_id += 1; 17 | 18 | handle 19 | } 20 | 21 | pub fn get(&self, camera_handle: CameraHandle) -> &CameraController { 22 | self.cameras.get(&camera_handle).unwrap_or_else(|| { 23 | panic!("camera does not exist: {:?}", camera_handle) 24 | }) 25 | } 26 | 27 | pub fn get_mut( 28 | &mut self, 29 | camera_handle: CameraHandle, 30 | ) -> &mut CameraController { 31 | self.cameras.get_mut(&camera_handle).unwrap_or_else(|| { 32 | panic!("camera does not exist: {:?}", camera_handle) 33 | }) 34 | } 35 | 36 | pub fn iter_mut( 37 | &mut self, 38 | ) -> impl Iterator + '_ { 39 | self.cameras.values_mut() 40 | } 41 | 42 | pub fn remove(&mut self, camera_handle: CameraHandle) { 43 | self.cameras.remove(&camera_handle); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/bvh_heatmap.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct BvhHeatmapPass { 7 | pass: CameraComputePass<()>, 8 | } 9 | 10 | impl BvhHeatmapPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("bvh_heatmap") 21 | .bind([ 22 | &engine.triangles.bind_readable(), 23 | &engine.bvh.bind_readable(), 24 | &engine.materials.bind_readable(), 25 | &engine.images.bind_atlas(), 26 | ]) 27 | .bind([ 28 | &buffers.curr_camera.bind_readable(), 29 | &buffers.ref_colors.bind_writable(), 30 | ]) 31 | .build(device, &engine.shaders.bvh_heatmap); 32 | 33 | Self { pass } 34 | } 35 | 36 | pub fn run( 37 | &self, 38 | camera: &CameraController, 39 | encoder: &mut wgpu::CommandEncoder, 40 | ) { 41 | // This pass uses 8x8 warps: 42 | let size = (camera.camera.viewport.size + 7) / 8; 43 | 44 | self.pass.run(camera, encoder, size, ()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/frame_reprojection.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct FrameReprojectionPass { 7 | pass: CameraComputePass<()>, 8 | } 9 | 10 | impl FrameReprojectionPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("frame_reprojection") 21 | .bind([ 22 | &buffers.curr_camera.bind_readable(), 23 | &buffers.prev_camera.bind_readable(), 24 | &buffers.prim_surface_map.curr().bind_readable(), 25 | &buffers.prim_surface_map.prev().bind_readable(), 26 | &buffers.velocity_map.bind_readable(), 27 | &buffers.reprojection_map.bind_writable(), 28 | ]) 29 | .build(device, &engine.shaders.frame_reprojection); 30 | 31 | Self { pass } 32 | } 33 | 34 | pub fn run( 35 | &self, 36 | camera: &CameraController, 37 | encoder: &mut wgpu::CommandEncoder, 38 | ) { 39 | // This pass uses 8x8 warps: 40 | let size = (camera.camera.viewport.size + 7) / 8; 41 | 42 | self.pass.run(camera, encoder, size, ()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/gi_reprojection.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct GiReprojectionPass { 7 | pass: CameraComputePass, 8 | } 9 | 10 | impl GiReprojectionPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("gi_reprojection") 21 | .bind([ 22 | &buffers.curr_camera.bind_readable(), 23 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 24 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 25 | &buffers.reprojection_map.bind_readable(), 26 | &buffers.gi_reservoirs[0].bind_readable(), 27 | &buffers.gi_reservoirs[2].bind_writable(), 28 | ]) 29 | .build(device, &engine.shaders.gi_reprojection); 30 | 31 | Self { pass } 32 | } 33 | 34 | pub fn run( 35 | &self, 36 | camera: &CameraController, 37 | encoder: &mut wgpu::CommandEncoder, 38 | ) { 39 | // This pass uses 8x8 warps: 40 | let size = (camera.camera.viewport.size + 7) / 8; 41 | 42 | self.pass.run(camera, encoder, size, camera.pass_params()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /strolle/build.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::path::PathBuf; 3 | use std::process::{Command, Stdio}; 4 | use std::{env, process}; 5 | 6 | fn main() -> Result<(), Box> { 7 | let profile = env::var("PROFILE").unwrap(); 8 | 9 | println!("cargo:rerun-if-changed=build.rs"); 10 | println!("cargo:rerun-if-changed=../strolle-shader-builder/Cargo.toml"); 11 | println!("cargo:rerun-if-changed=../strolle-shader-builder/src/main.rs"); 12 | println!("cargo:rustc-env=PROFILE={profile}"); 13 | 14 | let mut dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 15 | 16 | // Strip `$profile/build/*/out`. 17 | let ok = dir.ends_with("out") 18 | && dir.pop() 19 | && dir.pop() 20 | && dir.ends_with("build") 21 | && dir.pop() 22 | && dir.ends_with(profile) 23 | && dir.pop(); 24 | 25 | assert!(ok); 26 | 27 | let dir = dir.join("shader-builder"); 28 | 29 | let status = Command::new("cargo") 30 | .args([ 31 | "run", 32 | "--release", 33 | "-p", 34 | "strolle-shader-builder", 35 | "--target-dir", 36 | ]) 37 | .arg(dir) 38 | .env_remove("CARGO_ENCODED_RUSTFLAGS") 39 | .stderr(Stdio::inherit()) 40 | .stdout(Stdio::inherit()) 41 | .status()?; 42 | 43 | if !status.success() { 44 | process::exit(status.code().unwrap_or(1)); 45 | } 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /bevy-strolle/src/graph.rs: -------------------------------------------------------------------------------- 1 | pub const NAME: &str = "strolle"; 2 | 3 | pub mod node { 4 | pub const RENDERING: &str = "strolle_rendering"; 5 | pub const TONEMAPPING: &str = "strolle_tonemapping"; 6 | pub const FXAA: &str = "strolle_fxaa"; 7 | pub const UPSCALING: &str = "strolle_upscaling"; 8 | } 9 | 10 | use bevy::core_pipeline::fxaa::FxaaNode; 11 | use bevy::core_pipeline::tonemapping::TonemappingNode; 12 | use bevy::core_pipeline::upscaling::UpscalingNode; 13 | use bevy::prelude::*; 14 | use bevy::render::render_graph::{RenderGraphApp, ViewNodeRunner}; 15 | 16 | use crate::RenderingNode; 17 | 18 | pub(crate) fn setup(render_app: &mut App) { 19 | render_app 20 | .add_render_sub_graph(NAME) 21 | .add_render_graph_node::>( 22 | NAME, 23 | node::RENDERING, 24 | ) 25 | .add_render_graph_node::>( 26 | NAME, 27 | node::TONEMAPPING, 28 | ) 29 | .add_render_graph_node::>( 30 | NAME, 31 | node::UPSCALING, 32 | ) 33 | .add_render_graph_node::>(NAME, node::FXAA) 34 | .add_render_graph_edges( 35 | NAME, 36 | &[ 37 | node::RENDERING, 38 | node::FXAA, 39 | node::TONEMAPPING, 40 | node::UPSCALING, 41 | ], 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/gi_temporal_resampling.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct GiTemporalResamplingPass { 7 | pass: CameraComputePass, 8 | } 9 | 10 | impl GiTemporalResamplingPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("gi_temporal_resampling") 21 | .bind([ 22 | &buffers.curr_camera.bind_readable(), 23 | &buffers.prev_camera.bind_readable(), 24 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 25 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 26 | &buffers.prim_gbuffer_d0.prev().bind_readable(), 27 | &buffers.prim_gbuffer_d1.prev().bind_readable(), 28 | &buffers.reprojection_map.bind_readable(), 29 | &buffers.gi_reservoirs[2].bind_readable(), 30 | &buffers.gi_reservoirs[1].bind_writable(), 31 | ]) 32 | .build(device, &engine.shaders.gi_temporal_resampling); 33 | 34 | Self { pass } 35 | } 36 | 37 | pub fn run( 38 | &self, 39 | camera: &CameraController, 40 | encoder: &mut wgpu::CommandEncoder, 41 | ) { 42 | // This pass uses 8x8 warps: 43 | let size = (camera.camera.viewport.size + 7) / 8; 44 | 45 | self.pass.run(camera, encoder, size, camera.pass_params()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/di_sampling.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct DiSamplingPass { 7 | pass: CameraComputePass, 8 | } 9 | 10 | impl DiSamplingPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("di_sampling") 21 | .bind([ 22 | &engine.noise.bind_blue_noise(), 23 | &engine.triangles.bind_readable(), 24 | &engine.bvh.bind_readable(), 25 | &engine.materials.bind_readable(), 26 | &engine.lights.bind_readable(), 27 | &engine.images.bind_atlas(), 28 | &engine.world.bind_readable(), 29 | ]) 30 | .bind([ 31 | &buffers.curr_camera.bind_readable(), 32 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 33 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 34 | &buffers.di_reservoirs[1].bind_writable(), 35 | ]) 36 | .build(device, &engine.shaders.di_sampling); 37 | 38 | Self { pass } 39 | } 40 | 41 | pub fn run( 42 | &self, 43 | camera: &CameraController, 44 | encoder: &mut wgpu::CommandEncoder, 45 | ) { 46 | // This pass uses 8x8 warps: 47 | let size = (camera.camera.viewport.size + 7) / 8; 48 | 49 | self.pass.run(camera, encoder, size, camera.pass_params()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/ref_tracing.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | use crate::{ 4 | gpu, Camera, CameraBuffers, CameraComputePass, CameraController, Engine, 5 | Params, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct RefTracingPass { 10 | pass: CameraComputePass, 11 | } 12 | 13 | impl RefTracingPass { 14 | pub fn new

( 15 | engine: &Engine

, 16 | device: &wgpu::Device, 17 | _: &Camera, 18 | buffers: &CameraBuffers, 19 | ) -> Self 20 | where 21 | P: Params, 22 | { 23 | let pass = CameraComputePass::builder("ref_tracing") 24 | .bind([ 25 | &engine.triangles.bind_readable(), 26 | &engine.bvh.bind_readable(), 27 | &engine.materials.bind_readable(), 28 | &engine.images.bind_atlas(), 29 | ]) 30 | .bind([ 31 | &buffers.curr_camera.bind_readable(), 32 | &buffers.ref_rays.bind_readable(), 33 | &buffers.ref_hits.bind_writable(), 34 | ]) 35 | .build(device, &engine.shaders.ref_tracing); 36 | 37 | Self { pass } 38 | } 39 | 40 | pub fn run( 41 | &self, 42 | camera: &CameraController, 43 | encoder: &mut wgpu::CommandEncoder, 44 | depth: u8, 45 | ) { 46 | // This pass uses 8x8 warps: 47 | let size = (camera.camera.viewport.size + 7) / 8; 48 | 49 | let params = gpu::RefPassParams { 50 | seed: rand::thread_rng().gen(), 51 | frame: camera.frame, 52 | depth: depth as u32, 53 | }; 54 | 55 | self.pass.run(camera, encoder, size, params); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/di_temporal_resampling.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct DiTemporalResamplingPass { 7 | pass: CameraComputePass, 8 | } 9 | 10 | impl DiTemporalResamplingPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("di_temporal_resampling") 21 | .bind([&engine.lights.bind_readable()]) 22 | .bind([ 23 | &buffers.curr_camera.bind_readable(), 24 | &buffers.prev_camera.bind_readable(), 25 | &buffers.reprojection_map.bind_readable(), 26 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 27 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 28 | &buffers.prim_gbuffer_d0.prev().bind_readable(), 29 | &buffers.prim_gbuffer_d1.prev().bind_readable(), 30 | &buffers.di_reservoirs[0].bind_readable(), 31 | &buffers.di_reservoirs[1].bind_writable(), 32 | ]) 33 | .build(device, &engine.shaders.di_temporal_resampling); 34 | 35 | Self { pass } 36 | } 37 | 38 | pub fn run( 39 | &self, 40 | camera: &CameraController, 41 | encoder: &mut wgpu::CommandEncoder, 42 | ) { 43 | // This pass uses 8x8 warps: 44 | let size = (camera.camera.viewport.size + 7) / 8; 45 | 46 | self.pass.run(camera, encoder, size, camera.pass_params()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /strolle-gpu/src/surface.rs: -------------------------------------------------------------------------------- 1 | use glam::{UVec2, Vec3, Vec4Swizzles}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | use crate::{Normal, TexRgba32}; 6 | 7 | #[derive(Clone, Copy)] 8 | pub struct Surface { 9 | pub normal: Vec3, 10 | pub depth: f32, 11 | pub roughness: f32, 12 | } 13 | 14 | impl Surface { 15 | pub fn is_sky(self) -> bool { 16 | self.depth == 0.0 17 | } 18 | 19 | /// Returns a score `<0.0, 1.0>` that determines the similarity of two given 20 | /// surfaces. 21 | pub fn evaluate_similarity_to(self, other: Self) -> f32 { 22 | if self.is_sky() || other.is_sky() { 23 | return 0.0; 24 | } 25 | 26 | let normal_score = { 27 | let dot = self.normal.dot(other.normal).max(0.0); 28 | 29 | if dot <= 0.5 { 30 | 0.0 31 | } else { 32 | 2.0 * dot 33 | } 34 | }; 35 | 36 | let depth_score = { 37 | let t = (self.depth - other.depth).abs(); 38 | 39 | if t >= 0.1 * other.depth { 40 | 0.0 41 | } else { 42 | 1.0 43 | } 44 | }; 45 | 46 | normal_score * depth_score 47 | } 48 | } 49 | 50 | #[derive(Clone, Copy)] 51 | pub struct SurfaceMap<'a> { 52 | tex: TexRgba32<'a>, 53 | } 54 | 55 | impl<'a> SurfaceMap<'a> { 56 | pub fn new(tex: TexRgba32<'a>) -> Self { 57 | Self { tex } 58 | } 59 | 60 | pub fn get(self, screen_pos: UVec2) -> Surface { 61 | let d0 = self.tex.read(screen_pos); 62 | 63 | Surface { 64 | normal: Normal::decode(d0.xy()), 65 | depth: d0.z, 66 | roughness: d0.w, 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /strolle-shaders/src/atmosphere/generate_transmittance_lut.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | use super::utils::*; 4 | 5 | pub fn main(global_id: UVec3, out: TexRgba16) { 6 | let global_id = global_id.xy(); 7 | 8 | let uv = global_id.as_vec2() 9 | / Atmosphere::TRANSMITTANCE_LUT_RESOLUTION.as_vec2(); 10 | 11 | let sun_cos_theta = 2.0 * uv.x - 1.0; 12 | let sun_theta = sun_cos_theta.clamp(-1.0, 1.0).acos(); 13 | 14 | let height = lerp( 15 | Atmosphere::GROUND_RADIUS_MM, 16 | Atmosphere::ATMOSPHERE_RADIUS_MM, 17 | uv.y, 18 | ); 19 | 20 | let pos = vec3(0.0, height, 0.0); 21 | let sun_dir = vec3(0.0, sun_cos_theta, -sun_theta.sin()).normalize(); 22 | let out_val = eval(pos, sun_dir); 23 | 24 | unsafe { 25 | out.write(global_id, out_val.extend(1.0)); 26 | } 27 | } 28 | 29 | pub fn eval(pos: Vec3, sun_dir: Vec3) -> Vec3 { 30 | if Ray::new(pos, sun_dir).intersect_sphere(Atmosphere::GROUND_RADIUS_MM) 31 | > 0.0 32 | { 33 | return Default::default(); 34 | } 35 | 36 | let atmosphere_distance = Ray::new(pos, sun_dir) 37 | .intersect_sphere(Atmosphere::ATMOSPHERE_RADIUS_MM); 38 | 39 | let mut t = 0.0; 40 | let mut transmittance = Vec3::splat(1.0); 41 | let mut i = 0.0; 42 | 43 | while i < Atmosphere::TRANSMITTANCE_LUT_STEPS { 44 | let new_t = ((i + 0.3) / Atmosphere::TRANSMITTANCE_LUT_STEPS) 45 | * atmosphere_distance; 46 | 47 | let dt = new_t - t; 48 | 49 | t = new_t; 50 | 51 | let new_pos = pos + t * sun_dir; 52 | let (_, _, extinction) = eval_scattering(new_pos); 53 | 54 | transmittance *= (-dt * extinction).exp(); 55 | i += 1.0; 56 | } 57 | 58 | transmittance 59 | } 60 | -------------------------------------------------------------------------------- /strolle-shaders/src/gi_reprojection.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(descriptor_set = 0, binding = 0, uniform)] camera: &Camera, 7 | #[spirv(descriptor_set = 0, binding = 1)] prim_gbuffer_d0: TexRgba32, 8 | #[spirv(descriptor_set = 0, binding = 2)] prim_gbuffer_d1: TexRgba32, 9 | #[spirv(descriptor_set = 0, binding = 3)] reprojection_map: TexRgba32, 10 | #[spirv(descriptor_set = 0, binding = 4, storage_buffer)] 11 | in_reservoirs: &[Vec4], 12 | #[spirv(descriptor_set = 0, binding = 5, storage_buffer)] 13 | out_reservoirs: &mut [Vec4], 14 | ) { 15 | let screen_pos = global_id.xy(); 16 | let screen_idx = camera.screen_to_idx(screen_pos); 17 | let reprojection_map = ReprojectionMap::new(reprojection_map); 18 | 19 | if !camera.contains(screen_pos) { 20 | return; 21 | } 22 | 23 | // ------------------------------------------------------------------------- 24 | 25 | let hit = Hit::new( 26 | camera.ray(screen_pos), 27 | GBufferEntry::unpack([ 28 | prim_gbuffer_d0.read(screen_pos), 29 | prim_gbuffer_d1.read(screen_pos), 30 | ]), 31 | ); 32 | 33 | if hit.is_none() { 34 | return; 35 | } 36 | 37 | let reprojection = reprojection_map.get(screen_pos); 38 | 39 | let mut res = if reprojection.is_some() { 40 | GiReservoir::read( 41 | in_reservoirs, 42 | camera.screen_to_idx(reprojection.prev_pos_round()), 43 | ) 44 | } else { 45 | GiReservoir::default() 46 | }; 47 | 48 | res.confidence = 1.0; 49 | res.sample.v1_point = hit.point; 50 | res.write(out_reservoirs, screen_idx); 51 | } 52 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/gi_resolving.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | gpu, Camera, CameraBuffers, CameraComputePass, CameraController, Engine, 3 | Params, 4 | }; 5 | 6 | #[derive(Debug)] 7 | pub struct GiResolvingPass { 8 | pass: CameraComputePass, 9 | } 10 | 11 | impl GiResolvingPass { 12 | pub fn new

( 13 | engine: &Engine

, 14 | device: &wgpu::Device, 15 | _: &Camera, 16 | buffers: &CameraBuffers, 17 | ) -> Self 18 | where 19 | P: Params, 20 | { 21 | let pass = CameraComputePass::builder("gi_resolving") 22 | .bind([ 23 | &buffers.curr_camera.bind_readable(), 24 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 25 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 26 | &buffers.gi_reservoirs[1].bind_readable(), 27 | &buffers.gi_reservoirs[2].bind_readable(), 28 | &buffers.gi_reservoirs[0].bind_writable(), 29 | &buffers.gi_diff_samples.bind_writable(), 30 | &buffers.gi_spec_samples.bind_writable(), 31 | ]) 32 | .build(device, &engine.shaders.gi_resolving); 33 | 34 | Self { pass } 35 | } 36 | 37 | pub fn run( 38 | &self, 39 | camera: &CameraController, 40 | encoder: &mut wgpu::CommandEncoder, 41 | source: u32, 42 | ) { 43 | // This pass uses 8x8 warps: 44 | let size = (camera.camera.viewport.size + 7) / 8; 45 | 46 | self.pass.run( 47 | camera, 48 | encoder, 49 | size, 50 | gpu::GiResolvingPassParams { 51 | frame: camera.frame, 52 | source, 53 | }, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /strolle-shaders/src/atmosphere/utils.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | pub fn eval_scattering(pos: Vec3) -> (Vec3, f32, Vec3) { 4 | let altitude_km = (pos.length() - Atmosphere::GROUND_RADIUS_MM) * 1000.0; 5 | let rayleigh_density = (-altitude_km / 8.0).exp(); 6 | let mie_density = (-altitude_km / 1.2).exp(); 7 | 8 | let rayleigh_scattering = 9 | Atmosphere::RAYLEIGH_SCATTERING_BASE * rayleigh_density; 10 | 11 | let rayleigh_absorption = 12 | Atmosphere::RAYLEIGH_ABSORPTION_BASE * rayleigh_density; 13 | 14 | let mie_scattering = Atmosphere::MIE_SCATTERING_BASE * mie_density; 15 | let mie_absorption = Atmosphere::MIE_ABSORPTION_BASE * mie_density; 16 | 17 | let ozone_absorption = Atmosphere::OZONE_ABSORPTION_BASE 18 | * (1.0 - (altitude_km - 25.0).abs() / 15.0).max(0.0); 19 | 20 | let extinction = rayleigh_scattering 21 | + rayleigh_absorption 22 | + mie_scattering 23 | + mie_absorption 24 | + ozone_absorption; 25 | 26 | (rayleigh_scattering, mie_scattering, extinction) 27 | } 28 | 29 | pub fn eval_mie_phase(cos_theta: f32) -> f32 { 30 | const G: f32 = 0.8; 31 | const SCALE: f32 = 3.0 / (8.0 * PI); 32 | 33 | let num = (1.0 - G * G) * (1.0 + cos_theta * cos_theta); 34 | let denom = (2.0 + G * G) * (1.0 + G * G - 2.0 * G * cos_theta).powf(1.5); 35 | 36 | SCALE * num / denom 37 | } 38 | 39 | pub fn eval_rayleigh_phase(cos_theta: f32) -> f32 { 40 | const K: f32 = 3.0 / (16.0 * PI); 41 | 42 | K * (1.0 + cos_theta * cos_theta) 43 | } 44 | 45 | pub fn spherical_direction(theta: f32, phi: f32) -> Vec3 { 46 | let cos_phi = phi.cos(); 47 | let sin_phi = phi.sin(); 48 | let cos_theta = theta.cos(); 49 | let sin_theta = theta.sin(); 50 | 51 | vec3(sin_phi * sin_theta, cos_phi, sin_phi * cos_theta) 52 | } 53 | -------------------------------------------------------------------------------- /strolle/src/bvh/nodes.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, ops}; 2 | 3 | use super::{BvhNode, BvhNodeId}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct BvhNodes { 7 | pub nodes: Vec, 8 | pub free_nodes: Vec, 9 | } 10 | 11 | impl BvhNodes { 12 | pub fn add(&mut self, node: BvhNode) -> BvhNodeId { 13 | if let Some(id) = self.free_nodes.pop() { 14 | let prev_node = mem::replace(&mut self[id], node); 15 | 16 | if let BvhNode::Internal { 17 | left_id, right_id, .. 18 | } = prev_node 19 | { 20 | self.free_nodes.push(left_id); 21 | self.free_nodes.push(right_id); 22 | } 23 | 24 | id 25 | } else { 26 | self.nodes.push(node); 27 | 28 | BvhNodeId::new((self.nodes.len() - 1) as u32) 29 | } 30 | } 31 | 32 | pub fn remove(&mut self, id: BvhNodeId) -> BvhNode { 33 | self.free_nodes.push(id); 34 | 35 | mem::take(&mut self[id]) 36 | } 37 | 38 | pub fn remove_tree(&mut self, id: BvhNodeId) { 39 | self.free_nodes.push(id); 40 | } 41 | 42 | pub fn set_root(&mut self, node: BvhNode) -> Option { 43 | if self.nodes.is_empty() { 44 | self.nodes.push(node); 45 | None 46 | } else { 47 | Some(mem::replace(&mut self[BvhNodeId::root()], node)) 48 | } 49 | } 50 | } 51 | 52 | impl ops::Index for BvhNodes { 53 | type Output = BvhNode; 54 | 55 | fn index(&self, index: BvhNodeId) -> &Self::Output { 56 | &self.nodes[index.get() as usize] 57 | } 58 | } 59 | 60 | impl ops::IndexMut for BvhNodes { 61 | fn index_mut(&mut self, index: BvhNodeId) -> &mut Self::Output { 62 | &mut self.nodes[index.get() as usize] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | 3 | use crate::{Camera, CameraBuffers, Engine, Params}; 4 | 5 | macro_rules! passes { 6 | ([ $( $name:ident => $class:ident, )* ]) => { 7 | $( mod $name; )* 8 | $( pub use self::$name::*; )* 9 | 10 | #[derive(Debug)] 11 | pub struct CameraPasses { 12 | $( pub $name: $class, )* 13 | } 14 | 15 | impl CameraPasses { 16 | pub fn new

( 17 | engine: &Engine

, 18 | device: &wgpu::Device, 19 | config: &Camera, 20 | buffers: &CameraBuffers, 21 | ) -> Self 22 | where 23 | P: Params, 24 | { 25 | debug!("Initializing camera passes"); 26 | 27 | Self { 28 | $( $name: $class::new(engine, device, config, buffers), )* 29 | } 30 | } 31 | } 32 | }; 33 | } 34 | 35 | passes!([ 36 | atmosphere => AtmospherePass, 37 | bvh_heatmap => BvhHeatmapPass, 38 | di_resolving => DiResolvingPass, 39 | di_sampling => DiSamplingPass, 40 | di_spatial_resampling => DiSpatialResamplingPass, 41 | di_temporal_resampling => DiTemporalResamplingPass, 42 | frame_composition => FrameCompositionPass, 43 | frame_denoising => FrameDenoisingPass, 44 | frame_reprojection => FrameReprojectionPass, 45 | gi_preview_resampling => GiPreviewResamplingPass, 46 | gi_reprojection => GiReprojectionPass, 47 | gi_resolving => GiResolvingPass, 48 | gi_sampling => GiSamplingPass, 49 | gi_spatial_resampling => GiSpatialResamplingPass, 50 | gi_temporal_resampling => GiTemporalResamplingPass, 51 | prim_raster => PrimRasterPass, 52 | ref_shading => RefShadingPass, 53 | ref_tracing => RefTracingPass, 54 | ]); 55 | -------------------------------------------------------------------------------- /strolle/src/bvh/primitives.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::ops::Range; 3 | 4 | use super::{BvhPrimitive, BvhPrimitiveId, BvhPrimitivesRef}; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct BvhPrimitives { 8 | all: Vec, 9 | current: Vec, 10 | previous: Vec, 11 | } 12 | 13 | impl BvhPrimitives { 14 | pub fn add(&mut self, prim: BvhPrimitive) { 15 | self.all.push(prim); 16 | } 17 | 18 | pub fn update( 19 | &mut self, 20 | ids: Range, 21 | ) -> impl Iterator { 22 | self.all[ids].iter_mut() 23 | } 24 | 25 | pub fn current_ref(&self) -> BvhPrimitivesRef { 26 | BvhPrimitivesRef::new( 27 | BvhPrimitiveId::new(0), 28 | BvhPrimitiveId::new(self.current.len() as u32), 29 | ) 30 | } 31 | 32 | pub fn current(&self, range: BvhPrimitivesRef) -> &[BvhPrimitive] { 33 | let start = range.start().get() as usize; 34 | let end = range.end().get() as usize; 35 | 36 | &self.current[start..end] 37 | } 38 | 39 | pub fn current_mut( 40 | &mut self, 41 | range: BvhPrimitivesRef, 42 | ) -> &mut [BvhPrimitive] { 43 | let start = range.start().get() as usize; 44 | let end = range.end().get() as usize; 45 | 46 | &mut self.current[start..end] 47 | } 48 | 49 | pub fn copy_previous_to_current( 50 | &mut self, 51 | previous: BvhPrimitivesRef, 52 | current: BvhPrimitivesRef, 53 | ) { 54 | self.current[current.as_range()] 55 | .copy_from_slice(&self.previous[previous.as_range()]); 56 | } 57 | 58 | pub fn begin_refresh(&mut self) { 59 | self.current = 60 | self.all.iter().filter(|p| p.is_alive()).copied().collect(); 61 | } 62 | 63 | pub fn end_refresh(&mut self) { 64 | self.previous = mem::take(&mut self.current); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /strolle-gpu/src/reservoir.rs: -------------------------------------------------------------------------------- 1 | mod di; 2 | mod ephemeral; 3 | mod gi; 4 | mod mis; 5 | 6 | pub use self::di::*; 7 | pub use self::ephemeral::*; 8 | pub use self::gi::*; 9 | pub use self::mis::*; 10 | use crate::WhiteNoise; 11 | 12 | #[derive(Clone, Copy, Default, PartialEq)] 13 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 14 | pub struct Reservoir { 15 | pub sample: T, 16 | pub m: f32, 17 | pub w: f32, 18 | } 19 | 20 | impl Reservoir 21 | where 22 | T: Clone + Copy, 23 | { 24 | pub fn update( 25 | &mut self, 26 | wnoise: &mut WhiteNoise, 27 | sample: T, 28 | weight: f32, 29 | ) -> bool { 30 | self.m += 1.0; 31 | self.w += weight; 32 | 33 | if wnoise.sample() * self.w < weight { 34 | self.sample = sample; 35 | true 36 | } else { 37 | false 38 | } 39 | } 40 | 41 | pub fn merge( 42 | &mut self, 43 | wnoise: &mut WhiteNoise, 44 | sample: &Self, 45 | pdf: f32, 46 | ) -> bool { 47 | if sample.m <= 0.0 { 48 | return false; 49 | } 50 | 51 | self.m += sample.m - 1.0; 52 | self.update(wnoise, sample.sample, sample.w * sample.m * pdf) 53 | } 54 | 55 | pub fn clamp_m(&mut self, max: f32) { 56 | self.m = self.m.min(max); 57 | } 58 | 59 | pub fn clamp_w(&mut self, max: f32) { 60 | self.w = self.w.min(max); 61 | } 62 | 63 | pub fn norm(&mut self, pdf: f32, norm_num: f32, norm_denom: f32) { 64 | let denom = pdf * norm_denom; 65 | 66 | self.w = if denom == 0.0 { 67 | 0.0 68 | } else { 69 | (self.w * norm_num) / denom 70 | }; 71 | } 72 | 73 | pub fn norm_avg(&mut self, pdf: f32) { 74 | self.norm(pdf, 1.0, self.m); 75 | } 76 | 77 | pub fn norm_mis(&mut self, pdf: f32) { 78 | self.norm(pdf, 1.0, 1.0); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/di_resolving.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct DiResolvingPass { 7 | pass: CameraComputePass, 8 | } 9 | 10 | impl DiResolvingPass { 11 | pub fn new

( 12 | engine: &Engine

, 13 | device: &wgpu::Device, 14 | _: &Camera, 15 | buffers: &CameraBuffers, 16 | ) -> Self 17 | where 18 | P: Params, 19 | { 20 | let pass = CameraComputePass::builder("di_resolving") 21 | .bind([ 22 | &engine.triangles.bind_readable(), 23 | &engine.bvh.bind_readable(), 24 | &engine.materials.bind_readable(), 25 | &engine.lights.bind_readable(), 26 | &engine.images.bind_atlas(), 27 | &engine.world.bind_readable(), 28 | ]) 29 | .bind([ 30 | &buffers.curr_camera.bind_readable(), 31 | &buffers.atmosphere_transmittance_lut.bind_sampled(), 32 | &buffers.atmosphere_sky_lut.bind_sampled(), 33 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 34 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 35 | &buffers.di_reservoirs[2].bind_readable(), 36 | &buffers.di_reservoirs[0].bind_writable(), 37 | &buffers.di_diff_samples.bind_writable(), 38 | &buffers.di_spec_samples.bind_writable(), 39 | ]) 40 | .build(device, &engine.shaders.di_resolving); 41 | 42 | Self { pass } 43 | } 44 | 45 | pub fn run( 46 | &self, 47 | camera: &CameraController, 48 | encoder: &mut wgpu::CommandEncoder, 49 | ) { 50 | // This pass uses 8x8 warps: 51 | let size = (camera.camera.viewport.size + 7) / 8; 52 | 53 | self.pass.run(camera, encoder, size, camera.pass_params()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils/vec3_ext.rs: -------------------------------------------------------------------------------- 1 | use glam::{vec3, Vec3}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | pub trait Vec3Ext 6 | where 7 | Self: Sized, 8 | { 9 | /// Reflects this direction-vector around `other`. 10 | fn reflect(self, other: Self) -> Self; 11 | 12 | /// Clips this color-vector into given bounding box. 13 | /// 14 | /// See: 15 | /// - https://s3.amazonaws.com/arena-attachments/655504/c5c71c5507f0f8bf344252958254fb7d.pdf?1468341463 16 | fn clip(self, aabb_min: Self, aabb_max: Self) -> Self; 17 | 18 | /// Returns luminance of this color-vector. 19 | fn luma(self) -> f32; 20 | 21 | /// Returns perceptual luminance of this color-vector. 22 | /// 23 | /// As compared to the standard luminance, perceptual luminance gets a boost 24 | /// for darker colors and attenuates the brigher colors, so that comparisons 25 | /// between them behave more human-vision like. 26 | fn perc_luma(self) -> f32; 27 | 28 | /// Adjusts luminance of this color-vector. 29 | fn with_luma(self, luma: f32) -> Self; 30 | } 31 | 32 | impl Vec3Ext for Vec3 { 33 | fn reflect(self, other: Self) -> Self { 34 | self - 2.0 * other.dot(self) * other 35 | } 36 | 37 | fn clip(self, aabb_min: Self, aabb_max: Self) -> Self { 38 | let p_clip = 0.5 * (aabb_max + aabb_min); 39 | let e_clip = 0.5 * (aabb_max - aabb_min); 40 | let v_clip = self - p_clip; 41 | let v_unit = v_clip / e_clip; 42 | let a_unit = v_unit.abs(); 43 | let ma_unit = a_unit.max_element(); 44 | 45 | if ma_unit > 1.0 { 46 | p_clip + v_clip / ma_unit 47 | } else { 48 | self 49 | } 50 | } 51 | 52 | fn luma(self) -> f32 { 53 | self.dot(vec3(0.2126, 0.7152, 0.0722)) 54 | } 55 | 56 | fn perc_luma(self) -> f32 { 57 | self.luma().sqrt() 58 | } 59 | 60 | fn with_luma(self, luma: f32) -> Self { 61 | self * (luma / self.luma()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bevy-strolle/src/stages.rs: -------------------------------------------------------------------------------- 1 | mod extract; 2 | mod prepare; 3 | 4 | use bevy::prelude::*; 5 | use bevy::render::{Render, RenderSet}; 6 | 7 | pub(crate) fn setup(render_app: &mut App) { 8 | render_app.add_systems( 9 | ExtractSchedule, 10 | extract::meshes.in_set(RenderSet::ExtractCommands), 11 | ); 12 | 13 | render_app.add_systems( 14 | ExtractSchedule, 15 | extract::materials.in_set(RenderSet::ExtractCommands), 16 | ); 17 | 18 | render_app.add_systems( 19 | ExtractSchedule, 20 | extract::instances.in_set(RenderSet::ExtractCommands), 21 | ); 22 | 23 | render_app.add_systems( 24 | ExtractSchedule, 25 | extract::images.in_set(RenderSet::ExtractCommands), 26 | ); 27 | 28 | render_app.add_systems( 29 | ExtractSchedule, 30 | extract::lights.in_set(RenderSet::ExtractCommands), 31 | ); 32 | 33 | render_app.add_systems( 34 | ExtractSchedule, 35 | extract::cameras.in_set(RenderSet::ExtractCommands), 36 | ); 37 | 38 | render_app.add_systems( 39 | ExtractSchedule, 40 | extract::sun.in_set(RenderSet::ExtractCommands), 41 | ); 42 | 43 | render_app.add_systems(Render, prepare::meshes.in_set(RenderSet::Prepare)); 44 | 45 | render_app 46 | .add_systems(Render, prepare::materials.in_set(RenderSet::Prepare)); 47 | 48 | render_app.add_systems( 49 | Render, 50 | prepare::instances 51 | .in_set(RenderSet::Prepare) 52 | .after(prepare::meshes) 53 | .after(prepare::materials), 54 | ); 55 | 56 | render_app.add_systems(Render, prepare::images.in_set(RenderSet::Prepare)); 57 | render_app.add_systems(Render, prepare::lights.in_set(RenderSet::Prepare)); 58 | render_app.add_systems(Render, prepare::sun.in_set(RenderSet::Prepare)); 59 | render_app.add_systems(Render, prepare::cameras.in_set(RenderSet::Prepare)); 60 | 61 | render_app 62 | .add_systems(Render, prepare::flush.in_set(RenderSet::PrepareFlush)); 63 | } 64 | -------------------------------------------------------------------------------- /strolle/src/noise.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use image::io::Reader as ImageReader; 4 | 5 | use crate::{gpu, Bindable, Texture}; 6 | 7 | #[derive(Debug)] 8 | pub struct Noise { 9 | blue_noise: Texture, 10 | flushed: bool, 11 | } 12 | 13 | impl Noise { 14 | pub fn new(device: &wgpu::Device) -> Self { 15 | Self { 16 | blue_noise: Texture::builder("blue_noise") 17 | .with_size(gpu::BlueNoise::SIZE) 18 | .with_format(wgpu::TextureFormat::Rgba8Unorm) 19 | .with_usage(wgpu::TextureUsages::COPY_DST) 20 | .with_usage(wgpu::TextureUsages::STORAGE_BINDING) 21 | .build(device), 22 | flushed: false, 23 | } 24 | } 25 | 26 | pub fn bind_blue_noise(&self) -> impl Bindable + '_ { 27 | self.blue_noise.bind_readable() 28 | } 29 | 30 | pub fn flush(&mut self, queue: &wgpu::Queue) { 31 | if self.flushed { 32 | return; 33 | } 34 | 35 | let bytes = include_bytes!("../assets/blue-noise.png"); 36 | 37 | let img = ImageReader::new(Cursor::new(bytes)) 38 | .with_guessed_format() 39 | .unwrap() 40 | .decode() 41 | .unwrap(); 42 | 43 | let img = img.as_rgba8().unwrap().as_raw(); 44 | 45 | queue.write_texture( 46 | wgpu::ImageCopyTexture { 47 | texture: self.blue_noise.tex(), 48 | mip_level: 0, 49 | origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, 50 | aspect: wgpu::TextureAspect::All, 51 | }, 52 | img, 53 | wgpu::ImageDataLayout { 54 | offset: 0, 55 | bytes_per_row: Some(256 * 4), 56 | rows_per_image: None, 57 | }, 58 | wgpu::Extent3d { 59 | width: 256, 60 | height: 256, 61 | depth_or_array_layers: 1, 62 | }, 63 | ); 64 | 65 | self.flushed = true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /strolle/src/bvh/node.rs: -------------------------------------------------------------------------------- 1 | use super::BvhPrimitivesRef; 2 | use crate::BoundingBox; 3 | 4 | #[derive(Clone, Copy, Debug)] 5 | pub enum BvhNode { 6 | Internal { 7 | bounds: BoundingBox, 8 | primitives_ref: BvhPrimitivesRef, 9 | left_id: BvhNodeId, 10 | left_hash: BvhNodeHash, 11 | right_id: BvhNodeId, 12 | right_hash: BvhNodeHash, 13 | }, 14 | 15 | Leaf { 16 | bounds: BoundingBox, 17 | primitives_ref: BvhPrimitivesRef, 18 | }, 19 | } 20 | 21 | impl BvhNode { 22 | pub fn bounds(&self) -> BoundingBox { 23 | match self { 24 | BvhNode::Internal { bounds, .. } => *bounds, 25 | BvhNode::Leaf { bounds, .. } => *bounds, 26 | } 27 | } 28 | 29 | pub fn primitives_ref(&self) -> BvhPrimitivesRef { 30 | match self { 31 | BvhNode::Internal { primitives_ref, .. } => *primitives_ref, 32 | BvhNode::Leaf { primitives_ref, .. } => *primitives_ref, 33 | } 34 | } 35 | 36 | pub fn sah_cost(&self) -> f32 { 37 | if let BvhNode::Leaf { 38 | bounds, 39 | primitives_ref, 40 | } = self 41 | { 42 | (primitives_ref.len() as f32) * bounds.half_area() 43 | } else { 44 | 0.0 45 | } 46 | } 47 | } 48 | 49 | impl Default for BvhNode { 50 | fn default() -> Self { 51 | BvhNode::Leaf { 52 | bounds: Default::default(), 53 | primitives_ref: Default::default(), 54 | } 55 | } 56 | } 57 | 58 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 59 | pub struct BvhNodeId(u32); 60 | 61 | impl BvhNodeId { 62 | pub fn new(id: u32) -> Self { 63 | Self(id) 64 | } 65 | 66 | pub fn root() -> Self { 67 | Self::new(0) 68 | } 69 | 70 | pub fn get(&self) -> u32 { 71 | self.0 72 | } 73 | } 74 | 75 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 76 | pub struct BvhNodeHash(u64); 77 | 78 | impl BvhNodeHash { 79 | pub fn new(hash: u64) -> Self { 80 | Self(hash) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/ref_shading.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | use crate::{ 4 | gpu, Camera, CameraBuffers, CameraComputePass, CameraController, Engine, 5 | Params, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct RefShadingPass { 10 | pass: CameraComputePass, 11 | } 12 | 13 | impl RefShadingPass { 14 | pub fn new

( 15 | engine: &Engine

, 16 | device: &wgpu::Device, 17 | _: &Camera, 18 | buffers: &CameraBuffers, 19 | ) -> Self 20 | where 21 | P: Params, 22 | { 23 | let pass = CameraComputePass::builder("ref_shading") 24 | .bind([ 25 | &engine.triangles.bind_readable(), 26 | &engine.bvh.bind_readable(), 27 | &engine.lights.bind_readable(), 28 | &engine.materials.bind_readable(), 29 | &engine.images.bind_atlas(), 30 | &engine.world.bind_readable(), 31 | ]) 32 | .bind([ 33 | &buffers.curr_camera.bind_readable(), 34 | &buffers.prev_camera.bind_readable(), 35 | &buffers.atmosphere_transmittance_lut.bind_sampled(), 36 | &buffers.atmosphere_sky_lut.bind_sampled(), 37 | &buffers.ref_rays.bind_writable(), 38 | &buffers.ref_hits.bind_readable(), 39 | &buffers.ref_colors.bind_writable(), 40 | ]) 41 | .build(device, &engine.shaders.ref_shading); 42 | 43 | Self { pass } 44 | } 45 | 46 | pub fn run( 47 | &self, 48 | camera: &CameraController, 49 | encoder: &mut wgpu::CommandEncoder, 50 | depth: u8, 51 | ) { 52 | // This pass uses 8x8 warps: 53 | let size = (camera.camera.viewport.size + 7) / 8; 54 | 55 | let params = gpu::RefPassParams { 56 | seed: rand::thread_rng().gen(), 57 | frame: camera.frame, 58 | depth: depth as u32, 59 | }; 60 | 61 | self.pass.run(camera, encoder, size, params); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /strolle-shaders/src/ref_tracing.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(local_invocation_index)] local_idx: u32, 7 | #[spirv(push_constant)] params: &RefPassParams, 8 | #[spirv(workgroup)] stack: BvhStack, 9 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 10 | triangles: &[Triangle], 11 | #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] bvh: &[Vec4], 12 | #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] 13 | materials: &[Material], 14 | #[spirv(descriptor_set = 0, binding = 3)] atlas_tex: Tex, 15 | #[spirv(descriptor_set = 0, binding = 4)] atlas_sampler: &Sampler, 16 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 17 | #[spirv(descriptor_set = 1, binding = 1, storage_buffer)] rays: &[Vec4], 18 | #[spirv(descriptor_set = 1, binding = 2, storage_buffer)] 19 | hits: &mut [Vec4], 20 | ) { 21 | let screen_pos = global_id.xy(); 22 | let screen_idx = camera.screen_to_idx(screen_pos); 23 | let triangles = TrianglesView::new(triangles); 24 | let bvh = BvhView::new(bvh); 25 | let materials = MaterialsView::new(materials); 26 | 27 | if !camera.contains(screen_pos) { 28 | return; 29 | } 30 | 31 | // ------------------------------------------------------------------------- 32 | 33 | let ray = if params.depth == 0 { 34 | camera.ray(screen_pos) 35 | } else { 36 | let d0 = rays[3 * screen_idx]; 37 | let d1 = rays[3 * screen_idx + 1]; 38 | 39 | if d1 == Default::default() { 40 | return; 41 | } 42 | 43 | Ray::new(d0.xyz(), d1.xyz()) 44 | }; 45 | 46 | let (hit, _) = ray.trace( 47 | local_idx, 48 | stack, 49 | triangles, 50 | bvh, 51 | materials, 52 | atlas_tex, 53 | atlas_sampler, 54 | ); 55 | 56 | let [hit_d0, hit_d1] = hit.pack(); 57 | 58 | hits[2 * screen_idx] = hit_d0; 59 | hits[2 * screen_idx + 1] = hit_d1; 60 | } 61 | -------------------------------------------------------------------------------- /strolle/src/light.rs: -------------------------------------------------------------------------------- 1 | use glam::{vec4, Vec3}; 2 | 3 | use crate::gpu; 4 | 5 | #[derive(Clone, Debug)] 6 | pub enum Light { 7 | Point { 8 | position: Vec3, 9 | radius: f32, 10 | color: Vec3, 11 | range: f32, 12 | }, 13 | 14 | Spot { 15 | position: Vec3, 16 | radius: f32, 17 | color: Vec3, 18 | range: f32, 19 | direction: Vec3, 20 | angle: f32, 21 | }, 22 | } 23 | 24 | impl Light { 25 | pub(crate) fn serialize(&self) -> gpu::Light { 26 | let d0; 27 | let d1; 28 | let d2; 29 | 30 | match self { 31 | Light::Point { 32 | position, 33 | radius, 34 | color, 35 | range, 36 | } => { 37 | d0 = position.extend(*radius); 38 | d1 = color.extend(*range); 39 | 40 | d2 = vec4( 41 | f32::from_bits(gpu::Light::TYPE_POINT), 42 | Default::default(), 43 | Default::default(), 44 | Default::default(), 45 | ); 46 | } 47 | 48 | Light::Spot { 49 | position, 50 | radius, 51 | color, 52 | range, 53 | direction, 54 | angle, 55 | } => { 56 | let direction = gpu::Normal::encode(*direction); 57 | 58 | d0 = position.extend(*radius); 59 | d1 = color.extend(*range); 60 | 61 | d2 = vec4( 62 | f32::from_bits(gpu::Light::TYPE_SPOT), 63 | direction.x, 64 | direction.y, 65 | *angle, 66 | ); 67 | } 68 | } 69 | 70 | gpu::Light { 71 | d0, 72 | d1, 73 | d2, 74 | d3: Default::default(), 75 | prev_d0: Default::default(), 76 | prev_d1: Default::default(), 77 | prev_d2: Default::default(), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /strolle-gpu/src/reservoir/ephemeral.rs: -------------------------------------------------------------------------------- 1 | use core::ops::{Deref, DerefMut}; 2 | 3 | use crate::{ 4 | Hit, LightId, LightRadiance, LightsView, Reservoir, Vec3Ext, WhiteNoise, 5 | World, 6 | }; 7 | 8 | #[derive(Clone, Copy, Default)] 9 | pub struct EphemeralReservoir { 10 | pub reservoir: Reservoir, 11 | } 12 | 13 | impl EphemeralReservoir { 14 | pub fn build( 15 | wnoise: &mut WhiteNoise, 16 | lights: LightsView, 17 | world: World, 18 | hit: Hit, 19 | ) -> Self { 20 | let mut res = EphemeralReservoir::default(); 21 | let mut res_pdf = 0.0; 22 | 23 | // TODO rust-gpu seems to miscompile `.min()` 24 | let max_samples = if world.light_count < 16 { 25 | world.light_count 26 | } else { 27 | 16 28 | }; 29 | 30 | let sample_ipdf = world.light_count as f32; 31 | let mut sample_nth = 0; 32 | 33 | while sample_nth < max_samples { 34 | let light_id = 35 | LightId::new(wnoise.sample_int() % world.light_count); 36 | 37 | let light_rad = lights.get(light_id).radiance(hit); 38 | 39 | let sample = EphemeralSample { 40 | light_id, 41 | light_rad, 42 | }; 43 | 44 | let sample_pdf = sample.pdf(); 45 | 46 | if res.update(wnoise, sample, sample_pdf * sample_ipdf) { 47 | res_pdf = sample_pdf; 48 | } 49 | 50 | sample_nth += 1; 51 | } 52 | 53 | res.norm_avg(res_pdf); 54 | res 55 | } 56 | } 57 | 58 | impl Deref for EphemeralReservoir { 59 | type Target = Reservoir; 60 | 61 | fn deref(&self) -> &Self::Target { 62 | &self.reservoir 63 | } 64 | } 65 | 66 | impl DerefMut for EphemeralReservoir { 67 | fn deref_mut(&mut self) -> &mut Self::Target { 68 | &mut self.reservoir 69 | } 70 | } 71 | 72 | #[derive(Clone, Copy, Default)] 73 | pub struct EphemeralSample { 74 | pub light_id: LightId, 75 | pub light_rad: LightRadiance, 76 | } 77 | 78 | impl EphemeralSample { 79 | pub fn pdf(self) -> f32 { 80 | self.light_rad.radiance.perc_luma() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bevy-strolle/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod camera; 2 | mod debug; 3 | mod event; 4 | pub mod graph; 5 | mod rendering_node; 6 | mod stages; 7 | mod state; 8 | mod sun; 9 | mod utils; 10 | 11 | pub mod prelude { 12 | pub use crate::*; 13 | } 14 | 15 | use std::ops; 16 | 17 | use bevy::prelude::*; 18 | use bevy::render::render_resource::Texture; 19 | use bevy::render::renderer::RenderDevice; 20 | use bevy::render::RenderApp; 21 | pub use strolle as st; 22 | 23 | pub use self::camera::*; 24 | pub use self::debug::*; 25 | pub use self::event::*; 26 | pub(crate) use self::rendering_node::*; 27 | pub(crate) use self::state::*; 28 | pub use self::sun::*; 29 | 30 | pub struct StrollePlugin; 31 | 32 | impl Plugin for StrollePlugin { 33 | fn build(&self, app: &mut App) { 34 | app.add_event::(); 35 | app.insert_resource(StrolleSun::default()); 36 | 37 | if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { 38 | render_app.insert_resource(SyncedState::default()); 39 | 40 | stages::setup(render_app); 41 | graph::setup(render_app); 42 | } 43 | } 44 | 45 | fn finish(&self, app: &mut App) { 46 | let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { 47 | return; 48 | }; 49 | 50 | let render_device = render_app.world.resource::(); 51 | let engine = st::Engine::new(render_device.wgpu_device()); 52 | 53 | render_app.insert_resource(EngineResource(engine)); 54 | } 55 | } 56 | 57 | #[derive(Resource)] 58 | struct EngineResource(st::Engine); 59 | 60 | #[derive(Clone, Debug)] 61 | struct EngineParams; 62 | 63 | impl st::Params for EngineParams { 64 | type ImageHandle = AssetId; 65 | type ImageTexture = Texture; 66 | type InstanceHandle = Entity; 67 | type LightHandle = Entity; 68 | type MaterialHandle = AssetId; 69 | type MeshHandle = AssetId; 70 | } 71 | 72 | impl ops::Deref for EngineResource { 73 | type Target = st::Engine; 74 | 75 | fn deref(&self) -> &Self::Target { 76 | &self.0 77 | } 78 | } 79 | 80 | impl ops::DerefMut for EngineResource { 81 | fn deref_mut(&mut self) -> &mut Self::Target { 82 | &mut self.0 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /strolle-gpu/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Common structs, algorithms etc. used by Strolle's shaders and renderer. 2 | 3 | #![cfg_attr(target_arch = "spirv", no_std)] 4 | #![allow(clippy::len_without_is_empty)] 5 | #![allow(clippy::manual_range_contains)] 6 | #![allow(clippy::too_many_arguments)] 7 | 8 | mod atmosphere; 9 | mod brdf; 10 | mod bvh_view; 11 | mod camera; 12 | mod frame; 13 | mod gbuffer; 14 | mod hit; 15 | mod light; 16 | mod lights; 17 | mod material; 18 | mod materials; 19 | mod noise; 20 | mod normal; 21 | mod passes; 22 | mod ray; 23 | mod reprojection; 24 | mod reservoir; 25 | mod surface; 26 | mod triangle; 27 | mod triangles; 28 | mod utils; 29 | mod world; 30 | 31 | pub use self::atmosphere::*; 32 | pub use self::brdf::*; 33 | pub use self::bvh_view::*; 34 | pub use self::camera::*; 35 | pub use self::frame::*; 36 | pub use self::gbuffer::*; 37 | pub use self::hit::*; 38 | pub use self::light::*; 39 | pub use self::lights::*; 40 | pub use self::material::*; 41 | pub use self::materials::*; 42 | pub use self::noise::*; 43 | pub use self::normal::*; 44 | pub use self::passes::*; 45 | pub use self::ray::*; 46 | pub use self::reprojection::*; 47 | pub use self::reservoir::*; 48 | pub use self::surface::*; 49 | pub use self::triangle::*; 50 | pub use self::triangles::*; 51 | pub use self::utils::*; 52 | pub use self::world::*; 53 | 54 | pub mod prelude { 55 | pub use core::f32::consts::PI; 56 | 57 | pub use spirv_std::arch::IndexUnchecked; 58 | pub use spirv_std::glam::*; 59 | #[cfg(target_arch = "spirv")] 60 | pub use spirv_std::num_traits::Float; 61 | pub use spirv_std::{spirv, Image, Sampler}; 62 | 63 | pub use crate::*; 64 | } 65 | 66 | /// Stack for nodes yet-to-be-visited when traversing the BVH. 67 | /// 68 | /// For performance reasons, we use a per-workgroup shared-memory array where 69 | /// each workgroup-thread simply indexes into a different slice of this memory. 70 | pub type BvhStack<'a> = &'a mut [u32; BVH_STACK_SIZE * 8 * 8]; 71 | 72 | /// Maximum stack size per each workgroup-thread when traversing the BVH. 73 | /// 74 | /// Affects the maximum size of BVH tree (it must not grow larger than 75 | /// `2 ^ BVH_STACK_SIZE`). 76 | pub const BVH_STACK_SIZE: usize = 24; 77 | 78 | /// Golden angle, used for spatial filters. 79 | pub const GOLDEN_ANGLE: f32 = 2.39996; 80 | -------------------------------------------------------------------------------- /strolle/src/bvh.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod node; 3 | mod nodes; 4 | mod primitive; 5 | mod primitives; 6 | mod serializer; 7 | 8 | use std::fmt::Debug; 9 | use std::ops::Range; 10 | 11 | use spirv_std::glam::Vec4; 12 | 13 | pub use self::node::*; 14 | pub use self::nodes::*; 15 | pub use self::primitive::*; 16 | pub use self::primitives::*; 17 | use crate::{ 18 | utils, Bindable, BufferFlushOutcome, MappedStorageBuffer, Materials, Params, 19 | }; 20 | 21 | #[derive(Debug)] 22 | pub struct Bvh { 23 | buffer: MappedStorageBuffer>, 24 | nodes: BvhNodes, 25 | primitives: BvhPrimitives, 26 | } 27 | 28 | impl Bvh { 29 | pub fn new(device: &wgpu::Device) -> Self { 30 | Self { 31 | buffer: MappedStorageBuffer::new_default(device, "bvh"), 32 | nodes: Default::default(), 33 | primitives: Default::default(), 34 | } 35 | } 36 | 37 | pub fn add(&mut self, prim: BvhPrimitive) { 38 | self.primitives.add(prim); 39 | } 40 | 41 | pub fn update( 42 | &mut self, 43 | ids: Range, 44 | ) -> impl Iterator { 45 | self.primitives.update(ids) 46 | } 47 | 48 | pub fn refresh

(&mut self, materials: &Materials

) 49 | where 50 | P: Params, 51 | { 52 | utils::measure("tick.bvh.begin", || { 53 | self.primitives.begin_refresh(); 54 | }); 55 | 56 | utils::measure("tick.bvh.build", || { 57 | builder::run(&mut self.nodes, &mut self.primitives); 58 | }); 59 | 60 | utils::measure("tick.bvh.serialize", || { 61 | serializer::run( 62 | materials, 63 | &self.nodes, 64 | &self.primitives, 65 | &mut self.buffer, 66 | ); 67 | }); 68 | 69 | self.primitives.end_refresh(); 70 | } 71 | 72 | pub fn flush( 73 | &mut self, 74 | device: &wgpu::Device, 75 | queue: &wgpu::Queue, 76 | ) -> BufferFlushOutcome { 77 | self.buffer.flush(device, queue) 78 | } 79 | 80 | pub fn len(&self) -> usize { 81 | self.nodes.nodes.len() 82 | } 83 | 84 | pub fn bind_readable(&self) -> impl Bindable + '_ { 85 | self.buffer.bind_readable() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /strolle/src/shaders.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | 3 | macro_rules! shaders { 4 | ([ $( $name:ident, )* ]) => { 5 | #[derive(Debug)] 6 | pub struct Shaders { 7 | $( pub $name: (wgpu::ShaderModule, &'static str), )* 8 | } 9 | 10 | impl Shaders { 11 | pub fn new(device: &wgpu::Device) -> Self { 12 | $( 13 | info!("Initializing shader: {}", stringify!($name)); 14 | 15 | let module = wgpu::include_spirv!( 16 | env!(concat!("strolle_shaders::", stringify!($name), ".path")) 17 | ); 18 | 19 | // Safety: fingers crossed™ 20 | // 21 | // We do our best, but our shaders are so array-intensive 22 | // that adding the checks decreases performance by 33%, so 23 | // it's pretty much a no-go. 24 | let module = unsafe { 25 | device.create_shader_module_unchecked(module) 26 | }; 27 | 28 | let entry_point = env!(concat!("strolle_shaders::", stringify!($name), ".entry_point")); 29 | 30 | let $name = (module, entry_point); 31 | )* 32 | 33 | Self { 34 | $($name,)* 35 | } 36 | } 37 | } 38 | }; 39 | } 40 | 41 | shaders!([ 42 | atmosphere_generate_scattering_lut, 43 | atmosphere_generate_sky_lut, 44 | atmosphere_generate_transmittance_lut, 45 | bvh_heatmap, 46 | di_resolving, 47 | di_sampling, 48 | di_spatial_resampling_pick, 49 | di_spatial_resampling_sample, 50 | di_spatial_resampling_trace, 51 | di_temporal_resampling, 52 | frame_composition_fs, 53 | frame_composition_vs, 54 | frame_denoising_estimate_variance, 55 | frame_denoising_reproject, 56 | frame_denoising_wavelet, 57 | frame_reprojection, 58 | gi_preview_resampling, 59 | gi_reprojection, 60 | gi_resolving, 61 | gi_sampling_a, 62 | gi_sampling_b, 63 | gi_spatial_resampling_pick, 64 | gi_spatial_resampling_sample, 65 | gi_spatial_resampling_trace, 66 | gi_temporal_resampling, 67 | prim_raster_fs, 68 | prim_raster_vs, 69 | ref_shading, 70 | ref_tracing, 71 | ]); 72 | -------------------------------------------------------------------------------- /strolle/src/bvh/primitive.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | use std::ops::Range; 3 | 4 | use glam::Vec3; 5 | 6 | use crate::gpu; 7 | use crate::utils::BoundingBox; 8 | 9 | #[derive(Clone, Copy, Debug)] 10 | pub struct BvhPrimitive { 11 | pub triangle_id: gpu::TriangleId, 12 | pub material_id: gpu::MaterialId, 13 | pub center: Vec3, 14 | pub bounds: BoundingBox, 15 | } 16 | 17 | impl BvhPrimitive { 18 | pub fn kill(&mut self) { 19 | self.center = Vec3::MAX; 20 | } 21 | 22 | pub fn is_alive(&self) -> bool { 23 | self.center.x != f32::MAX 24 | } 25 | } 26 | 27 | impl Hash for BvhPrimitive { 28 | fn hash(&self, state: &mut H) 29 | where 30 | H: Hasher, 31 | { 32 | self.center.x.to_bits().hash(state); 33 | self.center.y.to_bits().hash(state); 34 | self.center.z.to_bits().hash(state); 35 | } 36 | } 37 | 38 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 39 | pub struct BvhPrimitiveId(u32); 40 | 41 | impl BvhPrimitiveId { 42 | pub fn new(id: u32) -> Self { 43 | Self(id) 44 | } 45 | 46 | pub fn get(&self) -> u32 { 47 | self.0 48 | } 49 | } 50 | 51 | #[derive(Clone, Copy, Debug)] 52 | pub struct BvhPrimitivesRef { 53 | start: BvhPrimitiveId, 54 | end: BvhPrimitiveId, 55 | } 56 | 57 | impl BvhPrimitivesRef { 58 | pub fn new(start: BvhPrimitiveId, end: BvhPrimitiveId) -> Self { 59 | Self { start, end } 60 | } 61 | 62 | pub fn start(&self) -> BvhPrimitiveId { 63 | self.start 64 | } 65 | 66 | pub fn end(&self) -> BvhPrimitiveId { 67 | self.end 68 | } 69 | 70 | pub fn offset(&mut self, offset: i32) { 71 | self.start = 72 | BvhPrimitiveId::new((self.start.get() as i32 + offset) as u32); 73 | 74 | self.end = BvhPrimitiveId::new((self.end.get() as i32 + offset) as u32); 75 | } 76 | 77 | pub fn as_range(&self) -> Range { 78 | let start = self.start.get() as usize; 79 | let end = self.end.get() as usize; 80 | 81 | start..end 82 | } 83 | 84 | pub fn len(&self) -> usize { 85 | (self.end.get() - self.start.get()) as usize 86 | } 87 | } 88 | 89 | impl Default for BvhPrimitivesRef { 90 | fn default() -> Self { 91 | Self::new(BvhPrimitiveId::new(0), BvhPrimitiveId::new(0)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /strolle-shaders/src/gi_resolving.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(push_constant)] params: &GiResolvingPassParams, 7 | #[spirv(descriptor_set = 0, binding = 0, uniform)] camera: &Camera, 8 | #[spirv(descriptor_set = 0, binding = 1)] prim_gbuffer_d0: TexRgba32, 9 | #[spirv(descriptor_set = 0, binding = 2)] prim_gbuffer_d1: TexRgba32, 10 | #[spirv(descriptor_set = 0, binding = 3, storage_buffer)] 11 | in_reservoirs_a: &[Vec4], 12 | #[spirv(descriptor_set = 0, binding = 4, storage_buffer)] 13 | in_reservoirs_b: &[Vec4], 14 | #[spirv(descriptor_set = 0, binding = 5, storage_buffer)] 15 | out_reservoirs: &mut [Vec4], 16 | #[spirv(descriptor_set = 0, binding = 6)] diff_output: TexRgba32, 17 | #[spirv(descriptor_set = 0, binding = 7)] spec_output: TexRgba32, 18 | ) { 19 | let screen_pos = global_id.xy(); 20 | let screen_idx = camera.screen_to_idx(screen_pos); 21 | 22 | if !camera.contains(screen_pos) { 23 | return; 24 | } 25 | 26 | // ------------------------------------------------------------------------- 27 | 28 | let hit = Hit::new( 29 | camera.ray(screen_pos), 30 | GBufferEntry::unpack([ 31 | prim_gbuffer_d0.read(screen_pos), 32 | prim_gbuffer_d1.read(screen_pos), 33 | ]), 34 | ); 35 | 36 | let res = GiReservoir::read(out_reservoirs, screen_idx); 37 | 38 | let confidence; 39 | let radiance; 40 | 41 | if hit.is_some() { 42 | confidence = res.confidence; 43 | radiance = res.w * res.sample.cosine(hit) * res.sample.radiance; 44 | } else { 45 | confidence = 1.0; 46 | radiance = Vec3::ZERO; 47 | }; 48 | 49 | unsafe { 50 | let diff_brdf = (1.0 - hit.gbuffer.metallic) / PI; 51 | let spec_brdf = res.sample.spec_brdf(hit); 52 | 53 | diff_output 54 | .write(screen_pos, (radiance * diff_brdf).extend(confidence)); 55 | 56 | spec_output 57 | .write(screen_pos, (radiance * spec_brdf).extend(confidence)); 58 | } 59 | 60 | // --- 61 | 62 | if params.source == 0 { 63 | GiReservoir::copy(in_reservoirs_a, out_reservoirs, screen_idx); 64 | } else { 65 | GiReservoir::copy(in_reservoirs_b, out_reservoirs, screen_idx); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /strolle-shaders/src/bvh_heatmap.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(local_invocation_index)] local_idx: u32, 7 | #[spirv(workgroup)] stack: BvhStack, 8 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 9 | triangles: &[Triangle], 10 | #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] bvh: &[Vec4], 11 | #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] 12 | materials: &[Material], 13 | #[spirv(descriptor_set = 0, binding = 3)] atlas_tex: Tex, 14 | #[spirv(descriptor_set = 0, binding = 4)] atlas_sampler: &Sampler, 15 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 16 | #[spirv(descriptor_set = 1, binding = 1)] output: TexRgba32, 17 | ) { 18 | let screen_pos = global_id.xy(); 19 | let triangles = TrianglesView::new(triangles); 20 | let bvh = BvhView::new(bvh); 21 | let materials = MaterialsView::new(materials); 22 | 23 | if !camera.contains(screen_pos) { 24 | return; 25 | } 26 | 27 | // ------------------------------------------------------------------------- 28 | 29 | let (_, used_memory) = camera.ray(screen_pos).trace( 30 | local_idx, 31 | stack, 32 | triangles, 33 | bvh, 34 | materials, 35 | atlas_tex, 36 | atlas_sampler, 37 | ); 38 | 39 | let color = gradient( 40 | [ 41 | vec3(0.0, 0.0, 1.0), 42 | vec3(0.0, 1.0, 0.0), 43 | vec3(1.0, 0.0, 0.0), 44 | vec3(0.0, 0.0, 0.0), 45 | ], 46 | used_memory as f32 / 8192.0, 47 | ); 48 | 49 | unsafe { 50 | output.write(screen_pos, color.extend(1.0)); 51 | } 52 | } 53 | 54 | fn gradient(colors: [Vec3; N], progress: f32) -> Vec3 { 55 | if progress <= 0.0 { 56 | return colors[0]; 57 | } 58 | 59 | let step = 1.0 / (N as f32 - 1.0); 60 | let mut i = 0; 61 | 62 | while i < (N - 1) { 63 | let min = step * (i as f32); 64 | let max = step * (i as f32 + 1.0); 65 | 66 | if progress >= min && progress <= max { 67 | let rhs = (progress - min) / step; 68 | let lhs = 1.0 - rhs; 69 | 70 | return lhs * colors[i] + rhs * colors[i + 1]; 71 | } 72 | 73 | i += 1; 74 | } 75 | 76 | colors[N - 1] 77 | } 78 | -------------------------------------------------------------------------------- /strolle/src/utils/bounding_box.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign}; 2 | 3 | use spirv_std::glam::Vec3; 4 | 5 | #[derive(Clone, Copy, Debug, PartialEq)] 6 | pub struct BoundingBox { 7 | min: Vec3, 8 | max: Vec3, 9 | } 10 | 11 | impl BoundingBox { 12 | pub fn new(min: Vec3, max: Vec3) -> Self { 13 | Self { min, max } 14 | } 15 | 16 | pub fn min(&self) -> Vec3 { 17 | self.min 18 | } 19 | 20 | pub fn max(&self) -> Vec3 { 21 | self.max 22 | } 23 | 24 | pub fn extent(&self) -> Vec3 { 25 | self.max() - self.min() 26 | } 27 | 28 | pub fn half_area(&self) -> f32 { 29 | let extent = self.extent(); 30 | 31 | extent.x * extent.y + extent.y * extent.z + extent.z * extent.x 32 | } 33 | 34 | pub fn is_set(&self) -> bool { 35 | self.min.x != Self::default().min.x 36 | } 37 | } 38 | 39 | impl Default for BoundingBox { 40 | fn default() -> Self { 41 | Self::new(Vec3::MAX, Vec3::MIN) 42 | } 43 | } 44 | 45 | impl Add for BoundingBox { 46 | type Output = Self; 47 | 48 | fn add(mut self, rhs: Vec3) -> Self::Output { 49 | self += rhs; 50 | self 51 | } 52 | } 53 | 54 | impl AddAssign for BoundingBox { 55 | fn add_assign(&mut self, rhs: Vec3) { 56 | self.min = self.min.min(rhs); 57 | self.max = self.max.max(rhs); 58 | } 59 | } 60 | 61 | impl FromIterator for BoundingBox { 62 | fn from_iter(iter: T) -> Self 63 | where 64 | T: IntoIterator, 65 | { 66 | let mut this = Self::default(); 67 | 68 | for item in iter { 69 | this += item; 70 | } 71 | 72 | this 73 | } 74 | } 75 | 76 | impl Add for BoundingBox { 77 | type Output = Self; 78 | 79 | fn add(mut self, rhs: Self) -> Self::Output { 80 | self += rhs; 81 | self 82 | } 83 | } 84 | 85 | impl AddAssign for BoundingBox { 86 | fn add_assign(&mut self, rhs: Self) { 87 | *self += rhs.min; 88 | *self += rhs.max; 89 | } 90 | } 91 | 92 | impl FromIterator for BoundingBox { 93 | fn from_iter(iter: T) -> Self 94 | where 95 | T: IntoIterator, 96 | { 97 | let mut this = Self::default(); 98 | 99 | for item in iter { 100 | this += item; 101 | } 102 | 103 | this 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /strolle-gpu/src/reprojection.rs: -------------------------------------------------------------------------------- 1 | use glam::{vec2, vec4, UVec2, Vec2, Vec4}; 2 | 3 | use crate::TexRgba32; 4 | 5 | #[derive(Clone, Copy, Default)] 6 | pub struct Reprojection { 7 | pub prev_x: f32, 8 | pub prev_y: f32, 9 | pub confidence: f32, 10 | pub validity: u32, 11 | } 12 | 13 | impl Reprojection { 14 | pub fn serialize(self) -> Vec4 { 15 | vec4( 16 | self.prev_x, 17 | self.prev_y, 18 | self.confidence, 19 | f32::from_bits(self.validity), 20 | ) 21 | } 22 | 23 | pub fn deserialize(d0: Vec4) -> Self { 24 | Self { 25 | prev_x: d0.x, 26 | prev_y: d0.y, 27 | confidence: d0.z, 28 | validity: d0.w.to_bits(), 29 | } 30 | } 31 | 32 | pub fn is_some(self) -> bool { 33 | self.confidence > 0.0 34 | } 35 | 36 | pub fn is_none(self) -> bool { 37 | !self.is_some() 38 | } 39 | 40 | pub fn prev_pos(self) -> Vec2 { 41 | vec2(self.prev_x, self.prev_y) 42 | } 43 | 44 | pub fn prev_pos_round(self) -> UVec2 { 45 | self.prev_pos().round().as_uvec2() 46 | } 47 | 48 | pub fn prev_pos_fract(self) -> Vec2 { 49 | self.prev_pos().fract() 50 | } 51 | 52 | pub fn is_exact(self) -> bool { 53 | self.prev_pos_fract().length_squared() == 0.0 54 | } 55 | } 56 | 57 | pub struct ReprojectionMap<'a> { 58 | tex: TexRgba32<'a>, 59 | } 60 | 61 | impl<'a> ReprojectionMap<'a> { 62 | pub fn new(tex: TexRgba32<'a>) -> Self { 63 | Self { tex } 64 | } 65 | 66 | pub fn get(self, screen_pos: UVec2) -> Reprojection { 67 | Reprojection::deserialize(self.tex.read(screen_pos)) 68 | } 69 | 70 | pub fn set(self, screen_pos: UVec2, reprojection: &Reprojection) { 71 | unsafe { 72 | self.tex.write(screen_pos, reprojection.serialize()); 73 | } 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | 81 | #[test] 82 | fn serialization() { 83 | let target = Reprojection { 84 | prev_x: 123.45, 85 | prev_y: 234.56, 86 | confidence: 1.23, 87 | validity: 0xcafebabe, 88 | }; 89 | 90 | let target = Reprojection::deserialize(target.serialize()); 91 | 92 | assert_eq!(123.45, target.prev_x); 93 | assert_eq!(234.56, target.prev_y); 94 | assert_eq!(1.23, target.confidence); 95 | assert_eq!(0xcafebabe, target.validity); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /strolle-gpu/src/noise/white.rs: -------------------------------------------------------------------------------- 1 | use core::f32::consts::PI; 2 | 3 | use glam::{vec2, vec3, UVec2, Vec2, Vec3}; 4 | #[cfg(target_arch = "spirv")] 5 | use spirv_std::num_traits::Float; 6 | 7 | use crate::F32Ext; 8 | 9 | #[derive(Clone, Copy)] 10 | pub struct WhiteNoise { 11 | state: u32, 12 | } 13 | 14 | impl WhiteNoise { 15 | pub fn new(seed: u32, id: UVec2) -> Self { 16 | Self { 17 | state: seed ^ (48619 * id.x) ^ (95461 * id.y), 18 | } 19 | } 20 | 21 | pub fn from_state(state: u32) -> Self { 22 | Self { state } 23 | } 24 | 25 | pub fn state(self) -> u32 { 26 | self.state 27 | } 28 | 29 | /// Generates a uniform sample in range `<0.0, 1.0>`. 30 | pub fn sample(&mut self) -> f32 { 31 | (self.sample_int() as f32) / (u32::MAX as f32) 32 | } 33 | 34 | /// Generates a uniform sample in range `<0, u32::MAX>`. 35 | pub fn sample_int(&mut self) -> u32 { 36 | self.state = self.state * 747796405 + 2891336453; 37 | 38 | let word = 39 | ((self.state >> ((self.state >> 28) + 4)) ^ self.state) * 277803737; 40 | 41 | (word >> 22) ^ word 42 | } 43 | 44 | /// Generates a uniform sample on a circle. 45 | pub fn sample_circle(&mut self) -> Vec2 { 46 | let angle = self.sample() * PI * 2.0; 47 | 48 | vec2(angle.cos(), angle.sin()) 49 | } 50 | 51 | /// Generates a uniform sample inside of a disk. 52 | pub fn sample_disk(&mut self) -> Vec2 { 53 | let radius = self.sample().sqrt(); 54 | 55 | self.sample_circle() * radius 56 | } 57 | 58 | /// Generates a uniform sample on a sphere. 59 | pub fn sample_sphere(&mut self) -> Vec3 { 60 | let phi = self.sample() * 2.0 * PI; 61 | let cos_theta = self.sample() * 2.0 - 1.0; 62 | let u = self.sample(); 63 | 64 | let theta = cos_theta.acos(); 65 | let r = u.sqrt(); 66 | 67 | vec3( 68 | r * theta.sin() * phi.cos(), 69 | r * theta.sin() * phi.sin(), 70 | r * theta.cos(), 71 | ) 72 | } 73 | 74 | /// Generates a uniform sample on a hemisphere around given normal. 75 | pub fn sample_hemisphere(&mut self, normal: Vec3) -> Vec3 { 76 | let cos_theta = self.sample(); 77 | let sin_theta = (1.0f32 - cos_theta.sqr()).sqrt(); 78 | let phi = 2.0 * PI * self.sample(); 79 | let (t, b) = normal.any_orthonormal_pair(); 80 | 81 | (t * phi.cos() + b * phi.sin()) * sin_theta + normal * cos_theta 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /strolle/src/mesh_triangle.rs: -------------------------------------------------------------------------------- 1 | use glam::Affine3A; 2 | use spirv_std::glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; 3 | 4 | use crate::Triangle; 5 | 6 | #[derive(Clone, Debug, Default)] 7 | pub struct MeshTriangle { 8 | positions: [Vec3; 3], 9 | normals: [Vec3; 3], 10 | uvs: [Vec2; 3], 11 | tangents: [Vec4; 3], 12 | } 13 | 14 | impl MeshTriangle { 15 | pub fn with_positions(mut self, positions: [impl Into; 3]) -> Self { 16 | self.positions = positions.map(Into::into); 17 | self 18 | } 19 | 20 | pub fn with_normals(mut self, normals: [impl Into; 3]) -> Self { 21 | self.normals = normals.map(Into::into); 22 | self 23 | } 24 | 25 | pub fn with_uvs(mut self, uvs: [impl Into; 3]) -> Self { 26 | self.uvs = uvs.map(Into::into); 27 | self 28 | } 29 | 30 | pub fn with_tangents(mut self, tangents: [impl Into; 3]) -> Self { 31 | self.tangents = tangents.map(Into::into); 32 | self 33 | } 34 | 35 | pub fn positions(&self) -> [Vec3; 3] { 36 | self.positions 37 | } 38 | 39 | pub fn normals(&self) -> [Vec3; 3] { 40 | self.normals 41 | } 42 | 43 | pub fn uvs(&self) -> [Vec2; 3] { 44 | self.uvs 45 | } 46 | 47 | pub(crate) fn build( 48 | &self, 49 | xform: Affine3A, 50 | xform_inv: Affine3A, 51 | ) -> Triangle { 52 | let positions = 53 | self.positions.map(|vertex| xform.transform_point3(vertex)); 54 | 55 | let normals = { 56 | // Transforming normals requires inversing and transposing the 57 | // matrix in order to get correct results under scaling, see: 58 | // 59 | // https://paroj.github.io/gltut/Illumination/Tut09%20Normal%20Transformation.html 60 | let mat = Mat4::from(xform_inv).transpose(); 61 | 62 | self.normals 63 | .map(|normal| mat.transform_vector3(normal).normalize()) 64 | }; 65 | 66 | let tangents = { 67 | let sign = if xform.matrix3.determinant().is_sign_positive() { 68 | 1.0 69 | } else { 70 | -1.0 71 | }; 72 | 73 | self.tangents.map(|tangent| { 74 | (xform.matrix3 * tangent.xyz()) 75 | .normalize() 76 | .extend(tangent.w * sign) 77 | }) 78 | }; 79 | 80 | Triangle { 81 | positions, 82 | normals, 83 | uvs: self.uvs, 84 | tangents, 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /strolle/src/buffers/storage_buffer.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | 3 | use crate::buffers::utils; 4 | use crate::Bindable; 5 | 6 | /// Storage buffer that exists only in VRAM. 7 | /// 8 | /// This kind of storage buffer should be used for data structures that don't 9 | /// have to be accessed on the host machine. 10 | #[derive(Debug)] 11 | pub struct StorageBuffer { 12 | buffer: wgpu::Buffer, 13 | } 14 | 15 | impl StorageBuffer { 16 | // TODO provide `::builder()` pattern 17 | pub fn new( 18 | device: &wgpu::Device, 19 | label: impl AsRef, 20 | size: usize, 21 | ) -> Self { 22 | let label = label.as_ref(); 23 | let size = utils::pad_size(size); 24 | 25 | debug!("Allocating storage buffer `{label}`; size={size}"); 26 | 27 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { 28 | label: Some(label), 29 | usage: wgpu::BufferUsages::STORAGE, 30 | size: size as _, 31 | mapped_at_creation: false, 32 | }); 33 | 34 | Self { buffer } 35 | } 36 | 37 | /// Creates an immutable storage-buffer binding: 38 | /// 39 | /// ``` 40 | /// #[spirv(descriptor_set = ..., binding = ..., storage_buffer)] 41 | /// items: &[T], 42 | /// ``` 43 | pub fn bind_readable(&self) -> impl Bindable + '_ { 44 | StorageBufferBinder { 45 | parent: self, 46 | read_only: true, 47 | } 48 | } 49 | 50 | /// Creates a mutable storage-buffer binding: 51 | /// 52 | /// ``` 53 | /// #[spirv(descriptor_set = ..., binding = ..., storage_buffer)] 54 | /// items: &mut [T], 55 | /// ``` 56 | pub fn bind_writable(&self) -> impl Bindable + '_ { 57 | StorageBufferBinder { 58 | parent: self, 59 | read_only: false, 60 | } 61 | } 62 | } 63 | 64 | pub struct StorageBufferBinder<'a> { 65 | parent: &'a StorageBuffer, 66 | read_only: bool, 67 | } 68 | 69 | impl Bindable for StorageBufferBinder<'_> { 70 | fn bind( 71 | &self, 72 | binding: u32, 73 | ) -> Vec<(wgpu::BindGroupLayoutEntry, wgpu::BindingResource)> { 74 | let layout = wgpu::BindGroupLayoutEntry { 75 | binding, 76 | visibility: wgpu::ShaderStages::all(), 77 | ty: wgpu::BindingType::Buffer { 78 | ty: wgpu::BufferBindingType::Storage { 79 | read_only: self.read_only, 80 | }, 81 | has_dynamic_offset: false, 82 | min_binding_size: None, 83 | }, 84 | count: None, 85 | }; 86 | 87 | let resource = self.parent.buffer.as_entire_binding(); 88 | 89 | vec![(layout, resource)] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/gi_preview_resampling.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | gpu, Camera, CameraBuffers, CameraComputePass, CameraController, Engine, 3 | Params, 4 | }; 5 | 6 | #[derive(Debug)] 7 | pub struct GiPreviewResamplingPass { 8 | passes: [CameraComputePass; 2], 9 | } 10 | 11 | impl GiPreviewResamplingPass { 12 | pub fn new

( 13 | engine: &Engine

, 14 | device: &wgpu::Device, 15 | _: &Camera, 16 | buffers: &CameraBuffers, 17 | ) -> Self 18 | where 19 | P: Params, 20 | { 21 | let pass_1 = CameraComputePass::builder("gi_preview_resampling_1") 22 | .bind([ 23 | &buffers.curr_camera.bind_readable(), 24 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 25 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 26 | &buffers.prim_surface_map.curr().bind_readable(), 27 | &buffers.gi_reservoirs[1].bind_readable(), 28 | &buffers.gi_reservoirs[2].bind_readable(), 29 | &buffers.gi_reservoirs[3].bind_writable(), 30 | ]) 31 | .build(device, &engine.shaders.gi_preview_resampling); 32 | 33 | let pass_2 = CameraComputePass::builder("gi_preview_resampling_2") 34 | .bind([ 35 | &buffers.curr_camera.bind_readable(), 36 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 37 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 38 | &buffers.prim_surface_map.curr().bind_readable(), 39 | &buffers.gi_reservoirs[1].bind_readable(), 40 | &buffers.gi_reservoirs[3].bind_readable(), 41 | &buffers.gi_reservoirs[0].bind_writable(), 42 | ]) 43 | .build(device, &engine.shaders.gi_preview_resampling); 44 | 45 | Self { 46 | passes: [pass_1, pass_2], 47 | } 48 | } 49 | 50 | pub fn run( 51 | &self, 52 | camera: &CameraController, 53 | encoder: &mut wgpu::CommandEncoder, 54 | source: u32, 55 | ) { 56 | // This pass uses 8x8 warps: 57 | let size = (camera.camera.viewport.size + 7) / 8; 58 | let params = camera.pass_params(); 59 | 60 | for (nth, pass) in self.passes.iter().enumerate() { 61 | let source = if nth == 0 { source } else { 1 }; 62 | 63 | pass.run( 64 | camera, 65 | encoder, 66 | size, 67 | gpu::GiPreviewResamplingPass { 68 | seed: params.seed, 69 | frame: params.frame, 70 | nth: nth as u32, 71 | source, 72 | }, 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /strolle-shaders/src/frame_composition.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(vertex)] 4 | pub fn vs( 5 | #[spirv(vertex_index)] vert_idx: i32, 6 | #[spirv(position)] output: &mut Vec4, 7 | ) { 8 | fn full_screen_triangle(vert_idx: i32) -> Vec4 { 9 | let uv = vec2(((vert_idx << 1) & 2) as f32, (vert_idx & 2) as f32); 10 | let pos = 2.0 * uv - Vec2::ONE; 11 | 12 | pos.extend(0.0).extend(1.0) 13 | } 14 | 15 | *output = full_screen_triangle(vert_idx); 16 | } 17 | 18 | #[spirv(fragment)] 19 | pub fn fs( 20 | #[spirv(frag_coord)] pos: Vec4, 21 | #[spirv(push_constant)] params: &FrameCompositionPassParams, 22 | #[spirv(descriptor_set = 0, binding = 0)] prim_gbuffer_d0: TexRgba32, 23 | #[spirv(descriptor_set = 0, binding = 1)] prim_gbuffer_d1: TexRgba32, 24 | #[spirv(descriptor_set = 0, binding = 2)] di_diff_colors: TexRgba32, 25 | #[spirv(descriptor_set = 0, binding = 3)] di_spec_colors: TexRgba32, 26 | #[spirv(descriptor_set = 0, binding = 4)] gi_diff_colors: TexRgba32, 27 | #[spirv(descriptor_set = 0, binding = 5)] gi_spec_colors: TexRgba32, 28 | #[spirv(descriptor_set = 0, binding = 6)] ref_colors: TexRgba32, 29 | frag_color: &mut Vec4, 30 | ) { 31 | let screen_pos = pos.xy().as_uvec2(); 32 | 33 | let gbuffer = GBufferEntry::unpack([ 34 | prim_gbuffer_d0.read(screen_pos), 35 | prim_gbuffer_d1.read(screen_pos), 36 | ]); 37 | 38 | let color = match params.camera_mode { 39 | // CameraMode::Image 40 | 0 => { 41 | let di_diff = di_diff_colors.read(screen_pos).xyz(); 42 | let di_spec = di_spec_colors.read(screen_pos).xyz(); 43 | let gi_diff = gi_diff_colors.read(screen_pos).xyz(); 44 | let gi_spec = gi_spec_colors.read(screen_pos).xyz(); 45 | 46 | if gbuffer.is_some() { 47 | gbuffer.emissive 48 | + (di_diff + gi_diff) * gbuffer.base_color.xyz() 49 | + di_spec 50 | + gi_spec 51 | } else { 52 | di_diff 53 | } 54 | } 55 | 56 | // CameraMode::DiDiffuse 57 | 1 => di_diff_colors.read(screen_pos).xyz(), 58 | 59 | // CameraMode::DiSpecular 60 | 2 => di_spec_colors.read(screen_pos).xyz(), 61 | 62 | // CameraMode::GiDiffuse 63 | 3 => gi_diff_colors.read(screen_pos).xyz(), 64 | 65 | // CameraMode::GiSpecular 66 | 4 => gi_spec_colors.read(screen_pos).xyz(), 67 | 68 | // CameraMode::BvhHeatmap 69 | 5 => ref_colors.read(screen_pos).xyz(), 70 | 71 | // CameraMode::Reference 72 | 6 => { 73 | let color = ref_colors.read(screen_pos); 74 | 75 | color.xyz() / color.w 76 | } 77 | 78 | _ => Default::default(), 79 | }; 80 | 81 | *frag_color = color.extend(1.0); 82 | } 83 | -------------------------------------------------------------------------------- /strolle/src/buffers/double_buffered.rs: -------------------------------------------------------------------------------- 1 | use crate::{Bindable, DoubleBufferedBindable, Texture, TextureBuilder}; 2 | 3 | #[derive(Debug)] 4 | pub struct DoubleBuffered { 5 | a: T, 6 | b: T, 7 | } 8 | 9 | impl DoubleBuffered { 10 | /// Creates a double-buffered texture. 11 | /// 12 | /// See: [`Texture::new()`]. 13 | pub fn new(device: &wgpu::Device, texture: TextureBuilder) -> Self { 14 | let label_a = format!("{}_a", texture.label()); 15 | let label_b = format!("{}_b", texture.label()); 16 | 17 | Self { 18 | a: texture.clone().with_label(label_a).build(device), 19 | b: texture.with_label(label_b).build(device), 20 | } 21 | } 22 | } 23 | 24 | impl DoubleBuffered<&Texture> { 25 | /// See: [`Texture::bind_readable()`]. 26 | pub fn bind_readable(&self) -> impl DoubleBufferedBindable + '_ { 27 | DoubleBufferedBinder { 28 | a: self.a.bind_readable(), 29 | b: self.b.bind_readable(), 30 | } 31 | } 32 | 33 | /// See: [`Texture::bind_writable()`]. 34 | pub fn bind_writable(&self) -> impl DoubleBufferedBindable + '_ { 35 | DoubleBufferedBinder { 36 | a: self.a.bind_writable(), 37 | b: self.b.bind_writable(), 38 | } 39 | } 40 | } 41 | 42 | impl DoubleBuffered { 43 | pub fn get(&self, alternate: bool) -> &T { 44 | if alternate { 45 | &self.b 46 | } else { 47 | &self.a 48 | } 49 | } 50 | 51 | pub fn curr(&self) -> DoubleBuffered<&T> { 52 | DoubleBuffered { 53 | a: &self.a, 54 | b: &self.b, 55 | } 56 | } 57 | 58 | pub fn prev(&self) -> DoubleBuffered<&T> { 59 | DoubleBuffered { 60 | a: &self.b, 61 | b: &self.a, 62 | } 63 | } 64 | } 65 | 66 | pub struct DoubleBufferedBinder { 67 | a: T, 68 | b: T, 69 | } 70 | 71 | impl DoubleBufferedBindable for DoubleBufferedBinder 72 | where 73 | T: Bindable, 74 | { 75 | fn bind( 76 | &self, 77 | binding: u32, 78 | ) -> Vec<(wgpu::BindGroupLayoutEntry, [wgpu::BindingResource; 2])> { 79 | let entries_a = self.a.bind(binding); 80 | let entries_b = self.b.bind(binding); 81 | 82 | assert_eq!(entries_a.len(), entries_b.len()); 83 | 84 | entries_a 85 | .into_iter() 86 | .zip(entries_b) 87 | .map(|((layout_a, resource_a), (layout_b, resource_b))| { 88 | assert_eq!(layout_a, layout_b); 89 | 90 | #[allow( 91 | clippy::tuple_array_conversions, 92 | reason="https://github.com/rust-lang/rust-clippy/issues/11144" 93 | )] 94 | (layout_a, [resource_a, resource_b]) 95 | }) 96 | .collect() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /strolle/src/buffers/mapped_uniform_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use log::debug; 5 | 6 | use crate::buffers::utils; 7 | use crate::{Bindable, Bufferable}; 8 | 9 | #[derive(Debug)] 10 | pub struct MappedUniformBuffer { 11 | buffer: wgpu::Buffer, 12 | data: T, 13 | dirty: bool, 14 | } 15 | 16 | impl MappedUniformBuffer 17 | where 18 | T: Bufferable, 19 | { 20 | pub fn new(device: &wgpu::Device, label: impl AsRef, data: T) -> Self { 21 | let label = format!("strolle_{}", label.as_ref()); 22 | let size = utils::pad_size(data.size()); 23 | 24 | debug!("Allocating uniform buffer `{label}`; size={size}"); 25 | 26 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { 27 | label: Some(&label), 28 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, 29 | size: size as _, 30 | mapped_at_creation: false, 31 | }); 32 | 33 | Self { 34 | buffer, 35 | data, 36 | dirty: true, 37 | } 38 | } 39 | 40 | pub fn flush(&mut self, queue: &wgpu::Queue) { 41 | if !mem::take(&mut self.dirty) { 42 | return; 43 | } 44 | 45 | queue.write_buffer(&self.buffer, 0, self.data.data()); 46 | } 47 | 48 | /// Creates an immutable uniform binding: 49 | /// 50 | /// ``` 51 | /// #[spirv(descriptor_set = ..., binding = ..., uniform)] 52 | /// item: &T, 53 | /// ``` 54 | pub fn bind_readable(&self) -> impl Bindable + '_ { 55 | MappedUniformBufferBinder { parent: self } 56 | } 57 | } 58 | 59 | impl Deref for MappedUniformBuffer { 60 | type Target = T; 61 | 62 | fn deref(&self) -> &Self::Target { 63 | &self.data 64 | } 65 | } 66 | 67 | impl DerefMut for MappedUniformBuffer { 68 | fn deref_mut(&mut self) -> &mut Self::Target { 69 | self.dirty = true; 70 | 71 | &mut self.data 72 | } 73 | } 74 | 75 | pub struct MappedUniformBufferBinder<'a, T> { 76 | parent: &'a MappedUniformBuffer, 77 | } 78 | 79 | impl Bindable for MappedUniformBufferBinder<'_, T> { 80 | fn bind( 81 | &self, 82 | binding: u32, 83 | ) -> Vec<(wgpu::BindGroupLayoutEntry, wgpu::BindingResource)> { 84 | let layout = wgpu::BindGroupLayoutEntry { 85 | binding, 86 | visibility: wgpu::ShaderStages::all(), 87 | ty: wgpu::BindingType::Buffer { 88 | ty: wgpu::BufferBindingType::Uniform, 89 | has_dynamic_offset: false, 90 | min_binding_size: None, 91 | }, 92 | count: None, 93 | }; 94 | 95 | let resource = self.parent.buffer.as_entire_binding(); 96 | 97 | vec![(layout, resource)] 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /bevy-strolle/examples/cornell.rs: -------------------------------------------------------------------------------- 1 | #[path = "_common.rs"] 2 | mod common; 3 | 4 | use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}; 5 | use bevy::math::vec3; 6 | use bevy::prelude::*; 7 | use bevy::render::camera::CameraRenderGraph; 8 | use bevy::window::WindowResolution; 9 | use bevy_strolle::prelude::*; 10 | use smooth_bevy_cameras::controllers::orbit::{ 11 | OrbitCameraBundle, OrbitCameraController, OrbitCameraPlugin, 12 | }; 13 | use smooth_bevy_cameras::LookTransformPlugin; 14 | 15 | fn main() { 16 | common::extract_assets(); 17 | 18 | App::new() 19 | .add_plugins(( 20 | DefaultPlugins.set(WindowPlugin { 21 | primary_window: Some(Window { 22 | resolution: WindowResolution::new(512.0, 512.0), 23 | ..default() 24 | }), 25 | ..default() 26 | }), 27 | LogDiagnosticsPlugin::default(), 28 | FrameTimeDiagnosticsPlugin, 29 | LookTransformPlugin, 30 | OrbitCameraPlugin::default(), 31 | StrollePlugin, 32 | StrolleDebugPlugin, 33 | )) 34 | .add_systems(Startup, setup) 35 | .add_systems(Update, animate) 36 | .run(); 37 | } 38 | 39 | fn setup(mut commands: Commands, assets: Res) { 40 | commands.spawn(SceneBundle { 41 | scene: assets.load("cornell/scene.gltf#Scene0"), 42 | ..default() 43 | }); 44 | 45 | commands.spawn(PointLightBundle { 46 | point_light: PointLight { 47 | color: Color::WHITE, 48 | intensity: 50.0, 49 | radius: 0.15, 50 | shadows_enabled: true, 51 | ..default() 52 | }, 53 | ..default() 54 | }); 55 | 56 | commands 57 | .spawn(Camera3dBundle { 58 | camera_render_graph: CameraRenderGraph::new( 59 | bevy_strolle::graph::NAME, 60 | ), 61 | camera: Camera { 62 | hdr: true, 63 | ..default() 64 | }, 65 | ..default() 66 | }) 67 | .insert(OrbitCameraBundle::new( 68 | { 69 | OrbitCameraController { 70 | enabled: true, 71 | mouse_rotate_sensitivity: Vec2::ONE * 0.2, 72 | mouse_translate_sensitivity: Vec2::ONE * 0.5, 73 | ..default() 74 | } 75 | }, 76 | vec3(0.0, 1.0, 3.2), 77 | vec3(0.0, 1.0, 0.0), 78 | vec3(0.0, 1.0, 0.0), 79 | )); 80 | } 81 | 82 | fn animate( 83 | time: Res

( 15 | engine: &Engine

, 16 | device: &wgpu::Device, 17 | _: &Camera, 18 | buffers: &CameraBuffers, 19 | ) -> Self 20 | where 21 | P: Params, 22 | { 23 | let pass_a = CameraComputePass::builder("gi_sampling_a") 24 | .bind([ 25 | &engine.triangles.bind_readable(), 26 | &engine.bvh.bind_readable(), 27 | &engine.materials.bind_readable(), 28 | &engine.images.bind_atlas(), 29 | ]) 30 | .bind([ 31 | &buffers.curr_camera.bind_readable(), 32 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 33 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 34 | &buffers.gi_d0.bind_writable(), 35 | &buffers.gi_d1.bind_writable(), 36 | &buffers.gi_d2.bind_writable(), 37 | &buffers.gi_reservoirs[2].bind_readable(), 38 | ]) 39 | .build(device, &engine.shaders.gi_sampling_a); 40 | 41 | let pass_b = CameraComputePass::builder("gi_sampling_b") 42 | .bind([ 43 | &engine.triangles.bind_readable(), 44 | &engine.bvh.bind_readable(), 45 | &engine.lights.bind_readable(), 46 | &engine.materials.bind_readable(), 47 | &engine.images.bind_atlas(), 48 | &engine.world.bind_readable(), 49 | ]) 50 | .bind([ 51 | &buffers.curr_camera.bind_readable(), 52 | &buffers.atmosphere_transmittance_lut.bind_sampled(), 53 | &buffers.atmosphere_sky_lut.bind_sampled(), 54 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 55 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 56 | &buffers.gi_d0.bind_readable(), 57 | &buffers.gi_d1.bind_readable(), 58 | &buffers.gi_d2.bind_readable(), 59 | &buffers.gi_reservoirs[2].bind_readable(), 60 | &buffers.gi_reservoirs[1].bind_writable(), 61 | ]) 62 | .build(device, &engine.shaders.gi_sampling_b); 63 | 64 | Self { pass_a, pass_b } 65 | } 66 | 67 | pub fn run( 68 | &self, 69 | camera: &CameraController, 70 | encoder: &mut wgpu::CommandEncoder, 71 | ) { 72 | // These passes use 8x8 warps and 2x1 checkerboard: 73 | let size = (camera.camera.viewport.size + 7) / 8 / uvec2(2, 1); 74 | 75 | self.pass_a.run(camera, encoder, size, camera.pass_params()); 76 | self.pass_b.run(camera, encoder, size, camera.pass_params()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /strolle/src/material.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use spirv_std::glam::{vec4, Vec4}; 4 | 5 | use crate::{gpu, Images, Params}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Material

9 | where 10 | P: Params, 11 | { 12 | pub base_color: Vec4, 13 | pub base_color_texture: Option, 14 | pub emissive: Vec4, 15 | pub emissive_texture: Option, 16 | pub perceptual_roughness: f32, 17 | pub metallic: f32, 18 | pub metallic_roughness_texture: Option, 19 | pub reflectance: f32, 20 | pub ior: f32, 21 | pub normal_map_texture: Option, 22 | pub alpha_mode: AlphaMode, 23 | } 24 | 25 | impl

Material

26 | where 27 | P: Params, 28 | { 29 | pub(crate) fn serialize(&self, images: &Images

) -> gpu::Material { 30 | gpu::Material { 31 | base_color: self.base_color, 32 | base_color_texture: images 33 | .lookup_opt(self.base_color_texture) 34 | .unwrap_or_default(), 35 | emissive: self.emissive, 36 | emissive_texture: images 37 | .lookup_opt(self.emissive_texture) 38 | .unwrap_or_default(), 39 | roughness: self.perceptual_roughness.powf(2.0), 40 | metallic: self.metallic, 41 | metallic_roughness_texture: images 42 | .lookup_opt(self.metallic_roughness_texture) 43 | .unwrap_or_default(), 44 | reflectance: self.reflectance, 45 | ior: self.ior, 46 | normal_map_texture: images 47 | .lookup_opt(self.normal_map_texture) 48 | .unwrap_or_default(), 49 | } 50 | } 51 | } 52 | 53 | impl

Default for Material

54 | where 55 | P: Params, 56 | { 57 | fn default() -> Self { 58 | Self { 59 | base_color: vec4(1.0, 1.0, 1.0, 1.0), 60 | base_color_texture: None, 61 | emissive: Vec4::ZERO, 62 | emissive_texture: None, 63 | perceptual_roughness: 0.5, 64 | metallic: 0.0, 65 | metallic_roughness_texture: None, 66 | reflectance: 0.5, 67 | ior: 1.0, 68 | normal_map_texture: None, 69 | alpha_mode: Default::default(), 70 | } 71 | } 72 | } 73 | 74 | /// Specifies if a material is allowed to be transparent 75 | #[derive(Clone, Copy, Debug, Default)] 76 | pub enum AlphaMode { 77 | /// Material is always opaque (this is the default). 78 | /// 79 | /// When this is active, the base color's alpha is always set to 1.0. 80 | #[default] 81 | Opaque, 82 | 83 | /// Material is allowed to be transparent (i.e. base color's and base color 84 | /// texture's alpha channel is honored). 85 | /// 86 | /// Note that enabling this option has negative effects on ray-tracing 87 | /// performance (non-opaque materials need special handling during the ray 88 | /// traversal process), so this option should be enabled conservatively, 89 | /// only for materials that actually use transparency. 90 | Blend, 91 | } 92 | -------------------------------------------------------------------------------- /strolle/src/buffers/bind_group.rs: -------------------------------------------------------------------------------- 1 | use crate::DoubleBufferedBindable; 2 | 3 | #[derive(Debug)] 4 | pub struct BindGroup { 5 | bind_group_a: wgpu::BindGroup, 6 | bind_group_b: wgpu::BindGroup, 7 | bind_group_layout: wgpu::BindGroupLayout, 8 | } 9 | 10 | impl BindGroup { 11 | pub fn builder<'ctx>(label: impl ToString) -> BindGroupBuilder<'ctx> { 12 | BindGroupBuilder { 13 | label: label.to_string(), 14 | layouts: Default::default(), 15 | resources: Default::default(), 16 | } 17 | } 18 | 19 | pub fn get(&self, alternate: bool) -> &wgpu::BindGroup { 20 | if alternate { 21 | &self.bind_group_b 22 | } else { 23 | &self.bind_group_a 24 | } 25 | } 26 | 27 | pub fn layout(&self) -> &wgpu::BindGroupLayout { 28 | &self.bind_group_layout 29 | } 30 | } 31 | 32 | pub struct BindGroupBuilder<'a> { 33 | label: String, 34 | layouts: Vec, 35 | resources: Vec<[wgpu::BindingResource<'a>; 2]>, 36 | } 37 | 38 | impl<'a> BindGroupBuilder<'a> { 39 | pub fn add(mut self, item: &'a dyn DoubleBufferedBindable) -> Self { 40 | for (layout, resources) in item.bind(self.resources.len() as u32) { 41 | self.layouts.push(layout); 42 | self.resources.push(resources); 43 | } 44 | 45 | self 46 | } 47 | 48 | pub fn build(self, device: &wgpu::Device) -> BindGroup { 49 | let label = format!("strolle_{}", self.label); 50 | 51 | let bind_group_layout = 52 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 53 | label: Some(&format!("{label}_layout")), 54 | entries: &self.layouts, 55 | }); 56 | 57 | let (resources_a, resources_b): (Vec<_>, Vec<_>) = self 58 | .resources 59 | .into_iter() 60 | .enumerate() 61 | .map(|(binding, resources)| { 62 | let [resource_a, resource_b] = 63 | resources.map(|resource| wgpu::BindGroupEntry { 64 | binding: binding as _, 65 | resource, 66 | }); 67 | 68 | #[allow( 69 | clippy::tuple_array_conversions, 70 | reason="https://github.com/rust-lang/rust-clippy/issues/11144" 71 | )] 72 | (resource_a, resource_b) 73 | }) 74 | .unzip(); 75 | 76 | let bind_group_a = 77 | device.create_bind_group(&wgpu::BindGroupDescriptor { 78 | label: Some(&label), 79 | layout: &bind_group_layout, 80 | entries: &resources_a, 81 | }); 82 | 83 | let bind_group_b = 84 | device.create_bind_group(&wgpu::BindGroupDescriptor { 85 | label: Some(&label), 86 | layout: &bind_group_layout, 87 | entries: &resources_b, 88 | }); 89 | 90 | BindGroup { 91 | bind_group_a, 92 | bind_group_b, 93 | bind_group_layout, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /bevy-strolle/src/state.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::Affine3A; 2 | use bevy::prelude::*; 3 | use bevy::render::renderer::{RenderDevice, RenderQueue}; 4 | use bevy::utils::HashMap; 5 | use strolle as st; 6 | 7 | use crate::EngineParams; 8 | 9 | #[derive(Default, Resource)] 10 | pub(crate) struct SyncedState { 11 | pub cameras: HashMap, 12 | } 13 | 14 | impl SyncedState { 15 | pub fn is_active(&self) -> bool { 16 | !self.cameras.is_empty() 17 | } 18 | 19 | pub fn tick( 20 | &mut self, 21 | engine: &mut st::Engine, 22 | device: &RenderDevice, 23 | queue: &RenderQueue, 24 | ) { 25 | if self.is_active() { 26 | engine.tick(device.wgpu_device(), queue); 27 | } 28 | } 29 | } 30 | 31 | #[derive(Debug)] 32 | pub(crate) struct SyncedCamera { 33 | pub handle: st::CameraHandle, 34 | } 35 | 36 | #[derive(Debug, Resource)] 37 | pub(crate) struct ExtractedMeshes { 38 | pub changed: Vec, 39 | pub removed: Vec>, 40 | } 41 | 42 | #[derive(Debug)] 43 | pub(crate) struct ExtractedMesh { 44 | pub handle: AssetId, 45 | pub mesh: Mesh, 46 | } 47 | 48 | #[derive(Debug, Resource)] 49 | pub(crate) struct ExtractedMaterials { 50 | pub changed: Vec, 51 | pub removed: Vec>, 52 | } 53 | 54 | #[derive(Debug)] 55 | pub(crate) struct ExtractedMaterial { 56 | pub handle: AssetId, 57 | pub material: StandardMaterial, 58 | } 59 | 60 | #[derive(Debug, Resource)] 61 | pub(crate) struct ExtractedImages { 62 | pub changed: Vec, 63 | pub removed: Vec>, 64 | } 65 | 66 | #[derive(Debug)] 67 | pub(crate) struct ExtractedImage { 68 | pub handle: AssetId, 69 | pub texture_descriptor: wgpu::TextureDescriptor<'static>, 70 | pub sampler_descriptor: wgpu::SamplerDescriptor<'static>, 71 | pub data: ExtractedImageData, 72 | } 73 | 74 | #[derive(Debug)] 75 | pub(crate) enum ExtractedImageData { 76 | Raw { data: Vec }, 77 | Texture { is_dynamic: bool }, 78 | } 79 | 80 | #[derive(Debug, Resource)] 81 | pub(crate) struct ExtractedInstances { 82 | pub changed: Vec, 83 | pub removed: Vec, 84 | } 85 | 86 | #[derive(Debug)] 87 | pub(crate) struct ExtractedInstance { 88 | pub handle: Entity, 89 | pub mesh_handle: AssetId, 90 | pub material_handle: AssetId, 91 | pub xform: Affine3A, 92 | } 93 | 94 | #[derive(Debug, Resource)] 95 | pub(crate) struct ExtractedLights { 96 | pub changed: Vec, 97 | pub removed: Vec, 98 | } 99 | 100 | #[derive(Debug)] 101 | pub(crate) struct ExtractedLight { 102 | pub handle: Entity, 103 | pub light: st::Light, 104 | } 105 | 106 | #[derive(Debug, Component)] 107 | pub(crate) struct ExtractedCamera { 108 | pub transform: Mat4, 109 | pub projection: Mat4, 110 | pub mode: Option, 111 | } 112 | 113 | #[derive(Debug, Resource)] 114 | pub(crate) struct ExtractedSun { 115 | pub sun: Option, 116 | } 117 | -------------------------------------------------------------------------------- /strolle/src/materials.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::fmt::Debug; 4 | use std::ops::Index; 5 | 6 | use crate::utils::Allocator; 7 | use crate::{ 8 | gpu, Bindable, BufferFlushOutcome, Images, MappedStorageBuffer, Material, 9 | Params, 10 | }; 11 | 12 | #[derive(Debug)] 13 | pub struct Materials

14 | where 15 | P: Params, 16 | { 17 | allocator: Allocator, 18 | buffer: MappedStorageBuffer>, 19 | index: HashMap, 20 | materials: Vec>, 21 | } 22 | 23 | impl

Materials

24 | where 25 | P: Params, 26 | { 27 | pub fn new(device: &wgpu::Device) -> Self { 28 | Self { 29 | allocator: Default::default(), 30 | buffer: MappedStorageBuffer::new_default(device, "materials"), 31 | index: Default::default(), 32 | materials: Default::default(), 33 | } 34 | } 35 | 36 | pub fn insert(&mut self, handle: P::MaterialHandle, item: Material

) { 37 | match self.index.entry(handle) { 38 | Entry::Occupied(entry) => { 39 | let id = *entry.get(); 40 | 41 | self.materials[id.get() as usize] = item; 42 | } 43 | 44 | Entry::Vacant(entry) => { 45 | let id = if let Some(alloc) = self.allocator.take(1) { 46 | alloc.start 47 | } else { 48 | self.materials.push(item); 49 | self.materials.len() - 1 50 | }; 51 | 52 | entry.insert(gpu::MaterialId::new(id as u32)); 53 | } 54 | } 55 | } 56 | 57 | pub fn has(&self, handle: P::MaterialHandle) -> bool { 58 | self.index.contains_key(&handle) 59 | } 60 | 61 | pub fn remove(&mut self, handle: P::MaterialHandle) { 62 | let Some(id) = self.index.remove(&handle) else { 63 | return; 64 | }; 65 | 66 | let id = id.get() as usize; 67 | 68 | self.allocator.give(id..id); 69 | } 70 | 71 | pub fn len(&self) -> usize { 72 | self.buffer.len() 73 | } 74 | 75 | pub fn lookup(&self, handle: P::MaterialHandle) -> Option { 76 | self.index.get(&handle).copied() 77 | } 78 | 79 | pub fn refresh(&mut self, images: &Images

) { 80 | *self.buffer = self 81 | .materials 82 | .iter() 83 | .map(|material| material.serialize(images)) 84 | .collect(); 85 | } 86 | 87 | pub fn flush( 88 | &mut self, 89 | device: &wgpu::Device, 90 | queue: &wgpu::Queue, 91 | ) -> BufferFlushOutcome { 92 | self.buffer.flush(device, queue) 93 | } 94 | 95 | pub fn bind_readable(&self) -> impl Bindable + '_ { 96 | self.buffer.bind_readable() 97 | } 98 | } 99 | 100 | impl

Index for Materials

101 | where 102 | P: Params, 103 | { 104 | type Output = Material

; 105 | 106 | fn index(&self, index: gpu::MaterialId) -> &Self::Output { 107 | &self.materials[index.get() as usize] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/gi_spatial_resampling.rs: -------------------------------------------------------------------------------- 1 | use glam::uvec2; 2 | 3 | use crate::{ 4 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub struct GiSpatialResamplingPass { 9 | pick_pass: CameraComputePass, 10 | trace_pass: CameraComputePass, 11 | sample_pass: CameraComputePass, 12 | } 13 | 14 | impl GiSpatialResamplingPass { 15 | pub fn new

( 16 | engine: &Engine

, 17 | device: &wgpu::Device, 18 | _: &Camera, 19 | buffers: &CameraBuffers, 20 | ) -> Self 21 | where 22 | P: Params, 23 | { 24 | let pick_pass = 25 | CameraComputePass::builder("gi_spatial_resampling_pick") 26 | .bind([ 27 | &buffers.curr_camera.bind_readable(), 28 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 29 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 30 | &buffers.gi_reservoirs[1].bind_readable(), 31 | &buffers.gi_d0.bind_writable(), 32 | &buffers.gi_d1.bind_writable(), 33 | ]) 34 | .build(device, &engine.shaders.gi_spatial_resampling_pick); 35 | 36 | let trace_pass = 37 | CameraComputePass::builder("gi_spatial_resampling_trace") 38 | .bind([ 39 | &engine.triangles.bind_readable(), 40 | &engine.bvh.bind_readable(), 41 | &engine.materials.bind_readable(), 42 | &engine.images.bind_atlas(), 43 | ]) 44 | .bind([ 45 | &buffers.curr_camera.bind_readable(), 46 | &buffers.gi_d0.bind_readable(), 47 | &buffers.gi_d1.bind_readable(), 48 | &buffers.gi_d2.bind_writable(), 49 | ]) 50 | .build(device, &engine.shaders.gi_spatial_resampling_trace); 51 | 52 | let sample_pass = 53 | CameraComputePass::builder("gi_spatial_resampling_sample") 54 | .bind([ 55 | &buffers.curr_camera.bind_readable(), 56 | &buffers.gi_reservoirs[1].bind_readable(), 57 | &buffers.gi_reservoirs[2].bind_writable(), 58 | &buffers.gi_d2.bind_readable(), 59 | ]) 60 | .build(device, &engine.shaders.gi_spatial_resampling_sample); 61 | 62 | Self { 63 | pick_pass, 64 | trace_pass, 65 | sample_pass, 66 | } 67 | } 68 | 69 | pub fn run( 70 | &self, 71 | camera: &CameraController, 72 | encoder: &mut wgpu::CommandEncoder, 73 | ) { 74 | self.pick_pass.run( 75 | camera, 76 | encoder, 77 | (camera.camera.viewport.size + 7) / 8 / uvec2(2, 1), 78 | camera.pass_params(), 79 | ); 80 | 81 | self.trace_pass.run( 82 | camera, 83 | encoder, 84 | (camera.camera.viewport.size + 7) / 8, 85 | camera.pass_params(), 86 | ); 87 | 88 | self.sample_pass.run( 89 | camera, 90 | encoder, 91 | (camera.camera.viewport.size + 7) / 8 / uvec2(2, 1), 92 | camera.pass_params(), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /strolle-shaders/src/di_sampling.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(push_constant)] params: &PassParams, 7 | #[spirv(local_invocation_index)] local_idx: u32, 8 | #[spirv(workgroup)] stack: BvhStack, 9 | #[spirv(descriptor_set = 0, binding = 0)] blue_noise_tex: TexRgba8, 10 | #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] 11 | triangles: &[Triangle], 12 | #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] bvh: &[Vec4], 13 | #[spirv(descriptor_set = 0, binding = 3, storage_buffer)] 14 | materials: &[Material], 15 | #[spirv(descriptor_set = 0, binding = 4, storage_buffer)] 16 | lights: &[Light], 17 | #[spirv(descriptor_set = 0, binding = 5)] atlas_tex: Tex, 18 | #[spirv(descriptor_set = 0, binding = 6)] atlas_sampler: &Sampler, 19 | #[spirv(descriptor_set = 0, binding = 7, uniform)] world: &World, 20 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 21 | #[spirv(descriptor_set = 1, binding = 1)] prim_gbuffer_d0: TexRgba32, 22 | #[spirv(descriptor_set = 1, binding = 2)] prim_gbuffer_d1: TexRgba32, 23 | #[spirv(descriptor_set = 1, binding = 3, storage_buffer)] 24 | out_reservoirs: &mut [Vec4], 25 | ) { 26 | let screen_pos = global_id.xy(); 27 | let screen_idx = camera.screen_to_idx(screen_pos); 28 | let bnoise = BlueNoise::new(blue_noise_tex, screen_pos, params.frame); 29 | let mut wnoise = WhiteNoise::new(params.seed, screen_pos); 30 | let triangles = TrianglesView::new(triangles); 31 | let bvh = BvhView::new(bvh); 32 | let materials = MaterialsView::new(materials); 33 | let lights = LightsView::new(lights); 34 | 35 | if !camera.contains(screen_pos) { 36 | return; 37 | } 38 | 39 | // ------------------------------------------------------------------------- 40 | 41 | let hit = Hit::new( 42 | camera.ray(screen_pos), 43 | GBufferEntry::unpack([ 44 | prim_gbuffer_d0.read(screen_pos), 45 | prim_gbuffer_d1.read(screen_pos), 46 | ]), 47 | ); 48 | 49 | if hit.is_none() { 50 | return; 51 | } 52 | 53 | // --- 54 | 55 | let mut res = EphemeralReservoir::build(&mut wnoise, lights, *world, hit); 56 | 57 | let res = if res.m > 0.0 { 58 | let ray = lights 59 | .get(res.sample.light_id) 60 | .ray_bnoise(bnoise.first_sample(), hit.point); 61 | 62 | let is_occluded = ray.intersect( 63 | local_idx, 64 | stack, 65 | triangles, 66 | bvh, 67 | materials, 68 | atlas_tex, 69 | atlas_sampler, 70 | ); 71 | 72 | if is_occluded { 73 | res.w = 0.0; 74 | } 75 | 76 | DiReservoir { 77 | reservoir: Reservoir { 78 | sample: DiSample { 79 | pdf: 0.0, 80 | confidence: 0.0, 81 | light_id: res.sample.light_id, 82 | light_point: ray.origin(), 83 | is_occluded, 84 | }, 85 | m: 1.0, 86 | w: res.w, 87 | }, 88 | } 89 | } else { 90 | Default::default() 91 | }; 92 | 93 | res.write(out_reservoirs, screen_idx); 94 | } 95 | -------------------------------------------------------------------------------- /strolle-shaders/src/frame_reprojection.rs: -------------------------------------------------------------------------------- 1 | //! This pass performs camera reprojection, i.e. it finds out where each pixel 2 | //! was located in the previous frame. 3 | 4 | use strolle_gpu::prelude::*; 5 | 6 | #[spirv(compute(threads(8, 8)))] 7 | pub fn main( 8 | #[spirv(global_invocation_id)] global_id: UVec3, 9 | #[spirv(descriptor_set = 0, binding = 0, uniform)] camera: &Camera, 10 | #[spirv(descriptor_set = 0, binding = 1, uniform)] prev_camera: &Camera, 11 | #[spirv(descriptor_set = 0, binding = 2)] prim_surface_map: TexRgba32, 12 | #[spirv(descriptor_set = 0, binding = 3)] prev_prim_surface_map: TexRgba32, 13 | #[spirv(descriptor_set = 0, binding = 4)] velocity_map: TexRgba32, 14 | #[spirv(descriptor_set = 0, binding = 5)] reprojection_map: TexRgba32, 15 | ) { 16 | let screen_pos = global_id.xy(); 17 | let prim_surface_map = SurfaceMap::new(prim_surface_map); 18 | let prev_prim_surface_map = SurfaceMap::new(prev_prim_surface_map); 19 | let reprojection_map = ReprojectionMap::new(reprojection_map); 20 | 21 | if !camera.contains(screen_pos) { 22 | return; 23 | } 24 | 25 | // ------------------------------------------------------------------------- 26 | 27 | let mut reprojection = Reprojection::default(); 28 | let surface = prim_surface_map.get(screen_pos); 29 | 30 | if surface.is_sky() { 31 | reprojection_map.set(screen_pos, &reprojection); 32 | return; 33 | } 34 | 35 | // ------------------------------------------------------------------------- 36 | 37 | let prev_screen_pos = 38 | screen_pos.as_vec2() - velocity_map.read(screen_pos).xy(); 39 | 40 | if prev_camera.contains(prev_screen_pos.round()) { 41 | let prev_surface = 42 | prev_prim_surface_map.get(prev_screen_pos.round().as_uvec2()); 43 | 44 | let confidence = prev_surface.evaluate_similarity_to(surface); 45 | 46 | if confidence > 0.0 { 47 | reprojection = Reprojection { 48 | prev_x: prev_screen_pos.x, 49 | prev_y: prev_screen_pos.y, 50 | confidence, 51 | validity: 0, 52 | }; 53 | } 54 | } 55 | 56 | // ------------------------------------------------------------------------- 57 | 58 | if reprojection.is_some() { 59 | let check_validity = move |sample_pos: IVec2| { 60 | if !camera.contains(sample_pos) { 61 | return false; 62 | } 63 | 64 | prev_prim_surface_map 65 | .get(sample_pos.as_uvec2()) 66 | .evaluate_similarity_to(surface) 67 | >= 0.25 68 | }; 69 | 70 | let [p00, p10, p01, p11] = BilinearFilter::reprojection_coords( 71 | reprojection.prev_x, 72 | reprojection.prev_y, 73 | ); 74 | 75 | if check_validity(p00) { 76 | reprojection.validity |= 0b0001; 77 | } 78 | 79 | if check_validity(p10) { 80 | reprojection.validity |= 0b0010; 81 | } 82 | 83 | if check_validity(p01) { 84 | reprojection.validity |= 0b0100; 85 | } 86 | 87 | if check_validity(p11) { 88 | reprojection.validity |= 0b1000; 89 | } 90 | } 91 | 92 | // ------------------------------------------------------------------------- 93 | 94 | reprojection_map.set(screen_pos, &reprojection); 95 | } 96 | -------------------------------------------------------------------------------- /strolle-shaders/src/atmosphere.rs: -------------------------------------------------------------------------------- 1 | //! This pass generates lookup textures used to render sky. 2 | //! 3 | //! Thanks to: 4 | //! 5 | //! - https://www.shadertoy.com/view/slSXRW 6 | //! (Production Sky Rendering by AndrewHelmer) 7 | //! 8 | //! - https://github.com/sebh/UnrealEngineSkyAtmosphere 9 | //! 10 | //! Original license: 11 | //! 12 | //! ```text 13 | //! MIT License 14 | //! 15 | //! Copyright (c) 2020 Epic Games, Inc. 16 | //! 17 | //! Permission is hereby granted, free of charge, to any person obtaining a copy 18 | //! of this software and associated documentation files (the "Software"), to deal 19 | //! in the Software without restriction, including without limitation the rights 20 | //! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | //! copies of the Software, and to permit persons to whom the Software is 22 | //! furnished to do so, subject to the following conditions: 23 | //! 24 | //! The above copyright notice and this permission notice shall be included in all 25 | //! copies or substantial portions of the Software. 26 | //! 27 | //! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | //! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | //! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | //! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | //! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | //! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | //! SOFTWARE. 34 | //! ``` 35 | 36 | pub mod generate_scattering_lut; 37 | pub mod generate_sky_lut; 38 | pub mod generate_transmittance_lut; 39 | mod utils; 40 | 41 | use strolle_gpu::prelude::*; 42 | 43 | #[spirv(compute(threads(8, 8)))] 44 | pub fn generate_scattering_lut( 45 | #[spirv(global_invocation_id)] global_id: UVec3, 46 | #[spirv(descriptor_set = 0, binding = 0)] transmittance_lut_tex: Tex, 47 | #[spirv(descriptor_set = 0, binding = 1)] 48 | transmittance_lut_sampler: &Sampler, 49 | #[spirv(descriptor_set = 0, binding = 2)] out: TexRgba16, 50 | ) { 51 | generate_scattering_lut::main( 52 | global_id, 53 | transmittance_lut_tex, 54 | transmittance_lut_sampler, 55 | out, 56 | ); 57 | } 58 | 59 | #[spirv(compute(threads(8, 8)))] 60 | pub fn generate_sky_lut( 61 | #[spirv(global_invocation_id)] global_id: UVec3, 62 | #[spirv(descriptor_set = 0, binding = 0, uniform)] world: &World, 63 | #[spirv(descriptor_set = 0, binding = 1)] transmittance_lut_tex: Tex, 64 | #[spirv(descriptor_set = 0, binding = 2)] 65 | transmittance_lut_sampler: &Sampler, 66 | #[spirv(descriptor_set = 0, binding = 3)] scattering_lut_tex: Tex, 67 | #[spirv(descriptor_set = 0, binding = 4)] scattering_lut_sampler: &Sampler, 68 | #[spirv(descriptor_set = 0, binding = 5)] out: TexRgba16, 69 | ) { 70 | generate_sky_lut::main( 71 | global_id, 72 | world, 73 | transmittance_lut_tex, 74 | transmittance_lut_sampler, 75 | scattering_lut_tex, 76 | scattering_lut_sampler, 77 | out, 78 | ); 79 | } 80 | 81 | #[spirv(compute(threads(8, 8)))] 82 | pub fn generate_transmittance_lut( 83 | #[spirv(global_invocation_id)] global_id: UVec3, 84 | #[spirv(descriptor_set = 0, binding = 0)] out: TexRgba16, 85 | ) { 86 | generate_transmittance_lut::main(global_id, out); 87 | } 88 | -------------------------------------------------------------------------------- /strolle-gpu/src/triangle.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::{vec2, Vec2, Vec3, Vec4, Vec4Swizzles}; 3 | #[cfg(target_arch = "spirv")] 4 | use spirv_std::num_traits::Float; 5 | 6 | use crate::{Ray, TriangleHit}; 7 | 8 | #[repr(C)] 9 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 10 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug, PartialEq))] 11 | pub struct Triangle { 12 | pub d0: Vec4, 13 | pub d1: Vec4, 14 | pub d2: Vec4, 15 | pub d3: Vec4, 16 | pub d4: Vec4, 17 | pub d5: Vec4, 18 | pub d6: Vec4, 19 | pub d7: Vec4, 20 | pub d8: Vec4, 21 | } 22 | 23 | impl Triangle { 24 | pub fn position0(self) -> Vec3 { 25 | self.d0.xyz() 26 | } 27 | 28 | pub fn normal0(self) -> Vec3 { 29 | self.d1.xyz() 30 | } 31 | 32 | pub fn uv0(self) -> Vec2 { 33 | vec2(self.d0.w, self.d1.w) 34 | } 35 | 36 | pub fn position1(self) -> Vec3 { 37 | self.d3.xyz() 38 | } 39 | 40 | pub fn normal1(self) -> Vec3 { 41 | self.d4.xyz() 42 | } 43 | 44 | pub fn uv1(self) -> Vec2 { 45 | vec2(self.d3.w, self.d4.w) 46 | } 47 | 48 | pub fn position2(self) -> Vec3 { 49 | self.d6.xyz() 50 | } 51 | 52 | pub fn normal2(self) -> Vec3 { 53 | self.d7.xyz() 54 | } 55 | 56 | pub fn uv2(self) -> Vec2 { 57 | vec2(self.d6.w, self.d7.w) 58 | } 59 | 60 | pub fn positions(self) -> [Vec3; 3] { 61 | [self.position0(), self.position1(), self.position2()] 62 | } 63 | 64 | pub fn hit(self, ray: Ray, hit: &mut TriangleHit) -> bool { 65 | let v0v1 = self.position1() - self.position0(); 66 | let v0v2 = self.position2() - self.position0(); 67 | 68 | // --- 69 | 70 | let pvec = ray.dir().cross(v0v2); 71 | let det = v0v1.dot(pvec); 72 | 73 | if det.abs() < f32::EPSILON { 74 | return false; 75 | } 76 | 77 | // --- 78 | 79 | let inv_det = 1.0 / det; 80 | let tvec = ray.origin() - self.position0(); 81 | let u = tvec.dot(pvec) * inv_det; 82 | let qvec = tvec.cross(v0v1); 83 | let v = ray.dir().dot(qvec) * inv_det; 84 | let distance = v0v2.dot(qvec) * inv_det; 85 | 86 | if (u < 0.0) 87 | | (u > 1.0) 88 | | (v < 0.0) 89 | | (u + v > 1.0) 90 | | (distance <= 0.0) 91 | | (distance >= hit.distance) 92 | { 93 | return false; 94 | } 95 | 96 | let normal = { 97 | let normal = u * self.normal1() 98 | + v * self.normal2() 99 | + (1.0 - u - v) * self.normal0(); 100 | 101 | normal.normalize() * 1.0f32.copysign(inv_det) 102 | }; 103 | 104 | let uv = self.uv0() 105 | + (self.uv1() - self.uv0()) * u 106 | + (self.uv2() - self.uv0()) * v; 107 | 108 | hit.uv = uv; 109 | hit.normal = normal; 110 | hit.distance = distance; 111 | 112 | true 113 | } 114 | } 115 | 116 | #[derive(Clone, Copy)] 117 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug, PartialEq))] 118 | pub struct TriangleId(u32); 119 | 120 | impl TriangleId { 121 | pub fn new(id: u32) -> Self { 122 | Self(id) 123 | } 124 | 125 | pub fn get(self) -> u32 { 126 | self.0 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /strolle/src/bvh/serializer.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec4; 2 | use spirv_std::glam::vec4; 3 | 4 | use super::{BvhNodeId, BvhNodes, BvhPrimitives}; 5 | use crate::{AlphaMode, BvhNode, Materials, Params}; 6 | 7 | pub fn run

( 8 | materials: &Materials

, 9 | nodes: &BvhNodes, 10 | primitives: &BvhPrimitives, 11 | buffer: &mut Vec, 12 | ) where 13 | P: Params, 14 | { 15 | buffer.clear(); 16 | 17 | serialize(materials, nodes, primitives, buffer, BvhNodeId::root()); 18 | } 19 | 20 | fn serialize

( 21 | materials: &Materials

, 22 | nodes: &BvhNodes, 23 | primitives: &BvhPrimitives, 24 | buffer: &mut Vec, 25 | id: BvhNodeId, 26 | ) -> u32 27 | where 28 | P: Params, 29 | { 30 | const OP_INTERNAL: u32 = 0; 31 | const OP_LEAF: u32 = 1; 32 | 33 | let ptr = buffer.len(); 34 | 35 | match nodes[id] { 36 | BvhNode::Internal { 37 | left_id, right_id, .. 38 | } => { 39 | buffer.push(Default::default()); 40 | buffer.push(Default::default()); 41 | buffer.push(Default::default()); 42 | buffer.push(Default::default()); 43 | 44 | let left_bb = nodes[left_id].bounds(); 45 | let right_bb = nodes[right_id].bounds(); 46 | 47 | let _left_ptr = 48 | serialize(materials, nodes, primitives, buffer, left_id); 49 | 50 | let right_ptr = 51 | serialize(materials, nodes, primitives, buffer, right_id); 52 | 53 | buffer[ptr] = vec4( 54 | left_bb.min().x, 55 | left_bb.min().y, 56 | left_bb.min().z, 57 | f32::from_bits(OP_INTERNAL), 58 | ); 59 | 60 | buffer[ptr + 1] = vec4( 61 | left_bb.max().x, 62 | left_bb.max().y, 63 | left_bb.max().z, 64 | f32::from_bits(right_ptr), 65 | ); 66 | 67 | buffer[ptr + 2] = vec4( 68 | right_bb.min().x, 69 | right_bb.min().y, 70 | right_bb.min().z, 71 | Default::default(), 72 | ); 73 | 74 | buffer[ptr + 3] = vec4( 75 | right_bb.max().x, 76 | right_bb.max().y, 77 | right_bb.max().z, 78 | Default::default(), 79 | ); 80 | } 81 | 82 | BvhNode::Leaf { primitives_ref, .. } => { 83 | for (primitive_idx, primitive) in 84 | primitives.current(primitives_ref).iter().enumerate() 85 | { 86 | let material = &materials[primitive.material_id]; 87 | 88 | let flags = { 89 | let got_more_entries = 90 | primitive_idx + 1 < primitives_ref.len(); 91 | 92 | let has_alpha_blending = 93 | matches!(material.alpha_mode, AlphaMode::Blend); 94 | 95 | (got_more_entries as u32) 96 | | ((has_alpha_blending as u32) << 1) 97 | }; 98 | 99 | buffer.push(vec4( 100 | f32::from_bits(flags), 101 | f32::from_bits(primitive.triangle_id.get()), 102 | f32::from_bits(primitive.material_id.get()), 103 | f32::from_bits(OP_LEAF), 104 | )); 105 | } 106 | } 107 | } 108 | 109 | ptr as u32 110 | } 111 | -------------------------------------------------------------------------------- /strolle-gpu/src/utils/bilinear_filter.rs: -------------------------------------------------------------------------------- 1 | use glam::{ivec2, vec2, vec4, IVec2, UVec2, Vec2, Vec4}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | use crate::Reprojection; 6 | 7 | #[derive(Clone, Copy)] 8 | pub struct BilinearFilter { 9 | /// Sample at `f(x=0, y=0)` 10 | pub s00: Vec4, 11 | 12 | /// Sample at `f(x=1, y=0)` 13 | pub s10: Vec4, 14 | 15 | /// Sample at `f(x=0, y=1)` 16 | pub s01: Vec4, 17 | 18 | /// Sample at `f(x=1, y=1)` 19 | pub s11: Vec4, 20 | 21 | /// Weights for each sample 22 | pub weights: Vec4, 23 | } 24 | 25 | impl BilinearFilter { 26 | // TODO make it generic over `Vec4` 27 | pub fn reproject( 28 | reprojection: Reprojection, 29 | sample: impl Fn(UVec2) -> (Vec4, f32), 30 | ) -> Vec4 { 31 | if reprojection.is_exact() { 32 | sample(reprojection.prev_pos_round()).0 33 | } else { 34 | Self::from_reprojection(reprojection, sample).eval(vec2( 35 | reprojection.prev_x.fract(), 36 | reprojection.prev_y.fract(), 37 | )) 38 | } 39 | } 40 | 41 | pub fn from_reprojection( 42 | reprojection: Reprojection, 43 | sample: impl Fn(UVec2) -> (Vec4, f32), 44 | ) -> Self { 45 | let mut s00 = Vec4::ZERO; 46 | let mut s10 = Vec4::ZERO; 47 | let mut s01 = Vec4::ZERO; 48 | let mut s11 = Vec4::ZERO; 49 | let mut weights = Vec4::ZERO; 50 | 51 | let [p00, p10, p01, p11] = 52 | Self::reprojection_coords(reprojection.prev_x, reprojection.prev_y); 53 | 54 | if reprojection.validity & 0b0001 > 0 && p00.x >= 0 && p00.y >= 0 { 55 | (s00, weights.x) = sample(p00.as_uvec2()); 56 | } 57 | 58 | if reprojection.validity & 0b0010 > 0 && p10.x >= 0 && p10.y >= 0 { 59 | (s10, weights.y) = sample(p10.as_uvec2()); 60 | } 61 | 62 | if reprojection.validity & 0b0100 > 0 && p01.x >= 0 && p01.y >= 0 { 63 | (s01, weights.z) = sample(p01.as_uvec2()); 64 | } 65 | 66 | if reprojection.validity & 0b1000 > 0 && p11.x >= 0 && p11.y >= 0 { 67 | (s11, weights.w) = sample(p11.as_uvec2()); 68 | } 69 | 70 | Self { 71 | s00, 72 | s10, 73 | s01, 74 | s11, 75 | weights, 76 | } 77 | } 78 | 79 | pub fn reprojection_coords(prev_x: f32, prev_y: f32) -> [IVec2; 4] { 80 | let p00 = ivec2(prev_x.floor() as i32, prev_y.floor() as i32); 81 | let p10 = ivec2(prev_x.ceil() as i32, prev_y.floor() as i32); 82 | let p01 = ivec2(prev_x.floor() as i32, prev_y.ceil() as i32); 83 | let p11 = ivec2(prev_x.ceil() as i32, prev_y.ceil() as i32); 84 | 85 | [p00, p10, p01, p11] 86 | } 87 | 88 | pub fn eval(self, uv: Vec2) -> Vec4 { 89 | let weights = self.weights 90 | * vec4( 91 | (1.0 - uv.x) * (1.0 - uv.y), 92 | uv.x * (1.0 - uv.y), 93 | (1.0 - uv.x) * uv.y, 94 | uv.x * uv.y, 95 | ); 96 | 97 | let w_sum = weights.dot(Vec4::ONE); 98 | 99 | if w_sum == 0.0 { 100 | Default::default() 101 | } else { 102 | (self.s00 * weights.x 103 | + self.s10 * weights.y 104 | + self.s01 * weights.z 105 | + self.s11 * weights.w) 106 | / w_sum 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /strolle-gpu/src/hit.rs: -------------------------------------------------------------------------------- 1 | use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; 2 | #[cfg(target_arch = "spirv")] 3 | use spirv_std::num_traits::Float; 4 | 5 | use crate::{GBufferEntry, MaterialId, Normal, Ray, Surface, Vec3Ext}; 6 | 7 | #[derive(Clone, Copy, Default)] 8 | pub struct Hit { 9 | pub origin: Vec3, 10 | pub dir: Vec3, 11 | pub point: Vec3, 12 | pub gbuffer: GBufferEntry, 13 | } 14 | 15 | impl Hit { 16 | /// How far to move a hit point away from its surface to avoid 17 | /// self-intersection when casting shadow rays 18 | pub const NUDGE_OFFSET: f32 = 0.01; 19 | 20 | pub fn new(ray: Ray, gbuffer: GBufferEntry) -> Self { 21 | Self { 22 | origin: ray.origin(), 23 | dir: ray.dir(), 24 | point: ray.at(gbuffer.depth - Self::NUDGE_OFFSET), 25 | gbuffer, 26 | } 27 | } 28 | 29 | pub fn is_some(self) -> bool { 30 | self.gbuffer.is_some() 31 | } 32 | 33 | pub fn is_none(self) -> bool { 34 | !self.is_some() 35 | } 36 | 37 | pub fn as_surface(self) -> Surface { 38 | Surface { 39 | normal: self.gbuffer.normal, 40 | depth: self.gbuffer.depth, 41 | roughness: self.gbuffer.roughness, 42 | } 43 | } 44 | 45 | pub fn kernel_basis( 46 | normal: Vec3, 47 | direction: Vec3, 48 | roughness: f32, 49 | size: f32, 50 | ) -> (Vec3, Vec3) { 51 | fn dominant_direction(n: Vec3, v: Vec3, roughness: f32) -> Vec3 { 52 | let f = (1.0 - roughness) * ((1.0 - roughness).sqrt() + roughness); 53 | let r = (-v).reflect(n); 54 | 55 | n.lerp(r, f).normalize() 56 | } 57 | 58 | let t; 59 | let b; 60 | 61 | if roughness == 1.0 { 62 | (t, b) = normal.any_orthonormal_pair(); 63 | } else { 64 | let d = dominant_direction(normal, -direction, roughness); 65 | let r = (-d).reflect(normal); 66 | 67 | t = normal.cross(r).normalize(); 68 | b = r.cross(t); 69 | } 70 | 71 | (t * size, b * size) 72 | } 73 | } 74 | 75 | #[derive(Clone, Copy)] 76 | pub struct TriangleHit { 77 | pub distance: f32, 78 | pub point: Vec3, 79 | pub normal: Vec3, 80 | pub uv: Vec2, 81 | pub material_id: MaterialId, 82 | } 83 | 84 | impl TriangleHit { 85 | pub fn none() -> Self { 86 | Self { 87 | distance: f32::MAX, 88 | point: Default::default(), 89 | normal: Default::default(), 90 | uv: Default::default(), 91 | material_id: MaterialId::new(0), 92 | } 93 | } 94 | 95 | pub fn unpack([d0, d1]: [Vec4; 2]) -> Self { 96 | if d0.xyz() == Default::default() { 97 | Self::none() 98 | } else { 99 | let normal = Normal::decode(d1.xy()); 100 | let point = d0.xyz(); 101 | 102 | Self { 103 | distance: 0.0, 104 | point, 105 | normal, 106 | uv: d1.zw(), 107 | material_id: MaterialId::new(d0.w.to_bits()), 108 | } 109 | } 110 | } 111 | 112 | pub fn pack(self) -> [Vec4; 2] { 113 | let d0 = self.point.extend(f32::from_bits(self.material_id.get())); 114 | 115 | let d1 = Normal::encode(self.normal) 116 | .extend(self.uv.x) 117 | .extend(self.uv.y); 118 | 119 | [d0, d1] 120 | } 121 | 122 | pub fn is_some(self) -> bool { 123 | self.distance < f32::MAX 124 | } 125 | 126 | pub fn is_none(self) -> bool { 127 | !self.is_some() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/di_spatial_resampling.rs: -------------------------------------------------------------------------------- 1 | use glam::uvec2; 2 | 3 | use crate::{ 4 | Camera, CameraBuffers, CameraComputePass, CameraController, Engine, Params, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub struct DiSpatialResamplingPass { 9 | pick_pass: CameraComputePass, 10 | trace_pass: CameraComputePass, 11 | sample_pass: CameraComputePass, 12 | } 13 | 14 | impl DiSpatialResamplingPass { 15 | pub fn new

( 16 | engine: &Engine

, 17 | device: &wgpu::Device, 18 | _: &Camera, 19 | buffers: &CameraBuffers, 20 | ) -> Self 21 | where 22 | P: Params, 23 | { 24 | // We just need a couple of temporary buffers - instead of allocating 25 | // new ones, let's reuse the ones we've got: 26 | let buf_d0 = &buffers.di_diff_samples; 27 | let buf_d1 = &buffers.di_diff_curr_colors; 28 | let buf_d2 = &buffers.di_diff_stash; 29 | 30 | let pick_pass = 31 | CameraComputePass::builder("di_spatial_resampling_pick") 32 | .bind([&engine.lights.bind_readable()]) 33 | .bind([ 34 | &buffers.curr_camera.bind_readable(), 35 | &buffers.prim_gbuffer_d0.curr().bind_readable(), 36 | &buffers.prim_gbuffer_d1.curr().bind_readable(), 37 | &buffers.di_reservoirs[1].bind_readable(), 38 | &buf_d0.bind_writable(), 39 | &buf_d1.bind_writable(), 40 | ]) 41 | .build(device, &engine.shaders.di_spatial_resampling_pick); 42 | 43 | let trace_pass = 44 | CameraComputePass::builder("di_spatial_resampling_trace") 45 | .bind([ 46 | &engine.triangles.bind_readable(), 47 | &engine.bvh.bind_readable(), 48 | &engine.materials.bind_readable(), 49 | &engine.images.bind_atlas(), 50 | ]) 51 | .bind([ 52 | &buffers.curr_camera.bind_readable(), 53 | &buf_d0.bind_readable(), 54 | &buf_d1.bind_readable(), 55 | &buf_d2.bind_writable(), 56 | ]) 57 | .build(device, &engine.shaders.di_spatial_resampling_trace); 58 | 59 | let sample_pass = 60 | CameraComputePass::builder("di_spatial_resampling_sample") 61 | .bind([ 62 | &buffers.curr_camera.bind_readable(), 63 | &buffers.di_reservoirs[1].bind_readable(), 64 | &buffers.di_reservoirs[2].bind_writable(), 65 | &buf_d2.bind_readable(), 66 | ]) 67 | .build(device, &engine.shaders.di_spatial_resampling_sample); 68 | 69 | Self { 70 | pick_pass, 71 | trace_pass, 72 | sample_pass, 73 | } 74 | } 75 | 76 | pub fn run( 77 | &self, 78 | camera: &CameraController, 79 | encoder: &mut wgpu::CommandEncoder, 80 | ) { 81 | self.pick_pass.run( 82 | camera, 83 | encoder, 84 | (camera.camera.viewport.size + 7) / 8 / uvec2(2, 1), 85 | camera.pass_params(), 86 | ); 87 | 88 | self.trace_pass.run( 89 | camera, 90 | encoder, 91 | (camera.camera.viewport.size + 7) / 8, 92 | camera.pass_params(), 93 | ); 94 | 95 | self.sample_pass.run( 96 | camera, 97 | encoder, 98 | (camera.camera.viewport.size + 7) / 8 / uvec2(2, 1), 99 | camera.pass_params(), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /strolle/src/utils/allocator.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::ops::Range; 3 | 4 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 5 | pub struct Allocator { 6 | slots: Vec>, 7 | dirty: bool, 8 | } 9 | 10 | impl Allocator { 11 | pub fn give(&mut self, slot: Range) { 12 | if let Some(last_slot) = self.slots.last() { 13 | self.dirty |= slot.start <= last_slot.end; 14 | } 15 | 16 | self.slots.push(slot); 17 | } 18 | 19 | pub fn take(&mut self, len: usize) -> Option> { 20 | assert!(len > 0); 21 | 22 | self.compact(); 23 | 24 | let slot_id = self.slots.iter().position(|slot| slot.len() >= len)?; 25 | let remaining_slot_size = self.slots[slot_id].len() - len; 26 | 27 | if remaining_slot_size > 0 { 28 | let slot = &mut self.slots[slot_id]; 29 | 30 | slot.start += len; 31 | 32 | Some(Range { 33 | start: slot.start - len, 34 | end: slot.start, 35 | }) 36 | } else { 37 | Some(self.slots.remove(slot_id)) 38 | } 39 | } 40 | 41 | fn compact(&mut self) { 42 | if !mem::take(&mut self.dirty) || self.slots.is_empty() { 43 | return; 44 | } 45 | 46 | self.slots.sort_by_key(|slot| slot.start); 47 | 48 | let mut idx = 0; 49 | 50 | while idx < (self.slots.len() - 1) { 51 | if self.slots[idx].end == self.slots[idx + 1].start { 52 | self.slots[idx].end = self.slots.remove(idx + 1).end; 53 | } else { 54 | idx += 1; 55 | } 56 | } 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn test() { 66 | let mut target = Allocator::default(); 67 | 68 | assert_eq!(None, target.take(16)); 69 | 70 | // --- 71 | // Case 1: Just one range 72 | 73 | target.give(0..32); 74 | 75 | assert_eq!(Some(0..8), target.take(8)); 76 | assert_eq!(Some(8..16), target.take(8)); 77 | assert_eq!(Some(16..24), target.take(8)); 78 | assert_eq!(Some(24..32), target.take(8)); 79 | assert_eq!(None, target.take(8)); 80 | 81 | // --- 82 | // Case 2: Many ranges 83 | 84 | target.give(0..8); 85 | target.give(10..15); 86 | 87 | assert_eq!(Some(0..4), target.take(4)); 88 | assert_eq!(Some(4..8), target.take(4)); 89 | assert_eq!(Some(10..14), target.take(4)); 90 | assert_eq!(None, target.take(4)); 91 | assert_eq!(Some(14..15), target.take(1)); 92 | assert_eq!(None, target.take(1)); 93 | 94 | // --- 95 | // Case 3a: Compaction 96 | 97 | target.give(0..8); 98 | target.give(8..16); 99 | target.give(16..24); 100 | target.give(24..32); 101 | target.give(32..40); 102 | target.give(64..256); 103 | 104 | assert_eq!(Some(64..128), target.take(64)); 105 | assert_eq!(Some(0..20), target.take(20)); 106 | assert_eq!(Some(20..40), target.take(20)); 107 | assert_eq!(Some(128..148), target.take(20)); 108 | 109 | // --- 110 | // Case 3b: Compaction + checking if the `dirty` flag gets set properly 111 | 112 | target = Default::default(); 113 | 114 | target.give(64..256); 115 | target.give(32..40); 116 | target.give(24..32); 117 | target.give(16..24); 118 | target.give(8..16); 119 | target.give(0..8); 120 | 121 | assert_eq!(Some(64..128), target.take(64)); 122 | assert_eq!(Some(0..20), target.take(20)); 123 | assert_eq!(Some(20..40), target.take(20)); 124 | assert_eq!(Some(128..148), target.take(20)); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /strolle-shaders/src/di_temporal_resampling.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(push_constant)] params: &PassParams, 7 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 8 | lights: &[Light], 9 | #[spirv(descriptor_set = 1, binding = 0, uniform)] curr_camera: &Camera, 10 | #[spirv(descriptor_set = 1, binding = 1, uniform)] prev_camera: &Camera, 11 | #[spirv(descriptor_set = 1, binding = 2)] reprojection_map: TexRgba32, 12 | #[spirv(descriptor_set = 1, binding = 3)] curr_prim_gbuffer_d0: TexRgba32, 13 | #[spirv(descriptor_set = 1, binding = 4)] curr_prim_gbuffer_d1: TexRgba32, 14 | #[spirv(descriptor_set = 1, binding = 5)] prev_prim_gbuffer_d0: TexRgba32, 15 | #[spirv(descriptor_set = 1, binding = 6)] prev_prim_gbuffer_d1: TexRgba32, 16 | #[spirv(descriptor_set = 1, binding = 7, storage_buffer)] 17 | prev_reservoirs: &[Vec4], 18 | #[spirv(descriptor_set = 1, binding = 8, storage_buffer)] 19 | curr_reservoirs: &mut [Vec4], 20 | ) { 21 | let lhs_pos = global_id.xy(); 22 | let lhs_idx = curr_camera.screen_to_idx(lhs_pos); 23 | let mut wnoise = WhiteNoise::new(params.seed, lhs_pos); 24 | let lights = LightsView::new(lights); 25 | let reprojection_map = ReprojectionMap::new(reprojection_map); 26 | 27 | if !curr_camera.contains(lhs_pos) { 28 | return; 29 | } 30 | 31 | // ------------------------------------------------------------------------- 32 | 33 | let lhs_hit = Hit::new( 34 | curr_camera.ray(lhs_pos), 35 | GBufferEntry::unpack([ 36 | curr_prim_gbuffer_d0.read(lhs_pos), 37 | curr_prim_gbuffer_d1.read(lhs_pos), 38 | ]), 39 | ); 40 | 41 | if lhs_hit.is_none() { 42 | return; 43 | } 44 | 45 | // --- 46 | 47 | let mut lhs = DiReservoir::read(curr_reservoirs, lhs_idx); 48 | 49 | if !lhs.is_empty() { 50 | lhs.sample.pdf = lhs.sample.pdf(lights, lhs_hit); 51 | } 52 | 53 | // --- 54 | 55 | let mut rhs = DiReservoir::default(); 56 | let mut rhs_hit = Hit::default(); 57 | let mut rhs_killed = false; 58 | 59 | let reprojection = reprojection_map.get(lhs_pos); 60 | 61 | if reprojection.is_some() { 62 | let rhs_pos = reprojection.prev_pos_round(); 63 | 64 | rhs = DiReservoir::read( 65 | prev_reservoirs, 66 | curr_camera.screen_to_idx(rhs_pos), 67 | ); 68 | 69 | rhs.clamp_m(64.0); 70 | 71 | if !rhs.is_empty() { 72 | let rhs_light = lights.get(rhs.sample.light_id); 73 | 74 | if rhs_light.is_slot_killed() { 75 | rhs.w = 0.0; 76 | rhs_killed = true; 77 | } else if rhs_light.is_slot_remapped() { 78 | rhs.sample.light_id = rhs_light.slot_remapped_to(); 79 | } 80 | 81 | rhs_hit = Hit::new( 82 | prev_camera.ray(rhs_pos), 83 | GBufferEntry::unpack([ 84 | prev_prim_gbuffer_d0.read(rhs_pos), 85 | prev_prim_gbuffer_d1.read(rhs_pos), 86 | ]), 87 | ); 88 | } 89 | } 90 | 91 | // --- 92 | 93 | let mut main = DiReservoir::default(); 94 | let mut main_pdf = 0.0; 95 | 96 | let mis = 97 | Mis::di_temporal(lights, lhs, lhs_hit, rhs, rhs_hit, rhs_killed).eval(); 98 | 99 | if main.update(&mut wnoise, lhs.sample, mis.lhs_mis * mis.lhs_pdf * lhs.w) { 100 | main_pdf = mis.lhs_pdf; 101 | } 102 | 103 | if main.update(&mut wnoise, rhs.sample, mis.rhs_mis * mis.rhs_pdf * rhs.w) { 104 | main_pdf = mis.rhs_pdf; 105 | } 106 | 107 | main.m = lhs.m + mis.m; 108 | main.sample.pdf = main_pdf; 109 | main.sample.confidence = if rhs_killed { 0.0 } else { 1.0 }; 110 | main.norm_mis(main_pdf); 111 | main.write(curr_reservoirs, lhs_idx); 112 | } 113 | -------------------------------------------------------------------------------- /strolle-gpu/src/passes.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::{vec3a, vec4, Affine3A, Mat3A, Vec4}; 3 | 4 | use crate::Frame; 5 | 6 | #[repr(C)] 7 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 8 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 9 | pub struct PassParams { 10 | pub seed: u32, 11 | pub frame: Frame, 12 | } 13 | 14 | #[repr(C)] 15 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 16 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 17 | pub struct PrimRasterPassParams { 18 | pub payload: Vec4, 19 | pub curr_xform_inv_d0: Vec4, 20 | pub curr_xform_inv_d1: Vec4, 21 | pub curr_xform_inv_d2: Vec4, 22 | pub prev_xform_d0: Vec4, 23 | pub prev_xform_d1: Vec4, 24 | pub prev_xform_d2: Vec4, 25 | } 26 | 27 | impl PrimRasterPassParams { 28 | pub fn instance_uuid(self) -> u32 { 29 | self.payload.x.to_bits() 30 | } 31 | 32 | pub fn material_id(self) -> u32 { 33 | self.payload.y.to_bits() 34 | } 35 | 36 | pub fn curr_xform_inv(self) -> Affine3A { 37 | Self::decode_affine([ 38 | self.curr_xform_inv_d0, 39 | self.curr_xform_inv_d1, 40 | self.curr_xform_inv_d2, 41 | ]) 42 | } 43 | 44 | pub fn prev_xform(self) -> Affine3A { 45 | Self::decode_affine([ 46 | self.prev_xform_d0, 47 | self.prev_xform_d1, 48 | self.prev_xform_d2, 49 | ]) 50 | } 51 | 52 | /// Encodes a 3D affine transformation as three Vec4s; we use this to 53 | /// overcome padding issues when copying data from CPU into GPU. 54 | pub fn encode_affine(xform: Affine3A) -> [Vec4; 3] { 55 | let d0 = vec4( 56 | xform.matrix3.x_axis.x, 57 | xform.matrix3.x_axis.y, 58 | xform.matrix3.x_axis.z, 59 | xform.translation.x, 60 | ); 61 | 62 | let d1 = vec4( 63 | xform.matrix3.y_axis.x, 64 | xform.matrix3.y_axis.y, 65 | xform.matrix3.y_axis.z, 66 | xform.translation.y, 67 | ); 68 | 69 | let d2 = vec4( 70 | xform.matrix3.z_axis.x, 71 | xform.matrix3.z_axis.y, 72 | xform.matrix3.z_axis.z, 73 | xform.translation.z, 74 | ); 75 | 76 | [d0, d1, d2] 77 | } 78 | 79 | /// See: [`Self::encode_affine()`]. 80 | pub fn decode_affine([d0, d1, d2]: [Vec4; 3]) -> Affine3A { 81 | Affine3A { 82 | matrix3: Mat3A { 83 | x_axis: vec3a(d0.x, d0.y, d0.z), 84 | y_axis: vec3a(d1.x, d1.y, d1.z), 85 | z_axis: vec3a(d2.x, d2.y, d2.z), 86 | }, 87 | translation: vec3a(d0.w, d1.w, d2.w), 88 | } 89 | } 90 | } 91 | 92 | #[repr(C)] 93 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 94 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 95 | pub struct FrameDenoisingWaveletPassParams { 96 | pub frame: Frame, 97 | pub stride: u32, 98 | pub strength: f32, 99 | } 100 | 101 | #[repr(C)] 102 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 103 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 104 | pub struct FrameCompositionPassParams { 105 | pub camera_mode: u32, 106 | } 107 | 108 | #[repr(C)] 109 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 110 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 111 | pub struct RefPassParams { 112 | pub seed: u32, 113 | pub frame: Frame, 114 | pub depth: u32, 115 | } 116 | 117 | #[repr(C)] 118 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 119 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 120 | pub struct GiPreviewResamplingPass { 121 | pub seed: u32, 122 | pub frame: Frame, 123 | pub source: u32, 124 | pub nth: u32, 125 | } 126 | 127 | #[repr(C)] 128 | #[derive(Clone, Copy, Default, Pod, Zeroable)] 129 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 130 | pub struct GiResolvingPassParams { 131 | pub frame: Frame, 132 | pub source: u32, 133 | } 134 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/passes/atmosphere.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use crate::{ 4 | gpu, Camera, CameraBuffers, CameraComputePass, CameraController, Engine, 5 | Params, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct AtmospherePass { 10 | generate_transmittance_lut_pass: CameraComputePass<()>, 11 | generate_scattering_lut_pass: CameraComputePass<()>, 12 | generate_sky_lut_pass: CameraComputePass<()>, 13 | 14 | is_initialized: Mutex, 15 | known_sun_altitude: Mutex>, 16 | } 17 | 18 | impl AtmospherePass { 19 | pub fn new

( 20 | engine: &Engine

, 21 | device: &wgpu::Device, 22 | _: &Camera, 23 | buffers: &CameraBuffers, 24 | ) -> Self 25 | where 26 | P: Params, 27 | { 28 | let generate_transmittance_lut_pass = 29 | CameraComputePass::builder("atmosphere_generate_transmittance_lut") 30 | .bind([&buffers.atmosphere_transmittance_lut.bind_writable()]) 31 | .build( 32 | device, 33 | &engine.shaders.atmosphere_generate_transmittance_lut, 34 | ); 35 | 36 | let generate_scattering_lut_pass = 37 | CameraComputePass::builder("atmosphere_generate_scattering_lut") 38 | .bind([ 39 | &buffers.atmosphere_transmittance_lut.bind_sampled(), 40 | &buffers.atmosphere_scattering_lut.bind_writable(), 41 | ]) 42 | .build( 43 | device, 44 | &engine.shaders.atmosphere_generate_scattering_lut, 45 | ); 46 | 47 | let generate_sky_lut_pass = 48 | CameraComputePass::builder("atmosphere_generate_sky_lut") 49 | .bind([ 50 | &engine.world.bind_readable(), 51 | &buffers.atmosphere_transmittance_lut.bind_sampled(), 52 | &buffers.atmosphere_scattering_lut.bind_sampled(), 53 | &buffers.atmosphere_sky_lut.bind_writable(), 54 | ]) 55 | .build(device, &engine.shaders.atmosphere_generate_sky_lut); 56 | 57 | Self { 58 | generate_transmittance_lut_pass, 59 | generate_scattering_lut_pass, 60 | generate_sky_lut_pass, 61 | 62 | is_initialized: Mutex::new(false), 63 | known_sun_altitude: Mutex::new(None), 64 | } 65 | } 66 | 67 | pub fn run

( 68 | &self, 69 | engine: &Engine

, 70 | camera: &CameraController, 71 | encoder: &mut wgpu::CommandEncoder, 72 | ) where 73 | P: Params, 74 | { 75 | let mut is_initialized = self.is_initialized.lock().unwrap(); 76 | let mut known_sun_altitude = self.known_sun_altitude.lock().unwrap(); 77 | 78 | // Transmittance and scattering don't depend on anything so it's enough 79 | // if we just generate them once, the first time they are needed 80 | if !*is_initialized { 81 | self.generate_transmittance_lut_pass.run( 82 | camera, 83 | encoder, 84 | (gpu::Atmosphere::TRANSMITTANCE_LUT_RESOLUTION + 7) / 8, 85 | (), 86 | ); 87 | 88 | self.generate_scattering_lut_pass.run( 89 | camera, 90 | encoder, 91 | (gpu::Atmosphere::SCATTERING_LUT_RESOLUTION + 7) / 8, 92 | (), 93 | ); 94 | 95 | *is_initialized = true; 96 | } 97 | 98 | // On the other hand, the sky lookup texture depends on sun's altitude 99 | if known_sun_altitude 100 | .map_or(true, |altitude| altitude != engine.sun.altitude) 101 | { 102 | self.generate_sky_lut_pass.run( 103 | camera, 104 | encoder, 105 | (gpu::Atmosphere::SKY_LUT_RESOLUTION + 7) / 8, 106 | (), 107 | ); 108 | 109 | *known_sun_altitude = Some(engine.sun.altitude); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /strolle-shaders/src/gi_sampling_a.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(local_invocation_index)] local_idx: u32, 7 | #[spirv(push_constant)] params: &PassParams, 8 | #[spirv(workgroup)] stack: BvhStack, 9 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 10 | triangles: &[Triangle], 11 | #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] bvh: &[Vec4], 12 | #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] 13 | materials: &[Material], 14 | #[spirv(descriptor_set = 0, binding = 3)] atlas_tex: Tex, 15 | #[spirv(descriptor_set = 0, binding = 4)] atlas_sampler: &Sampler, 16 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 17 | #[spirv(descriptor_set = 1, binding = 1)] prim_gbuffer_d0: TexRgba32, 18 | #[spirv(descriptor_set = 1, binding = 2)] prim_gbuffer_d1: TexRgba32, 19 | #[spirv(descriptor_set = 1, binding = 3)] gi_d0: TexRgba32, 20 | #[spirv(descriptor_set = 1, binding = 4)] gi_d1: TexRgba32, 21 | #[spirv(descriptor_set = 1, binding = 5)] gi_d2: TexRgba32, 22 | #[spirv(descriptor_set = 1, binding = 6, storage_buffer)] 23 | reservoirs: &[Vec4], 24 | ) { 25 | let global_id = global_id.xy(); 26 | 27 | let screen_pos = if params.frame.is_gi_tracing() { 28 | resolve_checkerboard(global_id, params.frame.get() / 2) 29 | } else { 30 | resolve_checkerboard(global_id, params.frame.get()) 31 | }; 32 | 33 | let screen_idx = camera.screen_to_idx(screen_pos); 34 | let triangles = TrianglesView::new(triangles); 35 | let bvh = BvhView::new(bvh); 36 | let materials = MaterialsView::new(materials); 37 | 38 | if !camera.contains(screen_pos) { 39 | return; 40 | } 41 | 42 | // ------------------------------------------------------------------------- 43 | 44 | let gi_ray; 45 | let gi_ray_pdf; 46 | 47 | if params.frame.is_gi_tracing() { 48 | let mut wnoise = WhiteNoise::new(params.seed, screen_pos); 49 | 50 | let hit = Hit::new( 51 | camera.ray(screen_pos), 52 | GBufferEntry::unpack([ 53 | prim_gbuffer_d0.read(screen_pos), 54 | prim_gbuffer_d1.read(screen_pos), 55 | ]), 56 | ); 57 | 58 | if hit.is_none() { 59 | return; 60 | } else { 61 | let sample = 62 | LayeredBrdf::new(hit.gbuffer).sample(&mut wnoise, -hit.dir); 63 | 64 | gi_ray = Ray::new(hit.point, sample.dir); 65 | gi_ray_pdf = sample.pdf; 66 | } 67 | } else { 68 | let res = GiReservoir::read(reservoirs, screen_idx); 69 | 70 | if res.is_empty() { 71 | return; 72 | } 73 | 74 | gi_ray = 75 | Ray::new(res.sample.v1_point, res.sample.dir(res.sample.v1_point)); 76 | 77 | gi_ray_pdf = 1.0; 78 | }; 79 | 80 | let (gi_hit, _) = gi_ray.trace( 81 | local_idx, 82 | stack, 83 | triangles, 84 | bvh, 85 | materials, 86 | atlas_tex, 87 | atlas_sampler, 88 | ); 89 | 90 | // --- 91 | 92 | let gi_gbuffer = if gi_hit.is_some() { 93 | let mut gi_material = materials.get(gi_hit.material_id); 94 | 95 | gi_material.regularize(); 96 | 97 | GBufferEntry { 98 | base_color: gi_material.base_color( 99 | atlas_tex, 100 | atlas_sampler, 101 | gi_hit.uv, 102 | ), 103 | normal: gi_hit.normal, 104 | metallic: gi_material.metallic, 105 | emissive: gi_material.emissive(atlas_tex, atlas_sampler, gi_hit.uv), 106 | roughness: gi_material.roughness, 107 | reflectance: gi_material.reflectance, 108 | depth: gi_ray.origin().distance(gi_hit.point), 109 | } 110 | } else { 111 | Default::default() 112 | }; 113 | 114 | let d0 = gi_ray.dir().extend(gi_ray_pdf); 115 | let [d1, d2] = gi_gbuffer.pack(); 116 | 117 | unsafe { 118 | gi_d0.write(global_id, d0); 119 | gi_d1.write(global_id, d1); 120 | gi_d2.write(global_id, d2); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /strolle-shaders/src/di_resolving.rs: -------------------------------------------------------------------------------- 1 | use strolle_gpu::prelude::*; 2 | 3 | #[spirv(compute(threads(8, 8)))] 4 | pub fn main( 5 | #[spirv(global_invocation_id)] global_id: UVec3, 6 | #[spirv(local_invocation_index)] local_idx: u32, 7 | #[spirv(workgroup)] stack: BvhStack, 8 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 9 | triangles: &[Triangle], 10 | #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] bvh: &[Vec4], 11 | #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] 12 | materials: &[Material], 13 | #[spirv(descriptor_set = 0, binding = 3, storage_buffer)] 14 | lights: &[Light], 15 | #[spirv(descriptor_set = 0, binding = 4)] atlas_tex: Tex, 16 | #[spirv(descriptor_set = 0, binding = 5)] atlas_sampler: &Sampler, 17 | #[spirv(descriptor_set = 0, binding = 6, uniform)] world: &World, 18 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 19 | #[spirv(descriptor_set = 1, binding = 1)] 20 | atmosphere_transmittance_lut_tex: Tex, 21 | #[spirv(descriptor_set = 1, binding = 2)] 22 | atmosphere_transmittance_lut_sampler: &Sampler, 23 | #[spirv(descriptor_set = 1, binding = 3)] atmosphere_sky_lut_tex: Tex, 24 | #[spirv(descriptor_set = 1, binding = 4)] 25 | atmosphere_sky_lut_sampler: &Sampler, 26 | #[spirv(descriptor_set = 1, binding = 5)] prim_gbuffer_d0: TexRgba32, 27 | #[spirv(descriptor_set = 1, binding = 6)] prim_gbuffer_d1: TexRgba32, 28 | #[spirv(descriptor_set = 1, binding = 7, storage_buffer)] 29 | next_reservoirs: &[Vec4], 30 | #[spirv(descriptor_set = 1, binding = 8, storage_buffer)] 31 | prev_reservoirs: &mut [Vec4], 32 | #[spirv(descriptor_set = 1, binding = 9)] diff_output: TexRgba32, 33 | #[spirv(descriptor_set = 1, binding = 10)] spec_output: TexRgba32, 34 | ) { 35 | let screen_pos = global_id.xy(); 36 | let screen_idx = camera.screen_to_idx(screen_pos); 37 | let triangles = TrianglesView::new(triangles); 38 | let bvh = BvhView::new(bvh); 39 | let materials = MaterialsView::new(materials); 40 | let lights = LightsView::new(lights); 41 | let atmosphere = Atmosphere::new( 42 | atmosphere_transmittance_lut_tex, 43 | atmosphere_transmittance_lut_sampler, 44 | atmosphere_sky_lut_tex, 45 | atmosphere_sky_lut_sampler, 46 | ); 47 | 48 | if !camera.contains(screen_pos) { 49 | return; 50 | } 51 | 52 | // ------------------------------------------------------------------------- 53 | 54 | let hit = Hit::new( 55 | camera.ray(screen_pos), 56 | GBufferEntry::unpack([ 57 | prim_gbuffer_d0.read(screen_pos), 58 | prim_gbuffer_d1.read(screen_pos), 59 | ]), 60 | ); 61 | 62 | let mut res = 63 | DiReservoir::read(next_reservoirs, camera.screen_to_idx(screen_pos)); 64 | 65 | let confidence; 66 | let radiance; 67 | 68 | if hit.is_some() { 69 | let is_occluded = res.sample.ray(hit.point).intersect( 70 | local_idx, 71 | stack, 72 | triangles, 73 | bvh, 74 | materials, 75 | atlas_tex, 76 | atlas_sampler, 77 | ); 78 | 79 | confidence = if res.sample.is_occluded == is_occluded { 80 | res.sample.confidence 81 | } else { 82 | 0.0 83 | }; 84 | 85 | res.sample.confidence = 1.0; 86 | res.sample.is_occluded = is_occluded; 87 | 88 | radiance = if res.sample.is_occluded { 89 | LightRadiance::default() 90 | } else { 91 | lights.get(res.sample.light_id).radiance(hit) * res.w 92 | }; 93 | } else { 94 | confidence = 1.0; 95 | 96 | radiance = LightRadiance { 97 | radiance: atmosphere.sample(world.sun_dir(), hit.dir), 98 | diff_brdf: Vec3::ONE, 99 | spec_brdf: Vec3::ZERO, 100 | }; 101 | }; 102 | 103 | unsafe { 104 | let diff_brdf = (1.0 - hit.gbuffer.metallic) / PI; 105 | let spec_brdf = radiance.spec_brdf; 106 | 107 | diff_output.write( 108 | screen_pos, 109 | (radiance.radiance * diff_brdf).extend(confidence), 110 | ); 111 | 112 | spec_output.write( 113 | screen_pos, 114 | (radiance.radiance * spec_brdf).extend(confidence), 115 | ); 116 | } 117 | 118 | res.write(prev_reservoirs, screen_idx); 119 | } 120 | -------------------------------------------------------------------------------- /strolle-shaders/src/prim_raster.rs: -------------------------------------------------------------------------------- 1 | use spirv_std::arch; 2 | use strolle_gpu::prelude::*; 3 | 4 | #[spirv(vertex)] 5 | pub fn vs( 6 | // Params 7 | #[spirv(push_constant)] params: &PrimRasterPassParams, 8 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 9 | #[spirv(descriptor_set = 1, binding = 1, uniform)] prev_camera: &Camera, 10 | 11 | // Inputs 12 | vertex_d0: Vec4, 13 | vertex_d1: Vec4, 14 | 15 | // Outputs 16 | #[spirv(position)] out_vertex: &mut Vec4, 17 | out_curr_vertex: &mut Vec4, 18 | out_prev_vertex: &mut Vec4, 19 | out_point: &mut Vec3, 20 | out_normal: &mut Vec3, 21 | out_uv: &mut Vec2, 22 | ) { 23 | let point = vertex_d0.xyz(); 24 | 25 | let prev_point = params 26 | .prev_xform() 27 | .transform_point3(params.curr_xform_inv().transform_point3(point)); 28 | 29 | let normal = vertex_d1.xyz(); 30 | let uv = vec2(vertex_d0.w, vertex_d1.w); 31 | 32 | *out_vertex = camera.world_to_clip(point); 33 | *out_curr_vertex = camera.world_to_clip(point); 34 | *out_prev_vertex = prev_camera.world_to_clip(prev_point); 35 | *out_point = point; 36 | *out_normal = normal; 37 | *out_uv = uv; 38 | } 39 | 40 | #[spirv(fragment)] 41 | pub fn fs( 42 | // Params 43 | #[spirv(push_constant)] params: &PrimRasterPassParams, 44 | #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] 45 | materials: &[Material], 46 | #[spirv(descriptor_set = 0, binding = 1)] atlas_tex: Tex, 47 | #[spirv(descriptor_set = 0, binding = 2)] atlas_sampler: &Sampler, 48 | #[spirv(descriptor_set = 1, binding = 0, uniform)] camera: &Camera, 49 | #[spirv(descriptor_set = 1, binding = 1, uniform)] prev_camera: &Camera, 50 | #[spirv(front_facing)] front_facing: bool, 51 | 52 | // Inputs 53 | curr_vertex: Vec4, 54 | prev_vertex: Vec4, 55 | point: Vec3, 56 | normal: Vec3, 57 | uv: Vec2, 58 | 59 | // Outputs 60 | out_prim_gbuffer_d0: &mut Vec4, 61 | out_prim_gbuffer_d1: &mut Vec4, 62 | out_surface: &mut Vec4, 63 | out_velocity: &mut Vec4, 64 | ) { 65 | let material = MaterialsView::new(materials) 66 | .get(MaterialId::new(params.material_id())); 67 | 68 | let base_color = material.base_color(atlas_tex, atlas_sampler, uv); 69 | let metallic_roughness = 70 | material.metallic_roughness(atlas_tex, atlas_sampler, uv); 71 | // If our material is transparent and doesn't rely on refraction, kill the 72 | // current fragment to re-use GPU in finding the next triangle 73 | if base_color.w < 0.01 && material.ior == 1.0 { 74 | arch::kill(); 75 | } 76 | 77 | let normal = { 78 | // TODO bring back normal mapping 79 | let normal = normal.normalize(); 80 | 81 | if front_facing { 82 | normal 83 | } else { 84 | -normal 85 | } 86 | }; 87 | 88 | let ray = camera.ray(camera.clip_to_screen(curr_vertex).round().as_uvec2()); 89 | let depth = ray.origin().distance(point); 90 | 91 | let gbuffer = GBufferEntry { 92 | base_color, 93 | normal, 94 | metallic: metallic_roughness.x, 95 | emissive: material.emissive(atlas_tex, atlas_sampler, uv), 96 | roughness: metallic_roughness.y, 97 | reflectance: material.reflectance, 98 | depth, 99 | }; 100 | 101 | let [gbuffer_d0, gbuffer_d1] = gbuffer.pack(); 102 | 103 | *out_prim_gbuffer_d0 = gbuffer_d0; 104 | *out_prim_gbuffer_d1 = gbuffer_d1; 105 | 106 | // ------------------------------------------------------------------------- 107 | 108 | *out_surface = Normal::encode(normal) 109 | .extend(depth) 110 | .extend(material.roughness); 111 | 112 | // ------------------------------------------------------------------------- 113 | 114 | *out_velocity = { 115 | let velocity = camera.clip_to_screen(curr_vertex) 116 | - prev_camera.clip_to_screen(prev_vertex); 117 | 118 | if velocity.length_squared() >= 0.001 { 119 | velocity.extend(0.0).extend(0.0) 120 | } else { 121 | // Due to floting-point inaccuracies, stationary objects can end up 122 | // having a very small velocity instead of zero - this causes our 123 | // reprojection shader to freak out, so let's truncate small 124 | // velocities to zero 125 | Default::default() 126 | } 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strolle 2 | 3 | Strolle (from _strålspårning_) is a real-time renderer with support for dynamic 4 | global illumination: 5 | 6 |

7 | 8 |

9 | 10 | Strolle's goal is to experiment with modern real-time lighting techniques such 11 | as ReSTIR and see how far we can go on consumer hardware, especially the one 12 | _without_ dedicated ray-tracing cores. 13 | 14 | Strolle comes integrated with [Bevy](https://bevyengine.org/), but can be also 15 | used on its own (through `wgpu`). 16 | 17 | Status: Abandoned, I've moved on to other things; feel free to fork and/or ping 18 | me if you're interested in details as to how this engine works. 19 | 20 | * [Gallery](#gallery) 21 | * [Examples](#examples) 22 | * [Usage](#usage) 23 | * [Roadmap](#roadmap) 24 | 25 | ## Gallery 26 | 27 |

28 | 29 |

30 | 31 |

32 | 33 |

34 | 35 |

36 | 37 |

38 | 39 | (note that currently there's no denoiser for specular lighting) 40 | 41 | ## Examples 42 | 43 | ### Dungeon 44 | 45 | ``` shell 46 | $ cargo run --release --example demo 47 | ``` 48 | 49 | Shows a dungeon tech demo, as in the example above. 50 | 51 | Use WASD to move, mouse to navigate the camera, and: 52 | 53 | - H/L: Adjust sun's azimuth, 54 | - J/K: Adjust sun's altitude, 55 | - F: Toggle flashlight on/off, 56 | - M: Toggle diffuse/specular materials, 57 | - T: Remove textures, 58 | - 1: Show the final, composed image¹ (default), 59 | - 2: Show direct-diffuse lighting only¹, 60 | - 3: Show direct-specular lighting only¹, 61 | - 4: Show indirect-diffuse lighting only¹, 62 | - 5: Show indirect-specular lighting only¹, 63 | - 8: Show BVH heatmap, 64 | - 9: Switch camera to a path-traced reference mode (slow), 65 | - 0: Switch camera to Bevy's renderer, 66 | - ;: Toggle camera's controls on/off - useful for taking screenshots. 67 | 68 | ¹ press the same key again to toggle denoising on/off 69 | 70 | Model thanks to: 71 | https://sketchfab.com/3d-models/low-poly-game-level-82b7a937ae504cfa9f277d9bf6874ad2 72 | 73 | ### Cornell Box 74 | 75 | ``` shell 76 | $ cargo run --release --example cornell 77 | ``` 78 | 79 | ## Usage 80 | 81 | ### Bevy 82 | 83 | Currently supported Bevy version: 0.12.1. 84 | 85 | 1. Add Strolle to your dependencies: 86 | 87 | ``` toml 88 | [dependencies] 89 | bevy_strolle = { git = "https://github.com/patryk27/strolle" } 90 | ``` 91 | 92 | 2. Add a patch to work-around [a bug in Naga](https://github.com/gfx-rs/naga/issues/2373): 93 | 94 | ``` toml 95 | [patch."crates-io"] 96 | naga = { git = "https://github.com/Patryk27/naga", branch = "v0.13.0-strolle" } 97 | ``` 98 | 99 | 3. Setup & enjoy! 100 | 101 | ```rust 102 | App::new() 103 | /* ... */ 104 | .add_plugins(StrollePlugin); 105 | 106 | commands 107 | .spawn(Camera3dBundle { 108 | camera_render_graph: CameraRenderGraph::new( 109 | bevy_strolle::graph::NAME, 110 | ), 111 | camera: Camera { 112 | hdr: true, 113 | ..default() 114 | }, 115 | ..default() 116 | }); 117 | ``` 118 | 119 | Note that Strolle completely overrides Bevy's camera graph, so you can't use a 120 | Strolle camera together with Bevy's effects such as bloom or TAA - fragment and 121 | vertex shaders won't work as well. 122 | 123 | Also, Strolle is not optimized well towards higher resolutions - on non-high-end 124 | GPUs, it's recommended to stick to ~800x600 and upscale the camera instead (see 125 | the `demo.rs` here). 126 | 127 | ## Roadmap 128 | 129 | https://github.com/Patryk27/strolle/issues?q=is%3Aissue+is%3Aopen+label%3AC-bug%2CC-feature 130 | 131 | ## Algorithms 132 | 133 | Notable algorithms implemented in Strolle include: 134 | 135 | - [ReSTIR DI](https://research.nvidia.com/sites/default/files/pubs/2020-07_Spatiotemporal-reservoir-resampling/ReSTIR.pdf) 136 | - [ReSTIR GI](https://d1qx31qr3h6wln.cloudfront.net/publications/ReSTIR%20GI.pdf) 137 | - [SVGF](https://research.nvidia.com/publication/2017-07_spatiotemporal-variance-guided-filtering-real-time-reconstruction-path-traced) 138 | - [A Scalable and Production Ready Sky and Atmosphere Rendering Technique](https://sebh.github.io/publications/egsr2020.pdf) 139 | 140 | ## License 141 | 142 | MIT License 143 | 144 | Copyright (c) 2022 Patryk Wychowaniec & Jakub Trąd 145 | -------------------------------------------------------------------------------- /strolle-gpu/src/material.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; 3 | use spirv_std::Sampler; 4 | 5 | use crate::Tex; 6 | 7 | #[repr(C)] 8 | #[derive(Clone, Copy, PartialEq, Pod, Zeroable)] 9 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] 10 | pub struct Material { 11 | pub base_color: Vec4, 12 | pub base_color_texture: Vec4, 13 | pub emissive: Vec4, 14 | pub emissive_texture: Vec4, 15 | pub roughness: f32, 16 | pub metallic: f32, 17 | pub reflectance: f32, 18 | pub ior: f32, 19 | pub metallic_roughness_texture: Vec4, 20 | pub normal_map_texture: Vec4, 21 | } 22 | 23 | impl Material { 24 | /// Adjusts material so that it's ready for computing indirect lighting. 25 | pub fn regularize(&mut self) { 26 | self.roughness = self.roughness.max(0.75 * 0.75); 27 | } 28 | 29 | pub fn base_color( 30 | self, 31 | atlas_tex: Tex, 32 | atlas_sampler: &Sampler, 33 | hit_uv: Vec2, 34 | ) -> Vec4 { 35 | Self::sample_atlas( 36 | atlas_tex, 37 | atlas_sampler, 38 | hit_uv, 39 | self.base_color, 40 | self.base_color_texture, 41 | ) 42 | } 43 | 44 | pub fn metallic_roughness( 45 | self, 46 | atlas_tex: Tex, 47 | atlas_sampler: &Sampler, 48 | hit_uv: Vec2, 49 | ) -> Vec2 { 50 | Self::sample_atlas( 51 | atlas_tex, 52 | atlas_sampler, 53 | hit_uv, 54 | Vec4::new(1.0, self.roughness, self.metallic, 1.0), 55 | self.metallic_roughness_texture, 56 | ) 57 | .zy() 58 | } 59 | 60 | pub fn emissive( 61 | self, 62 | atlas_tex: Tex, 63 | atlas_sampler: &Sampler, 64 | hit_uv: Vec2, 65 | ) -> Vec3 { 66 | Self::sample_atlas( 67 | atlas_tex, 68 | atlas_sampler, 69 | hit_uv, 70 | self.emissive, 71 | self.emissive_texture, 72 | ) 73 | .xyz() 74 | } 75 | 76 | fn sample_atlas( 77 | atlas_tex: Tex, 78 | atlas_sampler: &Sampler, 79 | mut hit_uv: Vec2, 80 | multiplier: Vec4, 81 | texture: Vec4, 82 | ) -> Vec4 { 83 | // TODO this assumes the texture's sampler is configured to U/V-repeat 84 | // which might not be the case; we should propagate sampler info up 85 | // to here and decide 86 | let wrap = |t: f32| { 87 | if t > 0.0 { 88 | t % 1.0 89 | } else { 90 | 1.0 - (-t % 1.0) 91 | } 92 | }; 93 | 94 | if texture == Vec4::ZERO { 95 | multiplier 96 | } else { 97 | hit_uv.x = wrap(hit_uv.x); 98 | hit_uv.y = wrap(hit_uv.y); 99 | 100 | let uv = texture.xy() + hit_uv * texture.zw(); 101 | 102 | multiplier * atlas_tex.sample_by_lod(*atlas_sampler, uv, 0.0) 103 | } 104 | } 105 | 106 | // TODO bring back 107 | // 108 | // pub fn normal( 109 | // &self, 110 | // hit_uv: Vec2, 111 | // hit_normal: Vec3, 112 | // hit_tangent: Vec4, 113 | // ) -> Vec3 { 114 | // if self.normal_map_texture == u32::MAX { 115 | // hit_normal 116 | // } else { 117 | // let normal_map_tex = unsafe { 118 | // images.index_unchecked(self.normal_map_texture as usize) 119 | // }; 120 | 121 | // let normal_map_sampler = unsafe { 122 | // samplers.index_unchecked(self.normal_map_texture as usize) 123 | // }; 124 | 125 | // let tangent = hit_tangent.xyz(); 126 | // let bitangent = hit_tangent.w * hit_normal.cross(tangent); 127 | 128 | // let mapped_normal = 129 | // normal_map_tex.sample(*normal_map_sampler, hit_uv); 130 | 131 | // let mapped_normal = 2.0 * mapped_normal - 1.0; 132 | 133 | // (mapped_normal.x * tangent 134 | // + mapped_normal.y * bitangent 135 | // + mapped_normal.z * hit_normal) 136 | // .normalize() 137 | // } 138 | // } 139 | } 140 | 141 | #[derive(Clone, Copy)] 142 | #[cfg_attr(not(target_arch = "spirv"), derive(Debug, PartialEq))] 143 | pub struct MaterialId(u32); 144 | 145 | impl MaterialId { 146 | pub fn new(id: u32) -> Self { 147 | Self(id) 148 | } 149 | 150 | pub fn get(self) -> u32 { 151 | self.0 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /strolle/src/camera_controller/pass.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::mem; 3 | use std::ops::Range; 4 | 5 | use bytemuck::Pod; 6 | use log::debug; 7 | use spirv_std::glam::UVec2; 8 | 9 | use crate::{ 10 | gpu, BindGroup, BindGroupBuilder, CameraController, DoubleBufferedBindable, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct CameraComputePass

{ 15 | label: String, 16 | bind_groups: Vec, 17 | pipeline: wgpu::ComputePipeline, 18 | _params: PhantomData

, 19 | } 20 | 21 | impl

CameraComputePass

22 | where 23 | P: Pod, 24 | { 25 | pub fn builder<'a>(label: impl ToString) -> CameraPassBuilder<'a, P> { 26 | CameraPassBuilder { 27 | label: label.to_string(), 28 | bind_groups: Default::default(), 29 | _params: Default::default(), 30 | } 31 | } 32 | 33 | pub fn run( 34 | &self, 35 | camera: &CameraController, 36 | encoder: &mut wgpu::CommandEncoder, 37 | size: UVec2, 38 | params: P, 39 | ) { 40 | let label = format!("strolle_{}_pass", self.label); 41 | 42 | let mut pass = 43 | encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { 44 | label: Some(&label), 45 | }); 46 | 47 | pass.set_pipeline(&self.pipeline); 48 | 49 | if mem::size_of::

() > 0 { 50 | pass.set_push_constants(0, bytemuck::bytes_of(¶ms)); 51 | } 52 | 53 | for (bind_group_idx, bind_group) in self.bind_groups.iter().enumerate() 54 | { 55 | pass.set_bind_group( 56 | bind_group_idx as u32, 57 | bind_group.get(camera.is_alternate()), 58 | &[], 59 | ); 60 | } 61 | 62 | pass.dispatch_workgroups(size.x, size.y, 1); 63 | } 64 | } 65 | 66 | pub struct CameraPassBuilder<'a, P> { 67 | label: String, 68 | bind_groups: Vec>, 69 | _params: PhantomData

, 70 | } 71 | 72 | impl<'a, P> CameraPassBuilder<'a, P> 73 | where 74 | P: Pod, 75 | { 76 | pub fn bind( 77 | mut self, 78 | items: [&'a dyn DoubleBufferedBindable; N], 79 | ) -> Self { 80 | let mut bind_group = BindGroup::builder(format!( 81 | "{}_bg{}", 82 | self.label, 83 | self.bind_groups.len() 84 | )); 85 | 86 | for item in items { 87 | bind_group = bind_group.add(item); 88 | } 89 | 90 | self.bind_groups.push(bind_group); 91 | self 92 | } 93 | 94 | pub fn build( 95 | self, 96 | device: &wgpu::Device, 97 | (module, entry_point): &(wgpu::ShaderModule, &'static str), 98 | ) -> CameraComputePass

{ 99 | debug!("Initializing pass: {}:{}", self.label, entry_point); 100 | 101 | let bind_groups: Vec<_> = self 102 | .bind_groups 103 | .into_iter() 104 | .map(|bg| bg.build(device)) 105 | .collect(); 106 | 107 | let bind_group_layouts: Vec<_> = 108 | bind_groups.iter().map(|bg| bg.layout()).collect(); 109 | 110 | let push_constant_ranges = if mem::size_of::

() > 0 { 111 | vec![wgpu::PushConstantRange { 112 | stages: wgpu::ShaderStages::COMPUTE, 113 | range: Range { 114 | start: 0, 115 | end: mem::size_of::

() as u32, 116 | }, 117 | }] 118 | } else { 119 | vec![] 120 | }; 121 | 122 | let pipeline_layout_label = 123 | format!("strolle_{}_pipeline_layout", self.label); 124 | 125 | let pipeline_layout = 126 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 127 | label: Some(&pipeline_layout_label), 128 | bind_group_layouts: &bind_group_layouts, 129 | push_constant_ranges: &push_constant_ranges, 130 | }); 131 | 132 | let pipeline_label = format!("strolle_{}_pipeline", self.label); 133 | 134 | let pipeline = 135 | device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 136 | label: Some(&pipeline_label), 137 | layout: Some(&pipeline_layout), 138 | module, 139 | entry_point, 140 | }); 141 | 142 | CameraComputePass { 143 | label: self.label, 144 | bind_groups, 145 | pipeline, 146 | _params: PhantomData, 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------