diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 94801214..563f126d 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -10,7 +10,7 @@ on: env: # For setup-rust, see https://github.com/moonrepo/setup-rust/issues/22 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CARGO_GPU_COMMITSH: 39b238f25b7652ba79d153626e252321942cb558 + CARGO_GPU_COMMITSH: 31153a8edd3bc626d4b9fb0cd7bdb7a8b30797d3 jobs: # Installs cargo deps and sets the cache directory for subsequent jobs diff --git a/Cargo.lock b/Cargo.lock index 7c6122ad..2750aed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,26 +748,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" -dependencies = [ - "log", - "web-sys", -] - [[package]] name = "console_log" version = "1.0.0" @@ -853,9 +833,9 @@ dependencies = [ [[package]] name = "craballoc" -version = "0.2.3" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a2da5589f6c4076db520b5e1a96cbfbc954f5ef50d7ade9fd219860a8d8467" +checksum = "6042c2cfdfce510235f88fea69e263ab3d3a1780f4a645fbddca2adf3af8d6bc" dependencies = [ "async-channel 1.9.0", "bytemuck", @@ -869,9 +849,9 @@ dependencies = [ [[package]] name = "crabslab" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe77aba0fb1ad6ddb4d30744bfc61e1420a9cc1fa981b3556cc423506df9450" +checksum = "9c9866f967260166a968eb2c7382c62f97bbf0a9896fa5c444730e49b3ca3f08" dependencies = [ "crabslab-derive", "futures-lite 1.13.0", @@ -881,9 +861,9 @@ dependencies = [ [[package]] name = "crabslab-derive" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32554318ca91eb0c2c6a05659ec2cd1627fd44142d54547bb3d0aa7405a979b2" +checksum = "cdc3fdfc4f885d410742aad05c6cc3d2867627be16f79f6f3859c2d0484ce778" dependencies = [ "proc-macro2", "quote", @@ -1207,23 +1187,15 @@ dependencies = [ ] [[package]] -name = "example-wasm" +name = "examples" version = "0.1.0" dependencies = [ - "console_error_panic_hook", - "console_log 0.2.2", - "example", - "fern", + "doc-comment", + "env_logger", "futures-lite 1.13.0", - "gltf", - "log", "renderling", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", - "web-sys", - "wgpu", - "winit", + "renderling_build", + "tokio", ] [[package]] @@ -1265,15 +1237,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - [[package]] name = "fixedbitset" version = "0.5.7" @@ -3737,7 +3700,7 @@ dependencies = [ "async-channel 1.9.0", "bytemuck", "cfg_aliases", - "console_log 1.0.0", + "console_log", "craballoc", "crabslab", "crunch", @@ -4270,7 +4233,7 @@ dependencies = [ [[package]] name = "spirv-std" version = "0.9.0" -source = "git+https://github.com/LegNeato/rust-gpu.git?rev=425328a#425328a3ac7f1f18db914d24b3d4754bf13bb7ac" +source = "git+https://github.com/rust-gpu/rust-gpu.git?rev=05b34493ce661dccd6694cf58afc13e3c8f7a7e0#05b34493ce661dccd6694cf58afc13e3c8f7a7e0" dependencies = [ "bitflags 1.3.2", "glam", @@ -4283,7 +4246,7 @@ dependencies = [ [[package]] name = "spirv-std-macros" version = "0.9.0" -source = "git+https://github.com/LegNeato/rust-gpu.git?rev=425328a#425328a3ac7f1f18db914d24b3d4754bf13bb7ac" +source = "git+https://github.com/rust-gpu/rust-gpu.git?rev=05b34493ce661dccd6694cf58afc13e3c8f7a7e0#05b34493ce661dccd6694cf58afc13e3c8f7a7e0" dependencies = [ "proc-macro2", "quote", @@ -4294,7 +4257,7 @@ dependencies = [ [[package]] name = "spirv-std-types" version = "0.9.0" -source = "git+https://github.com/LegNeato/rust-gpu.git?rev=425328a#425328a3ac7f1f18db914d24b3d4754bf13bb7ac" +source = "git+https://github.com/rust-gpu/rust-gpu.git?rev=05b34493ce661dccd6694cf58afc13e3c8f7a7e0#05b34493ce661dccd6694cf58afc13e3c8f7a7e0" [[package]] name = "stable_deref_trait" @@ -5722,9 +5685,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.11" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "ahash", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index 65e707e9..dc2adaf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [workspace] members = [ "crates/example", + "crates/examples", "crates/example-culling", - "crates/example-wasm", + #"crates/example-wasm", "crates/loading-bytes", "crates/renderling", "crates/renderling-build", @@ -23,8 +24,8 @@ bytemuck = { version = "1.19.0", features = ["derive"] } cfg_aliases = "0.2" clap = { version = "4.5.23", features = ["derive"] } console_log = "1.0.0" -craballoc = { version = "0.2.3" } -crabslab = { version = "0.6.5", default-features = false } +craballoc = { version = "0.3.1" } +crabslab = { version = "0.6.6", default-features = false } plotters = "0.3.7" ctor = "0.2.2" dagga = "0.2.1" @@ -50,8 +51,8 @@ serde_json = "1.0.117" send_wrapper = "0.6.0" similarity = "0.2.0" snafu = "0.8" -spirv-std = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "425328a" } -spirv-std-macros = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "425328a" } +spirv-std = { git = "https://github.com/rust-gpu/rust-gpu.git", rev = "05b34493ce661dccd6694cf58afc13e3c8f7a7e0" } +spirv-std-macros = { git = "https://github.com/rust-gpu/rust-gpu.git", rev = "05b34493ce661dccd6694cf58afc13e3c8f7a7e0" } syn = { version = "2.0.49", features = ["full", "extra-traits", "parsing"] } tokio = "1.47.1" tracing = "0.1.41" @@ -59,7 +60,7 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" wasm-bindgen-test = "0.3" web-sys = "0.3" -winit = { version = "0.30" } +winit = { version = "0.30.12" } wgpu = { version = "26.0" } wgpu-core = { version = "26.0" } metal = "0.32" @@ -74,4 +75,4 @@ opt-level = 3 opt-level = 3 [patch.crates-io] -spirv-std = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "425328a" } +spirv-std = { git = "https://github.com/rust-gpu/rust-gpu.git", rev = "05b34493ce661dccd6694cf58afc13e3c8f7a7e0" } diff --git a/crates/example-culling/src/main.rs b/crates/example-culling/src/main.rs index 42eaef78..7d927667 100644 --- a/crates/example-culling/src/main.rs +++ b/crates/example-culling/src/main.rs @@ -1,13 +1,19 @@ //! An example app showing (and verifying) how frustum culling works in //! `renderling`. -use std::{any::Any, sync::Arc}; +use std::sync::Arc; use example::{camera::CameraController, utils::*}; -use glam::*; use renderling::{ bvol::{Aabb, BoundingSphere}, + camera::{shader::CameraDescriptor, Camera}, + context::Context, + geometry::Vertex, + glam::{EulerRot, Mat4, Quat, UVec2, Vec3, Vec4}, + light::{AnalyticalLight, DirectionalLight}, + material::Material, math::hex_to_vec4, - prelude::*, + primitive::Primitive, + stage::Stage, tonemapping::srgba_to_linear, }; use winit::{ @@ -25,22 +31,22 @@ const BOUNDS: Aabb = Aabb { max: Vec3::new(MAX_DIST, MAX_DIST, MAX_DIST), }; -struct AppCamera(Hybrid); -struct FrustumCamera(Camera); +struct AppCamera(Camera); +struct FrustumCamera(CameraDescriptor); + +type Type = Primitive; #[allow(dead_code)] struct CullingExample { app_camera: AppCamera, controller: example::camera::TurntableCameraController, stage: Stage, - dlights: [AnalyticalLight; 2], - material_aabb_overlapping: Hybrid, - material_aabb_outside: Hybrid, - material_frustum: Hybrid, + dlights: [AnalyticalLight; 2], frustum_camera: FrustumCamera, - frustum_vertices: HybridArray, - frustum_renderlet: Hybrid, - resources: BagOfResources, + frustum_primitive: Primitive, + material_aabb_outside: Material, + material_aabb_overlapping: Material, + primitives: Vec, next_k: u64, } @@ -55,65 +61,57 @@ impl CullingExample { seed: u64, stage: &Stage, frustum_camera: &FrustumCamera, - material_outside: &Hybrid, - material_overlapping: &Hybrid, - ) -> Box { + material_outside: &Material, + material_overlapping: &Material, + ) -> Vec { log::info!("generating aabbs with seed {seed}"); fastrand::seed(seed); - Box::new( - (0..25u32) - .map(|i| { - log::info!("aabb {i}"); - let x = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; - let y = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; - let z = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; - let w = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; - let h = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; - let l = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; + (0..25u32) + .map(|i| { + log::info!("aabb {i}"); + let x = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; + let y = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; + let z = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0; + let w = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; + let h = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; + let l = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE; - let rx = std::f32::consts::PI * fastrand::f32(); - let ry = std::f32::consts::PI * fastrand::f32(); - let rz = std::f32::consts::PI * fastrand::f32(); + let rx = std::f32::consts::PI * fastrand::f32(); + let ry = std::f32::consts::PI * fastrand::f32(); + let rz = std::f32::consts::PI * fastrand::f32(); - let rotation = Quat::from_euler(EulerRot::XYZ, rx, ry, rz); + let rotation = Quat::from_euler(EulerRot::XYZ, rx, ry, rz); - let center = Vec3::new(x, y, z); - let half_size = Vec3::new(w, h, l); - let aabb = Self::make_aabb(Vec3::ZERO, half_size); - let aabb_transform = Transform { - translation: center, - rotation, - ..Default::default() - }; + let center = Vec3::new(x, y, z); + let half_size = Vec3::new(w, h, l); + let aabb = Self::make_aabb(Vec3::ZERO, half_size); - let transform = stage.new_transform(aabb_transform); - let (aabb_vertices, aabb_renderlet) = { - let material_id = if BoundingSphere::from(aabb) - .is_inside_camera_view(&frustum_camera.0, transform.get()) + let transform = stage + .new_transform() + .with_translation(center) + .with_rotation(rotation); + stage + .new_primitive() + .with_vertices(stage.new_vertices(aabb.get_mesh().into_iter().map( + |(position, normal)| Vertex { + position, + normal, + ..Default::default() + }, + ))) + .with_material( + if BoundingSphere::from(aabb) + .is_inside_camera_view(&frustum_camera.0, transform.descriptor()) .0 { - material_overlapping.id() + material_overlapping } else { - material_outside.id() - }; - let (renderlet, vertices) = stage - .builder() - .with_vertices(aabb.get_mesh().into_iter().map(|(position, normal)| { - Vertex { - position, - normal, - ..Default::default() - } - })) - .with_transform_id(transform.id()) - .with_material_id(material_id) - .build(); - (renderlet, vertices.into_gpu_only()) - }; - (aabb_renderlet, aabb_vertices, transform) - }) - .collect::>(), - ) + material_outside + }, + ) + .with_transform(transform) + }) + .collect::>() } } @@ -139,9 +137,13 @@ impl ApplicationHandler for CullingExample { .. } => { if c.as_str() == "r" { - self.resources.drain(); + // remove all the primitives, dropping their resources + for primitive in self.primitives.drain(..) { + self.stage.remove_primitive(&primitive); + } + let _ = self.stage.commit(); - self.resources.push(Self::make_aabbs( + self.primitives.extend(Self::make_aabbs( self.next_k, &self.stage, &self.frustum_camera, @@ -181,18 +183,18 @@ impl TestAppHandler for CullingExample { ctx: &Context, ) -> Self { let mut seed = 46; - let mut resources = BagOfResources::default(); + let mut prims = vec![]; let stage = ctx.new_stage().with_lighting(true); - let sunlight_a = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::new(-0.8, -1.0, 0.5).normalize(), - color: Vec4::ONE, - intensity: 10.0, - }); - let sunlight_b = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::new(1.0, 1.0, -0.1).normalize(), - color: Vec4::ONE, - intensity: 1.0, - }); + let sunlight_a = stage + .new_directional_light() + .with_direction(Vec3::new(-0.8, -1.0, 0.5).normalize()) + .with_color(Vec4::ONE) + .with_intensity(10.0); + let sunlight_b = stage + .new_directional_light() + .with_direction(Vec3::new(1.0, 1.0, -0.1).normalize()) + .with_color(Vec4::ONE) + .with_intensity(1.0); let dlights = [sunlight_a, sunlight_b]; @@ -208,7 +210,7 @@ impl TestAppHandler for CullingExample { let view = Mat4::look_at_rh(eye, target, up); // let projection = Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, -10.0, // 10.0); let view = Mat4::IDENTITY; - Camera::new(projection, view) + CameraDescriptor::new(projection, view) }); let frustum = frustum_camera.0.frustum(); @@ -218,20 +220,11 @@ impl TestAppHandler for CullingExample { let red_color = srgba_to_linear(hex_to_vec4(0xC96868FF)); let yellow_color = srgba_to_linear(hex_to_vec4(0xFADFA1FF)); - let material_aabb_overlapping = stage.new_material(Material { - albedo_factor: blue_color, - ..Default::default() - }); - let material_aabb_outside = stage.new_material(Material { - albedo_factor: red_color, - ..Default::default() - }); - let material_frustum = stage.new_material(Material { - albedo_factor: yellow_color, - ..Default::default() - }); - let app_camera = AppCamera(stage.new_camera(Camera::default())); - resources.push(Self::make_aabbs( + let material_aabb_overlapping = stage.new_material().with_albedo_factor(blue_color); + let material_aabb_outside = stage.new_material().with_albedo_factor(red_color); + let material_frustum = stage.new_material().with_albedo_factor(yellow_color); + let app_camera = AppCamera(stage.new_camera()); + prims.extend(Self::make_aabbs( seed, &stage, &frustum_camera, @@ -248,12 +241,11 @@ impl TestAppHandler for CullingExample { ..Default::default() }, )); - let frustum_renderlet = stage.new_renderlet(Renderlet { - vertices_array: frustum_vertices.array(), - material_id: material_frustum.id(), - ..Default::default() - }); - stage.add_renderlet(&frustum_renderlet); + let frustum_prim = stage + .new_primitive() + .with_vertices(&frustum_vertices) + .with_material(&material_frustum); + stage.add_primitive(&frustum_prim); Self { next_k: seed, @@ -269,10 +261,8 @@ impl TestAppHandler for CullingExample { stage, material_aabb_overlapping, material_aabb_outside, - material_frustum, - frustum_vertices, - frustum_renderlet, - resources, + frustum_primitive: frustum_prim, + primitives: prims, } } diff --git a/crates/example-wasm/src/lib.rs b/crates/example-wasm/src/lib.rs index 30ba1cbf..d67e0696 100644 --- a/crates/example-wasm/src/lib.rs +++ b/crates/example-wasm/src/lib.rs @@ -1,6 +1,6 @@ -use futures_lite::StreamExt; -use glam::{Vec2, Vec3, Vec4}; -use renderling::{prelude::*, ui::prelude::*}; +#![allow(dead_code)] +use glam::{Vec2, Vec4}; +use renderling::{camera::Camera, gltf::GltfDocument, stage::Stage, ui::prelude::*}; use wasm_bindgen::prelude::*; use web_sys::HtmlCanvasElement; @@ -27,7 +27,7 @@ pub struct App { path: UiPath, stage: Stage, doc: GltfDocument, - camera: Hybrid, + camera: Camera, text: UiText, } @@ -73,7 +73,7 @@ pub async fn main() { let ui = ctx.new_ui(); let path = ui - .new_path() + .path_builder() .with_circle(Vec2::splat(100.0), 20.0) .with_fill_color(Vec4::new(1.0, 1.0, 0.0, 1.0)) .fill(); @@ -82,7 +82,7 @@ pub async fn main() { .await .expect_throw("Could not load font"); let text = ui - .new_text() + .text_builder() .with_color( // white Vec4::ONE, @@ -105,7 +105,8 @@ pub async fn main() { let fox = stage.load_gltf_document_from_bytes(GLTF_FOX_BYTES).unwrap(); log::info!("fox aabb: {:?}", fox.bounding_volume()); - let camera = stage.new_camera(Camera::default_perspective(800.0, 600.0)); + let (p, v) = renderling::camera::default_perspective(800.0, 600.0); + let camera = stage.new_camera().with_projection_and_view(p, v); let app = App { ctx, diff --git a/crates/example/src/camera.rs b/crates/example/src/camera.rs index 785b365e..cec4b35d 100644 --- a/crates/example/src/camera.rs +++ b/crates/example/src/camera.rs @@ -1,9 +1,11 @@ //! Camera control. use std::str::FromStr; -use craballoc::prelude::Hybrid; -use renderling::prelude::glam::{Mat4, Quat, UVec2, Vec2, Vec3}; -use renderling::{bvol::Aabb, camera::Camera}; +use renderling::{ + bvol::Aabb, + camera::Camera, + glam::{Mat4, Quat, UVec2, Vec2, Vec3}, +}; use winit::{event::KeyEvent, keyboard::Key}; const RADIUS_SCROLL_DAMPENING: f32 = 0.001; @@ -68,10 +70,10 @@ impl CameraController for TurntableCameraController { self.left_mb_down = false; } - fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, current_camera: &Hybrid) { + fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, current_camera: &Camera) { let camera_position = Self::camera_position(self.radius, self.phi, self.theta); let znear = self.depth / 1000.0; - let camera = Camera::new( + current_camera.set_projection_and_view( Mat4::perspective_rh( std::f32::consts::FRAC_PI_4, w as f32 / h as f32, @@ -80,18 +82,6 @@ impl CameraController for TurntableCameraController { ), Mat4::look_at_rh(camera_position, self.center, Vec3::Y), ); - debug_assert!( - camera.view().is_finite(), - "camera view is borked w:{w} h:{h} camera_position: {camera_position} center: {} \ - radius: {} phi: {} theta: {}", - self.center, - self.radius, - self.phi, - self.theta - ); - if current_camera.get() != camera { - current_camera.set(camera); - } } fn mouse_scroll(&mut self, delta: f32) { @@ -194,17 +184,13 @@ impl CameraController for WasdMouseCameraController { } } - fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, camera: &Hybrid) { - let camera_rotation = Quat::from_euler( - renderling::prelude::glam::EulerRot::XYZ, - self.phi, - self.theta, - 0.0, - ); + fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, camera: &Camera) { + let camera_rotation = + Quat::from_euler(renderling::glam::EulerRot::XYZ, self.phi, self.theta, 0.0); let projection = Mat4::perspective_infinite_rh(std::f32::consts::FRAC_PI_4, w as f32 / h as f32, 0.01); let view = Mat4::from_quat(camera_rotation) * Mat4::from_translation(-self.position); - camera.modify(|c| c.set_projection_and_view(projection, view)); + camera.set_projection_and_view(projection, view); } fn reset(&mut self, _bounds: Aabb) { @@ -258,7 +244,7 @@ impl CameraController for WasdMouseCameraController { pub trait CameraController { fn reset(&mut self, bounds: Aabb); fn tick(&mut self); - fn update_camera(&self, size: UVec2, camera: &Hybrid); + fn update_camera(&self, size: UVec2, camera: &Camera); fn mouse_scroll(&mut self, delta: f32); fn mouse_moved(&mut self, position: Vec2); fn mouse_motion(&mut self, delta: Vec2); diff --git a/crates/example/src/lib.rs b/crates/example/src/lib.rs index 46784893..d7497135 100644 --- a/crates/example/src/lib.rs +++ b/crates/example/src/lib.rs @@ -5,18 +5,20 @@ use std::{ sync::{Arc, Mutex}, }; -use craballoc::prelude::{GpuArray, Hybrid}; use glam::{Mat4, UVec2, Vec2, Vec3, Vec4}; use renderling::{ atlas::AtlasImage, bvol::{Aabb, BoundingSphere}, camera::Camera, - light::{AnalyticalLight, DirectionalLightDescriptor}, - prelude::*, + context::Context, + geometry::Vertex, + glam, + gltf::{Animator, GltfDocument}, + light::AnalyticalLight, + primitive::Primitive, skybox::Skybox, - stage::{Animator, GltfDocument, Renderlet, Stage, Vertex}, + stage::Stage, ui::{FontArc, Section, Text, Ui, UiPath, UiText}, - Context, }; pub mod camera; @@ -85,15 +87,15 @@ impl AppUi { let translation = Vec2::new(2.0, 2.0); let text = format!("{}fps", fps_counter.current_fps_string()); let fps_text = ui - .new_text() + .text_builder() .with_color(Vec3::ZERO.extend(1.0)) .with_section(Section::new().add_text(Text::new(&text).with_scale(32.0))) .build(); - fps_text.transform.set_translation(translation); + fps_text.transform().set_translation(translation); let background = ui - .new_path() + .path_builder() .with_fill_color(Vec4::ONE) - .with_rectangle(fps_text.bounds.0, fps_text.bounds.1) + .with_rectangle(fps_text.bounds().0, fps_text.bounds().1) .fill(); background.transform.set_translation(translation); background.transform.set_z(-0.9); @@ -112,15 +114,9 @@ impl AppUi { } } -#[allow(dead_code)] -pub struct DefaultModel { - vertices: GpuArray, - renderlet: Hybrid, -} - pub enum Model { Gltf(Box), - Default(DefaultModel), + Default(Primitive), None, } @@ -129,7 +125,7 @@ pub struct App { skybox_image_bytes: Option>, loads: Arc>>>, pub stage: Stage, - camera: Hybrid, + camera: Camera, _lighting: AnalyticalLight, model: Model, animators: Option>, @@ -146,13 +142,15 @@ impl App { .with_bloom_mix_strength(0.5) .with_bloom_filter_radius(4.0) .with_msaa_sample_count(4); - let camera = stage.new_camera(Camera::default()); - let directional_light = DirectionalLightDescriptor { - direction: Vec3::NEG_Y, - color: renderling::math::hex_to_vec4(0xFDFBD3FF), - intensity: 10.0, - }; - let sunlight_bundle = stage.new_analytical_light(directional_light); + let size = ctx.get_size(); + let (proj, view) = renderling::camera::default_perspective(size.x as f32, size.y as f32); + let camera = stage.new_camera().with_projection_and_view(proj, view); + + let sunlight = stage + .new_directional_light() + .with_direction(Vec3::NEG_Y) + .with_color(renderling::math::hex_to_vec4(0xFDFBD3FF)) + .with_intensity(10.0); stage .set_atlas_size(wgpu::Extent3d { @@ -177,7 +175,7 @@ impl App { }, stage, camera, - _lighting: sunlight_bundle, + _lighting: sunlight.into_generic(), model: Model::None, animators: None, animations_conflict: false, @@ -220,7 +218,9 @@ impl App { let img = AtlasImage::from_hdr_bytes(&bytes).unwrap(); let skybox = Skybox::new(self.stage.runtime(), img); self.skybox_image_bytes = Some(bytes); - self.stage.set_skybox(skybox); + self.stage.use_skybox(&skybox); + let ibl = self.stage.new_ibl(&skybox); + self.stage.use_ibl(&ibl); } pub fn load_default_model(&mut self) { @@ -229,10 +229,9 @@ impl App { let mut max = Vec3::splat(f32::NEG_INFINITY); self.last_frame_instant = now(); - let (vertices, renderlet) = self + let vertices = self .stage - .builder() - .with_vertices(renderling::math::unit_cube().into_iter().map(|(p, n)| { + .new_vertices(renderling::math::unit_cube().into_iter().map(|(p, n)| { let p = p * 2.0; min = min.min(p); max = max.max(p); @@ -240,17 +239,17 @@ impl App { .with_position(p) .with_normal(n) .with_color(Vec4::new(1.0, 0.0, 0.0, 1.0)) - })) + })); + let primitive = self + .stage + .new_primitive() + .with_vertices(vertices) .with_bounds({ log::info!("default model bounds: {min} {max}"); BoundingSphere::from((min, max)) - }) - .build(); + }); - self.model = Model::Default(DefaultModel { - vertices: vertices.into_gpu_only(), - renderlet, - }); + self.model = Model::Default(primitive); self.camera_controller.reset(Aabb::new(min, max)); self.camera_controller .update_camera(self.stage.get_size(), &self.camera); @@ -381,7 +380,7 @@ impl App { // self.lighting // .shadow_map - // .update(&self.lighting.lighting, doc.renderlets.values().flatten()); + // .update(&self.lighting.lighting, doc.primitives.values().flatten()); // self.lighting.light = light.light.clone(); // self.lighting.light_details = dir.clone(); // } diff --git a/crates/example/src/main.rs b/crates/example/src/main.rs index af18313e..0b1e9063 100644 --- a/crates/example/src/main.rs +++ b/crates/example/src/main.rs @@ -4,8 +4,8 @@ use std::sync::Arc; use clap::Parser; use example::{camera::CameraControl, App}; use renderling::{ - prelude::glam::{UVec2, Vec2}, - Context, + context::Context, + glam::{UVec2, Vec2}, }; use winit::{application::ApplicationHandler, event::WindowEvent, window::WindowAttributes}; diff --git a/crates/example/src/utils.rs b/crates/example/src/utils.rs index 46ce02b7..c2d3e949 100644 --- a/crates/example/src/utils.rs +++ b/crates/example/src/utils.rs @@ -1,23 +1,10 @@ //! Example app utilities. -use std::{any::Any, sync::Arc}; +use std::sync::Arc; -use renderling::Context; +use renderling::context::Context; use winit::monitor::MonitorHandle; -#[derive(Default)] -pub struct BagOfResources(Vec>); - -impl BagOfResources { - pub fn push(&mut self, rez: impl Any) { - self.0.push(Box::new(rez)); - } - - pub fn drain(&mut self) { - let _ = self.0.drain(..); - } -} - pub trait TestAppHandler: winit::application::ApplicationHandler { fn new( event_loop: &winit::event_loop::ActiveEventLoop, diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml new file mode 100644 index 00000000..06c8170a --- /dev/null +++ b/crates/examples/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "examples" +version = "0.1.0" +edition = "2024" + +[dependencies] +doc-comment = "0.3" +env_logger.workspace = true +futures-lite.workspace = true +renderling = { path = "../renderling", features = ["test-utils"] } +renderling_build = { path = "../renderling-build" } +tokio = { workspace = true, features = ["full"] } diff --git a/crates/examples/src/context.rs b/crates/examples/src/context.rs new file mode 100644 index 00000000..2f033ea8 --- /dev/null +++ b/crates/examples/src/context.rs @@ -0,0 +1,19 @@ +//! Context manual page. + +#[tokio::test] +async fn context_page() { + // ANCHOR: create + use renderling::context::Context; + + let ctx = Context::headless(256, 256).await; + // ANCHOR_END: create + + // ANCHOR: frame + let frame = ctx.get_next_frame().unwrap(); + // ...do some rendering + // + // Then capture the frame into an image, if you like + let _image_capture = frame.read_image().await.unwrap(); + frame.present(); + // ANCHOR_END: frame +} diff --git a/crates/examples/src/gltf.rs b/crates/examples/src/gltf.rs new file mode 100644 index 00000000..e1684646 --- /dev/null +++ b/crates/examples/src/gltf.rs @@ -0,0 +1,66 @@ +//! GLTF manual page. + +use crate::workspace_dir; + +#[tokio::test] +async fn manual_gltf() { + // ANCHOR: setup + use renderling::{ + camera::Camera, + context::Context, + glam::Vec4, + glam::{Mat4, Vec3}, + stage::Stage, + }; + + let ctx = Context::headless(256, 256).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let znear = 0.1; + let zfar = 10.0; + let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + let eye = Vec3::new(0.5, 0.5, 0.8); + let target = Vec3::new(0.0, 0.3, 0.0); + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + // ANCHOR_END: setup + + // ANCHOR: load + use renderling::{gltf::GltfDocument, types::GpuOnlyArray}; + let model: GltfDocument = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/marble_bust_1k.glb")) + .unwrap() + .into_gpu_only(); + println!("bounds: {:?}", model.bounding_volume()); + // ANCHOR_END: load + + super::cwd_to_manual_assets_dir(); + + // ANCHOR: render_1 + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().await.unwrap(); + img.save("gltf-example-shadow.png").unwrap(); + frame.present(); + // ANCHOR_END: render_1 + + // ANCHOR: no_lights + stage.set_has_lighting(false); + // ANCHOR_END: no_lights + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().await.unwrap(); + img.save("gltf-example-unlit.png").unwrap(); + frame.present(); +} diff --git a/crates/examples/src/lib.rs b/crates/examples/src/lib.rs new file mode 100644 index 00000000..ca671edd --- /dev/null +++ b/crates/examples/src/lib.rs @@ -0,0 +1,49 @@ +//! # Examples for the manual +//! +//! This crate contains examples and snippets that get pulled into the manual +//! via mdbook links. It also contains tests. + +#[cfg(test)] +mod context; + +#[cfg(test)] +mod stage; + +#[cfg(test)] +mod gltf; + +#[cfg(test)] +mod skybox; + +pub fn cwd_to_manual_assets_dir() -> std::path::PathBuf { + let current_dir = + std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("manual/src/assets"); + let current_dir = current_dir.canonicalize().unwrap(); + std::env::set_current_dir(¤t_dir).unwrap(); + println!("current dir: {:?}", std::env::current_dir()); + current_dir +} + +pub fn workspace_dir() -> std::path::PathBuf { + renderling_build::workspace_dir().canonicalize().unwrap() +} + +pub fn test_output_dir() -> std::path::PathBuf { + let dir = renderling_build::test_output_dir(); + std::fs::create_dir_all(&dir).unwrap(); + dir.canonicalize().unwrap() +} + +pub fn cwd_to_cargo_workspace() -> std::path::PathBuf { + let current_dir = workspace_dir(); + std::env::set_current_dir(¤t_dir).unwrap(); + println!("current dir: {:?}", std::env::current_dir()); + current_dir +} + +doc_comment::doctest!("../../../manual/src/stage.md", stage_md); + +#[test] +fn can_test() { + assert_eq!(1, 1); +} diff --git a/crates/examples/src/skybox.rs b/crates/examples/src/skybox.rs new file mode 100644 index 00000000..c039c9b1 --- /dev/null +++ b/crates/examples/src/skybox.rs @@ -0,0 +1,66 @@ +//! Skybox manual page. + +use crate::{cwd_to_manual_assets_dir, workspace_dir}; + +#[tokio::test] +async fn manual_skybox() { + // ANCHOR: setup + use renderling::{ + camera::Camera, + context::Context, + glam::Vec4, + glam::{Mat4, Vec3}, + stage::Stage, + }; + + let ctx = Context::headless(256, 256).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0)) + .with_lighting(false); + + let _camera: Camera = { + let aspect = 1.0; + let fovy = core::f32::consts::PI / 4.0; + let znear = 0.1; + let zfar = 10.0; + let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + let eye = Vec3::new(0.5, 0.5, 0.8); + let target = Vec3::new(0.0, 0.3, 0.0); + let up = Vec3::Y; + let view = Mat4::look_at_rh(eye, target, up); + + stage + .new_camera() + .with_projection_and_view(projection, view) + }; + + use renderling::{gltf::GltfDocument, types::GpuOnlyArray}; + let model: GltfDocument = stage + .load_gltf_document_from_path(workspace_dir().join("gltf/marble_bust_1k.glb")) + .unwrap() + .into_gpu_only(); + println!("bounds: {:?}", model.bounding_volume()); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame.present(); + // ANCHOR_END: setup + + // ANCHOR: skybox + let skybox = stage + .new_skybox_from_path(workspace_dir().join("img/hdr/helipad.hdr")) + .unwrap(); + stage.use_skybox(&skybox); + // ANCHOR_END: skybox + + cwd_to_manual_assets_dir(); + + // ANCHOR: render_skybox + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let image = frame.read_image().await.unwrap(); + image.save("skybox.png").unwrap(); + frame.present(); + // ANCHOR_END: render_skybox +} diff --git a/crates/examples/src/stage.rs b/crates/examples/src/stage.rs new file mode 100644 index 00000000..6d7963b7 --- /dev/null +++ b/crates/examples/src/stage.rs @@ -0,0 +1,107 @@ +//! Stage manual page. + +#[tokio::test] +async fn manual_stage() { + env_logger::init(); + + // ANCHOR: creation + use renderling::{context::Context, glam::Vec4, stage::Stage}; + + let ctx = Context::headless(256, 256).await; + let stage: Stage = ctx + .new_stage() + .with_background_color(Vec4::new(0.5, 0.5, 0.5, 1.0)); + // ANCHOR_END: creation + + // ANCHOR: camera + use renderling::{ + camera::Camera, + glam::{Mat4, Vec3}, + }; + + let camera: Camera = stage + .new_camera() + .with_default_perspective(256.0, 256.0) + .with_view(Mat4::look_at_rh(Vec3::splat(1.5), Vec3::ZERO, Vec3::Y)); + // This is technically not necessary because Stage always "uses" the first + // camera created, but we do it here for demonstration. + stage.use_camera(&camera); + // ANCHOR_END: camera + + // ANCHOR: unit_cube_vertices + use renderling::geometry::{Vertex, Vertices}; + + let vertices: Vertices = stage.new_vertices(renderling::math::unit_cube().into_iter().map( + |(position, normal)| { + Vertex::default() + .with_position(position) + .with_normal(normal) + .with_color({ + // The color can vary from vertex to vertex + // + // X axis is green + let g: f32 = position.x + 0.5; + // Y axis is blue + let b: f32 = position.y + 0.5; + // Z is red + let r: f32 = position.z + 0.5; + Vec4::new(r, g, b, 1.0) + }) + }, + )); + // ANCHOR_END: unit_cube_vertices + + // ANCHOR: unload_vertices + use renderling::types::GpuOnlyArray; + + let vertices: Vertices = vertices.into_gpu_only(); + // ANCHOR_END: unload_vertices + + // ANCHOR: material + let material = stage + .new_material() + .with_albedo_factor(Vec4::ONE) + .with_has_lighting(false); + // ANCHOR_END: material + + // ANCHOR: prim + let prim = stage + .new_primitive() + .with_vertices(&vertices) + .with_material(&material); + // ANCHOR_END: prim + + // Excluded from the manual because it's off-topic + let current_dir = + std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("manual/src/assets"); + let current_dir = current_dir.canonicalize().unwrap(); + std::env::set_current_dir(current_dir).unwrap(); + + // ANCHOR: render + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + + let img = frame.read_image().await.unwrap(); + img.save("stage-example.png").unwrap(); + frame.present(); + // ANCHOR_END: render + + // ANCHOR: committed_size_bytes + let bytes_committed = stage.used_gpu_buffer_byte_size(); + println!("bytes_committed: {bytes_committed}"); + // ANCHOR_END: committed_size_bytes + + // ANCHOR: removal + let staged_prim_count = stage.remove_primitive(&prim); + assert_eq!(0, staged_prim_count); + drop(vertices); + drop(material); + drop(prim); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().await.unwrap(); + img.save("stage-example-gone.png").unwrap(); + frame.present(); + // ANCHOR_END: removal +} diff --git a/crates/examples/stage-example.png b/crates/examples/stage-example.png new file mode 100644 index 00000000..98ef0adf Binary files /dev/null and b/crates/examples/stage-example.png differ diff --git a/crates/renderling-build/src/lib.rs b/crates/renderling-build/src/lib.rs index 7ad7a0de..64a1a2dd 100644 --- a/crates/renderling-build/src/lib.rs +++ b/crates/renderling-build/src/lib.rs @@ -137,6 +137,16 @@ fn wgsl(spv_filepath: impl AsRef, destination: impl AsRef std::path::PathBuf { + std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")) +} + +/// The test_output directory. +pub fn test_output_dir() -> std::path::PathBuf { + workspace_dir().join("test_output") +} + #[derive(Debug)] pub struct RenderlingPaths { /// `cargo_workspace` is not available when building outside of the project directory. diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index 40e726e3..8c9e6b81 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -26,6 +26,7 @@ crate-type = ["rlib", "cdylib"] default = ["gltf", "ui", "winit"] gltf = ["dep:gltf", "dep:serde_json"] test_i8_i16_extraction = [] +test-utils = ["dep:metal", "dep:wgpu-core"] ui = ["dep:glyph_brush", "dep:loading-bytes", "dep:lyon"] wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] debug-slab = [] @@ -90,17 +91,22 @@ human-repr = "1.1.0" icosahedron = "0.1" img-diff = { path = "../img-diff" } naga.workspace = true +renderling_build = { path = "../renderling-build" } ttf-parser = "0.20.0" wasm-bindgen-test.workspace = true -wgpu-core.workspace = true winit.workspace = true wire-types = { path = "../wire-types" } [target.'cfg(not(target_arch = "spirv"))'.dev-dependencies] glam = { workspace = true, features = ["std", "debug-glam-assert"] } +[target.'cfg(target_os = "macos")'.dependencies] +metal = { workspace = true, optional = true } +wgpu-core = { workspace = true, optional = true } + [target.'cfg(target_os = "macos")'.dev-dependencies] metal.workspace = true +wgpu-core.workspace = true [dev-dependencies.web-sys] workspace = true diff --git a/crates/renderling/shaders/atlas-atlas_blit_vertex.spv b/crates/renderling/shaders/atlas-atlas_blit_vertex.spv deleted file mode 100644 index 9cb3260a..00000000 Binary files a/crates/renderling/shaders/atlas-atlas_blit_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/atlas-atlas_blit_fragment.spv b/crates/renderling/shaders/atlas-shader-atlas_blit_fragment.spv similarity index 62% rename from crates/renderling/shaders/atlas-atlas_blit_fragment.spv rename to crates/renderling/shaders/atlas-shader-atlas_blit_fragment.spv index c8443401..5990bacc 100644 Binary files a/crates/renderling/shaders/atlas-atlas_blit_fragment.spv and b/crates/renderling/shaders/atlas-shader-atlas_blit_fragment.spv differ diff --git a/crates/renderling/shaders/atlas-shader-atlas_blit_vertex.spv b/crates/renderling/shaders/atlas-shader-atlas_blit_vertex.spv new file mode 100644 index 00000000..ae3e85f7 Binary files /dev/null and b/crates/renderling/shaders/atlas-shader-atlas_blit_vertex.spv differ diff --git a/crates/renderling/shaders/bloom-bloom_downsample_fragment.spv b/crates/renderling/shaders/bloom-bloom_downsample_fragment.spv deleted file mode 100644 index d6660d64..00000000 Binary files a/crates/renderling/shaders/bloom-bloom_downsample_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/bloom-bloom_mix_fragment.spv b/crates/renderling/shaders/bloom-bloom_mix_fragment.spv deleted file mode 100644 index 792d9178..00000000 Binary files a/crates/renderling/shaders/bloom-bloom_mix_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/bloom-bloom_upsample_fragment.spv b/crates/renderling/shaders/bloom-bloom_upsample_fragment.spv deleted file mode 100644 index b8f8b0ce..00000000 Binary files a/crates/renderling/shaders/bloom-bloom_upsample_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/bloom-shader-bloom_downsample_fragment.spv b/crates/renderling/shaders/bloom-shader-bloom_downsample_fragment.spv new file mode 100644 index 00000000..9ffcf8d3 Binary files /dev/null and b/crates/renderling/shaders/bloom-shader-bloom_downsample_fragment.spv differ diff --git a/crates/renderling/shaders/bloom-shader-bloom_mix_fragment.spv b/crates/renderling/shaders/bloom-shader-bloom_mix_fragment.spv new file mode 100644 index 00000000..4601da7c Binary files /dev/null and b/crates/renderling/shaders/bloom-shader-bloom_mix_fragment.spv differ diff --git a/crates/renderling/shaders/bloom-shader-bloom_upsample_fragment.spv b/crates/renderling/shaders/bloom-shader-bloom_upsample_fragment.spv new file mode 100644 index 00000000..3b19809c Binary files /dev/null and b/crates/renderling/shaders/bloom-shader-bloom_upsample_fragment.spv differ diff --git a/crates/renderling/shaders/bloom-bloom_vertex.spv b/crates/renderling/shaders/bloom-shader-bloom_vertex.spv similarity index 62% rename from crates/renderling/shaders/bloom-bloom_vertex.spv rename to crates/renderling/shaders/bloom-shader-bloom_vertex.spv index be801ad5..647e86ff 100644 Binary files a/crates/renderling/shaders/bloom-bloom_vertex.spv and b/crates/renderling/shaders/bloom-shader-bloom_vertex.spv differ diff --git a/crates/renderling/shaders/convolution-generate_mipmap_vertex.spv b/crates/renderling/shaders/convolution-generate_mipmap_vertex.spv deleted file mode 100644 index e8d8857c..00000000 Binary files a/crates/renderling/shaders/convolution-generate_mipmap_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/convolution-prefilter_environment_cubemap_vertex.spv b/crates/renderling/shaders/convolution-prefilter_environment_cubemap_vertex.spv deleted file mode 100644 index d77ce473..00000000 Binary files a/crates/renderling/shaders/convolution-prefilter_environment_cubemap_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/convolution-brdf_lut_convolution_fragment.spv b/crates/renderling/shaders/convolution-shader-brdf_lut_convolution_fragment.spv similarity index 78% rename from crates/renderling/shaders/convolution-brdf_lut_convolution_fragment.spv rename to crates/renderling/shaders/convolution-shader-brdf_lut_convolution_fragment.spv index cc9214b5..d4b69afe 100644 Binary files a/crates/renderling/shaders/convolution-brdf_lut_convolution_fragment.spv and b/crates/renderling/shaders/convolution-shader-brdf_lut_convolution_fragment.spv differ diff --git a/crates/renderling/shaders/convolution-brdf_lut_convolution_vertex.spv b/crates/renderling/shaders/convolution-shader-brdf_lut_convolution_vertex.spv similarity index 59% rename from crates/renderling/shaders/convolution-brdf_lut_convolution_vertex.spv rename to crates/renderling/shaders/convolution-shader-brdf_lut_convolution_vertex.spv index 9a7dd942..8cb38578 100644 Binary files a/crates/renderling/shaders/convolution-brdf_lut_convolution_vertex.spv and b/crates/renderling/shaders/convolution-shader-brdf_lut_convolution_vertex.spv differ diff --git a/crates/renderling/shaders/convolution-generate_mipmap_fragment.spv b/crates/renderling/shaders/convolution-shader-generate_mipmap_fragment.spv similarity index 61% rename from crates/renderling/shaders/convolution-generate_mipmap_fragment.spv rename to crates/renderling/shaders/convolution-shader-generate_mipmap_fragment.spv index e1f12d06..46c2734b 100644 Binary files a/crates/renderling/shaders/convolution-generate_mipmap_fragment.spv and b/crates/renderling/shaders/convolution-shader-generate_mipmap_fragment.spv differ diff --git a/crates/renderling/shaders/convolution-shader-generate_mipmap_vertex.spv b/crates/renderling/shaders/convolution-shader-generate_mipmap_vertex.spv new file mode 100644 index 00000000..90b67dab Binary files /dev/null and b/crates/renderling/shaders/convolution-shader-generate_mipmap_vertex.spv differ diff --git a/crates/renderling/shaders/convolution-prefilter_environment_cubemap_fragment.spv b/crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv similarity index 54% rename from crates/renderling/shaders/convolution-prefilter_environment_cubemap_fragment.spv rename to crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv index 91a72716..f071de36 100644 Binary files a/crates/renderling/shaders/convolution-prefilter_environment_cubemap_fragment.spv and b/crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv differ diff --git a/crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv b/crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv new file mode 100644 index 00000000..9db41d93 Binary files /dev/null and b/crates/renderling/shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv differ diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv b/crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_fragment.spv similarity index 61% rename from crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv rename to crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_fragment.spv index 6ece1052..26105c8e 100644 Binary files a/crates/renderling/shaders/cubemap-cubemap_sampling_test_fragment.spv and b/crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_fragment.spv differ diff --git a/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv b/crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_vertex.spv similarity index 50% rename from crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv rename to crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_vertex.spv index 07a2fe23..b78c3b2a 100644 Binary files a/crates/renderling/shaders/cubemap-cubemap_sampling_test_vertex.spv and b/crates/renderling/shaders/cubemap-shader-cubemap_sampling_test_vertex.spv differ diff --git a/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid.spv b/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid.spv deleted file mode 100644 index 8cb6058c..00000000 Binary files a/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid.spv and /dev/null differ diff --git a/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid_multisampled.spv b/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid_multisampled.spv deleted file mode 100644 index 01754b01..00000000 Binary files a/crates/renderling/shaders/cull-compute_copy_depth_to_pyramid_multisampled.spv and /dev/null differ diff --git a/crates/renderling/shaders/cull-compute_culling.spv b/crates/renderling/shaders/cull-compute_culling.spv deleted file mode 100644 index 95c9f82d..00000000 Binary files a/crates/renderling/shaders/cull-compute_culling.spv and /dev/null differ diff --git a/crates/renderling/shaders/cull-compute_downsample_depth_pyramid.spv b/crates/renderling/shaders/cull-compute_downsample_depth_pyramid.spv deleted file mode 100644 index 1fa75571..00000000 Binary files a/crates/renderling/shaders/cull-compute_downsample_depth_pyramid.spv and /dev/null differ diff --git a/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid.spv b/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid.spv new file mode 100644 index 00000000..15c9a616 Binary files /dev/null and b/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid.spv differ diff --git a/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv b/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv new file mode 100644 index 00000000..e29722e3 Binary files /dev/null and b/crates/renderling/shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv differ diff --git a/crates/renderling/shaders/cull-shader-compute_culling.spv b/crates/renderling/shaders/cull-shader-compute_culling.spv new file mode 100644 index 00000000..b4ea2425 Binary files /dev/null and b/crates/renderling/shaders/cull-shader-compute_culling.spv differ diff --git a/crates/renderling/shaders/cull-shader-compute_downsample_depth_pyramid.spv b/crates/renderling/shaders/cull-shader-compute_downsample_depth_pyramid.spv new file mode 100644 index 00000000..ee4324f2 Binary files /dev/null and b/crates/renderling/shaders/cull-shader-compute_downsample_depth_pyramid.spv differ diff --git a/crates/renderling/shaders/debug-debug_overlay_fragment.spv b/crates/renderling/shaders/debug-debug_overlay_fragment.spv deleted file mode 100644 index 5d0205f2..00000000 Binary files a/crates/renderling/shaders/debug-debug_overlay_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/debug-debug_overlay_vertex.spv b/crates/renderling/shaders/debug-debug_overlay_vertex.spv deleted file mode 100644 index dd32e914..00000000 Binary files a/crates/renderling/shaders/debug-debug_overlay_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/debug-shader-debug_overlay_fragment.spv b/crates/renderling/shaders/debug-shader-debug_overlay_fragment.spv new file mode 100644 index 00000000..61039f5d Binary files /dev/null and b/crates/renderling/shaders/debug-shader-debug_overlay_fragment.spv differ diff --git a/crates/renderling/shaders/debug-shader-debug_overlay_vertex.spv b/crates/renderling/shaders/debug-shader-debug_overlay_vertex.spv new file mode 100644 index 00000000..8c10f47b Binary files /dev/null and b/crates/renderling/shaders/debug-shader-debug_overlay_vertex.spv differ diff --git a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv b/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv deleted file mode 100644 index f3df3c56..00000000 Binary files a/crates/renderling/shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-light_tiling_bin_lights.spv b/crates/renderling/shaders/light-light_tiling_bin_lights.spv deleted file mode 100644 index 64bc0187..00000000 Binary files a/crates/renderling/shaders/light-light_tiling_bin_lights.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-light_tiling_clear_tiles.spv b/crates/renderling/shaders/light-light_tiling_clear_tiles.spv deleted file mode 100644 index 01484a69..00000000 Binary files a/crates/renderling/shaders/light-light_tiling_clear_tiles.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth.spv b/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth.spv deleted file mode 100644 index 80fa3445..00000000 Binary files a/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.spv b/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.spv deleted file mode 100644 index 835bb232..00000000 Binary files a/crates/renderling/shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv b/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv deleted file mode 100644 index cde999c7..00000000 Binary files a/crates/renderling/shaders/light-light_tiling_depth_pre_pass.spv and /dev/null differ diff --git a/crates/renderling/shaders/light-shader-light_tiling_bin_lights.spv b/crates/renderling/shaders/light-shader-light_tiling_bin_lights.spv new file mode 100644 index 00000000..fd7bb316 Binary files /dev/null and b/crates/renderling/shaders/light-shader-light_tiling_bin_lights.spv differ diff --git a/crates/renderling/shaders/light-shader-light_tiling_clear_tiles.spv b/crates/renderling/shaders/light-shader-light_tiling_clear_tiles.spv new file mode 100644 index 00000000..396bc271 Binary files /dev/null and b/crates/renderling/shaders/light-shader-light_tiling_clear_tiles.spv differ diff --git a/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv b/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv new file mode 100644 index 00000000..c6fff550 Binary files /dev/null and b/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv differ diff --git a/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv b/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv new file mode 100644 index 00000000..c29a469b Binary files /dev/null and b/crates/renderling/shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv differ diff --git a/crates/renderling/shaders/light-shader-light_tiling_depth_pre_pass.spv b/crates/renderling/shaders/light-shader-light_tiling_depth_pre_pass.spv new file mode 100644 index 00000000..6a1d48a6 Binary files /dev/null and b/crates/renderling/shaders/light-shader-light_tiling_depth_pre_pass.spv differ diff --git a/crates/renderling/shaders/light-shadow_mapping_fragment.spv b/crates/renderling/shaders/light-shader-shadow_mapping_fragment.spv similarity index 63% rename from crates/renderling/shaders/light-shadow_mapping_fragment.spv rename to crates/renderling/shaders/light-shader-shadow_mapping_fragment.spv index c6edd539..e34f4c81 100644 Binary files a/crates/renderling/shaders/light-shadow_mapping_fragment.spv and b/crates/renderling/shaders/light-shader-shadow_mapping_fragment.spv differ diff --git a/crates/renderling/shaders/light-shader-shadow_mapping_vertex.spv b/crates/renderling/shaders/light-shader-shadow_mapping_vertex.spv new file mode 100644 index 00000000..9ed8e4cf Binary files /dev/null and b/crates/renderling/shaders/light-shader-shadow_mapping_vertex.spv differ diff --git a/crates/renderling/shaders/light-shadow_mapping_vertex.spv b/crates/renderling/shaders/light-shadow_mapping_vertex.spv deleted file mode 100644 index 43da3b39..00000000 Binary files a/crates/renderling/shaders/light-shadow_mapping_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/manifest.json b/crates/renderling/shaders/manifest.json index b38af795..b2297abe 100644 --- a/crates/renderling/shaders/manifest.json +++ b/crates/renderling/shaders/manifest.json @@ -1,173 +1,173 @@ [ { - "source_path": "shaders/atlas-atlas_blit_fragment.spv", - "entry_point": "atlas::atlas_blit_fragment", - "wgsl_entry_point": "atlasatlas_blit_fragment" + "source_path": "shaders/atlas-shader-atlas_blit_fragment.spv", + "entry_point": "atlas::shader::atlas_blit_fragment", + "wgsl_entry_point": "atlasshaderatlas_blit_fragment" }, { - "source_path": "shaders/atlas-atlas_blit_vertex.spv", - "entry_point": "atlas::atlas_blit_vertex", - "wgsl_entry_point": "atlasatlas_blit_vertex" + "source_path": "shaders/atlas-shader-atlas_blit_vertex.spv", + "entry_point": "atlas::shader::atlas_blit_vertex", + "wgsl_entry_point": "atlasshaderatlas_blit_vertex" }, { - "source_path": "shaders/bloom-bloom_downsample_fragment.spv", - "entry_point": "bloom::bloom_downsample_fragment", - "wgsl_entry_point": "bloombloom_downsample_fragment" + "source_path": "shaders/bloom-shader-bloom_downsample_fragment.spv", + "entry_point": "bloom::shader::bloom_downsample_fragment", + "wgsl_entry_point": "bloomshaderbloom_downsample_fragment" }, { - "source_path": "shaders/bloom-bloom_mix_fragment.spv", - "entry_point": "bloom::bloom_mix_fragment", - "wgsl_entry_point": "bloombloom_mix_fragment" + "source_path": "shaders/bloom-shader-bloom_mix_fragment.spv", + "entry_point": "bloom::shader::bloom_mix_fragment", + "wgsl_entry_point": "bloomshaderbloom_mix_fragment" }, { - "source_path": "shaders/bloom-bloom_upsample_fragment.spv", - "entry_point": "bloom::bloom_upsample_fragment", - "wgsl_entry_point": "bloombloom_upsample_fragment" + "source_path": "shaders/bloom-shader-bloom_upsample_fragment.spv", + "entry_point": "bloom::shader::bloom_upsample_fragment", + "wgsl_entry_point": "bloomshaderbloom_upsample_fragment" }, { - "source_path": "shaders/bloom-bloom_vertex.spv", - "entry_point": "bloom::bloom_vertex", - "wgsl_entry_point": "bloombloom_vertex" + "source_path": "shaders/bloom-shader-bloom_vertex.spv", + "entry_point": "bloom::shader::bloom_vertex", + "wgsl_entry_point": "bloomshaderbloom_vertex" }, { - "source_path": "shaders/convolution-brdf_lut_convolution_fragment.spv", - "entry_point": "convolution::brdf_lut_convolution_fragment", - "wgsl_entry_point": "convolutionbrdf_lut_convolution_fragment" + "source_path": "shaders/convolution-shader-brdf_lut_convolution_fragment.spv", + "entry_point": "convolution::shader::brdf_lut_convolution_fragment", + "wgsl_entry_point": "convolutionshaderbrdf_lut_convolution_fragment" }, { - "source_path": "shaders/convolution-brdf_lut_convolution_vertex.spv", - "entry_point": "convolution::brdf_lut_convolution_vertex", - "wgsl_entry_point": "convolutionbrdf_lut_convolution_vertex" + "source_path": "shaders/convolution-shader-brdf_lut_convolution_vertex.spv", + "entry_point": "convolution::shader::brdf_lut_convolution_vertex", + "wgsl_entry_point": "convolutionshaderbrdf_lut_convolution_vertex" }, { - "source_path": "shaders/convolution-generate_mipmap_fragment.spv", - "entry_point": "convolution::generate_mipmap_fragment", - "wgsl_entry_point": "convolutiongenerate_mipmap_fragment" + "source_path": "shaders/convolution-shader-generate_mipmap_fragment.spv", + "entry_point": "convolution::shader::generate_mipmap_fragment", + "wgsl_entry_point": "convolutionshadergenerate_mipmap_fragment" }, { - "source_path": "shaders/convolution-generate_mipmap_vertex.spv", - "entry_point": "convolution::generate_mipmap_vertex", - "wgsl_entry_point": "convolutiongenerate_mipmap_vertex" + "source_path": "shaders/convolution-shader-generate_mipmap_vertex.spv", + "entry_point": "convolution::shader::generate_mipmap_vertex", + "wgsl_entry_point": "convolutionshadergenerate_mipmap_vertex" }, { - "source_path": "shaders/convolution-prefilter_environment_cubemap_fragment.spv", - "entry_point": "convolution::prefilter_environment_cubemap_fragment", - "wgsl_entry_point": "convolutionprefilter_environment_cubemap_fragment" + "source_path": "shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv", + "entry_point": "convolution::shader::prefilter_environment_cubemap_fragment", + "wgsl_entry_point": "convolutionshaderprefilter_environment_cubemap_fragment" }, { - "source_path": "shaders/convolution-prefilter_environment_cubemap_vertex.spv", - "entry_point": "convolution::prefilter_environment_cubemap_vertex", - "wgsl_entry_point": "convolutionprefilter_environment_cubemap_vertex" + "source_path": "shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv", + "entry_point": "convolution::shader::prefilter_environment_cubemap_vertex", + "wgsl_entry_point": "convolutionshaderprefilter_environment_cubemap_vertex" }, { - "source_path": "shaders/cubemap-cubemap_sampling_test_fragment.spv", - "entry_point": "cubemap::cubemap_sampling_test_fragment", - "wgsl_entry_point": "cubemapcubemap_sampling_test_fragment" + "source_path": "shaders/cubemap-shader-cubemap_sampling_test_fragment.spv", + "entry_point": "cubemap::shader::cubemap_sampling_test_fragment", + "wgsl_entry_point": "cubemapshadercubemap_sampling_test_fragment" }, { - "source_path": "shaders/cubemap-cubemap_sampling_test_vertex.spv", - "entry_point": "cubemap::cubemap_sampling_test_vertex", - "wgsl_entry_point": "cubemapcubemap_sampling_test_vertex" + "source_path": "shaders/cubemap-shader-cubemap_sampling_test_vertex.spv", + "entry_point": "cubemap::shader::cubemap_sampling_test_vertex", + "wgsl_entry_point": "cubemapshadercubemap_sampling_test_vertex" }, { - "source_path": "shaders/cull-compute_copy_depth_to_pyramid.spv", - "entry_point": "cull::compute_copy_depth_to_pyramid", - "wgsl_entry_point": "cullcompute_copy_depth_to_pyramid" + "source_path": "shaders/cull-shader-compute_copy_depth_to_pyramid.spv", + "entry_point": "cull::shader::compute_copy_depth_to_pyramid", + "wgsl_entry_point": "cullshadercompute_copy_depth_to_pyramid" }, { - "source_path": "shaders/cull-compute_copy_depth_to_pyramid_multisampled.spv", - "entry_point": "cull::compute_copy_depth_to_pyramid_multisampled", - "wgsl_entry_point": "cullcompute_copy_depth_to_pyramid_multisampled" + "source_path": "shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv", + "entry_point": "cull::shader::compute_copy_depth_to_pyramid_multisampled", + "wgsl_entry_point": "cullshadercompute_copy_depth_to_pyramid_multisampled" }, { - "source_path": "shaders/cull-compute_culling.spv", - "entry_point": "cull::compute_culling", - "wgsl_entry_point": "cullcompute_culling" + "source_path": "shaders/cull-shader-compute_culling.spv", + "entry_point": "cull::shader::compute_culling", + "wgsl_entry_point": "cullshadercompute_culling" }, { - "source_path": "shaders/cull-compute_downsample_depth_pyramid.spv", - "entry_point": "cull::compute_downsample_depth_pyramid", - "wgsl_entry_point": "cullcompute_downsample_depth_pyramid" + "source_path": "shaders/cull-shader-compute_downsample_depth_pyramid.spv", + "entry_point": "cull::shader::compute_downsample_depth_pyramid", + "wgsl_entry_point": "cullshadercompute_downsample_depth_pyramid" }, { - "source_path": "shaders/debug-debug_overlay_fragment.spv", - "entry_point": "debug::debug_overlay_fragment", - "wgsl_entry_point": "debugdebug_overlay_fragment" + "source_path": "shaders/debug-shader-debug_overlay_fragment.spv", + "entry_point": "debug::shader::debug_overlay_fragment", + "wgsl_entry_point": "debugshaderdebug_overlay_fragment" }, { - "source_path": "shaders/debug-debug_overlay_vertex.spv", - "entry_point": "debug::debug_overlay_vertex", - "wgsl_entry_point": "debugdebug_overlay_vertex" + "source_path": "shaders/debug-shader-debug_overlay_vertex.spv", + "entry_point": "debug::shader::debug_overlay_vertex", + "wgsl_entry_point": "debugshaderdebug_overlay_vertex" }, { - "source_path": "shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv", - "entry_point": "ibl::diffuse_irradiance::di_convolution_fragment", - "wgsl_entry_point": "ibldiffuse_irradiancedi_convolution_fragment" + "source_path": "shaders/light-shader-light_tiling_bin_lights.spv", + "entry_point": "light::shader::light_tiling_bin_lights", + "wgsl_entry_point": "lightshaderlight_tiling_bin_lights" }, { - "source_path": "shaders/light-light_tiling_bin_lights.spv", - "entry_point": "light::light_tiling_bin_lights", - "wgsl_entry_point": "lightlight_tiling_bin_lights" + "source_path": "shaders/light-shader-light_tiling_clear_tiles.spv", + "entry_point": "light::shader::light_tiling_clear_tiles", + "wgsl_entry_point": "lightshaderlight_tiling_clear_tiles" }, { - "source_path": "shaders/light-light_tiling_clear_tiles.spv", - "entry_point": "light::light_tiling_clear_tiles", - "wgsl_entry_point": "lightlight_tiling_clear_tiles" + "source_path": "shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv", + "entry_point": "light::shader::light_tiling_compute_tile_min_and_max_depth", + "wgsl_entry_point": "lightshaderlight_tiling_compute_tile_min_and_max_depth" }, { - "source_path": "shaders/light-light_tiling_compute_tile_min_and_max_depth.spv", - "entry_point": "light::light_tiling_compute_tile_min_and_max_depth", - "wgsl_entry_point": "lightlight_tiling_compute_tile_min_and_max_depth" + "source_path": "shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv", + "entry_point": "light::shader::light_tiling_compute_tile_min_and_max_depth_multisampled", + "wgsl_entry_point": "lightshaderlight_tiling_compute_tile_min_and_max_depth_multisampled" }, { - "source_path": "shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.spv", - "entry_point": "light::light_tiling_compute_tile_min_and_max_depth_multisampled", - "wgsl_entry_point": "lightlight_tiling_compute_tile_min_and_max_depth_multisampled" + "source_path": "shaders/light-shader-light_tiling_depth_pre_pass.spv", + "entry_point": "light::shader::light_tiling_depth_pre_pass", + "wgsl_entry_point": "lightshaderlight_tiling_depth_pre_pass" }, { - "source_path": "shaders/light-light_tiling_depth_pre_pass.spv", - "entry_point": "light::light_tiling_depth_pre_pass", - "wgsl_entry_point": "lightlight_tiling_depth_pre_pass" + "source_path": "shaders/light-shader-shadow_mapping_fragment.spv", + "entry_point": "light::shader::shadow_mapping_fragment", + "wgsl_entry_point": "lightshadershadow_mapping_fragment" }, { - "source_path": "shaders/light-shadow_mapping_fragment.spv", - "entry_point": "light::shadow_mapping_fragment", - "wgsl_entry_point": "lightshadow_mapping_fragment" + "source_path": "shaders/light-shader-shadow_mapping_vertex.spv", + "entry_point": "light::shader::shadow_mapping_vertex", + "wgsl_entry_point": "lightshadershadow_mapping_vertex" }, { - "source_path": "shaders/light-shadow_mapping_vertex.spv", - "entry_point": "light::shadow_mapping_vertex", - "wgsl_entry_point": "lightshadow_mapping_vertex" + "source_path": "shaders/pbr-ibl-shader-di_convolution_fragment.spv", + "entry_point": "pbr::ibl::shader::di_convolution_fragment", + "wgsl_entry_point": "pbriblshaderdi_convolution_fragment" }, { - "source_path": "shaders/skybox-skybox_cubemap_fragment.spv", - "entry_point": "skybox::skybox_cubemap_fragment", - "wgsl_entry_point": "skyboxskybox_cubemap_fragment" + "source_path": "shaders/primitive-shader-primitive_fragment.spv", + "entry_point": "primitive::shader::primitive_fragment", + "wgsl_entry_point": "primitiveshaderprimitive_fragment" }, { - "source_path": "shaders/skybox-skybox_cubemap_vertex.spv", - "entry_point": "skybox::skybox_cubemap_vertex", - "wgsl_entry_point": "skyboxskybox_cubemap_vertex" + "source_path": "shaders/primitive-shader-primitive_vertex.spv", + "entry_point": "primitive::shader::primitive_vertex", + "wgsl_entry_point": "primitiveshaderprimitive_vertex" }, { - "source_path": "shaders/skybox-skybox_equirectangular_fragment.spv", - "entry_point": "skybox::skybox_equirectangular_fragment", - "wgsl_entry_point": "skyboxskybox_equirectangular_fragment" + "source_path": "shaders/skybox-shader-skybox_cubemap_fragment.spv", + "entry_point": "skybox::shader::skybox_cubemap_fragment", + "wgsl_entry_point": "skyboxshaderskybox_cubemap_fragment" }, { - "source_path": "shaders/skybox-skybox_vertex.spv", - "entry_point": "skybox::skybox_vertex", - "wgsl_entry_point": "skyboxskybox_vertex" + "source_path": "shaders/skybox-shader-skybox_cubemap_vertex.spv", + "entry_point": "skybox::shader::skybox_cubemap_vertex", + "wgsl_entry_point": "skyboxshaderskybox_cubemap_vertex" }, { - "source_path": "shaders/stage-renderlet_fragment.spv", - "entry_point": "stage::renderlet_fragment", - "wgsl_entry_point": "stagerenderlet_fragment" + "source_path": "shaders/skybox-shader-skybox_equirectangular_fragment.spv", + "entry_point": "skybox::shader::skybox_equirectangular_fragment", + "wgsl_entry_point": "skyboxshaderskybox_equirectangular_fragment" }, { - "source_path": "shaders/stage-renderlet_vertex.spv", - "entry_point": "stage::renderlet_vertex", - "wgsl_entry_point": "stagerenderlet_vertex" + "source_path": "shaders/skybox-shader-skybox_vertex.spv", + "entry_point": "skybox::shader::skybox_vertex", + "wgsl_entry_point": "skyboxshaderskybox_vertex" }, { "source_path": "shaders/tonemapping-tonemapping_fragment.spv", diff --git a/crates/renderling/shaders/pbr-ibl-shader-di_convolution_fragment.spv b/crates/renderling/shaders/pbr-ibl-shader-di_convolution_fragment.spv new file mode 100644 index 00000000..3f26658d Binary files /dev/null and b/crates/renderling/shaders/pbr-ibl-shader-di_convolution_fragment.spv differ diff --git a/crates/renderling/shaders/primitive-shader-primitive_fragment.spv b/crates/renderling/shaders/primitive-shader-primitive_fragment.spv new file mode 100644 index 00000000..2cbfdecd Binary files /dev/null and b/crates/renderling/shaders/primitive-shader-primitive_fragment.spv differ diff --git a/crates/renderling/shaders/primitive-shader-primitive_vertex.spv b/crates/renderling/shaders/primitive-shader-primitive_vertex.spv new file mode 100644 index 00000000..2598456f Binary files /dev/null and b/crates/renderling/shaders/primitive-shader-primitive_vertex.spv differ diff --git a/crates/renderling/shaders/skybox-shader-skybox_cubemap_fragment.spv b/crates/renderling/shaders/skybox-shader-skybox_cubemap_fragment.spv new file mode 100644 index 00000000..026d2275 Binary files /dev/null and b/crates/renderling/shaders/skybox-shader-skybox_cubemap_fragment.spv differ diff --git a/crates/renderling/shaders/skybox-shader-skybox_cubemap_vertex.spv b/crates/renderling/shaders/skybox-shader-skybox_cubemap_vertex.spv new file mode 100644 index 00000000..3a1d2891 Binary files /dev/null and b/crates/renderling/shaders/skybox-shader-skybox_cubemap_vertex.spv differ diff --git a/crates/renderling/shaders/skybox-shader-skybox_equirectangular_fragment.spv b/crates/renderling/shaders/skybox-shader-skybox_equirectangular_fragment.spv new file mode 100644 index 00000000..d19617a8 Binary files /dev/null and b/crates/renderling/shaders/skybox-shader-skybox_equirectangular_fragment.spv differ diff --git a/crates/renderling/shaders/skybox-shader-skybox_vertex.spv b/crates/renderling/shaders/skybox-shader-skybox_vertex.spv new file mode 100644 index 00000000..ed7a965b Binary files /dev/null and b/crates/renderling/shaders/skybox-shader-skybox_vertex.spv differ diff --git a/crates/renderling/shaders/skybox-skybox_cubemap_fragment.spv b/crates/renderling/shaders/skybox-skybox_cubemap_fragment.spv deleted file mode 100644 index 792aea28..00000000 Binary files a/crates/renderling/shaders/skybox-skybox_cubemap_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv b/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv deleted file mode 100644 index ea184762..00000000 Binary files a/crates/renderling/shaders/skybox-skybox_cubemap_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/skybox-skybox_equirectangular_fragment.spv b/crates/renderling/shaders/skybox-skybox_equirectangular_fragment.spv deleted file mode 100644 index 3617b009..00000000 Binary files a/crates/renderling/shaders/skybox-skybox_equirectangular_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/skybox-skybox_vertex.spv b/crates/renderling/shaders/skybox-skybox_vertex.spv deleted file mode 100644 index 049c982e..00000000 Binary files a/crates/renderling/shaders/skybox-skybox_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/stage-renderlet_fragment.spv b/crates/renderling/shaders/stage-renderlet_fragment.spv deleted file mode 100644 index 112c469e..00000000 Binary files a/crates/renderling/shaders/stage-renderlet_fragment.spv and /dev/null differ diff --git a/crates/renderling/shaders/stage-renderlet_vertex.spv b/crates/renderling/shaders/stage-renderlet_vertex.spv deleted file mode 100644 index 48543f63..00000000 Binary files a/crates/renderling/shaders/stage-renderlet_vertex.spv and /dev/null differ diff --git a/crates/renderling/shaders/tonemapping-tonemapping_vertex.spv b/crates/renderling/shaders/tonemapping-tonemapping_vertex.spv index 2dd7a0e9..35d08888 100644 Binary files a/crates/renderling/shaders/tonemapping-tonemapping_vertex.spv and b/crates/renderling/shaders/tonemapping-tonemapping_vertex.spv differ diff --git a/crates/renderling/shaders/tutorial-slabbed_renderlet.spv b/crates/renderling/shaders/tutorial-slabbed_renderlet.spv index f54c68e3..dc0a297f 100644 Binary files a/crates/renderling/shaders/tutorial-slabbed_renderlet.spv and b/crates/renderling/shaders/tutorial-slabbed_renderlet.spv differ diff --git a/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv b/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv index 1e6d7ea4..c8aed39b 100644 Binary files a/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv and b/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv differ diff --git a/crates/renderling/src/atlas.rs b/crates/renderling/src/atlas.rs index abde7f57..2b2a73d0 100644 --- a/crates/renderling/src/atlas.rs +++ b/crates/renderling/src/atlas.rs @@ -1,33 +1,27 @@ //! Texture atlas. //! //! All images are packed into an atlas at staging time. -//! Texture descriptors describing where in the atlas an image is, -//! and how callsites should sample pixels is packed into a buffer -//! on the GPU. This keeps the number of texture binds to a minimum. +//! Texture descriptors describe where in the atlas an image is, +//! and how it should sample pixels. These descriptors are packed into a buffer +//! on the GPU. This keeps the number of texture binds to a minimum (one, in most cases). //! //! ## NOTE: //! `Atlas` is a temporary work around until we can use bindless techniques //! on web. //! //! `Atlas` is only available on CPU. Not available in shaders. -use crabslab::{Id, Slab, SlabItem}; -use glam::{UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use crabslab::SlabItem; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] mod atlas_image; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] pub use atlas_image::*; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] mod cpu; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] pub use cpu::*; -use spirv_std::{image::Image2d, spirv, Sampler}; -/// Describes various qualities of the atlas, to be used on the GPU. -#[derive(Clone, Copy, core::fmt::Debug, Default, PartialEq, SlabItem)] -pub struct AtlasDescriptor { - pub size: UVec3, -} +pub mod shader; /// Method of addressing the edges of a texture. #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] @@ -37,88 +31,6 @@ pub struct TextureModes { pub t: TextureAddressMode, } -/// A texture inside the atlas. -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, Default, PartialEq, SlabItem)] -pub struct AtlasTexture { - /// The top left offset of texture in the atlas. - pub offset_px: UVec2, - /// The size of the texture in the atlas. - pub size_px: UVec2, - /// The index of the layer within the atlas that this `AtlasTexture `belongs to. - pub layer_index: u32, - /// The index of this frame within the layer. - pub frame_index: u32, - /// Various toggles of texture modes. - pub modes: TextureModes, -} - -impl AtlasTexture { - /// Transform the given `uv` coordinates for this texture's address mode - /// and placement in the atlas of the given size. - pub fn uv(&self, mut uv: Vec2, atlas_size: UVec2) -> Vec3 { - uv.x = self.modes.s.wrap(uv.x); - uv.y = self.modes.t.wrap(uv.y); - - // get the pixel index of the uv coordinate in terms of the original image - let mut px_index_s = (uv.x * self.size_px.x as f32) as u32; - let mut px_index_t = (uv.y * self.size_px.y as f32) as u32; - - // convert the pixel index from image to atlas space - px_index_s += self.offset_px.x; - px_index_t += self.offset_px.y; - - let sx = atlas_size.x as f32; - let sy = atlas_size.y as f32; - // normalize the pixels by dividing by the atlas size - let uv_s = px_index_s as f32 / sx; - let uv_t = px_index_t as f32 / sy; - - Vec2::new(uv_s, uv_t).extend(self.layer_index as f32) - } - - /// Constrain the input `clip_pos` to be within the bounds of this texture - /// within its atlas, in texture space. - pub fn constrain_clip_coords_to_texture_space( - &self, - clip_pos: Vec2, - atlas_size: UVec2, - ) -> Vec2 { - // Convert `clip_pos` into uv coords to figure out where in the texture - // this point lives - let input_uv = (clip_pos * Vec2::new(1.0, -1.0) + Vec2::splat(1.0)) * Vec2::splat(0.5); - self.uv(input_uv, atlas_size).xy() - } - - /// Constrain the input `clip_pos` to be within the bounds of this texture - /// within its atlas. - pub fn constrain_clip_coords(&self, clip_pos: Vec2, atlas_size: UVec2) -> Vec2 { - let uv = self.constrain_clip_coords_to_texture_space(clip_pos, atlas_size); - // Convert `uv` back into clip space - (uv * Vec2::new(2.0, 2.0) - Vec2::splat(1.0)) * Vec2::new(1.0, -1.0) - } - - #[cfg(cpu)] - /// Returns the frame of this texture as a [`wgpu::Origin3d`]. - pub fn origin(&self) -> wgpu::Origin3d { - wgpu::Origin3d { - x: self.offset_px.x, - y: self.offset_px.y, - z: self.layer_index, - } - } - - #[cfg(cpu)] - /// Returns the frame of this texture as a [`wgpu::Extent3d`]. - pub fn size_as_extent(&self) -> wgpu::Extent3d { - wgpu::Extent3d { - width: self.size_px.x, - height: self.size_px.y, - depth_or_array_layers: 1, - } - } -} - /// Infinitely wrap the input between 0.0 and 1.0. /// /// Only handles `input` >= 0.0. @@ -200,56 +112,12 @@ impl TextureAddressMode { } } -#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] -pub struct AtlasBlittingDescriptor { - pub atlas_texture_id: Id, - pub atlas_desc_id: Id, -} - -/// Vertex shader for blitting a texture into a the frame of an [`AtlasTexture`]. -/// -/// This is useful for copying textures of unsupported formats, or -/// textures of different sizes. -#[spirv(vertex)] -pub fn atlas_blit_vertex( - #[spirv(vertex_index)] vertex_id: u32, - #[spirv(instance_index)] atlas_blitting_desc_id: Id, - #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &[u32], - out_uv: &mut Vec2, - #[spirv(position)] out_pos: &mut Vec4, -) { - let i = vertex_id as usize; - *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; - - crate::println!("atlas_blitting_desc_id: {atlas_blitting_desc_id:?}"); - let atlas_blitting_desc = slab.read_unchecked(atlas_blitting_desc_id); - crate::println!("atlas_blitting_desc: {atlas_blitting_desc:?}"); - let atlas_texture = slab.read_unchecked(atlas_blitting_desc.atlas_texture_id); - crate::println!("atlas_texture: {atlas_texture:?}"); - let atlas_desc = slab.read_unchecked(atlas_blitting_desc.atlas_desc_id); - crate::println!("atlas_desc: {atlas_desc:?}"); - let clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; - crate::println!("clip_pos: {clip_pos:?}"); - *out_pos = atlas_texture - .constrain_clip_coords(clip_pos.xy(), atlas_desc.size.xy()) - .extend(clip_pos.z) - .extend(clip_pos.w); - crate::println!("out_pos: {out_pos}"); -} - -/// Fragment shader for blitting a texture into a frame of an atlas. -#[spirv(fragment)] -pub fn atlas_blit_fragment( - #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - in_uv: Vec2, - frag_color: &mut Vec4, -) { - *frag_color = texture.sample(*sampler, in_uv); -} - #[cfg(test)] mod test { + use glam::{UVec2, UVec3, Vec2, Vec3Swizzles, Vec4Swizzles}; + + use crate::atlas::shader::AtlasTextureDescriptor; + use super::*; #[test] @@ -263,7 +131,7 @@ mod test { /// Tests that clip coordinates can be converted into texture coords within /// a specific `AtlasTexture`, and back again. fn constrain_clip_coords_sanity() { - let atlas_texture = AtlasTexture { + let atlas_texture = AtlasTextureDescriptor { offset_px: UVec2::splat(0), size_px: UVec2::splat(800), layer_index: 0, diff --git a/crates/renderling/src/atlas/atlas_image.rs b/crates/renderling/src/atlas/atlas_image.rs index 08aa9aee..ec4bf3a2 100644 --- a/crates/renderling/src/atlas/atlas_image.rs +++ b/crates/renderling/src/atlas/atlas_image.rs @@ -46,6 +46,26 @@ pub enum AtlasImageFormat { D32FLOAT, } +impl From for wgpu::TextureFormat { + fn from(value: AtlasImageFormat) -> Self { + match value { + AtlasImageFormat::R8 => wgpu::TextureFormat::R8Unorm, + AtlasImageFormat::R8G8 => wgpu::TextureFormat::Rg8Unorm, + AtlasImageFormat::R8G8B8 => wgpu::TextureFormat::Rgba8Unorm, // No direct 3-channel format, using 4-channel + AtlasImageFormat::R8G8B8A8 => wgpu::TextureFormat::Rgba8Unorm, + AtlasImageFormat::R16 => wgpu::TextureFormat::R16Unorm, + AtlasImageFormat::R16G16 => wgpu::TextureFormat::Rg16Unorm, + AtlasImageFormat::R16G16B16 => wgpu::TextureFormat::Rgba16Unorm, // No direct 3-channel format, using 4-channel + AtlasImageFormat::R16G16B16A16 => wgpu::TextureFormat::Rgba16Unorm, + AtlasImageFormat::R16G16B16A16FLOAT => wgpu::TextureFormat::Rgba16Float, + AtlasImageFormat::R32FLOAT => wgpu::TextureFormat::R32Float, + AtlasImageFormat::R32G32B32FLOAT => wgpu::TextureFormat::Rgba32Float, // No direct 3-channel format, using 4-channel + AtlasImageFormat::R32G32B32A32FLOAT => wgpu::TextureFormat::Rgba32Float, + AtlasImageFormat::D32FLOAT => wgpu::TextureFormat::Depth32Float, + } + } +} + impl AtlasImageFormat { pub fn from_wgpu_texture_format(value: wgpu::TextureFormat) -> Option { match value { diff --git a/crates/renderling/src/atlas/cpu.rs b/crates/renderling/src/atlas/cpu.rs index 23a14c82..7ec71622 100644 --- a/crates/renderling/src/atlas/cpu.rs +++ b/crates/renderling/src/atlas/cpu.rs @@ -12,15 +12,15 @@ use image::RgbaImage; use snafu::{prelude::*, OptionExt}; use crate::{ - atlas::AtlasDescriptor, + atlas::{ + shader::{AtlasBlittingDescriptor, AtlasDescriptor, AtlasTextureDescriptor}, + TextureModes, + }, bindgroup::ManagedBindGroup, - texture::{CopiedTextureBuffer, Texture}, + texture::{self, CopiedTextureBuffer, Texture}, }; -use super::{ - atlas_image::{convert_pixels, AtlasImage}, - AtlasBlittingDescriptor, AtlasTexture, -}; +use super::atlas_image::{convert_pixels, AtlasImage}; pub(crate) const ATLAS_SUGGESTED_SIZE: u32 = 2048; pub(crate) const ATLAS_SUGGESTED_LAYERS: u32 = 8; @@ -41,18 +41,83 @@ pub enum AtlasError { #[snafu(display("Missing bindgroup {layer}"))] MissingBindgroup { layer: u32 }, + + #[snafu(display("{source}"))] + Texture { + source: crate::texture::TextureError, + }, +} + +/// A staged texture in the texture atlas. +/// +/// An [`AtlasTexture`] can be acquired through: +/// +/// * [`Atlas::add_image`] +/// * [`Atlas::add_images`]. +/// * [`Atlas::set_images`] +/// +/// Clones of this type all point to the same underlying data. +/// +/// Dropping all clones of this type will cause it to be unloaded from the GPU. +/// +/// If a value of this type has been given to another staged resource, +/// like [`Material`](crate::material::Material), this will prevent the `AtlasTexture` from +/// being dropped and unloaded. +/// +/// Internally an `AtlasTexture` holds a reference to its descriptor, +/// [`AtlasTextureDescriptor`]. +#[derive(Clone)] +pub struct AtlasTexture { + pub(crate) descriptor: Hybrid, +} + +impl AtlasTexture { + /// Get the GPU slab identifier of the underlying descriptor. + /// + /// This is for internal use. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Return a copy of the underlying descriptor. + pub fn descriptor(&self) -> AtlasTextureDescriptor { + self.descriptor.get() + } + + /// Return the texture modes of the underlying descriptor. + pub fn modes(&self) -> TextureModes { + self.descriptor.get().modes + } + + /// Sets the texture modes of the underlying descriptor. + /// + /// ## Warning + /// + /// This also sets the modes for all clones of this value. + pub fn set_modes(&self, modes: TextureModes) { + self.descriptor.modify(|d| d.modes = modes); + } } /// Used to track textures internally. +/// +/// We need a separate struct for tracking textures because the atlas +/// reorganizes the layout (the packing) of textures each time a new +/// texture is added. +/// +/// This means the textures must be updated on the GPU, but we don't +/// want these internal representations to keep unreferenced textures +/// from dropping, so we have to maintain a separate representation +/// here. #[derive(Clone, Debug)] struct InternalAtlasTexture { /// Cached value. - cache: AtlasTexture, - weak: WeakHybrid, + cache: AtlasTextureDescriptor, + weak: WeakHybrid, } impl InternalAtlasTexture { - fn from_hybrid(hat: &Hybrid) -> Self { + fn from_hybrid(hat: &Hybrid) -> Self { InternalAtlasTexture { cache: hat.get(), weak: WeakHybrid::from_hybrid(hat), @@ -63,7 +128,7 @@ impl InternalAtlasTexture { self.weak.has_external_references() } - fn set(&mut self, at: AtlasTexture) { + fn set(&mut self, at: AtlasTextureDescriptor) { self.cache = at; if let Some(hy) = self.weak.upgrade() { hy.set(at); @@ -140,6 +205,8 @@ pub struct Atlas { layers: Arc>>, label: Option, descriptor: Hybrid, + /// Used for user updates into the atlas by blit images into specific frames. + blitter: AtlasBlitter, } impl Atlas { @@ -226,13 +293,18 @@ impl Atlas { size: UVec3::new(size.width, size.height, size.depth_or_array_layers), }); let label = label.map(|s| s.to_owned()); - + let blitter = AtlasBlitter::new( + slab.device(), + texture.texture.format(), + wgpu::FilterMode::Linear, + ); Atlas { slab: slab.clone(), layers: Arc::new(RwLock::new(layers)), - texture_array: Arc::new(RwLock::new(texture)), descriptor, label, + blitter, + texture_array: Arc::new(RwLock::new(texture)), } } @@ -265,10 +337,7 @@ impl Atlas { /// Reset this atlas with all new images. /// /// Any existing `Hybrid`s will be invalidated. - pub fn set_images( - &self, - images: &[AtlasImage], - ) -> Result>, AtlasError> { + pub fn set_images(&self, images: &[AtlasImage]) -> Result, AtlasError> { log::debug!("setting images"); { // UNWRAP: panic on purpose @@ -291,7 +360,7 @@ impl Atlas { pub fn add_images<'a>( &self, images: impl IntoIterator, - ) -> Result>, AtlasError> { + ) -> Result, AtlasError> { // UNWRAP: POP let mut layers = self.layers.write().unwrap(); let mut texture_array = self.texture_array.write().unwrap(); @@ -314,7 +383,20 @@ impl Atlas { *layers = staged.layers; staged.image_additions.sort_by_key(|a| a.0); - Ok(staged.image_additions.into_iter().map(|a| a.1).collect()) + Ok(staged + .image_additions + .into_iter() + .map(|a| AtlasTexture { descriptor: a.1 }) + .collect()) + } + + /// Add one image. + /// + /// If you have more than one image, you should use [`Atlas::add_images`], as every + /// change in images causes a repacking, which might be expensive. + pub fn add_image(&self, image: &AtlasImage) -> Result { + // UNWRAP: safe because we know there's at least one image + Ok(self.add_images(Some(image))?.pop().unwrap()) } /// Resize the atlas. @@ -453,6 +535,107 @@ impl Atlas { } images } + + /// Update the given [`AtlasTexture`] with a [`Texture`](crate::texture::Texture). + /// + /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to by the + /// `AtlasTexture`. + /// + /// Returns a submission index that can be polled with [`wgpu::Device::poll`]. + pub fn update_texture( + &self, + atlas_texture: &AtlasTexture, + source_texture: &texture::Texture, + ) -> Result { + self.update_textures(Some((atlas_texture, source_texture))) + } + + /// Update the given [`AtlasTexture`]s with [`Texture`](crate::texture::Texture)s. + /// + /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to by the + /// `AtlasTexture`. + /// + /// Returns a submission index that can be polled with [`wgpu::Device::poll`]. + pub fn update_textures<'a>( + &self, + updates: impl IntoIterator, + ) -> Result { + let updates = updates.into_iter().collect::>(); + let op = AtlasBlittingOperation::new(&self.blitter, self, updates.len()); + let runtime = self.slab.runtime(); + let mut encoder = runtime + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Atlas::update_texture"), + }); + for (i, (atlas_texture, source_texture)) in updates.into_iter().enumerate() { + op.run( + runtime, + &mut encoder, + source_texture, + i as u32, + self, + atlas_texture, + )?; + } + Ok(runtime.queue.submit(Some(encoder.finish()))) + } + + /// Update the given [`AtlasTexture`]s with new data. + /// + /// This will blit the image data into the frame of the [`Atlas`] pointed to by the + /// `AtlasTexture`. + /// + /// Returns a submission index that can be polled with [`wgpu::Device::poll`]. + pub fn update_images<'a>( + &self, + updates: impl IntoIterator)>, + ) -> Result { + let (atlas_textures, images): (Vec<_>, Vec<_>) = updates.into_iter().unzip(); + let mut textures = vec![]; + for image in images.into_iter() { + let image: AtlasImage = image.into(); + let atlas_format = self.get_texture().texture.format(); + let bytes = super::atlas_image::convert_pixels( + image.pixels, + image.format, + atlas_format, + image.apply_linear_transfer, + ); + let (channels, subpixel_bytes) = + texture::wgpu_texture_format_channels_and_subpixel_bytes(atlas_format) + .context(TextureSnafu)?; + let texture = texture::Texture::new_with( + self.slab.runtime(), + Some("atlas-image-update"), + Some(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST), + None, + atlas_format, + channels, + subpixel_bytes, + image.size.x, + image.size.y, + 1, + &bytes, + ); + textures.push(texture); + } + self.update_textures(atlas_textures.into_iter().zip(textures.iter())) + } + + /// Update the given [`AtlasTexture`]s with new data. + /// + /// This will blit the image data into the frame of the [`Atlas`] pointed to by the + /// `AtlasTexture`. + /// + /// Returns a submission index that can be polled with [`wgpu::Device::poll`]. + pub fn update_image( + &self, + atlas_texture: &AtlasTexture, + source_image: impl Into, + ) -> Result { + self.update_images(Some((atlas_texture, source_image))) + } } fn pack_images<'a>( @@ -513,7 +696,7 @@ fn pack_images<'a>( /// Internal atlas resources. struct StagedResources { texture: Texture, - image_additions: Vec<(usize, Hybrid)>, + image_additions: Vec<(usize, Hybrid)>, layers: Vec, } @@ -559,7 +742,7 @@ impl StagedResources { original_index, image, } => { - let atlas_texture = AtlasTexture { + let atlas_texture = AtlasTextureDescriptor { offset_px, size_px, frame_index: frame_index as u32, @@ -680,6 +863,30 @@ pub struct AtlasBlittingOperation { } impl AtlasBlittingOperation { + pub fn new( + blitter: &AtlasBlitter, + into_atlas: &Atlas, + source_layers: usize, + ) -> AtlasBlittingOperation { + AtlasBlittingOperation { + desc: into_atlas + .slab + .new_value(AtlasBlittingDescriptor::default()), + atlas_slab_buffer: Arc::new(Mutex::new(into_atlas.slab.commit())), + bindgroups: { + let mut bgs = vec![]; + for _ in 0..source_layers { + bgs.push(ManagedBindGroup::default()); + } + Arc::new(bgs) + }, + pipeline: blitter.pipeline.clone(), + sampler: blitter.sampler.clone(), + bindgroup_layout: blitter.bind_group_layout.clone(), + from_texture_id: Default::default(), + } + } + /// Copies the data from texture this [`AtlasBlittingOperation`] was created with /// into the atlas. /// @@ -690,9 +897,9 @@ impl AtlasBlittingOperation { runtime: impl AsRef, encoder: &mut wgpu::CommandEncoder, from_texture: &crate::texture::Texture, - layer: u32, + from_layer: u32, to_atlas: &Atlas, - atlas_texture: &Hybrid, + atlas_texture: &AtlasTexture, ) -> Result<(), AtlasError> { let runtime = runtime.as_ref(); @@ -718,15 +925,15 @@ impl AtlasBlittingOperation { .texture .create_view(&wgpu::TextureViewDescriptor { label: Some("atlas-blitting"), - base_array_layer: layer, + base_array_layer: from_layer, array_layer_count: Some(1), dimension: Some(wgpu::TextureViewDimension::D2), ..Default::default() }); let bindgroup = self .bindgroups - .get(layer as usize) - .context(MissingBindgroupSnafu { layer })? + .get(from_layer as usize) + .context(MissingBindgroupSnafu { layer: from_layer })? .get(should_invalidate, || { runtime .device @@ -752,7 +959,7 @@ impl AtlasBlittingOperation { }) }); - let atlas_texture = atlas_texture.get(); + let atlas_texture = atlas_texture.descriptor(); let atlas_view = to_atlas_texture .texture .create_view(&wgpu::TextureViewDescriptor { @@ -804,19 +1011,20 @@ impl AtlasBlitter { /// /// # Arguments /// - `device` - A [`wgpu::Device`] - /// - `format` - The [`wgpu::TextureFormat`] of the texture that will be copied to. This has to be renderable. - /// - `sample_type` - The [`wgpu::Sampler`] Filtering Mode + /// - `format` - The [`wgpu::TextureFormat`] of the atlas being updated. + /// - `mag_filter` - The filtering algorithm to use when magnifying. + /// This is used when the input source is smaller than the destination. pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, - sample_type: wgpu::FilterMode, + mag_filter: wgpu::FilterMode, ) -> Self { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("atlas-blitter"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: sample_type, + mag_filter, ..Default::default() }); @@ -838,7 +1046,7 @@ impl AtlasBlitter { visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { - filterable: sample_type == wgpu::FilterMode::Linear, + filterable: mag_filter == wgpu::FilterMode::Linear, }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, @@ -848,7 +1056,7 @@ impl AtlasBlitter { wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(if sample_type == wgpu::FilterMode::Linear { + ty: wgpu::BindingType::Sampler(if mag_filter == wgpu::FilterMode::Linear { wgpu::SamplerBindingType::Filtering } else { wgpu::SamplerBindingType::NonFiltering @@ -906,43 +1114,16 @@ impl AtlasBlitter { sampler: sampler.into(), } } - - pub fn new_blitting_operation( - &self, - into_atlas: &Atlas, - layers: usize, - ) -> AtlasBlittingOperation { - AtlasBlittingOperation { - desc: into_atlas - .slab - .new_value(AtlasBlittingDescriptor::default()), - atlas_slab_buffer: Arc::new(Mutex::new(into_atlas.slab.commit())), - bindgroups: { - let mut bgs = vec![]; - for _ in 0..layers { - bgs.push(ManagedBindGroup::default()); - } - Arc::new(bgs) - }, - pipeline: self.pipeline.clone(), - sampler: self.sampler.clone(), - bindgroup_layout: self.bind_group_layout.clone(), - from_texture_id: Default::default(), - } - } } #[cfg(test)] mod test { use crate::{ - atlas::{AtlasTexture, TextureAddressMode}, - camera::Camera, + atlas::{shader::AtlasTextureDescriptor, TextureAddressMode}, + context::Context, + geometry::Vertex, material::Materials, - pbr::Material, - stage::Vertex, test::BlockOnFuture, - transform::Transform, - Context, }; use glam::{UVec3, Vec2, Vec3, Vec4}; @@ -960,7 +1141,9 @@ mod test { .with_background_color(Vec3::splat(0.0).extend(1.0)) .with_bloom(false); let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); let texels = AtlasImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap(); @@ -971,13 +1154,14 @@ mod test { let texels_entry = &atlas_entries[2]; let _rez = stage - .builder() - .with_material(Material { - albedo_texture_id: texels_entry.id(), - has_lighting: false, - ..Default::default() - }) - .with_vertices({ + .new_primitive() + .with_material( + stage + .new_material() + .with_albedo_texture(texels_entry) + .with_has_lighting(false), + ) + .with_vertices(stage.new_vertices({ let tl = Vertex::default() .with_position(Vec3::ZERO) .with_uv0(Vec2::ZERO); @@ -991,12 +1175,8 @@ mod test { .with_position(Vec3::new(1.0, 1.0, 0.0)) .with_uv0(Vec2::splat(1.0)); [tl, bl, br, tl, br, tr] - }) - .with_transform(Transform { - scale: Vec3::new(32.0, 32.0, 1.0), - ..Default::default() - }) - .build(); + })) + .with_transform(stage.new_transform().with_scale(Vec3::new(32.0, 32.0, 1.0))); log::info!("rendering"); let frame = ctx.get_next_frame().unwrap(); @@ -1019,19 +1199,21 @@ mod test { .new_stage() .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap(); let clamp_tex = &entries[0]; let repeat_tex = &entries[1]; - repeat_tex.modify(|t| { - t.modes.s = TextureAddressMode::Repeat; - t.modes.t = TextureAddressMode::Repeat; + repeat_tex.set_modes(TextureModes { + s: TextureAddressMode::Repeat, + t: TextureAddressMode::Repeat, }); let mirror_tex = &entries[2]; - mirror_tex.modify(|t| { - t.modes.s = TextureAddressMode::MirroredRepeat; - t.modes.t = TextureAddressMode::MirroredRepeat; + mirror_tex.set_modes(TextureModes { + s: TextureAddressMode::MirroredRepeat, + t: TextureAddressMode::MirroredRepeat, }); let sheet_w = sheet_w as f32; @@ -1052,42 +1234,44 @@ mod test { [tl, bl, br, tl, br, tr] }); let _clamp_rez = stage - .builder() - .with_vertices_array(geometry.array()) - .with_material(Material { - albedo_texture_id: clamp_tex.id(), - has_lighting: false, - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(&geometry) + .with_material( + stage + .new_material() + .with_albedo_texture(clamp_tex) + .with_has_lighting(false), + ); let _repeat_rez = stage - .builder() - .with_transform(Transform { - translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), - ..Default::default() - }) - .with_material(Material { - albedo_texture_id: repeat_tex.id(), - has_lighting: false, - ..Default::default() - }) - .with_vertices_array(geometry.array()) - .build(); + .new_primitive() + .with_transform(stage.new_transform().with_translation(Vec3::new( + sheet_w + 1.0, + 0.0, + 0.0, + ))) + .with_material( + stage + .new_material() + .with_albedo_texture(repeat_tex) + .with_has_lighting(false), + ) + .with_vertices(&geometry); let _mirror_rez = stage - .builder() - .with_transform(Transform { - translation: Vec3::new(sheet_w * 2.0 + 2.0, 0.0, 0.0), - ..Default::default() - }) - .with_material(Material { - albedo_texture_id: mirror_tex.id(), - has_lighting: false, - ..Default::default() - }) - .with_vertices_array(geometry.array()) - .build(); + .new_primitive() + .with_transform(stage.new_transform().with_translation(Vec3::new( + sheet_w * 2.0 + 2.0, + 0.0, + 0.0, + ))) + .with_material( + stage + .new_material() + .with_albedo_texture(mirror_tex) + .with_has_lighting(false), + ) + .with_vertices(geometry); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -1110,77 +1294,82 @@ mod test { .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap(); let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap(); let clamp_tex = &entries[0]; let repeat_tex = &entries[1]; - repeat_tex.modify(|t| { - t.modes.s = TextureAddressMode::Repeat; - t.modes.t = TextureAddressMode::Repeat; + repeat_tex.set_modes(TextureModes { + s: TextureAddressMode::Repeat, + t: TextureAddressMode::Repeat, }); let mirror_tex = &entries[2]; - mirror_tex.modify(|t| { - t.modes.s = TextureAddressMode::MirroredRepeat; - t.modes.t = TextureAddressMode::MirroredRepeat; + mirror_tex.set_modes(TextureModes { + s: TextureAddressMode::MirroredRepeat, + t: TextureAddressMode::MirroredRepeat, }); let sheet_w = sheet_w as f32; let sheet_h = sheet_h as f32; - let (geometry, _clamp_material, _clamp_prim) = stage - .builder() - .with_vertices({ - let tl = Vertex::default() - .with_position(Vec3::ZERO) - .with_uv0(Vec2::ZERO); - let tr = Vertex::default() - .with_position(Vec3::new(sheet_w, 0.0, 0.0)) - .with_uv0(Vec2::new(-3.0, 0.0)); - let bl = Vertex::default() - .with_position(Vec3::new(0.0, sheet_h, 0.0)) - .with_uv0(Vec2::new(0.0, -3.0)); - let br = Vertex::default() - .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) - .with_uv0(Vec2::splat(-3.0)); - [tl, bl, br, tl, br, tr] - }) - .with_material(Material { - albedo_texture_id: clamp_tex.id(), - has_lighting: false, - ..Default::default() - }) - .build(); + let geometry = stage.new_vertices({ + let tl = Vertex::default() + .with_position(Vec3::ZERO) + .with_uv0(Vec2::ZERO); + let tr = Vertex::default() + .with_position(Vec3::new(sheet_w, 0.0, 0.0)) + .with_uv0(Vec2::new(-3.0, 0.0)); + let bl = Vertex::default() + .with_position(Vec3::new(0.0, sheet_h, 0.0)) + .with_uv0(Vec2::new(0.0, -3.0)); + let br = Vertex::default() + .with_position(Vec3::new(sheet_w, sheet_h, 0.0)) + .with_uv0(Vec2::splat(-3.0)); + [tl, bl, br, tl, br, tr] + }); + let _clamp_prim = stage + .new_primitive() + .with_vertices(&geometry) + .with_material( + stage + .new_material() + .with_albedo_texture(clamp_tex) + .with_has_lighting(false), + ); let _repeat_rez = stage - .builder() - .with_material(Material { - albedo_texture_id: repeat_tex.id(), - has_lighting: false, - ..Default::default() - }) - .with_transform(Transform { - translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0), - ..Default::default() - }) - .with_vertices_array(geometry.array()) - .build(); + .new_primitive() + .with_material( + stage + .new_material() + .with_albedo_texture(repeat_tex) + .with_has_lighting(false), + ) + .with_transform(stage.new_transform().with_translation(Vec3::new( + sheet_w + 1.0, + 0.0, + 0.0, + ))) + .with_vertices(&geometry); let _mirror_rez = stage - .builder() - .with_material(Material { - albedo_texture_id: mirror_tex.id(), - has_lighting: false, - ..Default::default() - }) - .with_transform(Transform { - translation: Vec3::new(sheet_w * 2.0 + 2.0, 0.0, 0.0), - ..Default::default() - }) - .with_vertices_array(geometry.array()) - .build(); + .new_primitive() + .with_material( + stage + .new_material() + .with_albedo_texture(mirror_tex) + .with_has_lighting(false), + ) + .with_transform(stage.new_transform().with_translation(Vec3::new( + sheet_w * 2.0 + 2.0, + 0.0, + 0.0, + ))) + .with_vertices(&geometry); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -1190,7 +1379,7 @@ mod test { #[test] fn transform_uvs_for_atlas() { - let mut tex = AtlasTexture { + let mut tex = AtlasTextureDescriptor { offset_px: UVec2::ZERO, size_px: UVec2::ONE, ..Default::default() diff --git a/crates/renderling/src/atlas/shader.rs b/crates/renderling/src/atlas/shader.rs new file mode 100644 index 00000000..7ea18ffd --- /dev/null +++ b/crates/renderling/src/atlas/shader.rs @@ -0,0 +1,140 @@ +use crabslab::{Id, Slab, SlabItem}; +use glam::{UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use spirv_std::{image::Image2d, spirv, Sampler}; + +/// Describes various qualities of the atlas, to be used on the GPU. +#[derive(Clone, Copy, core::fmt::Debug, Default, PartialEq, SlabItem)] +pub struct AtlasDescriptor { + pub size: UVec3, +} + +/// A texture inside the atlas. +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] +pub struct AtlasTextureDescriptor { + /// The top left offset of texture in the atlas. + pub offset_px: UVec2, + /// The size of the texture in the atlas. + pub size_px: UVec2, + /// The index of the layer within the atlas that this `AtlasTexture `belongs to. + pub layer_index: u32, + /// The index of this frame within the layer. + pub frame_index: u32, + /// Various toggles of texture modes. + pub modes: super::TextureModes, +} + +impl AtlasTextureDescriptor { + /// Transform the given `uv` coordinates for this texture's address mode + /// and placement in the atlas of the given size. + pub fn uv(&self, mut uv: Vec2, atlas_size: UVec2) -> Vec3 { + uv.x = self.modes.s.wrap(uv.x); + uv.y = self.modes.t.wrap(uv.y); + + // get the pixel index of the uv coordinate in terms of the original image + let mut px_index_s = (uv.x * self.size_px.x as f32) as u32; + let mut px_index_t = (uv.y * self.size_px.y as f32) as u32; + + // convert the pixel index from image to atlas space + px_index_s += self.offset_px.x; + px_index_t += self.offset_px.y; + + let sx = atlas_size.x as f32; + let sy = atlas_size.y as f32; + // normalize the pixels by dividing by the atlas size + let uv_s = px_index_s as f32 / sx; + let uv_t = px_index_t as f32 / sy; + + Vec2::new(uv_s, uv_t).extend(self.layer_index as f32) + } + + /// Constrain the input `clip_pos` to be within the bounds of this texture + /// within its atlas, in texture space. + pub fn constrain_clip_coords_to_texture_space( + &self, + clip_pos: Vec2, + atlas_size: UVec2, + ) -> Vec2 { + // Convert `clip_pos` into uv coords to figure out where in the texture + // this point lives + let input_uv = (clip_pos * Vec2::new(1.0, -1.0) + Vec2::splat(1.0)) * Vec2::splat(0.5); + self.uv(input_uv, atlas_size).xy() + } + + /// Constrain the input `clip_pos` to be within the bounds of this texture + /// within its atlas. + pub fn constrain_clip_coords(&self, clip_pos: Vec2, atlas_size: UVec2) -> Vec2 { + let uv = self.constrain_clip_coords_to_texture_space(clip_pos, atlas_size); + // Convert `uv` back into clip space + (uv * Vec2::new(2.0, 2.0) - Vec2::splat(1.0)) * Vec2::new(1.0, -1.0) + } + + #[cfg(cpu)] + /// Returns the frame of this texture as a [`wgpu::Origin3d`]. + pub fn origin(&self) -> wgpu::Origin3d { + wgpu::Origin3d { + x: self.offset_px.x, + y: self.offset_px.y, + z: self.layer_index, + } + } + + #[cfg(cpu)] + /// Returns the frame of this texture as a [`wgpu::Extent3d`]. + pub fn size_as_extent(&self) -> wgpu::Extent3d { + wgpu::Extent3d { + width: self.size_px.x, + height: self.size_px.y, + depth_or_array_layers: 1, + } + } +} + +#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] +pub struct AtlasBlittingDescriptor { + pub atlas_texture_id: Id, + pub atlas_desc_id: Id, +} + +/// Vertex shader for blitting a texture into a the frame of an +/// [`AtlasTextureDescriptor`]. +/// +/// This is useful for copying textures of unsupported formats, or +/// textures of different sizes. +#[spirv(vertex)] +pub fn atlas_blit_vertex( + #[spirv(vertex_index)] vertex_id: u32, + #[spirv(instance_index)] atlas_blitting_desc_id: Id, + #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &[u32], + out_uv: &mut Vec2, + #[spirv(position)] out_pos: &mut Vec4, +) { + let i = vertex_id as usize; + *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; + + crate::println!("atlas_blitting_desc_id: {atlas_blitting_desc_id:?}"); + let atlas_blitting_desc = slab.read_unchecked(atlas_blitting_desc_id); + crate::println!("atlas_blitting_desc: {atlas_blitting_desc:?}"); + let atlas_texture = slab.read_unchecked(atlas_blitting_desc.atlas_texture_id); + crate::println!("atlas_texture: {atlas_texture:?}"); + let atlas_desc = slab.read_unchecked(atlas_blitting_desc.atlas_desc_id); + crate::println!("atlas_desc: {atlas_desc:?}"); + let clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; + crate::println!("clip_pos: {clip_pos:?}"); + *out_pos = atlas_texture + .constrain_clip_coords(clip_pos.xy(), atlas_desc.size.xy()) + .extend(clip_pos.z) + .extend(clip_pos.w); + crate::println!("out_pos: {out_pos}"); +} + +/// Fragment shader for blitting a texture into a frame of an atlas. +#[spirv(fragment)] +pub fn atlas_blit_fragment( + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + in_uv: Vec2, + frag_color: &mut Vec4, +) { + *frag_color = texture.sample(*sampler, in_uv); +} diff --git a/crates/renderling/src/bits.rs b/crates/renderling/src/bits.rs deleted file mode 100644 index dafed727..00000000 --- a/crates/renderling/src/bits.rs +++ /dev/null @@ -1,343 +0,0 @@ -//! Helpers for bitwise operations. - -use core::ops::RangeInclusive; - -use crabslab::{Id, Slab}; - -/// Statically define a shift/mask range as a literal range of bits. -pub const fn bits(range: RangeInclusive) -> (u32, u32) { - let mut start = *range.start(); - let end = *range.end(); - let mut mask = 0; - while start <= end { - mask = (mask << 1) | 1; - start += 1; - } - (*range.start(), mask) -} - -/// Insert the value of the bits defined by the shift/mask range. -pub fn insert(bits: &mut u32, (shift, mask): (u32, u32), value: u32) { - // rotate right - if shift >= 1 { - *bits = (*bits >> shift) | (*bits << (32 - shift)); - } - // unset - *bits &= !mask; - // set - *bits |= value & mask; - // unrotate (rotate left) - if shift >= 1 { - *bits = (*bits << shift) | (*bits >> (32 - shift)); - } -} - -/// Extract the value of the bits defined by the shift/mask range. -pub fn extract(bits: u32, (shift, mask): (u32, u32)) -> u32 { - (bits >> shift) & mask -} - -/// The shift/mask range for the first 8 bits of a u32. -pub const U8_0_BITS: (u32, u32) = bits(0..=7); -/// The shift/mask range for the second 8 bits of a u32. -pub const U8_1_BITS: (u32, u32) = bits(8..=15); -/// The shift/mask range for the third 8 bits of a u32. -pub const U8_2_BITS: (u32, u32) = bits(16..=23); -/// The shift/mask range for the fourth 8 bits of a u32. -pub const U8_3_BITS: (u32, u32) = bits(24..=31); - -/// The shift/mask range for the first 16 bits of a u32. -pub const U16_0_BITS: (u32, u32) = bits(0..=15); -/// The shift/mask range for the second 16 bits of a u32. -pub const U16_1_BITS: (u32, u32) = bits(16..=31); - -/// Extract 8 bits of the u32 at the given index in the slab. -/// -/// Returns the extracted value, the index of the next component and the index -/// of the next u32 in the slab. -pub fn extract_u8( - // index of the u32 in the slab - u32_index: usize, - // eg 0 for the first 8 bits, 1 for the second 8 bits, etc - byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (u32, usize, usize) { - const SHIFT_MASKS: [((u32, u32), usize); 4] = [ - (U8_0_BITS, 0), - (U8_1_BITS, 0), - (U8_2_BITS, 0), - (U8_3_BITS, 1), - ]; - let byte_mod = byte_offset % 4; - let (shift_mask, index_inc) = SHIFT_MASKS[byte_mod]; - let u32_value = slab.read(Id::from(u32_index)); - let value = extract(u32_value, shift_mask); - (value, u32_index + index_inc, byte_mod + 1) -} - -/// Extract 8 bits of the u32 at the given index in the slab. -/// -/// Returns the extracted value, the index of the next component and the index -/// of the next u32 in the slab. -pub fn extract_i8( - // index of the u32 in the slab - u32_index: usize, - // eg 0 for the first 8 bits, 1 for the second 8 bits, etc - byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (i32, usize, usize) { - let (value, u32_index, n) = extract_u8(u32_index, byte_offset, slab); - let value: i32 = (value as i32 & 0xFF) - ((value as i32 & 0x80) << 1); - (value, u32_index, n) -} - -/// Extract 16 bits of the u32 at the given index in the slab. -pub fn extract_u16( - // index of the u32 in the slab - u32_index: usize, - // eg 0 for the first 16 bits, 2 for the second 16 bits, etc - byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (u32, usize, usize) { - // NOTE: This should only have two entries, but we'll still handle the case - // where the extraction is not aligned to a u32 boundary by reading as if it - // were, and then re-aligning. - const SHIFT_MASKS: [((u32, u32), usize, usize); 4] = [ - (U16_0_BITS, 2, 0), - (U16_0_BITS, 2, 0), - (U16_1_BITS, 0, 1), - (U16_1_BITS, 0, 1), - ]; - let byte_mod = byte_offset % 4; - crate::println!("byte_mod: {byte_mod}"); - let (shift_mask, next_byte_offset, index_inc) = SHIFT_MASKS[byte_mod]; - let u32_value = slab.read(Id::from(u32_index)); - crate::println!("u32: {:032b}", u32_value); - let value = extract(u32_value, shift_mask); - crate::println!("u16: {:016b}", value); - crate::println!("u32: {:?}", u32_value); - (value, u32_index + index_inc, next_byte_offset) -} - -/// Extract 16 bits of the u32 at the given index in the slab. -pub fn extract_i16( - // index of the u32 in the slab - u32_index: usize, - // eg 0 for the first 16 bits, 1 for the second 16 bits, etc - byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (i32, usize, usize) { - let (value, u32_index, n) = extract_u16(u32_index, byte_offset, slab); - let value: i32 = (value as i32 & 0xFFFF) - ((value as i32 & 0x8000) << 1); - (value, u32_index, n) -} - -/// Extract 32 bits of the u32 at the given index in the slab. -pub fn extract_u32( - // index of the u32 in the slab - u32_index: usize, - // ignored and always passed back as `0` - _byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (u32, usize, usize) { - (slab.read(Id::from(u32_index)), u32_index + 1, 0) -} - -/// Extract 32 bits of the u32 at the given index in the slab. -pub fn extract_i32( - // index of the u32 in the slab - u32_index: usize, - // ignored and always passed back as `0` - _byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (i32, usize, usize) { - let (value, _, _) = extract_u32(u32_index, 0, slab); - (value as i32, u32_index + 1, 0) -} - -/// Extract 32 bits of the u32 at the given index in the slab. -pub fn extract_f32( - // index of the u32 in the slab - u32_index: usize, - // ignored and always passed back as `0` - _byte_offset: usize, - // slab of u32s - slab: &[u32], -) -> (f32, usize, usize) { - let (value, _, _) = extract_u32(u32_index, 0, slab); - (f32::from_bits(value), u32_index + 1, 0) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn bits_sanity() { - let mut store = 0; - assert_eq!( - "00000000000000000000000000000000", - &format!("{:032b}", store) - ); - insert(&mut store, bits(0..=7), u8::MAX as u32); - assert_eq!( - "00000000000000000000000011111111", - &format!("{:032b}", store) - ); - store = 0; - insert(&mut store, bits(8..=15), u8::MAX as u32); - assert_eq!( - "00000000000000001111111100000000", - &format!("{:032b}", store) - ); - } - - #[test] - fn bits_u8_sanity() { - let mut bits = 0; - println!("bits: {:032b}", bits); - super::insert(&mut bits, super::U8_0_BITS, 6u8 as u32); - println!("bits: {:032b}", bits); - assert_eq!(super::extract(bits, super::U8_0_BITS), 6); - super::insert(&mut bits, super::U8_1_BITS, 5u8 as u32); - println!("bits: {:032b}", bits); - assert_eq!(super::extract(bits, super::U8_0_BITS), 6); - assert_eq!(super::extract(bits, super::U8_1_BITS), 5); - super::insert(&mut bits, super::U8_2_BITS, 4u8 as u32); - println!("bits: {:032b}", bits); - assert_eq!(super::extract(bits, super::U8_0_BITS), 6); - assert_eq!(super::extract(bits, super::U8_1_BITS), 5); - assert_eq!(super::extract(bits, super::U8_2_BITS), 4); - super::insert(&mut bits, super::U8_3_BITS, 3u8 as u32); - println!("bits: {:032b}", bits); - assert_eq!(super::extract(bits, super::U8_0_BITS), 6); - assert_eq!(super::extract(bits, super::U8_1_BITS), 5); - assert_eq!(super::extract(bits, super::U8_2_BITS), 4); - assert_eq!(super::extract(bits, super::U8_3_BITS), 3); - } - - #[test] - fn extract_u8_sanity() { - let u8_slab = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 0u8, 0u8]; - let u32_slab: &[u32] = bytemuck::cast_slice(&u8_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_u8(index, n, u32_slab); - let (b, index, n) = extract_u8(index, n, u32_slab); - let (c, index, n) = extract_u8(index, n, u32_slab); - let (d, index, n) = extract_u8(index, n, u32_slab); - let (e, index, n) = extract_u8(index, n, u32_slab); - let (f, _, _) = extract_u8(index, n, u32_slab); - assert_eq!([0, 1, 2, 3, 4, 5], [a, b, c, d, e, f]); - } - - #[test] - fn extract_i8_sanity() { - let i8_slab = [0i8, -1i8, -2i8, -3i8, 4i8, 5i8, 0i8, 0i8]; - let u32_slab: &[u32] = bytemuck::cast_slice(&i8_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_i8(index, n, u32_slab); - let (b, index, n) = extract_i8(index, n, u32_slab); - let (c, index, n) = extract_i8(index, n, u32_slab); - let (d, index, n) = extract_i8(index, n, u32_slab); - let (e, index, n) = extract_i8(index, n, u32_slab); - let (f, _, _) = extract_i8(index, n, u32_slab); - assert_eq!([0, -1, -2, -3, 4, 5], [a, b, c, d, e, f]); - } - - #[test] - fn extract_u16_sanity() { - let u16_slab = [0u16, 1u16, 2u16, 3u16, 4u16, 5u16]; - let u32_slab: &[u32] = bytemuck::cast_slice(&u16_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_u16(index, n, u32_slab); - let (b, index, n) = extract_u16(index, n, u32_slab); - let (c, index, n) = extract_u16(index, n, u32_slab); - let (d, index, n) = extract_u16(index, n, u32_slab); - let (e, index, n) = extract_u16(index, n, u32_slab); - let (f, _, _) = extract_u16(index, n, u32_slab); - assert_eq!([0, 1, 2, 3, 4, 5], [a, b, c, d, e, f]); - } - - #[test] - fn extract_i16_sanity() { - let i16_slab = [0i16, -1i16, -2i16, -3i16, 4i16, 5i16, -12345i16, 0i16]; - let u32_slab: &[u32] = bytemuck::cast_slice(&i16_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_i16(index, n, u32_slab); - let (b, index, n) = extract_i16(index, n, u32_slab); - let (c, index, n) = extract_i16(index, n, u32_slab); - let (d, index, n) = extract_i16(index, n, u32_slab); - let (e, index, n) = extract_i16(index, n, u32_slab); - let (f, index, n) = extract_i16(index, n, u32_slab); - let (g, _, _) = extract_i16(index, n, u32_slab); - assert_eq!([0, -1, -2, -3, 4, 5, -12345], [a, b, c, d, e, f, g]); - } - - #[test] - fn extract_u32_sanity() { - let u32_slab = [0u32, 1u32, 2u32, 3u32, 4u32, 5u32]; - let u32_slab: &[u32] = bytemuck::cast_slice(&u32_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_u32(index, n, u32_slab); - let (b, index, n) = extract_u32(index, n, u32_slab); - let (c, index, n) = extract_u32(index, n, u32_slab); - let (d, index, n) = extract_u32(index, n, u32_slab); - let (e, index, n) = extract_u32(index, n, u32_slab); - let (f, _, _) = extract_u32(index, n, u32_slab); - assert_eq!([0, 1, 2, 3, 4, 5], [a, b, c, d, e, f]); - } - - #[test] - fn extract_i32_sanity() { - let i32_slab = [0i32, -1i32, -2i32, -3i32, 4i32, 5i32, -12345i32]; - let u32_slab: &[u32] = bytemuck::cast_slice(&i32_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_i32(index, n, u32_slab); - let (b, index, n) = extract_i32(index, n, u32_slab); - let (c, index, n) = extract_i32(index, n, u32_slab); - let (d, index, n) = extract_i32(index, n, u32_slab); - let (e, index, n) = extract_i32(index, n, u32_slab); - let (f, index, n) = extract_i32(index, n, u32_slab); - let (g, _, _) = extract_i32(index, n, u32_slab); - assert_eq!([0, -1, -2, -3, 4, 5, -12345], [a, b, c, d, e, f, g]); - } - - #[test] - fn extract_f32_sanity() { - let f32_slab = [ - 0.0f32, - -1.0f32, - -2.0f32, - -3.0f32, - 4.0f32, - 5.0f32, - -12345.0f32, - ]; - let u32_slab: &[u32] = bytemuck::cast_slice(&f32_slab); - let index = 0; - let n = 0; - let (a, index, n) = extract_f32(index, n, u32_slab); - let (b, index, n) = extract_f32(index, n, u32_slab); - let (c, index, n) = extract_f32(index, n, u32_slab); - let (d, index, n) = extract_f32(index, n, u32_slab); - let (e, index, n) = extract_f32(index, n, u32_slab); - let (f, index, n) = extract_f32(index, n, u32_slab); - let (g, _, _) = extract_f32(index, n, u32_slab); - assert_eq!( - [0f32, -1f32, -2f32, -3f32, 4f32, 5f32, -12345f32], - [a, b, c, d, e, f, g] - ); - } -} diff --git a/crates/renderling/src/bloom.rs b/crates/renderling/src/bloom.rs index b3bdddee..011ad9f4 100644 --- a/crates/renderling/src/bloom.rs +++ b/crates/renderling/src/bloom.rs @@ -1,190 +1,10 @@ //! Physically based bloom. //! -//! As described in [learnopengl.com's Physically Based Bloom article](https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom). -use crabslab::{Id, Slab}; -use glam::{Vec2, Vec4, Vec4Swizzles}; -use spirv_std::{image::Image2d, spirv, Sampler}; - +//! As described in [learnopengl.com's Physically Based Bloom +//! article](https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom). #[cfg(not(target_arch = "spirv"))] mod cpu; #[cfg(not(target_arch = "spirv"))] pub use cpu::*; -/// Bloom vertex shader. -/// -/// This is a pass-through vertex shader to facilitate a bloom effect. -#[spirv(vertex)] -pub fn bloom_vertex( - #[spirv(vertex_index)] vertex_index: u32, - #[spirv(instance_index)] in_id: u32, - out_uv: &mut Vec2, - #[spirv(flat)] out_id: &mut u32, - #[spirv(position)] out_clip_pos: &mut Vec4, -) { - let i = (vertex_index % 6) as usize; - *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; - *out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; - *out_id = in_id; -} - -/// Bloom downsampling shader. -/// -/// Performs successive downsampling from a source texture. -/// -/// As taken from Call Of Duty method - presented at ACM Siggraph 2014. -/// -/// This particular method was designed to eliminate -/// "pulsating artifacts and temporal stability issues". -#[spirv(fragment)] -pub fn bloom_downsample_fragment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - // Remember to add bilinear minification filter for this texture! - // Remember to use a floating-point texture format (for HDR)! - // Remember to use edge clamping for this texture! - #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - in_uv: Vec2, - #[spirv(flat)] in_pixel_size_id: Id, - // frag_color - downsample: &mut Vec4, -) { - use glam::Vec3; - - let Vec2 { x, y } = slab.read(in_pixel_size_id); - - // Take 13 samples around current texel: - // a - b - c - // - j - k - - // d - e - f - // - l - m - - // g - h - i - // === ('e' is the current texel) === - let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y)); - let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y)); - let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y)); - - let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y)); - let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)); - let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y)); - - let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y)); - let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y)); - let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y)); - - let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y)); - let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y)); - let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y)); - let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y)); - - // Apply weighted distribution: - // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1 - // a,b,d,e * 0.125 - // b,c,e,f * 0.125 - // d,e,g,h * 0.125 - // e,f,h,i * 0.125 - // j,k,l,m * 0.5 - // This shows 5 square areas that are being sampled. But some of them overlap, - // so to have an energy preserving downsample we need to make some adjustments. - // The weights are the distributed so that the sum of j,k,l,m (e.g.) - // contribute 0.5 to the final color output. The code below is written - // to effectively yield this sum. We get: - // 0.125*5 + 0.03125*4 + 0.0625*4 = 1 - let f1 = 0.125; - let f2 = 0.0625; - let f3 = 0.03125; - let center = e * f1; - let inner = (j + k + l + m) * f1; - let outer = (b + d + h + f) * f2; - let furthest = (a + c + g + i) * f3; - let min = Vec3::splat(f32::EPSILON).extend(1.0); - *downsample = (center + inner + outer + furthest).max(min); -} - -/// Bloom upsampling shader. -/// -/// This shader performs successive upsampling on a source texture. -/// -/// Taken from Call Of Duty method, presented at ACM Siggraph 2014. -#[spirv(fragment)] -pub fn bloom_upsample_fragment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - // Remember to add bilinear minification filter for this texture! - // Remember to use a floating-point texture format (for HDR)! - // Remember to use edge clamping for this texture! - #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - in_uv: Vec2, - #[spirv(flat)] filter_radius_id: Id, - // frag_color - upsample: &mut Vec4, -) { - // The filter kernel is applied with a radius, specified in texture - // coordinates, so that the radius will vary across mip resolutions. - let Vec2 { x, y } = slab.read(filter_radius_id); - - // Take 9 samples around current texel: - // a - b - c - // d - e - f - // g - h - i - // === ('e' is the current texel) === - let a = texture - .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y)) - .xyz(); - let b = texture - .sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y)) - .xyz(); - let c = texture - .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y)) - .xyz(); - - let d = texture - .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y)) - .xyz(); - let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz(); - let f = texture - .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y)) - .xyz(); - - let g = texture - .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y)) - .xyz(); - let h = texture - .sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y)) - .xyz(); - let i = texture - .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y)) - .xyz(); - - // Apply weighted distribution, by using a 3x3 tent filter: - // 1 | 1 2 1 | - // -- * | 2 4 2 | - // 16 | 1 2 1 | - let mut sample = e * 4.0; - sample += (b + d + f + h) * 2.0; - sample += a + c + g + i; - sample *= 1.0 / 16.0; - *upsample = sample.extend(0.5); -} - -#[spirv(fragment)] -#[allow(clippy::too_many_arguments)] -/// Bloom "mix" shader. -/// -/// This is the final step in applying bloom in which the computed bloom is -/// mixed with the source texture according to a strength factor. -pub fn bloom_mix_fragment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - #[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler, - #[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler, - in_uv: Vec2, - #[spirv(flat)] in_bloom_strength_id: Id, - frag_color: &mut Vec4, -) { - let bloom_strength = slab.read(in_bloom_strength_id); - let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz(); - let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz(); - let color = hdr.lerp(bloom, bloom_strength); - *frag_color = color.extend(1.0) -} +pub mod shader; diff --git a/crates/renderling/src/bloom/cpu.rs b/crates/renderling/src/bloom/cpu.rs index b8f1d355..c81152a1 100644 --- a/crates/renderling/src/bloom/cpu.rs +++ b/crates/renderling/src/bloom/cpu.rs @@ -470,6 +470,10 @@ impl Bloom { } } + pub(crate) fn slab_allocator(&self) -> &SlabAllocator { + &self.slab + } + pub fn set_mix_strength(&self, strength: f32) { self.mix_strength.set(strength); } @@ -701,7 +705,7 @@ impl Bloom { mod test { use glam::Vec3; - use crate::{camera::Camera, test::BlockOnFuture, Context}; + use crate::{context::Context, test::BlockOnFuture}; use super::*; @@ -749,16 +753,17 @@ mod test { let height = 128; let ctx = Context::headless(width, height).block(); let stage = ctx.new_stage().with_bloom(false); - // .with_frustum_culling(false) - // .with_occlusion_culling(false); - let projection = crate::camera::perspective(width as f32, height as f32); let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let skybox = stage .new_skybox_from_path("../../img/hdr/night.hdr") .unwrap(); - stage.set_skybox(skybox); + stage.use_skybox(&skybox); + let ibl = stage.new_ibl(&skybox); + stage.use_ibl(&ibl); let _doc = stage .load_gltf_document_from_path("../../gltf/EmissiveStrengthTest.glb") diff --git a/crates/renderling/src/bloom/shader.rs b/crates/renderling/src/bloom/shader.rs new file mode 100644 index 00000000..c8185c74 --- /dev/null +++ b/crates/renderling/src/bloom/shader.rs @@ -0,0 +1,182 @@ +use crabslab::{Id, Slab}; +use glam::{Vec2, Vec4, Vec4Swizzles}; +use spirv_std::{image::Image2d, spirv, Sampler}; + +/// Bloom vertex shader. +/// +/// This is a pass-through vertex shader to facilitate a bloom effect. +#[spirv(vertex)] +pub fn bloom_vertex( + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(instance_index)] in_id: u32, + out_uv: &mut Vec2, + #[spirv(flat)] out_id: &mut u32, + #[spirv(position)] out_clip_pos: &mut Vec4, +) { + let i = (vertex_index % 6) as usize; + *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; + *out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; + *out_id = in_id; +} + +/// Bloom downsampling shader. +/// +/// Performs successive downsampling from a source texture. +/// +/// As taken from Call Of Duty method - presented at ACM Siggraph 2014. +/// +/// This particular method was designed to eliminate +/// "pulsating artifacts and temporal stability issues". +#[spirv(fragment)] +pub fn bloom_downsample_fragment( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + // Remember to add bilinear minification filter for this texture! + // Remember to use a floating-point texture format (for HDR)! + // Remember to use edge clamping for this texture! + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + in_uv: Vec2, + #[spirv(flat)] in_pixel_size_id: Id, + // frag_color + downsample: &mut Vec4, +) { + use glam::Vec3; + + let Vec2 { x, y } = slab.read(in_pixel_size_id); + + // Take 13 samples around current texel: + // a - b - c + // - j - k - + // d - e - f + // - l - m - + // g - h - i + // === ('e' is the current texel) === + let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y)); + let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y)); + let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y)); + + let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y)); + let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)); + let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y)); + + let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y)); + let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y)); + let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y)); + + let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y)); + let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y)); + let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y)); + let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y)); + + // Apply weighted distribution: + // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1 + // a,b,d,e * 0.125 + // b,c,e,f * 0.125 + // d,e,g,h * 0.125 + // e,f,h,i * 0.125 + // j,k,l,m * 0.5 + // This shows 5 square areas that are being sampled. But some of them overlap, + // so to have an energy preserving downsample we need to make some adjustments. + // The weights are the distributed so that the sum of j,k,l,m (e.g.) + // contribute 0.5 to the final color output. The code below is written + // to effectively yield this sum. We get: + // 0.125*5 + 0.03125*4 + 0.0625*4 = 1 + let f1 = 0.125; + let f2 = 0.0625; + let f3 = 0.03125; + let center = e * f1; + let inner = (j + k + l + m) * f1; + let outer = (b + d + h + f) * f2; + let furthest = (a + c + g + i) * f3; + let min = Vec3::splat(f32::EPSILON).extend(1.0); + *downsample = (center + inner + outer + furthest).max(min); +} + +/// Bloom upsampling shader. +/// +/// This shader performs successive upsampling on a source texture. +/// +/// Taken from Call Of Duty method, presented at ACM Siggraph 2014. +#[spirv(fragment)] +pub fn bloom_upsample_fragment( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + // Remember to add bilinear minification filter for this texture! + // Remember to use a floating-point texture format (for HDR)! + // Remember to use edge clamping for this texture! + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + in_uv: Vec2, + #[spirv(flat)] filter_radius_id: Id, + // frag_color + upsample: &mut Vec4, +) { + // The filter kernel is applied with a radius, specified in texture + // coordinates, so that the radius will vary across mip resolutions. + let Vec2 { x, y } = slab.read(filter_radius_id); + + // Take 9 samples around current texel: + // a - b - c + // d - e - f + // g - h - i + // === ('e' is the current texel) === + let a = texture + .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y)) + .xyz(); + let b = texture + .sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y)) + .xyz(); + let c = texture + .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y)) + .xyz(); + + let d = texture + .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y)) + .xyz(); + let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz(); + let f = texture + .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y)) + .xyz(); + + let g = texture + .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y)) + .xyz(); + let h = texture + .sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y)) + .xyz(); + let i = texture + .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y)) + .xyz(); + + // Apply weighted distribution, by using a 3x3 tent filter: + // 1 | 1 2 1 | + // -- * | 2 4 2 | + // 16 | 1 2 1 | + let mut sample = e * 4.0; + sample += (b + d + f + h) * 2.0; + sample += a + c + g + i; + sample *= 1.0 / 16.0; + *upsample = sample.extend(0.5); +} + +#[spirv(fragment)] +#[allow(clippy::too_many_arguments)] +/// Bloom "mix" shader. +/// +/// This is the final step in applying bloom in which the computed bloom is +/// mixed with the source texture according to a strength factor. +pub fn bloom_mix_fragment( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + #[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler, + in_uv: Vec2, + #[spirv(flat)] in_bloom_strength_id: Id, + frag_color: &mut Vec4, +) { + let bloom_strength = slab.read(in_bloom_strength_id); + let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz(); + let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz(); + let color = hdr.lerp(bloom, bloom_strength); + *frag_color = color.extend(1.0) +} diff --git a/crates/renderling/src/build.rs b/crates/renderling/src/build.rs index 138e0627..0967910b 100644 --- a/crates/renderling/src/build.rs +++ b/crates/renderling/src/build.rs @@ -11,5 +11,6 @@ fn main() { cfg_aliases::cfg_aliases! { cpu: { not(target_arch = "spirv") }, gpu: { target_arch = "spirv" }, + gltf: { feature = "gltf" } } } diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index e76345fc..43bdf02d 100644 --- a/crates/renderling/src/bvol.rs +++ b/crates/renderling/src/bvol.rs @@ -16,7 +16,7 @@ use glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; #[cfg(gpu)] use spirv_std::num_traits::Float; -use crate::{camera::Camera, transform::Transform}; +use crate::{camera::shader::CameraDescriptor, transform::shader::TransformDescriptor}; /// Normalize a plane. pub fn normalize_plane(mut plane: Vec4) -> Vec4 { @@ -141,7 +141,7 @@ impl Aabb { self.min == self.max } - /// Returns the union of the two [`Aabbs`]. + /// Returns the union of the two [`Aabb`]s. pub fn union(a: Self, b: Self) -> Self { Aabb { min: a.min.min(a.max).min(b.min).min(b.max), @@ -151,7 +151,11 @@ impl Aabb { /// Determines whether this `Aabb` can be seen by `camera` after being /// transformed by `transform`. - pub fn is_outside_camera_view(&self, camera: &Camera, transform: Transform) -> bool { + pub fn is_outside_camera_view( + &self, + camera: &CameraDescriptor, + transform: TransformDescriptor, + ) -> bool { let transform = Mat4::from(transform); let min = transform.transform_point3(self.min); let max = transform.transform_point3(self.max); @@ -222,8 +226,8 @@ pub struct Frustum { } impl Frustum { - /// Compute a frustum in world space from the given [`Camera`]. - pub fn from_camera(camera: &Camera) -> Self { + /// Compute a frustum in world space from the given [`CameraDescriptor`]. + pub fn from_camera(camera: &CameraDescriptor) -> Self { let viewprojection = camera.view_projection(); let mvp = viewprojection.to_cols_array_2d(); @@ -462,8 +466,8 @@ impl BoundingSphere { /// being transformed by `transform`. pub fn is_inside_camera_view( &self, - camera: &Camera, - transform: Transform, + camera: &CameraDescriptor, + transform: TransformDescriptor, ) -> (bool, BoundingSphere) { let center = Mat4::from(transform).transform_point3(self.center); let scale = Vec3::splat(transform.scale.max_element()); @@ -490,7 +494,7 @@ impl BoundingSphere { /// Returns an [`Aabb`] with x and y coordinates in viewport pixels and z coordinate /// in NDC depth. - pub fn project_onto_viewport(&self, camera: &Camera, viewport: Vec2) -> Aabb { + pub fn project_onto_viewport(&self, camera: &CameraDescriptor, viewport: Vec2) -> Aabb { fn ndc_to_pixel(viewport: Vec2, ndc: Vec3) -> Vec2 { let screen = Vec3::new((ndc.x + 1.0) * 0.5, 1.0 - (ndc.y + 1.0) * 0.5, ndc.z); (screen * viewport.extend(1.0)).xy() @@ -595,14 +599,14 @@ impl BVol for Aabb { mod test { use glam::{Mat4, Quat}; - use crate::{pbr::Material, stage::Vertex, test::BlockOnFuture, Context}; + use crate::{context::Context, geometry::Vertex, test::BlockOnFuture}; use super::*; #[test] fn bvol_frustum_is_in_world_space_sanity() { let (p, v) = crate::camera::default_perspective(800.0, 600.0); - let camera = Camera::new(p, v); + let camera = CameraDescriptor::new(p, v); let aabb_outside = Aabb { min: Vec3::new(-10.0, -12.0, 20.0), max: Vec3::new(10.0, 12.0, 40.0), @@ -630,13 +634,13 @@ mod test { let target = Vec3::ZERO; let up = Vec3::Y; let view = Mat4::look_at_rh(eye, target, up); - Camera::new(projection, view) + CameraDescriptor::new(projection, view) }; let aabb = Aabb { min: Vec3::new(-3.2869213, -3.0652206, -3.8715153), max: Vec3::new(3.2869213, 3.0652206, 3.8715153), }; - let transform = Transform { + let transform = TransformDescriptor { translation: Vec3::new(7.5131035, -9.947085, -5.001645), rotation: Quat::from_xyzw(0.4700742, 0.34307128, 0.6853008, -0.43783003), scale: Vec3::new(1.0, 1.0, 1.0), @@ -655,34 +659,26 @@ mod test { .with_background_color(Vec4::ZERO) .with_msaa_sample_count(4) .with_lighting(true); - let _camera = stage.new_camera({ - Camera::new( - // BUG: using orthographic here renderes nothing - // Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 10.0, -10.0), - crate::camera::perspective(256.0, 256.0), - Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y), - ) - }); + let _camera = stage.new_camera().with_projection_and_view( + // TODO: BUG - using orthographic here renderes nothing + // Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 10.0, -10.0), + crate::camera::perspective(256.0, 256.0), + Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y), + ); let _lights = crate::test::make_two_directional_light_setup(&stage); - let white = stage.new_material(Material { - albedo_factor: Vec4::ONE, - ..Default::default() - }); - let red = stage.new_material(Material { - albedo_factor: Vec4::new(1.0, 0.0, 0.0, 1.0), - ..Default::default() - }); - - let _w = stage - .builder() - .with_material_id(white.id()) - .with_vertices( + let white = stage.new_material(); + let red = stage + .new_material() + .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0)); + + let _w = stage.new_primitive().with_material(&white).with_vertices( + stage.new_vertices( crate::math::unit_cube() .into_iter() .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), - ) - .build(); + ), + ); let mut corners = vec![]; for x in [-1.0, 1.0] { @@ -704,14 +700,12 @@ mod test { ); rs.push( - stage - .builder() - .with_material_id(red.id()) - .with_vertices( + stage.new_primitive().with_material(&red).with_vertices( + stage.new_vertices( bb.get_mesh() .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), - ) - .build(), + ), + ), ); } diff --git a/crates/renderling/src/camera.rs b/crates/renderling/src/camera.rs index 793ac04f..b1c1beb3 100644 --- a/crates/renderling/src/camera.rs +++ b/crates/renderling/src/camera.rs @@ -1,127 +1,12 @@ //! Camera projection, view and utilities. -use crabslab::SlabItem; -use glam::{Mat4, Vec2, Vec3, Vec4}; +use glam::{Mat4, Vec3}; -use crate::{bvol::Frustum, math::IsVector}; +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; -/// A camera used for transforming the stage during rendering. -/// -/// Use [`Camera::new`] to create a new camera. -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Default, Clone, Copy, PartialEq, SlabItem)] -pub struct Camera { - projection: Mat4, - view: Mat4, - position: Vec3, - frustum: Frustum, - /// Nearest center point on the frustum - z_near_point: Vec3, - /// Furthest center point on the frustum - z_far_point: Vec3, -} - -impl Camera { - pub fn new(projection: Mat4, view: Mat4) -> Self { - Camera::default().with_projection_and_view(projection, view) - } - - pub fn default_perspective(width: f32, height: f32) -> Self { - let (projection, view) = default_perspective(width, height); - Camera::new(projection, view) - } - - pub fn default_ortho2d(width: f32, height: f32) -> Self { - let (projection, view) = default_ortho2d(width, height); - Camera::new(projection, view) - } - - pub fn projection(&self) -> Mat4 { - self.projection - } - - pub fn set_projection_and_view(&mut self, projection: Mat4, view: Mat4) { - self.projection = projection; - self.view = view; - self.position = view.inverse().transform_point3(Vec3::ZERO); - let inverse = (projection * view).inverse(); - self.z_near_point = inverse.project_point3(Vec3::ZERO); - self.z_far_point = inverse.project_point3(Vec2::ZERO.extend(1.0)); - self.frustum = Frustum::from_camera(self); - } - - pub fn with_projection_and_view(mut self, projection: Mat4, view: Mat4) -> Self { - self.set_projection_and_view(projection, view); - self - } - - pub fn set_projection(&mut self, projection: Mat4) { - self.set_projection_and_view(projection, self.view); - } - - pub fn with_projection(mut self, projection: Mat4) -> Self { - self.set_projection(projection); - self - } - - pub fn view(&self) -> Mat4 { - self.view - } - - pub fn set_view(&mut self, view: Mat4) { - self.set_projection_and_view(self.projection, view); - } - - pub fn with_view(mut self, view: Mat4) -> Self { - self.set_view(view); - self - } - - pub fn position(&self) -> Vec3 { - self.position - } - - pub fn frustum(&self) -> Frustum { - self.frustum - } - - pub fn view_projection(&self) -> Mat4 { - self.projection * self.view - } - - pub fn near_plane(&self) -> Vec4 { - self.frustum.planes[0] - } - - pub fn far_plane(&self) -> Vec4 { - self.frustum.planes[5] - } - - /// Returns **roughly** the location of the znear plane. - pub fn z_near(&self) -> f32 { - self.z_near_point.distance(self.position) - } - - pub fn z_far(&self) -> f32 { - self.z_far_point.distance(self.position) - } - - pub fn depth(&self) -> f32 { - (self.z_far() - self.z_near()).abs() - } - - /// Returns the normalized forward vector which points in the direction the camera is looking. - pub fn forward(&self) -> Vec3 { - (self.z_far_point - self.z_near_point).alt_norm_or_zero() - } - - pub fn frustum_near_point(&self) -> Vec3 { - self.forward() * self.z_near() - } - - pub fn frustum_far_point(&self) -> Vec3 { - self.forward() * self.z_far() - } -} +pub mod shader; /// Returns the projection and view matrices for a camera with default /// perspective. @@ -130,7 +15,6 @@ impl Camera { /// /// ```rust /// use glam::*; -/// use renderling::prelude::*; /// /// let width = 800.0; /// let height = 600.0; @@ -143,7 +27,7 @@ impl Camera { /// let target = Vec3::ZERO; /// let up = Vec3::Y; /// let view = Mat4::look_at_rh(eye, target, up); -/// assert_eq!(default_perspective(width, height), (projection, view)); +/// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view)); /// ``` pub fn default_perspective(width: f32, height: f32) -> (Mat4, Mat4) { let projection = perspective(width, height); @@ -192,6 +76,8 @@ pub fn default_ortho2d(width: f32, height: f32) -> (Mat4, Mat4) { #[cfg(test)] mod tests { + use crate::camera::shader::CameraDescriptor; + use super::*; use glam::Vec3; @@ -207,7 +93,7 @@ mod tests { for (eye, expected_forward) in eyes.into_iter().zip(expected_forwards) { let projection = Mat4::perspective_rh(45.0_f32.to_radians(), 800.0 / 600.0, 0.1, 100.0); let view = Mat4::look_at_rh(eye, Vec3::ZERO, Vec3::Y); - let camera = Camera::new(projection, view); + let camera = CameraDescriptor::new(projection, view); let forward = camera.forward(); let distance = forward.distance(expected_forward); diff --git a/crates/renderling/src/camera/cpu.rs b/crates/renderling/src/camera/cpu.rs new file mode 100644 index 00000000..f4220161 --- /dev/null +++ b/crates/renderling/src/camera/cpu.rs @@ -0,0 +1,193 @@ +//! CPU side of [crate::camera]. + +use craballoc::{runtime::IsRuntime, slab::SlabAllocator, value::Hybrid}; +use crabslab::Id; + +use crate::camera::shader::CameraDescriptor; + +use super::*; + +/// A camera used for transforming the stage during rendering. +/// +/// * Use [`Stage::new_camera`](crate::stage::Stage::new_camera) to create a new camera. +/// * Use [`Stage::use_camera`](crate::stage::Stage::use_camera) to set a camera on the stage. +/// +/// ## Note +/// +/// Clones of this type all point to the same underlying data. +#[derive(Clone, Debug)] +pub struct Camera { + inner: Hybrid, +} + +impl AsRef for Camera { + fn as_ref(&self) -> &Camera { + self + } +} + +impl Camera { + /// Stage a new camera on the given slab. + pub fn new(slab: &SlabAllocator) -> Self { + Self { + inner: slab.new_value(CameraDescriptor::default()), + } + } + + /// Returns a pointer to the underlying descriptor on the GPU. + pub fn id(&self) -> Id { + self.inner.id() + } + + /// Returns a copy of the underlying descriptor. + pub fn descriptor(&self) -> CameraDescriptor { + self.inner.get() + } + + /// Set the camera to a default perspective projection and view based + /// on the width and height of the viewport. + /// + /// The default projection and view matrices are defined as: + /// + /// ```rust + /// use glam::*; + /// + /// let width = 800.0; + /// let height = 600.0; + /// let aspect = width / height; + /// let fovy = core::f32::consts::PI / 4.0; + /// let znear = 0.1; + /// let zfar = 100.0; + /// let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + /// let eye = Vec3::new(0.0, 12.0, 20.0); + /// let target = Vec3::ZERO; + /// let up = Vec3::Y; + /// let view = Mat4::look_at_rh(eye, target, up); + /// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view)); + /// ``` + pub fn set_default_perspective(&self, width: f32, height: f32) -> &Self { + self.inner + .modify(|d| *d = CameraDescriptor::default_perspective(width, height)); + self + } + + /// Set the camera to a default perspective projection and view based + /// on the width and height of the viewport. + /// + /// The default projection and view matrices are defined as: + /// + /// ```rust + /// use glam::*; + /// + /// let width = 800.0; + /// let height = 600.0; + /// let aspect = width / height; + /// let fovy = core::f32::consts::PI / 4.0; + /// let znear = 0.1; + /// let zfar = 100.0; + /// let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); + /// let eye = Vec3::new(0.0, 12.0, 20.0); + /// let target = Vec3::ZERO; + /// let up = Vec3::Y; + /// let view = Mat4::look_at_rh(eye, target, up); + /// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view)); + /// ``` + pub fn with_default_perspective(self, width: f32, height: f32) -> Self { + self.set_default_perspective(width, height); + self + } + + /// Set the camera to a default orthographic 2d projection and view based + /// on the width and height of the viewport. + pub fn set_default_ortho2d(&self, width: f32, height: f32) -> &Self { + self.inner + .modify(|d| *d = CameraDescriptor::default_ortho2d(width, height)); + self + } + + /// Set the camera to a default orthographic 2d projection and view based + /// on the width and height of the viewport. + pub fn with_default_ortho2d(self, width: f32, height: f32) -> Self { + self.set_default_ortho2d(width, height); + self + } + + /// Set the projection and view matrices of this camera. + pub fn set_projection_and_view( + &self, + projection: impl Into, + view: impl Into, + ) -> &Self { + self.inner + .modify(|d| d.set_projection_and_view(projection.into(), view.into())); + self + } + + /// Set the projection and view matrices and return this camera. + pub fn with_projection_and_view( + self, + projection: impl Into, + view: impl Into, + ) -> Self { + self.set_projection_and_view(projection, view); + self + } + + /// Returns the projection and view matrices. + pub fn projection_and_view(&self) -> (Mat4, Mat4) { + let d = self.inner.get(); + (d.projection(), d.view()) + } + + /// Set the projection matrix of this camera. + pub fn set_projection(&self, projection: impl Into) -> &Self { + self.inner.modify(|d| d.set_projection(projection.into())); + self + } + + /// Set the projection matrix and return this camera. + pub fn with_projection(self, projection: impl Into) -> Self { + self.set_projection(projection); + self + } + + /// Returns the projection matrix. + pub fn projection(&self) -> Mat4 { + self.inner.get().projection() + } + + /// Set the view matrix of this camera. + pub fn set_view(&self, view: impl Into) -> &Self { + self.inner.modify(|d| d.set_view(view.into())); + self + } + + /// Set the view matrix and return this camera. + pub fn with_view(self, view: impl Into) -> Self { + self.set_view(view); + self + } + + /// Returns the view matrix. + pub fn view(&self) -> Mat4 { + self.inner.get().view() + } +} + +#[cfg(test)] +mod test { + use craballoc::{runtime::CpuRuntime, slab::SlabAllocator}; + + use super::*; + + #[test] + fn camera_position_sanity() { + let slab = SlabAllocator::new(CpuRuntime, "camera test", ()); + let camera = Camera::new(&slab); + let projection = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.01, 10.0); + let view = Mat4::look_at_rh(Vec3::ONE, Vec3::ZERO, Vec3::Y); + camera.set_projection_and_view(projection, view); + let position = camera.descriptor().position(); + assert_eq!(Vec3::ONE, position); + } +} diff --git a/crates/renderling/src/camera/shader.rs b/crates/renderling/src/camera/shader.rs new file mode 100644 index 00000000..96f10874 --- /dev/null +++ b/crates/renderling/src/camera/shader.rs @@ -0,0 +1,127 @@ +//! [`CameraDescriptor`] and camera shader utilities. + +use crabslab::SlabItem; +use glam::{Mat4, Vec2, Vec3, Vec4}; + +use crate::{bvol::Frustum, math::IsVector}; + +/// GPU descriptor of a camera. +/// +/// Used for transforming the stage during rendering. +/// +/// Use [`CameraDescriptor::new`] to create a new camera. +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Default, Clone, Copy, PartialEq, SlabItem)] +pub struct CameraDescriptor { + projection: Mat4, + view: Mat4, + position: Vec3, + frustum: Frustum, + /// Nearest center point on the frustum + z_near_point: Vec3, + /// Furthest center point on the frustum + z_far_point: Vec3, +} + +impl CameraDescriptor { + pub fn new(projection: Mat4, view: Mat4) -> Self { + CameraDescriptor::default().with_projection_and_view(projection, view) + } + + pub fn default_perspective(width: f32, height: f32) -> Self { + let (projection, view) = super::default_perspective(width, height); + CameraDescriptor::new(projection, view) + } + + pub fn default_ortho2d(width: f32, height: f32) -> Self { + let (projection, view) = super::default_ortho2d(width, height); + CameraDescriptor::new(projection, view) + } + + pub fn projection(&self) -> Mat4 { + self.projection + } + + pub fn set_projection_and_view(&mut self, projection: Mat4, view: Mat4) { + self.projection = projection; + self.view = view; + self.position = view.inverse().transform_point3(Vec3::ZERO); + let inverse = (projection * view).inverse(); + self.z_near_point = inverse.project_point3(Vec3::ZERO); + self.z_far_point = inverse.project_point3(Vec2::ZERO.extend(1.0)); + self.frustum = Frustum::from_camera(self); + } + + pub fn with_projection_and_view(mut self, projection: Mat4, view: Mat4) -> Self { + self.set_projection_and_view(projection, view); + self + } + + pub fn set_projection(&mut self, projection: Mat4) { + self.set_projection_and_view(projection, self.view); + } + + pub fn with_projection(mut self, projection: Mat4) -> Self { + self.set_projection(projection); + self + } + + pub fn view(&self) -> Mat4 { + self.view + } + + pub fn set_view(&mut self, view: Mat4) { + self.set_projection_and_view(self.projection, view); + } + + pub fn with_view(mut self, view: Mat4) -> Self { + self.set_view(view); + self + } + + pub fn position(&self) -> Vec3 { + self.position + } + + pub fn frustum(&self) -> Frustum { + self.frustum + } + + pub fn view_projection(&self) -> Mat4 { + self.projection * self.view + } + + pub fn near_plane(&self) -> Vec4 { + self.frustum.planes[0] + } + + pub fn far_plane(&self) -> Vec4 { + self.frustum.planes[5] + } + + /// Returns **roughly** the location of the znear plane. + pub fn z_near(&self) -> f32 { + self.z_near_point.distance(self.position) + } + + pub fn z_far(&self) -> f32 { + self.z_far_point.distance(self.position) + } + + pub fn depth(&self) -> f32 { + (self.z_far() - self.z_near()).abs() + } + + /// Returns the normalized forward vector which points in the direction the camera is looking. + pub fn forward(&self) -> Vec3 { + (self.z_far_point - self.z_near_point).alt_norm_or_zero() + } + + pub fn frustum_near_point(&self) -> Vec3 { + self.forward() * self.z_near() + } + + pub fn frustum_far_point(&self) -> Vec3 { + self.forward() * self.z_far() + } +} diff --git a/crates/renderling/src/context.rs b/crates/renderling/src/context.rs index 2b4adfa2..87183a95 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -1,11 +1,14 @@ -//! Rendering context initialization and frame management. +//! Rendering context initialization +//! +//! This module contains [`Context`] initialization and frame management. +//! This module provides the setup and management of rendering targets, +//! frames, and surface configurations. use core::fmt::Debug; use std::{ ops::Deref, sync::{Arc, RwLock}, }; -use craballoc::runtime::WgpuRuntime; use glam::{UVec2, UVec3}; use snafu::prelude::*; @@ -15,6 +18,9 @@ use crate::{ ui::Ui, }; +pub use craballoc::runtime::WgpuRuntime; + +/// Represents the internal structure of a render target, which can either be a surface or a texture. pub(crate) enum RenderTargetInner { Surface { surface: wgpu::Surface<'static>, @@ -26,13 +32,12 @@ pub(crate) enum RenderTargetInner { } #[repr(transparent)] -/// Either a surface or a texture. -/// -/// Will be a surface if the context was created with a window or canvas. -/// -/// Will be a texture if the context is headless. +/// Represents a render target that can either be a surface or a texture. +/// It will be a surface if the context was created with a window or canvas, +/// and a texture if the context is headless. pub struct RenderTarget(pub(crate) RenderTargetInner); +/// Converts a `wgpu::Texture` into a `RenderTarget`. impl From for RenderTarget { fn from(value: wgpu::Texture) -> Self { RenderTarget(RenderTargetInner::Texture { @@ -42,6 +47,7 @@ impl From for RenderTarget { } impl RenderTarget { + /// Resizes the render target to the specified width and height using the provided device. pub fn resize(&mut self, width: u32, height: u32, device: &wgpu::Device) { match &mut self.0 { RenderTargetInner::Surface { @@ -74,6 +80,7 @@ impl RenderTarget { } } + /// Returns the format of the render target. pub fn format(&self) -> wgpu::TextureFormat { match &self.0 { RenderTargetInner::Surface { surface_config, .. } => surface_config.format, @@ -81,6 +88,7 @@ impl RenderTarget { } } + /// Checks if the render target is headless (i.e., a texture). pub fn is_headless(&self) -> bool { match &self.0 { RenderTargetInner::Surface { .. } => false, @@ -88,7 +96,7 @@ impl RenderTarget { } } - /// Return the underlying target as a texture, if possible. + /// Returns the underlying target as a texture, if possible. pub fn as_texture(&self) -> Option<&wgpu::Texture> { match &self.0 { RenderTargetInner::Surface { .. } => None, @@ -96,6 +104,7 @@ impl RenderTarget { } } + /// Returns the size of the render target as a `UVec2`. pub fn get_size(&self) -> UVec2 { match &self.0 { RenderTargetInner::Surface { @@ -112,6 +121,7 @@ impl RenderTarget { #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] +/// Represents errors that can occur within the rendering context. pub enum ContextError { #[snafu(display("missing surface texture: {}", source))] Surface { source: wgpu::SurfaceError }, @@ -143,6 +153,7 @@ impl Deref for FrameTextureView { } } +/// Represents the surface of a frame, which can either be a surface texture or a texture. pub(crate) enum FrameSurface { Surface(wgpu::SurfaceTexture), Texture(Arc), @@ -165,6 +176,7 @@ impl Frame { } } + /// Returns a view of the current frame's texture. pub fn view(&self) -> wgpu::TextureView { let texture = self.texture(); let format = texture.format().add_srgb_suffix(); @@ -175,6 +187,7 @@ impl Frame { }) } + /// Copies the current frame to a buffer for further processing. pub fn copy_to_buffer(&self, width: u32, height: u32) -> CopiedTextureBuffer { let dimensions = BufferDimensions::new(4, 1, width as usize, height as usize); // The output buffer lets us retrieve the self as an array @@ -223,7 +236,7 @@ impl Frame { UVec2::new(s.width, s.height) } - /// Read the current frame buffer into an image. + /// Reads the current frame buffer into an image. /// /// This should be called after rendering, before presentation. /// Good for getting headless screen grabs. @@ -244,13 +257,11 @@ impl Frame { Ok(img) } - /// Read the frame into an image. + /// Reads the frame into an image in a sRGB color space. /// /// This should be called after rendering, before presentation. /// Good for getting headless screen grabs. /// - /// The resulting image will be in a sRGB color space. - /// /// ## Note /// This operation can take a long time, depending on how big the screen is. pub async fn read_srgb_image(&self) -> Result { @@ -259,13 +270,11 @@ impl Frame { log::trace!("read image has the format: {:?}", buffer.format); buffer.into_srgba(&self.runtime.device).await } - /// Read the frame into an image. + /// Reads the frame into an image in a linear color space. /// /// This should be called after rendering, before presentation. /// Good for getting headless screen grabs. /// - /// The resulting image will be in a linear color space. - /// /// ## Note /// This operation can take a long time, depending on how big the screen is. pub async fn read_linear_image(&self) -> Result { @@ -274,9 +283,8 @@ impl Frame { buffer.into_linear_rgba(&self.runtime.device).await } - /// If self is `TargetFrame::Surface` this presents the surface frame. - /// - /// If self is a `TargetFrame::Texture` this is a noop. + /// Presents the surface frame if the frame is a `TargetFrame::Surface`. + /// If the frame is a `TargetFrame::Texture`, this is a no-op. pub fn present(self) { match self.surface { FrameSurface::Surface(s) => s.present(), @@ -299,7 +307,7 @@ pub(crate) struct GlobalStageConfig { /// texture. /// /// ``` -/// use renderling::Context; +/// use renderling::context::Context; /// /// let ctx = futures_lite::future::block_on(Context::headless(100, 100)); /// ``` @@ -317,6 +325,7 @@ impl AsRef for Context { } impl Context { + /// Creates a new `Context` with the specified target, adapter, device, and queue. pub fn new( target: RenderTarget, adapter: impl Into>, @@ -355,6 +364,7 @@ impl Context { } } + /// Attempts to create a new headless `Context` with the specified width, height, and backends. pub async fn try_new_headless( width: u32, height: u32, @@ -367,6 +377,7 @@ impl Context { Ok(Self::new(target, adapter, device, queue)) } + /// Attempts to create a new `Context` with a surface, using the specified width, height, backends, and window. pub async fn try_new_with_surface( width: u32, height: u32, @@ -395,12 +406,12 @@ impl Context { .unwrap() } - /// Create a new headless renderer. + /// Creates a new headless renderer. /// - /// Immediately proxies to [`Context::try_new_headless`] and unwraps. + /// Immediately proxies to `Context::try_new_headless` and unwraps. /// /// ## Panics - /// This function will panic if an adapter cannot be found. For example this + /// This function will panic if an adapter cannot be found. For example, this /// would happen on machines without a GPU. pub async fn headless(width: u32, height: u32) -> Self { let result = Self::try_new_headless(width, height, None).await; @@ -419,12 +430,13 @@ impl Context { self.render_target.get_size() } + /// Sets the size of the render target. pub fn set_size(&mut self, size: UVec2) { self.render_target .resize(size.x, size.y, &self.runtime.device); } - /// Convenience method for creating textures from an image buffer. + /// Creates a texture from an image buffer. pub fn create_texture

( &self, label: Option<&str>, @@ -444,6 +456,7 @@ impl Context { ) } + /// Creates a `Texture` from a `wgpu::Texture` and an optional sampler. pub fn texture_from_wgpu_tex( &self, texture: impl Into>, @@ -452,38 +465,44 @@ impl Context { Texture::from_wgpu_tex(self.get_device(), texture, sampler, None) } + /// Returns a reference to the `WgpuRuntime`. pub fn runtime(&self) -> &WgpuRuntime { &self.runtime } + /// Returns a reference to the `wgpu::Device`. pub fn get_device(&self) -> &wgpu::Device { &self.runtime.device } + /// Returns a reference to the `wgpu::Queue`. pub fn get_queue(&self) -> &wgpu::Queue { &self.runtime.queue } + /// Returns a reference to the `wgpu::Adapter`. pub fn get_adapter(&self) -> &wgpu::Adapter { &self.adapter } - /// Returns a the adapter in an owned wrapper. + /// Returns the adapter in an owned wrapper. pub fn get_adapter_owned(&self) -> Arc { self.adapter.clone() } + /// Returns a reference to the `RenderTarget`. pub fn get_render_target(&self) -> &RenderTarget { &self.render_target } - /// Get the next frame from the render target. + /// Gets the next frame from the render target. /// /// A surface context (window or canvas) will return the next swapchain /// texture. /// /// A headless context will return the underlying headless texture. /// + /// ## Errors /// Errs if the render target is a surface and there was an error getting /// the next swapchain texture. This can happen if the frame has already /// been acquired. @@ -502,7 +521,7 @@ impl Context { }) } - /// Set the default texture size for the material atlas. + /// Sets the default texture size for the material atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. @@ -519,7 +538,7 @@ impl Context { self } - /// Set the default texture size for the material atlas. + /// Sets the default texture size for the material atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. @@ -532,7 +551,7 @@ impl Context { self } - /// Set the default texture size for the shadow mapping atlas. + /// Sets the default texture size for the shadow mapping atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. @@ -549,7 +568,7 @@ impl Context { self } - /// Set the default texture size for the shadow mapping atlas. + /// Sets the default texture size for the shadow mapping atlas. /// /// * Width is `size.x` and must be a power of two. /// * Height is `size.y`, must match `size.x` and must be a power of two. @@ -562,7 +581,7 @@ impl Context { self } - /// Set the use of direct drawing. + /// Sets the use of direct drawing. /// /// Default is **false**. /// @@ -572,7 +591,7 @@ impl Context { self.stage_config.write().unwrap().use_compute_culling = !use_direct_drawing; } - /// Set the use of direct drawing. + /// Sets the use of direct drawing. /// /// Default is **false**. /// @@ -583,16 +602,17 @@ impl Context { self } + /// Returns whether direct drawing is used. pub fn get_use_direct_draw(&self) -> bool { !self.stage_config.read().unwrap().use_compute_culling } - /// Create and return a new [`Stage`] renderer. + /// Creates and returns a new [`Stage`] renderer. pub fn new_stage(&self) -> Stage { Stage::new(self) } - /// Create and return a new [`Ui`] renderer. + /// Creates and returns a new [`Ui`] renderer. pub fn new_ui(&self) -> Ui { Ui::new(self) } diff --git a/crates/renderling/src/convolution.rs b/crates/renderling/src/convolution.rs index 310114fa..991569a1 100644 --- a/crates/renderling/src/convolution.rs +++ b/crates/renderling/src/convolution.rs @@ -1,315 +1,319 @@ //! Convolution shaders. //! //! These shaders convolve various functions to produce cached maps. -use crabslab::{Id, Slab, SlabItem}; -use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; -use spirv_std::{ - image::{Cubemap, Image2d}, - num_traits::Zero, - spirv, Sampler, -}; - -#[allow(unused_imports)] -use spirv_std::num_traits::Float; - -use crate::{camera::Camera, math::IsVector}; - -// Allow manual bit rotation because this code is `no_std`. -#[allow(clippy::manual_rotate)] -fn radical_inverse_vdc(mut bits: u32) -> f32 { - bits = (bits << 16u32) | (bits >> 16u32); - bits = ((bits & 0x55555555u32) << 1u32) | ((bits & 0xAAAAAAAAu32) >> 1u32); - bits = ((bits & 0x33333333u32) << 2u32) | ((bits & 0xCCCCCCCCu32) >> 2u32); - bits = ((bits & 0x0F0F0F0Fu32) << 4u32) | ((bits & 0xF0F0F0F0u32) >> 4u32); - bits = ((bits & 0x00FF00FFu32) << 8u32) | ((bits & 0xFF00FF00u32) >> 8u32); - (bits as f32) * 2.328_306_4e-10 // / 0x100000000 -} -fn hammersley(i: u32, n: u32) -> Vec2 { - Vec2::new(i as f32 / n as f32, radical_inverse_vdc(i)) -} +pub mod shader { + //! Shader side of convolution. + use crabslab::{Id, Slab, SlabItem}; + use glam::{Vec2, Vec3, Vec4, Vec4Swizzles}; + use spirv_std::{ + image::{Cubemap, Image2d}, + num_traits::Zero, + spirv, Sampler, + }; -fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 { - let a = roughness * roughness; + #[allow(unused_imports)] + use spirv_std::num_traits::Float; - let phi = 2.0 * core::f32::consts::PI * xi.x; - let cos_theta = f32::sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)); - let sin_theta = f32::sqrt(1.0 - cos_theta * cos_theta); + use crate::{camera::shader::CameraDescriptor, math::IsVector}; - // Convert spherical to cartesian coordinates - let h = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta); + // Allow manual bit rotation because this code is `no_std`. + #[allow(clippy::manual_rotate)] + fn radical_inverse_vdc(mut bits: u32) -> f32 { + bits = (bits << 16u32) | (bits >> 16u32); + bits = ((bits & 0x55555555u32) << 1u32) | ((bits & 0xAAAAAAAAu32) >> 1u32); + bits = ((bits & 0x33333333u32) << 2u32) | ((bits & 0xCCCCCCCCu32) >> 2u32); + bits = ((bits & 0x0F0F0F0Fu32) << 4u32) | ((bits & 0xF0F0F0F0u32) >> 4u32); + bits = ((bits & 0x00FF00FFu32) << 8u32) | ((bits & 0xFF00FF00u32) >> 8u32); + (bits as f32) * 2.328_306_4e-10 // / 0x100000000 + } - // Convert tangent-space vector to world-space vector - let up = if n.z.abs() < 0.999 { - Vec3::new(0.0, 0.0, 1.0) - } else { - Vec3::new(1.0, 0.0, 0.0) - }; - let tangent = up.cross(n).alt_norm_or_zero(); - let bitangent = n.cross(tangent); + fn hammersley(i: u32, n: u32) -> Vec2 { + Vec2::new(i as f32 / n as f32, radical_inverse_vdc(i)) + } - let result = tangent * h.x + bitangent * h.y + n * h.z; - result.alt_norm_or_zero() -} + fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 { + let a = roughness * roughness; + + let phi = 2.0 * core::f32::consts::PI * xi.x; + let cos_theta = f32::sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)); + let sin_theta = f32::sqrt(1.0 - cos_theta * cos_theta); -fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { - let r = roughness; - let k = (r * r) / 2.0; + // Convert spherical to cartesian coordinates + let h = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta); - let nom = n_dot_v; - let denom = n_dot_v * (1.0 - k) + k; + // Convert tangent-space vector to world-space vector + let up = if n.z.abs() < 0.999 { + Vec3::new(0.0, 0.0, 1.0) + } else { + Vec3::new(1.0, 0.0, 0.0) + }; + let tangent = up.cross(n).alt_norm_or_zero(); + let bitangent = n.cross(tangent); - if denom.is_zero() { - 0.0 - } else { - nom / denom + let result = tangent * h.x + bitangent * h.y + n * h.z; + result.alt_norm_or_zero() } -} -fn geometry_smith(normal: Vec3, view_dir: Vec3, light_dir: Vec3, roughness: f32) -> f32 { - let n_dot_v = normal.dot(view_dir).max(0.0); - let n_dot_l = normal.dot(light_dir).max(0.0); - let ggx1 = geometry_schlick_ggx(n_dot_v, roughness); - let ggx2 = geometry_schlick_ggx(n_dot_l, roughness); + fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { + let r = roughness; + let k = (r * r) / 2.0; - ggx1 * ggx2 -} + let nom = n_dot_v; + let denom = n_dot_v * (1.0 - k) + k; -const SAMPLE_COUNT: u32 = 1024; + if denom.is_zero() { + 0.0 + } else { + nom / denom + } + } -pub fn integrate_brdf(mut n_dot_v: f32, roughness: f32) -> Vec2 { - n_dot_v = n_dot_v.max(f32::EPSILON); - let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v); + fn geometry_smith(normal: Vec3, view_dir: Vec3, light_dir: Vec3, roughness: f32) -> f32 { + let n_dot_v = normal.dot(view_dir).max(0.0); + let n_dot_l = normal.dot(light_dir).max(0.0); + let ggx1 = geometry_schlick_ggx(n_dot_v, roughness); + let ggx2 = geometry_schlick_ggx(n_dot_l, roughness); - let mut a = 0.0f32; - let mut b = 0.0f32; + ggx1 * ggx2 + } + + const SAMPLE_COUNT: u32 = 1024; + + pub fn integrate_brdf(mut n_dot_v: f32, roughness: f32) -> Vec2 { + n_dot_v = n_dot_v.max(f32::EPSILON); + let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v); - let n = Vec3::Z; + let mut a = 0.0f32; + let mut b = 0.0f32; - for i in 1..SAMPLE_COUNT { - let xi = hammersley(i, SAMPLE_COUNT); - let h = importance_sample_ggx(xi, n, roughness); - let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); + let n = Vec3::Z; - let n_dot_l = l.z.max(0.0); - let n_dot_h = h.z.max(0.0); - let v_dot_h = v.dot(h).max(0.0); + for i in 1..SAMPLE_COUNT { + let xi = hammersley(i, SAMPLE_COUNT); + let h = importance_sample_ggx(xi, n, roughness); + let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); - if n_dot_l > 0.0 { - let g = geometry_smith(n, v, l, roughness); - let denom = n_dot_h * n_dot_v; - let g_vis = (g * v_dot_h) / denom; - let f_c = (1.0 - v_dot_h).powf(5.0); + let n_dot_l = l.z.max(0.0); + let n_dot_h = h.z.max(0.0); + let v_dot_h = v.dot(h).max(0.0); - a += (1.0 - f_c) * g_vis; - b += f_c * g_vis; + if n_dot_l > 0.0 { + let g = geometry_smith(n, v, l, roughness); + let denom = n_dot_h * n_dot_v; + let g_vis = (g * v_dot_h) / denom; + let f_c = (1.0 - v_dot_h).powf(5.0); + + a += (1.0 - f_c) * g_vis; + b += f_c * g_vis; + } } - } - a /= SAMPLE_COUNT as f32; - b /= SAMPLE_COUNT as f32; + a /= SAMPLE_COUNT as f32; + b /= SAMPLE_COUNT as f32; - Vec2::new(a, b) -} + Vec2::new(a, b) + } -/// This function doesn't work on rust-gpu, presumably because of the loop. -pub fn integrate_brdf_doesnt_work(mut n_dot_v: f32, roughness: f32) -> Vec2 { - n_dot_v = n_dot_v.max(f32::EPSILON); - let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v); + /// This function doesn't work on rust-gpu, presumably because of the loop. + pub fn integrate_brdf_doesnt_work(mut n_dot_v: f32, roughness: f32) -> Vec2 { + n_dot_v = n_dot_v.max(f32::EPSILON); + let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v); - let mut a = 0.0f32; - let mut b = 0.0f32; + let mut a = 0.0f32; + let mut b = 0.0f32; - let n = Vec3::Z; + let n = Vec3::Z; - let mut i = 0u32; - while i < SAMPLE_COUNT { - i += 1; + let mut i = 0u32; + while i < SAMPLE_COUNT { + i += 1; - let xi = hammersley(i, SAMPLE_COUNT); - let h = importance_sample_ggx(xi, n, roughness); - let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); + let xi = hammersley(i, SAMPLE_COUNT); + let h = importance_sample_ggx(xi, n, roughness); + let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); - let n_dot_l = l.z.max(0.0); - let n_dot_h = h.z.max(0.0); - let v_dot_h = v.dot(h).max(0.0); + let n_dot_l = l.z.max(0.0); + let n_dot_h = h.z.max(0.0); + let v_dot_h = v.dot(h).max(0.0); - if n_dot_l > 0.0 { - let g = geometry_smith(n, v, l, roughness); - let denom = n_dot_h * n_dot_v; - let g_vis = (g * v_dot_h) / denom; - let f_c = (1.0 - v_dot_h).powf(5.0); + if n_dot_l > 0.0 { + let g = geometry_smith(n, v, l, roughness); + let denom = n_dot_h * n_dot_v; + let g_vis = (g * v_dot_h) / denom; + let f_c = (1.0 - v_dot_h).powf(5.0); - a += (1.0 - f_c) * g_vis; - b += f_c * g_vis; + a += (1.0 - f_c) * g_vis; + b += f_c * g_vis; + } } - } - a /= SAMPLE_COUNT as f32; - b /= SAMPLE_COUNT as f32; + a /= SAMPLE_COUNT as f32; + b /= SAMPLE_COUNT as f32; - Vec2::new(a, b) -} + Vec2::new(a, b) + } -/// Used by [`prefilter_environment_cubemap_vertex`] to read the camera and -/// roughness values from the slab. -#[derive(Clone, Copy, Default, SlabItem)] -pub struct VertexPrefilterEnvironmentCubemapIds { - pub camera: Id, - // TODO: does this have to be an Id? Pretty sure it can be inline - pub roughness: Id, -} + /// Used by [`prefilter_environment_cubemap_vertex`] to read the camera and + /// roughness values from the slab. + #[derive(Clone, Copy, Default, SlabItem)] + pub struct VertexPrefilterEnvironmentCubemapIds { + pub camera: Id, + // TODO: does this have to be an Id? Pretty sure it can be inline + pub roughness: Id, + } -/// Vertex shader for rendering a "prefilter environment" cubemap. -#[spirv(vertex)] -pub fn prefilter_environment_cubemap_vertex( - #[spirv(instance_index)] prefilter_id: Id, - #[spirv(vertex_index)] vertex_id: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - out_pos: &mut Vec3, - out_roughness: &mut f32, - #[spirv(position)] gl_pos: &mut Vec4, -) { - let in_pos = crate::math::CUBE[vertex_id as usize]; - let VertexPrefilterEnvironmentCubemapIds { camera, roughness } = slab.read(prefilter_id); - let camera = slab.read(camera); - *out_roughness = slab.read(roughness); - *out_pos = in_pos; - *gl_pos = camera.view_projection() * in_pos.extend(1.0); -} + /// Vertex shader for rendering a "prefilter environment" cubemap. + #[spirv(vertex)] + pub fn prefilter_environment_cubemap_vertex( + #[spirv(instance_index)] prefilter_id: Id, + #[spirv(vertex_index)] vertex_id: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + out_pos: &mut Vec3, + out_roughness: &mut f32, + #[spirv(position)] gl_pos: &mut Vec4, + ) { + let in_pos = crate::math::CUBE[vertex_id as usize]; + let VertexPrefilterEnvironmentCubemapIds { camera, roughness } = slab.read(prefilter_id); + let camera = slab.read(camera); + *out_roughness = slab.read(roughness); + *out_pos = in_pos; + *gl_pos = camera.view_projection() * in_pos.extend(1.0); + } -/// Fragment shader for rendering a "prefilter environment" cubemap. -/// -/// Lambertian prefilter. -#[spirv(fragment)] -pub fn prefilter_environment_cubemap_fragment( - #[spirv(descriptor_set = 0, binding = 1)] environment_cubemap: &Cubemap, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - in_pos: Vec3, - in_roughness: f32, - frag_color: &mut Vec4, -) { - let mut n = in_pos.alt_norm_or_zero(); - // `wgpu` and vulkan's y coords are flipped from opengl - n.y *= -1.0; - let r = n; - let v = r; - - let mut total_weight = 0.0f32; - let mut prefiltered_color = Vec3::ZERO; - - for i in 0..SAMPLE_COUNT { - let xi = hammersley(i, SAMPLE_COUNT); - let h = importance_sample_ggx(xi, n, in_roughness); - let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); - - let n_dot_l = n.dot(l).max(0.0); - if n_dot_l > 0.0 { - let mip_level = if in_roughness == 0.0 { - 0.0 - } else { - calc_lod(n_dot_l) - }; - prefiltered_color += environment_cubemap - .sample_by_lod(*sampler, l, mip_level) - .xyz() - * n_dot_l; - total_weight += n_dot_l; + /// Fragment shader for rendering a "prefilter environment" cubemap. + /// + /// Lambertian prefilter. + #[spirv(fragment)] + pub fn prefilter_environment_cubemap_fragment( + #[spirv(descriptor_set = 0, binding = 1)] environment_cubemap: &Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + in_pos: Vec3, + in_roughness: f32, + frag_color: &mut Vec4, + ) { + let mut n = in_pos.alt_norm_or_zero(); + // `wgpu` and vulkan's y coords are flipped from opengl + n.y *= -1.0; + let r = n; + let v = r; + + let mut total_weight = 0.0f32; + let mut prefiltered_color = Vec3::ZERO; + + for i in 0..SAMPLE_COUNT { + let xi = hammersley(i, SAMPLE_COUNT); + let h = importance_sample_ggx(xi, n, in_roughness); + let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero(); + + let n_dot_l = n.dot(l).max(0.0); + if n_dot_l > 0.0 { + let mip_level = if in_roughness == 0.0 { + 0.0 + } else { + calc_lod(n_dot_l) + }; + prefiltered_color += environment_cubemap + .sample_by_lod(*sampler, l, mip_level) + .xyz() + * n_dot_l; + total_weight += n_dot_l; + } } - } - prefiltered_color /= total_weight; - *frag_color = prefiltered_color.extend(1.0); -} + prefiltered_color /= total_weight; + *frag_color = prefiltered_color.extend(1.0); + } -pub fn calc_lod_old(n: Vec3, v: Vec3, h: Vec3, roughness: f32) -> f32 { - // sample from the environment's mip level based on roughness/pdf - let d = crate::pbr::normal_distribution_ggx(n, h, roughness); - let n_dot_h = n.dot(h).max(0.0); - let h_dot_v = h.dot(v).max(0.0); - let pdf = (d * n_dot_h / (4.0 * h_dot_v)).max(f32::EPSILON); + pub fn calc_lod_old(n: Vec3, v: Vec3, h: Vec3, roughness: f32) -> f32 { + // sample from the environment's mip level based on roughness/pdf + let d = crate::pbr::shader::normal_distribution_ggx(n, h, roughness); + let n_dot_h = n.dot(h).max(0.0); + let h_dot_v = h.dot(v).max(0.0); + let pdf = (d * n_dot_h / (4.0 * h_dot_v)).max(f32::EPSILON); - let resolution = 512.0; // resolution of source cubemap (per face) - let sa_texel = 4.0 * core::f32::consts::PI / (6.0 * resolution * resolution); - let sa_sample = 1.0 / (SAMPLE_COUNT as f32 * pdf + f32::EPSILON); + let resolution = 512.0; // resolution of source cubemap (per face) + let sa_texel = 4.0 * core::f32::consts::PI / (6.0 * resolution * resolution); + let sa_sample = 1.0 / (SAMPLE_COUNT as f32 * pdf + f32::EPSILON); - 0.5 * (sa_sample / sa_texel).log2() -} + 0.5 * (sa_sample / sa_texel).log2() + } -pub fn calc_lod(n_dot_l: f32) -> f32 { - let cube_width = 512.0; - let pdf = (n_dot_l * core::f32::consts::FRAC_1_PI).max(0.0); - 0.5 * (6.0 * cube_width * cube_width / (SAMPLE_COUNT as f32 * pdf).max(f32::EPSILON)).log2() -} + pub fn calc_lod(n_dot_l: f32) -> f32 { + let cube_width = 512.0; + let pdf = (n_dot_l * core::f32::consts::FRAC_1_PI).max(0.0); + 0.5 * (6.0 * cube_width * cube_width / (SAMPLE_COUNT as f32 * pdf).max(f32::EPSILON)).log2() + } -#[spirv(vertex)] -/// Vertex shader for generating texture mips. -pub fn generate_mipmap_vertex( - #[spirv(vertex_index)] vertex_id: u32, - out_uv: &mut Vec2, - #[spirv(position)] gl_pos: &mut Vec4, -) { - let i = vertex_id as usize; - *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; - *gl_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; -} + #[spirv(vertex)] + /// Vertex shader for generating texture mips. + pub fn generate_mipmap_vertex( + #[spirv(vertex_index)] vertex_id: u32, + out_uv: &mut Vec2, + #[spirv(position)] gl_pos: &mut Vec4, + ) { + let i = vertex_id as usize; + *out_uv = crate::math::UV_COORD_QUAD_CCW[i]; + *gl_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i]; + } -#[spirv(fragment)] -/// Fragment shader for generating texture mips. -pub fn generate_mipmap_fragment( - #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler, - in_uv: Vec2, - frag_color: &mut Vec4, -) { - *frag_color = texture.sample(*sampler, in_uv); -} + #[spirv(fragment)] + /// Fragment shader for generating texture mips. + pub fn generate_mipmap_fragment( + #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler, + in_uv: Vec2, + frag_color: &mut Vec4, + ) { + *frag_color = texture.sample(*sampler, in_uv); + } -#[repr(C)] -#[derive(Clone, Copy)] -struct Vert { - pos: [f32; 3], - uv: [f32; 2], -} + #[repr(C)] + #[derive(Clone, Copy)] + struct Vert { + pos: [f32; 3], + uv: [f32; 2], + } -/// A screen-space quad. -const BRDF_VERTS: [Vert; 6] = { - let bl = Vert { - pos: [-1.0, -1.0, 0.0], - uv: [0.0, 1.0], - }; - let br = Vert { - pos: [1.0, -1.0, 0.0], - uv: [1.0, 1.0], - }; - let tl = Vert { - pos: [-1.0, 1.0, 0.0], - uv: [0.0, 0.0], - }; - let tr = Vert { - pos: [1.0, 1.0, 0.0], - uv: [1.0, 0.0], + /// A screen-space quad. + const BRDF_VERTS: [Vert; 6] = { + let bl = Vert { + pos: [-1.0, -1.0, 0.0], + uv: [0.0, 1.0], + }; + let br = Vert { + pos: [1.0, -1.0, 0.0], + uv: [1.0, 1.0], + }; + let tl = Vert { + pos: [-1.0, 1.0, 0.0], + uv: [0.0, 0.0], + }; + let tr = Vert { + pos: [1.0, 1.0, 0.0], + uv: [1.0, 0.0], + }; + + [bl, br, tr, bl, tr, tl] }; - [bl, br, tr, bl, tr, tl] -}; - -#[spirv(vertex)] -/// Vertex shader for creating a BRDF LUT. -pub fn brdf_lut_convolution_vertex( - #[spirv(vertex_index)] vertex_id: u32, - out_uv: &mut glam::Vec2, - #[spirv(position)] gl_pos: &mut glam::Vec4, -) { - let Vert { pos, uv } = BRDF_VERTS[vertex_id as usize]; - *out_uv = Vec2::from(uv); - *gl_pos = Vec3::from(pos).extend(1.0); -} + #[spirv(vertex)] + /// Vertex shader for creating a BRDF LUT. + pub fn brdf_lut_convolution_vertex( + #[spirv(vertex_index)] vertex_id: u32, + out_uv: &mut glam::Vec2, + #[spirv(position)] gl_pos: &mut glam::Vec4, + ) { + let Vert { pos, uv } = BRDF_VERTS[vertex_id as usize]; + *out_uv = Vec2::from(uv); + *gl_pos = Vec3::from(pos).extend(1.0); + } -#[spirv(fragment)] -/// Fragment shader for creating a BRDF LUT. -pub fn brdf_lut_convolution_fragment(in_uv: glam::Vec2, out_color: &mut glam::Vec2) { - *out_color = integrate_brdf(in_uv.x, in_uv.y); + #[spirv(fragment)] + /// Fragment shader for creating a BRDF LUT. + pub fn brdf_lut_convolution_fragment(in_uv: glam::Vec2, out_color: &mut glam::Vec2) { + *out_color = integrate_brdf(in_uv.x, in_uv.y); + } } #[cfg(test)] @@ -320,14 +324,17 @@ mod test { fn integrate_brdf_sanity() { let points = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)]; for (x, y) in points.into_iter() { - assert!(!integrate_brdf(x, y).is_nan(), "brdf is NaN at {x},{y}"); + assert!( + !shader::integrate_brdf(x, y).is_nan(), + "brdf is NaN at {x},{y}" + ); } let size = 32; let mut img = image::RgbaImage::new(size, size); for (x, y, image::Rgba([r, g, _, a])) in img.enumerate_pixels_mut() { let u = x as f32 / size as f32; let v = y as f32 / size as f32; - let brdf = integrate_brdf(u, v); + let brdf = shader::integrate_brdf(u, v); *r = (brdf.x * 255.0) as u8; *g = (brdf.y * 255.0) as u8; *a = 255; diff --git a/crates/renderling/src/cubemap.rs b/crates/renderling/src/cubemap.rs index c986fda3..c58d6ad5 100644 --- a/crates/renderling/src/cubemap.rs +++ b/crates/renderling/src/cubemap.rs @@ -4,167 +4,19 @@ //! //! For more info see: //! * -use crabslab::{Array, Id, Slab}; -use glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4}; -use spirv_std::{num_traits::Zero, spirv}; #[cfg(cpu)] mod cpu; #[cfg(cpu)] pub use cpu::*; -use crate::{ - atlas::{AtlasDescriptor, AtlasTexture}, - math::{IsSampler, Sample2dArray}, -}; - -/// Vertex shader for testing cubemap sampling. -#[spirv(vertex)] -pub fn cubemap_sampling_test_vertex( - #[spirv(vertex_index)] vertex_index: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] uv: &Vec3, - out_uv: &mut Vec3, - #[spirv(position)] out_clip_coords: &mut Vec4, -) { - let vertex_index = vertex_index as usize % 6; - *out_clip_coords = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_index]; - *out_uv = *uv; -} - -/// Vertex shader for testing cubemap sampling. -#[spirv(fragment)] -pub fn cubemap_sampling_test_fragment( - #[spirv(descriptor_set = 0, binding = 1)] cubemap: &spirv_std::image::Cubemap, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &spirv_std::Sampler, - in_uv: Vec3, - frag_color: &mut Vec4, -) { - *frag_color = cubemap.sample(*sampler, in_uv); -} - -/// Represents one side of a cubemap. -/// -/// Assumes the camera is at the origin, inside the cube, with -/// a left-handed coordinate system (+Z going into the screen). -#[derive(Clone, Copy)] -pub struct CubemapFaceDirection { - /// Where is the camera - pub eye: Vec3, - /// Where is the camera looking - pub dir: Vec3, - /// Which direction is up - pub up: Vec3, -} - -impl CubemapFaceDirection { - pub const X: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::X, - up: Vec3::Y, - }; - pub const NEG_X: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::NEG_X, - up: Vec3::Y, - }; - - pub const Y: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::Y, - up: Vec3::NEG_Z, - }; - pub const NEG_Y: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::NEG_Y, - up: Vec3::Z, - }; - - pub const Z: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::Z, - up: Vec3::Y, - }; - pub const NEG_Z: Self = Self { - eye: Vec3::ZERO, - dir: Vec3::NEG_Z, - up: Vec3::Y, - }; - - pub const FACES: [Self; 6] = [ - CubemapFaceDirection::X, - CubemapFaceDirection::NEG_X, - CubemapFaceDirection::Y, - CubemapFaceDirection::NEG_Y, - CubemapFaceDirection::Z, - CubemapFaceDirection::NEG_Z, - ]; - - pub fn right(&self) -> Vec3 { - -self.dir.cross(self.up) - } - - /// The view from _inside_ the cube. - pub fn view(&self) -> Mat4 { - Mat4::look_at_lh(self.eye, self.eye + self.dir, self.up) - } -} - -pub struct CubemapDescriptor { - atlas_descriptor_id: Id, - faces: Array, -} - -impl CubemapDescriptor { - /// Return the face index and UV coordinates that can be used to sample - /// a cubemap from the given directional coordinate. - pub fn get_face_index_and_uv(coord: Vec3) -> (usize, Vec2) { - let abs_x = coord.x.abs(); - let abs_y = coord.y.abs(); - let abs_z = coord.z.abs(); - - let (face_index, uv) = if abs_x >= abs_y && abs_x >= abs_z { - if coord.x > 0.0 { - (0, Vec2::new(-coord.z, -coord.y) / abs_x) - } else { - (1, Vec2::new(coord.z, -coord.y) / abs_x) - } - } else if abs_y >= abs_x && abs_y >= abs_z { - if coord.y > 0.0 { - (2, Vec2::new(coord.x, coord.z) / abs_y) - } else { - (3, Vec2::new(coord.x, -coord.z) / abs_y) - } - } else if coord.z > 0.0 { - (4, Vec2::new(coord.x, -coord.y) / abs_z) - } else { - (5, Vec2::new(-coord.x, -coord.y) / abs_z) - }; - - (face_index, (uv + Vec2::ONE) / 2.0) - } - - /// Sample the cubemap with a directional coordinate. - pub fn sample(&self, coord: Vec3, slab: &[u32], atlas: &A, sampler: &S) -> Vec4 - where - A: Sample2dArray, - S: IsSampler, - { - let coord = if coord.length().is_zero() { - Vec3::X - } else { - coord.normalize() - }; - let (face_index, uv) = Self::get_face_index_and_uv(coord); - let atlas_image = slab.read_unchecked(self.faces.at(face_index)); - let atlas_desc = slab.read_unchecked(self.atlas_descriptor_id); - let uv = atlas_image.uv(uv, atlas_desc.size.xy()); - atlas.sample_by_lod(*sampler, uv, 0.0) - } -} +pub mod shader; #[cfg(test)] mod test { - use super::*; + use glam::{Vec2, Vec3}; + + use crate::cubemap::shader::{CubemapDescriptor, CubemapFaceDirection}; #[test] fn cubemap_right() { diff --git a/crates/renderling/src/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs index 663314aa..0ffa57d2 100644 --- a/crates/renderling/src/cubemap/cpu.rs +++ b/crates/renderling/src/cubemap/cpu.rs @@ -5,12 +5,11 @@ use glam::{Mat4, UVec2, Vec3, Vec4}; use image::GenericImageView; use crate::{ - camera::Camera, stage::{Stage, StageRendering}, texture::Texture, }; -use super::{CubemapDescriptor, CubemapFaceDirection}; +use super::shader::{CubemapDescriptor, CubemapFaceDirection}; pub fn cpu_sample_cubemap(cubemap: &[image::DynamicImage; 6], coord: Vec3) -> Vec4 { let coord = coord.normalize_or(Vec3::X); @@ -72,7 +71,7 @@ impl SceneCubemap { view_formats: &[], }); let depth_texture = Texture::create_depth_texture(device, size.x, size.y, 1, label); - let pipeline = Arc::new(Stage::create_renderlet_pipeline(device, format, 1)); + let pipeline = Arc::new(Stage::create_primitive_pipeline(device, format, 1)); Self { pipeline, cubemap_texture, @@ -90,7 +89,7 @@ impl SceneCubemap { let previous_camera_id = stage.used_camera_id(); // create a new camera for our cube, and use it to render with - let camera = stage.geometry.new_camera(Camera::default()); + let camera = stage.geometry.new_camera(); stage.use_camera(&camera); // By setting this to 90 degrees (PI/2 radians) we make sure the viewing field @@ -103,7 +102,7 @@ impl SceneCubemap { for (i, face) in CubemapFaceDirection::FACES.iter().enumerate() { // Update the camera angle, no need to sync as calling `Stage::render` does this // implicitly - camera.modify(|c| c.set_projection_and_view(projection, face.view())); + camera.set_projection_and_view(projection, face.view()); let label_s = format!("scene-to-cubemap-{i}"); let view = self .cubemap_texture @@ -270,8 +269,9 @@ mod test { use image::GenericImageView; use crate::{ + context::Context, + geometry::Vertex, math::{UNIT_INDICES, UNIT_POINTS}, - stage::Vertex, test::BlockOnFuture, texture::CopiedTextureBuffer, }; @@ -282,29 +282,28 @@ mod test { fn hand_rolled_cubemap_sampling() { let width = 256; let height = 256; - let ctx = crate::Context::headless(width, height).block(); + let ctx = Context::headless(width, height).block(); let stage = ctx .new_stage() .with_background_color(Vec4::ZERO) .with_lighting(false) .with_msaa_sample_count(4); - let _camera = - stage.new_camera( - Camera::default_perspective(width as f32, height as f32) - .with_view(Mat4::look_at_rh(Vec3::splat(3.0), Vec3::ZERO, Vec3::Y)), - ); + let projection = crate::camera::perspective(width as f32, height as f32); + let view = Mat4::look_at_rh(Vec3::splat(3.0), Vec3::ZERO, Vec3::Y); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); // geometry is the "clip cube" where colors are normalized 3d space coords let _rez = stage - .builder() - .with_vertices(UNIT_POINTS.map(|unit_cube_point| { + .new_primitive() + .with_vertices(stage.new_vertices(UNIT_POINTS.map(|unit_cube_point| { Vertex::default() // multiply by 2.0 because the unit cube's AABB bounds are at 0.5, and we want 1.0 .with_position(unit_cube_point * 2.0) // "normalize" (really "shift") the space coord from [-0.5, 0.5] to [0.0, 1.0] .with_color((unit_cube_point + 0.5).extend(1.0)) - })) - .with_indices(UNIT_INDICES.map(|u| u as u32)) - .build(); + }))) + .with_indices(stage.new_indices(UNIT_INDICES.map(|u| u as u32))); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); diff --git a/crates/renderling/src/cubemap/shader.rs b/crates/renderling/src/cubemap/shader.rs new file mode 100644 index 00000000..c77a1bff --- /dev/null +++ b/crates/renderling/src/cubemap/shader.rs @@ -0,0 +1,191 @@ +use crabslab::{Array, Id, Slab}; +use glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4}; +use spirv_std::{num_traits::Zero, spirv}; + +use crate::{ + atlas::shader::{AtlasDescriptor, AtlasTextureDescriptor}, + math::{IsSampler, Sample2dArray}, +}; + +/// Vertex shader for testing cubemap sampling. +#[spirv(vertex)] +pub fn cubemap_sampling_test_vertex( + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] uv: &Vec3, + out_uv: &mut Vec3, + #[spirv(position)] out_clip_coords: &mut Vec4, +) { + let vertex_index = vertex_index as usize % 6; + *out_clip_coords = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_index]; + *out_uv = *uv; +} + +/// Vertex shader for testing cubemap sampling. +#[spirv(fragment)] +pub fn cubemap_sampling_test_fragment( + #[spirv(descriptor_set = 0, binding = 1)] cubemap: &spirv_std::image::Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &spirv_std::Sampler, + in_uv: Vec3, + frag_color: &mut Vec4, +) { + *frag_color = cubemap.sample(*sampler, in_uv); +} + +/// Represents one side of a cubemap. +/// +/// Assumes the camera is at the origin, inside the cube, with +/// a left-handed coordinate system (+Z going into the screen). +#[derive(Clone, Copy)] +pub struct CubemapFaceDirection { + /// Where is the camera + pub eye: Vec3, + /// Where is the camera looking + pub dir: Vec3, + /// Which direction is up + pub up: Vec3, +} + +impl CubemapFaceDirection { + pub const X: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::X, + up: Vec3::Y, + }; + pub const NEG_X: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::NEG_X, + up: Vec3::Y, + }; + + pub const Y: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::Y, + up: Vec3::NEG_Z, + }; + pub const NEG_Y: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::NEG_Y, + up: Vec3::Z, + }; + + pub const Z: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::Z, + up: Vec3::Y, + }; + pub const NEG_Z: Self = Self { + eye: Vec3::ZERO, + dir: Vec3::NEG_Z, + up: Vec3::Y, + }; + + pub const FACES: [Self; 6] = [ + CubemapFaceDirection::X, + CubemapFaceDirection::NEG_X, + CubemapFaceDirection::Y, + CubemapFaceDirection::NEG_Y, + CubemapFaceDirection::Z, + CubemapFaceDirection::NEG_Z, + ]; + + pub fn right(&self) -> Vec3 { + -self.dir.cross(self.up) + } + + /// The view from _inside_ the cube. + pub fn view(&self) -> Mat4 { + Mat4::look_at_lh(self.eye, self.eye + self.dir, self.up) + } +} + +pub struct CubemapDescriptor { + atlas_descriptor_id: Id, + faces: Array, +} + +impl CubemapDescriptor { + /// Return the face index and UV coordinates that can be used to sample + /// a cubemap from the given directional coordinate. + pub fn get_face_index_and_uv(coord: Vec3) -> (usize, Vec2) { + let abs_x = coord.x.abs(); + let abs_y = coord.y.abs(); + let abs_z = coord.z.abs(); + + let (face_index, uv) = if abs_x >= abs_y && abs_x >= abs_z { + if coord.x > 0.0 { + (0, Vec2::new(-coord.z, -coord.y) / abs_x) + } else { + (1, Vec2::new(coord.z, -coord.y) / abs_x) + } + } else if abs_y >= abs_x && abs_y >= abs_z { + if coord.y > 0.0 { + (2, Vec2::new(coord.x, coord.z) / abs_y) + } else { + (3, Vec2::new(coord.x, -coord.z) / abs_y) + } + } else if coord.z > 0.0 { + (4, Vec2::new(coord.x, -coord.y) / abs_z) + } else { + (5, Vec2::new(-coord.x, -coord.y) / abs_z) + }; + + (face_index, (uv + Vec2::ONE) / 2.0) + } + + /// Sample the cubemap with a directional coordinate. + pub fn sample(&self, coord: Vec3, slab: &[u32], atlas: &A, sampler: &S) -> Vec4 + where + A: Sample2dArray, + S: IsSampler, + { + let coord = if coord.length().is_zero() { + Vec3::X + } else { + coord.normalize() + }; + let (face_index, uv) = Self::get_face_index_and_uv(coord); + let atlas_image = slab.read_unchecked(self.faces.at(face_index)); + let atlas_desc = slab.read_unchecked(self.atlas_descriptor_id); + let uv = atlas_image.uv(uv, atlas_desc.size.xy()); + atlas.sample_by_lod(*sampler, uv, 0.0) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn cubemap_right() { + assert_eq!(Vec3::NEG_Z, CubemapFaceDirection::X.right()); + assert_eq!(Vec3::Z, CubemapFaceDirection::NEG_X.right()); + assert_eq!(Vec3::X, CubemapFaceDirection::Y.right()); + assert_eq!(Vec3::X, CubemapFaceDirection::NEG_Y.right()); + assert_eq!(Vec3::X, CubemapFaceDirection::Z.right()); + assert_eq!(Vec3::NEG_X, CubemapFaceDirection::NEG_Z.right()); + + assert_eq!( + (1, Vec2::new(0.0, 1.0)), + CubemapDescriptor::get_face_index_and_uv(Vec3::NEG_ONE) + ); + } + + #[test] + fn cubemap_face_index() { + let center = Vec2::splat(0.5); + let data = [ + (Vec3::X, 0, center), + (Vec3::NEG_X, 1, center), + (Vec3::Y, 2, center), + (Vec3::NEG_Y, 3, center), + (Vec3::Z, 4, center), + (Vec3::NEG_Z, 5, center), + ]; + for (coord, expected_face_index, expected_uv) in data { + let (seen_face_index, seen_uv) = CubemapDescriptor::get_face_index_and_uv(coord); + dbg!((coord, seen_face_index, seen_uv)); + assert_eq!(expected_face_index, seen_face_index); + assert_eq!(expected_uv, seen_uv); + } + } +} diff --git a/crates/renderling/src/cull.rs b/crates/renderling/src/cull.rs index 58868778..84f921af 100644 --- a/crates/renderling/src/cull.rs +++ b/crates/renderling/src/cull.rs @@ -2,244 +2,10 @@ //! //! Frustum culling as explained in //! [the vulkan guide](https://vkguide.dev/docs/gpudriven/compute_culling/). -use crabslab::{Array, Id, Slab, SlabItem}; -use glam::{UVec2, UVec3, Vec2, Vec3Swizzles}; -#[allow(unused_imports)] -use spirv_std::num_traits::Float; -use spirv_std::{ - arch::IndexUnchecked, - image::{sample_with, Image, ImageWithMethods}, - spirv, -}; - -use crate::{draw::DrawIndirectArgs, geometry::GeometryDescriptor, stage::Renderlet}; #[cfg(not(target_arch = "spirv"))] mod cpu; #[cfg(not(target_arch = "spirv"))] pub use cpu::*; -#[spirv(compute(threads(16)))] -pub fn compute_culling( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] stage_slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] depth_pyramid_slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] args: &mut [DrawIndirectArgs], - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let gid = global_id.x as usize; - if gid >= args.len() { - return; - } - - crate::println!("gid: {gid}"); - // Get the draw arg - let arg = unsafe { args.index_unchecked_mut(gid) }; - // Get the renderlet using the draw arg's renderlet id - let renderlet_id = Id::::new(arg.first_instance); - let renderlet = stage_slab.read_unchecked(renderlet_id); - crate::println!("renderlet: {renderlet_id:?}"); - - arg.vertex_count = renderlet.get_vertex_count(); - arg.instance_count = if renderlet.visible { 1 } else { 0 }; - - if renderlet.bounds.radius == 0.0 { - crate::println!("renderlet bounding radius is zero, cannot cull"); - return; - } - - let config: GeometryDescriptor = stage_slab.read(Id::new(0)); - if !config.perform_frustum_culling { - return; - } - - let camera = stage_slab.read(config.camera_id); - let model = stage_slab.read(renderlet.transform_id); - // Compute frustum culling, and then occlusion culling, if need be - let (renderlet_is_inside_frustum, sphere_in_world_coords) = - renderlet.bounds.is_inside_camera_view(&camera, model); - - if renderlet_is_inside_frustum { - arg.instance_count = 1; - crate::println!("renderlet is inside frustum"); - crate::println!("znear: {}", camera.frustum().planes[0]); - crate::println!(" zfar: {}", camera.frustum().planes[5]); - if !config.perform_occlusion_culling { - return; - } - - // Compute occlusion culling using the hierachical z-buffer. - let hzb_desc = depth_pyramid_slab.read_unchecked::(0u32.into()); - let viewport_size = Vec2::new(hzb_desc.size.x as f32, hzb_desc.size.y as f32); - let sphere_aabb = sphere_in_world_coords.project_onto_viewport(&camera, viewport_size); - crate::println!("sphere_aabb: {sphere_aabb:#?}"); - - let size_in_pixels = sphere_aabb.max.xy() - sphere_aabb.min.xy(); - let size_in_pixels = if size_in_pixels.x > size_in_pixels.y { - size_in_pixels.x - } else { - size_in_pixels.y - }; - crate::println!("renderlet size in pixels: {size_in_pixels}"); - - let mip_level = size_in_pixels.log2().floor() as u32; - let max_mip_level = hzb_desc.mip.len() as u32 - 1; - let mip_level = if mip_level > max_mip_level { - crate::println!("mip_level maxed out at {mip_level}, setting to {max_mip_level}"); - max_mip_level - } else { - mip_level - }; - crate::println!( - "selected mip level: {mip_level} {}x{}", - viewport_size.x as u32 >> mip_level, - viewport_size.y as u32 >> mip_level - ); - - let center = sphere_aabb.center().xy(); - crate::println!("center: {center}"); - - let x = center.x.round() as u32 >> mip_level; - let y = center.y.round() as u32 >> mip_level; - crate::println!("mip (x, y): ({x}, {y})"); - - let depth_id = hzb_desc.id_of_depth(mip_level, UVec2::new(x, y), depth_pyramid_slab); - let depth_in_hzb = depth_pyramid_slab.read_unchecked(depth_id); - crate::println!("depth_in_hzb: {depth_in_hzb}"); - - let depth_of_sphere = sphere_aabb.min.z; - crate::println!("depth_of_sphere: {depth_of_sphere}"); - - let renderlet_is_behind_something = depth_of_sphere > depth_in_hzb; - let renderlet_surrounds_camera = depth_of_sphere > 1.0; - - if renderlet_is_behind_something || renderlet_surrounds_camera { - crate::println!("CULLED"); - arg.instance_count = 0; - } - } else { - arg.instance_count = 0; - } -} - -/// A hierarchichal depth buffer. -/// -/// AKA HZB -#[derive(Clone, Copy, Default, SlabItem)] -pub struct DepthPyramidDescriptor { - /// Size of the top layer mip. - size: UVec2, - /// Current mip level. - /// - /// This will be updated for each run of the downsample compute shader. - mip_level: u32, - /// Pointer to the mip data. - /// - /// This points to the depth data at each mip level. - /// - /// The depth data itself is somewhere else in the slab. - mip: Array>, -} - -impl DepthPyramidDescriptor { - fn should_skip_invocation(&self, global_invocation: UVec3) -> bool { - let current_size = self.size >> self.mip_level; - !(global_invocation.x < current_size.x && global_invocation.y < current_size.y) - } - - #[cfg(test)] - fn size_at(&self, mip_level: u32) -> UVec2 { - UVec2::new(self.size.x >> mip_level, self.size.y >> mip_level) - } - - /// Return the [`Id`] of the depth at the given `mip_level` and coordinate. - fn id_of_depth(&self, mip_level: u32, coord: UVec2, slab: &[u32]) -> Id { - let mip_array = slab.read(self.mip.at(mip_level as usize)); - let width_at_mip = self.size.x >> mip_level; - let index = coord.y * width_at_mip + coord.x; - mip_array.at(index as usize) - } -} - -pub type DepthImage2d = Image!(2D, type=f32, sampled, depth); -pub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled); - -/// Copies a depth texture to the top mip of a pyramid of mips. -/// -/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in -/// the given slab. -#[spirv(compute(threads(16, 16, 1)))] -pub fn compute_copy_depth_to_pyramid( - #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], - #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2d, - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let desc = slab.read_unchecked::(0u32.into()); - if desc.should_skip_invocation(global_id) { - return; - } - - let depth = depth_texture - .fetch_with(global_id.xy(), sample_with::lod(0)) - .x; - let dest_id = desc.id_of_depth(0, global_id.xy(), slab); - slab.write(dest_id, &depth); -} - -/// Copies a depth texture to the top mip of a pyramid of mips. -/// -/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in -/// the given slab. -#[spirv(compute(threads(16, 16, 1)))] -pub fn compute_copy_depth_to_pyramid_multisampled( - #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], - #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2dMultisampled, - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let desc = slab.read_unchecked::(0u32.into()); - if desc.should_skip_invocation(global_id) { - return; - } - - let depth = depth_texture - .fetch_with(global_id.xy(), sample_with::sample_index(0)) - .x; - let dest_id = desc.id_of_depth(0, global_id.xy(), slab); - slab.write(dest_id, &depth); -} - -/// Downsample from `DepthPyramidDescriptor::mip_level-1` into -/// `DepthPyramidDescriptor::mip_level`. -/// -/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in -/// the given slab. -/// -/// The `DepthPyramidDescriptor`'s `mip_level` field will point to that of the -/// mip level being downsampled to (the mip level being written into). -/// -/// This shader should be called in a loop from from `1..mip_count`. -#[spirv(compute(threads(16, 16, 1)))] -pub fn compute_downsample_depth_pyramid( - #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let desc = slab.read_unchecked::(0u32.into()); - if desc.should_skip_invocation(global_id) { - return; - } - // Sample the texel. - // - // The texel will look like this: - // - // a b - // c d - let a_coord = global_id.xy() * 2; - let a = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord, slab)); - let b = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 0), slab)); - let c = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(0, 1), slab)); - let d = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 1), slab)); - // Take the maximum depth of the region (max depth means furthest away) - let depth_value = a.max(b).max(c).max(d); - // Write the texel in the next mip - let depth_id = desc.id_of_depth(desc.mip_level, global_id.xy(), slab); - slab.write(depth_id, &depth_value); -} +pub mod shader; diff --git a/crates/renderling/src/cull/cpu.rs b/crates/renderling/src/cull/cpu.rs index 5accb2bd..8969f4f8 100644 --- a/crates/renderling/src/cull/cpu.rs +++ b/crates/renderling/src/cull/cpu.rs @@ -11,7 +11,7 @@ use snafu::{OptionExt, Snafu}; use crate::{bindgroup::ManagedBindGroup, texture::Texture}; -use super::DepthPyramidDescriptor; +use super::shader::DepthPyramidDescriptor; #[derive(Debug, Snafu)] pub enum CullingError { @@ -673,10 +673,16 @@ mod test { use std::collections::HashMap; use crate::{ - bvol::BoundingSphere, cull::DepthPyramidDescriptor, draw::DrawIndirectArgs, - geometry::Geometry, math::hex_to_vec4, prelude::*, test::BlockOnFuture, + bvol::BoundingSphere, + context::Context, + cull::shader::DepthPyramidDescriptor, + draw::DrawIndirectArgs, + geometry::{Geometry, Vertex}, + math::hex_to_vec4, + primitive::shader::PrimitiveDescriptor, + test::BlockOnFuture, }; - use crabslab::{GrowableSlab, Slab}; + use crabslab::{Array, GrowableSlab, Id, Slab}; use glam::{Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4}; #[test] @@ -684,19 +690,19 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 9.0, 9.0); - let _camera = stage.new_camera(Camera::new( + let _camera = stage.new_camera().with_projection_and_view( Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 1.0, 24.0), Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), - )); + ); let _rez = stage - .builder() - .with_vertices(crate::test::gpu_cube_vertices()) - .with_transform(Transform { - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(stage.new_vertices(crate::test::gpu_cube_vertices())) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -790,7 +796,9 @@ mod test { let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar); // Camera is looking straight down Z, towards the origin with Y up let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO, Vec3::Y); - stage.new_camera(Camera::new(projection, view)) + stage + .new_camera() + .with_projection_and_view(projection, view) }; let save_render = |s: &str| { @@ -802,7 +810,7 @@ mod test { }; // A hashmap to hold renderlet ids to their names. - let mut names = HashMap::, String>::default(); + let mut names = HashMap::, String>::default(); // Add four yellow cubes in each corner let _ycubes = [ @@ -813,25 +821,27 @@ mod test { ] .map(|(offset, suffix)| { let yellow = hex_to_vec4(0xFFE6A5FF); - let (transform, vertices, renderlet) = stage - .builder() - .with_transform(Transform { - // move it back behind the purple cube - translation: (offset * 10.0).extend(-20.0), - // scale it up since it's a unit cube - scale: Vec3::splat(2.0), - ..Default::default() - }) - .with_vertices(crate::math::unit_cube().into_iter().map(|(p, n)| { - Vertex::default() - .with_position(p) - .with_normal(n) - .with_color(yellow) - })) - .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())) - .build(); + let renderlet = stage + .new_primitive() + .with_transform( + stage + .new_transform() + // move it back behind the purple cube + .with_translation((offset * 10.0).extend(-20.0)) + // scale it up since it's a unit cube + .with_scale(Vec3::splat(2.0)), + ) + .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map( + |(p, n)| { + Vertex::default() + .with_position(p) + .with_normal(n) + .with_color(yellow) + }, + ))) + .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())); names.insert(renderlet.id(), format!("yellow_cube_{suffix}")); - (renderlet, transform, vertices) + renderlet }); save_render("0_yellow_cubes"); @@ -839,24 +849,27 @@ mod test { // We'll add a golden floor let _floor = { let golden = hex_to_vec4(0xFFBF61FF); - let (transform, vertices, renderlet) = stage - .builder() - .with_transform(Transform { - // flip it so it's facing up, like a floor should be - rotation: Quat::from_rotation_x(std::f32::consts::FRAC_PI_2), - // move it down and back a bit - translation: Vec3::new(0.0, -5.0, -10.0), - // scale it up, since it's a unit quad - scale: Vec3::new(100.0, 100.0, 1.0), - }) + let renderlet = stage + .new_primitive() + .with_transform( + stage + .new_transform() + // flip it so it's facing up, like a floor should be + .with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)) + // move it down and back a bit + .with_translation(Vec3::new(0.0, -5.0, -10.0)) + // scale it up, since it's a unit quad + .with_scale(Vec3::new(100.0, 100.0, 1.0)), + ) .with_vertices( - crate::math::UNIT_QUAD_CCW - .map(|p| Vertex::default().with_position(p).with_color(golden)), + stage.new_vertices( + crate::math::UNIT_QUAD_CCW + .map(|p| Vertex::default().with_position(p).with_color(golden)), + ), ) - .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec2::splat(0.5).length())) - .build(); + .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec2::splat(0.5).length())); names.insert(renderlet.id(), "floor".into()); - (renderlet, transform, vertices) + renderlet }; save_render("1_floor"); @@ -864,26 +877,28 @@ mod test { // Add a green cube let _gcube = { let green = hex_to_vec4(0x8ABFA3FF); - let (transform, vertices, renderlet) = stage - .builder() - .with_transform(Transform { - // move it back behind the purple cube - translation: Vec3::new(0.0, 0.0, -10.0), - // scale it up since it's a unit cube - scale: Vec3::splat(5.0), - ..Default::default() - }) - .with_vertices(crate::math::unit_cube().into_iter().map(|(p, n)| { - Vertex::default() - .with_position(p) - .with_normal(n) - .with_color(green) - })) - .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())) - .build(); - stage.add_renderlet(&renderlet); + let renderlet = stage + .new_primitive() + .with_transform( + stage + .new_transform() + // move it back behind the purple cube + .with_translation(Vec3::new(0.0, 0.0, -10.0)) + // scale it up since it's a unit cube + .with_scale(Vec3::splat(5.0)), + ) + .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map( + |(p, n)| { + Vertex::default() + .with_position(p) + .with_normal(n) + .with_color(green) + }, + ))) + .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())); + stage.add_primitive(&renderlet); names.insert(renderlet.id(), "green_cube".into()); - (renderlet, transform, vertices) + renderlet }; save_render("2_green_cube"); @@ -891,25 +906,27 @@ mod test { // And a purple cube let _pcube = { let purple = hex_to_vec4(0x605678FF); - let (transform, vertices, renderlet) = stage - .builder() - .with_transform(Transform { - // move it back a bit - translation: Vec3::new(0.0, 0.0, -3.0), - // scale it up since it's a unit cube - scale: Vec3::splat(5.0), - ..Default::default() - }) - .with_vertices(crate::math::unit_cube().into_iter().map(|(p, n)| { - Vertex::default() - .with_position(p) - .with_normal(n) - .with_color(purple) - })) - .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())) - .build(); + let renderlet = stage + .new_primitive() + .with_transform( + stage + .new_transform() + // move it back a bit + .with_translation(Vec3::new(0.0, 0.0, -3.0)) + // scale it up since it's a unit cube + .with_scale(Vec3::splat(5.0)), + ) + .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map( + |(p, n)| { + Vertex::default() + .with_position(p) + .with_normal(n) + .with_color(purple) + }, + ))) + .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length())); names.insert(renderlet.id(), "purple_cube".into()); - (renderlet, transform, vertices) + renderlet }; // Do two renders, because depth pyramid operates on depth data one frame @@ -977,14 +994,14 @@ mod test { } for i in 0..num_draw_calls as u32 { - let renderlet_id = Id::::new(args[i as usize].first_instance); + let renderlet_id = Id::::new(args[i as usize].first_instance); let name = names.get(&renderlet_id).unwrap(); if name != "green_cube" { continue; } log::info!(""); log::info!("name: {name}"); - crate::cull::compute_culling( + crate::cull::shader::compute_culling( &stage_slab, &depth_pyramid_slab, args, diff --git a/crates/renderling/src/cull/shader.rs b/crates/renderling/src/cull/shader.rs new file mode 100644 index 00000000..1eb9af16 --- /dev/null +++ b/crates/renderling/src/cull/shader.rs @@ -0,0 +1,239 @@ +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{UVec2, UVec3, Vec2, Vec3Swizzles}; +#[allow(unused_imports)] +use spirv_std::num_traits::Float; +use spirv_std::{ + arch::IndexUnchecked, + image::{sample_with, Image, ImageWithMethods}, + spirv, +}; + +use crate::{ + draw::DrawIndirectArgs, geometry::shader::GeometryDescriptor, + primitive::shader::PrimitiveDescriptor, +}; + +#[spirv(compute(threads(16)))] +pub fn compute_culling( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] stage_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] depth_pyramid_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] args: &mut [DrawIndirectArgs], + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let gid = global_id.x as usize; + if gid >= args.len() { + return; + } + + crate::println!("gid: {gid}"); + // Get the draw arg + let arg = unsafe { args.index_unchecked_mut(gid) }; + // Get the primitive using the draw arg's primitive id + let primitive_id = Id::::new(arg.first_instance); + let primitive = stage_slab.read_unchecked(primitive_id); + crate::println!("primitive: {primitive_id:?}"); + + arg.vertex_count = primitive.get_vertex_count(); + arg.instance_count = if primitive.visible { 1 } else { 0 }; + + if primitive.bounds.radius == 0.0 { + crate::println!("primitive bounding radius is zero, cannot cull"); + return; + } + + let config: GeometryDescriptor = stage_slab.read(Id::new(0)); + if !config.perform_frustum_culling { + return; + } + + let camera = stage_slab.read(config.camera_id); + let model = stage_slab.read(primitive.transform_id); + // Compute frustum culling, and then occlusion culling, if need be + let (primitive_is_inside_frustum, sphere_in_world_coords) = + primitive.bounds.is_inside_camera_view(&camera, model); + + if primitive_is_inside_frustum { + arg.instance_count = 1; + crate::println!("primitive is inside frustum"); + crate::println!("znear: {}", camera.frustum().planes[0]); + crate::println!(" zfar: {}", camera.frustum().planes[5]); + if !config.perform_occlusion_culling { + return; + } + + // Compute occlusion culling using the hierachical z-buffer. + let hzb_desc = depth_pyramid_slab.read_unchecked::(0u32.into()); + let viewport_size = Vec2::new(hzb_desc.size.x as f32, hzb_desc.size.y as f32); + let sphere_aabb = sphere_in_world_coords.project_onto_viewport(&camera, viewport_size); + crate::println!("sphere_aabb: {sphere_aabb:#?}"); + + let size_in_pixels = sphere_aabb.max.xy() - sphere_aabb.min.xy(); + let size_in_pixels = if size_in_pixels.x > size_in_pixels.y { + size_in_pixels.x + } else { + size_in_pixels.y + }; + crate::println!("primitive size in pixels: {size_in_pixels}"); + + let mip_level = size_in_pixels.log2().floor() as u32; + let max_mip_level = hzb_desc.mip.len() as u32 - 1; + let mip_level = if mip_level > max_mip_level { + crate::println!("mip_level maxed out at {mip_level}, setting to {max_mip_level}"); + max_mip_level + } else { + mip_level + }; + crate::println!( + "selected mip level: {mip_level} {}x{}", + viewport_size.x as u32 >> mip_level, + viewport_size.y as u32 >> mip_level + ); + + let center = sphere_aabb.center().xy(); + crate::println!("center: {center}"); + + let x = center.x.round() as u32 >> mip_level; + let y = center.y.round() as u32 >> mip_level; + crate::println!("mip (x, y): ({x}, {y})"); + + let depth_id = hzb_desc.id_of_depth(mip_level, UVec2::new(x, y), depth_pyramid_slab); + let depth_in_hzb = depth_pyramid_slab.read_unchecked(depth_id); + crate::println!("depth_in_hzb: {depth_in_hzb}"); + + let depth_of_sphere = sphere_aabb.min.z; + crate::println!("depth_of_sphere: {depth_of_sphere}"); + + let primitive_is_behind_something = depth_of_sphere > depth_in_hzb; + let primitive_surrounds_camera = depth_of_sphere > 1.0; + + if primitive_is_behind_something || primitive_surrounds_camera { + crate::println!("CULLED"); + arg.instance_count = 0; + } + } else { + arg.instance_count = 0; + } +} + +/// A hierarchichal depth buffer. +/// +/// AKA HZB +#[derive(Clone, Copy, Default, SlabItem)] +pub struct DepthPyramidDescriptor { + /// Size of the top layer mip. + pub size: UVec2, + /// Current mip level. + /// + /// This will be updated for each run of the downsample compute shader. + pub mip_level: u32, + /// Pointer to the mip data. + /// + /// This points to the depth data at each mip level. + /// + /// The depth data itself is somewhere else in the slab. + pub mip: Array>, +} + +impl DepthPyramidDescriptor { + fn should_skip_invocation(&self, global_invocation: UVec3) -> bool { + let current_size = self.size >> self.mip_level; + !(global_invocation.x < current_size.x && global_invocation.y < current_size.y) + } + + #[cfg(test)] + pub fn size_at(&self, mip_level: u32) -> UVec2 { + UVec2::new(self.size.x >> mip_level, self.size.y >> mip_level) + } + + /// Return the [`Id`] of the depth at the given `mip_level` and coordinate. + pub fn id_of_depth(&self, mip_level: u32, coord: UVec2, slab: &[u32]) -> Id { + let mip_array = slab.read(self.mip.at(mip_level as usize)); + let width_at_mip = self.size.x >> mip_level; + let index = coord.y * width_at_mip + coord.x; + mip_array.at(index as usize) + } +} + +pub type DepthImage2d = Image!(2D, type=f32, sampled, depth); +pub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled); + +/// Copies a depth texture to the top mip of a pyramid of mips. +/// +/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in +/// the given slab. +#[spirv(compute(threads(16, 16, 1)))] +pub fn compute_copy_depth_to_pyramid( + #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2d, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let desc = slab.read_unchecked::(0u32.into()); + if desc.should_skip_invocation(global_id) { + return; + } + + let depth = depth_texture + .fetch_with(global_id.xy(), sample_with::lod(0)) + .x; + let dest_id = desc.id_of_depth(0, global_id.xy(), slab); + slab.write(dest_id, &depth); +} + +/// Copies a depth texture to the top mip of a pyramid of mips. +/// +/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in +/// the given slab. +#[spirv(compute(threads(16, 16, 1)))] +pub fn compute_copy_depth_to_pyramid_multisampled( + #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2dMultisampled, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let desc = slab.read_unchecked::(0u32.into()); + if desc.should_skip_invocation(global_id) { + return; + } + + let depth = depth_texture + .fetch_with(global_id.xy(), sample_with::sample_index(0)) + .x; + let dest_id = desc.id_of_depth(0, global_id.xy(), slab); + slab.write(dest_id, &depth); +} + +/// Downsample from `DepthPyramidDescriptor::mip_level-1` into +/// `DepthPyramidDescriptor::mip_level`. +/// +/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in +/// the given slab. +/// +/// The `DepthPyramidDescriptor`'s `mip_level` field will point to that of the +/// mip level being downsampled to (the mip level being written into). +/// +/// This shader should be called in a loop from from `1..mip_count`. +#[spirv(compute(threads(16, 16, 1)))] +pub fn compute_downsample_depth_pyramid( + #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32], + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let desc = slab.read_unchecked::(0u32.into()); + if desc.should_skip_invocation(global_id) { + return; + } + // Sample the texel. + // + // The texel will look like this: + // + // a b + // c d + let a_coord = global_id.xy() * 2; + let a = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord, slab)); + let b = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 0), slab)); + let c = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(0, 1), slab)); + let d = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 1), slab)); + // Take the maximum depth of the region (max depth means furthest away) + let depth_value = a.max(b).max(c).max(d); + // Write the texel in the next mip + let depth_id = desc.id_of_depth(desc.mip_level, global_id.xy(), slab); + slab.write(depth_id, &depth_value); +} diff --git a/crates/renderling/src/debug.rs b/crates/renderling/src/debug.rs index 7eb5ca90..03861e8c 100644 --- a/crates/renderling/src/debug.rs +++ b/crates/renderling/src/debug.rs @@ -1,76 +1,79 @@ //! Debug overlay. -use crabslab::{Id, Slab}; -use glam::{Vec2, Vec3Swizzles, Vec4, Vec4Swizzles}; -use spirv_std::{arch::IndexUnchecked, spirv}; - -use crate::{ - draw::DrawIndirectArgs, geometry::GeometryDescriptor, sdf, stage::Renderlet, - transform::Transform, -}; - -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] mod cpu; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] pub use cpu::*; -/// Renders an implicit quad. -#[spirv(vertex)] -pub fn debug_overlay_vertex( - #[spirv(vertex_index)] vertex_id: u32, - #[spirv(position)] clip_pos: &mut Vec4, -) { - *clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_id as usize % 6]; -} +pub mod shader { + use crabslab::{Id, Slab}; + use glam::{Vec2, Vec3Swizzles, Vec4, Vec4Swizzles}; + use spirv_std::{arch::IndexUnchecked, spirv}; + + use crate::{ + draw::DrawIndirectArgs, geometry::shader::GeometryDescriptor, + primitive::shader::PrimitiveDescriptor, sdf, transform::shader::TransformDescriptor, + }; + /// Renders an implicit quad. + #[spirv(vertex)] + pub fn debug_overlay_vertex( + #[spirv(vertex_index)] vertex_id: u32, + #[spirv(position)] clip_pos: &mut Vec4, + ) { + *clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_id as usize % 6]; + } -/// Renders a debug overlay on top of the current framebuffer. -/// -/// Displays useful information in real time. -#[spirv(fragment)] -pub fn debug_overlay_fragment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] draw_calls: &[DrawIndirectArgs], - #[spirv(frag_coord)] frag_coord: Vec4, - frag_color: &mut Vec4, -) { - let camera_id_id = Id::from(GeometryDescriptor::OFFSET_OF_CAMERA_ID); - let camera_id = slab.read_unchecked(camera_id_id); - let camera = slab.read_unchecked(camera_id); - let resolution_id = Id::from(GeometryDescriptor::OFFSET_OF_RESOLUTION); - let viewport_size = slab.read_unchecked(resolution_id); + /// Renders a debug overlay on top of the current framebuffer. + /// + /// Displays useful information in real time. + #[spirv(fragment)] + pub fn debug_overlay_fragment( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] draw_calls: &[DrawIndirectArgs], + #[spirv(frag_coord)] frag_coord: Vec4, + frag_color: &mut Vec4, + ) { + let camera_id_id = Id::from(GeometryDescriptor::OFFSET_OF_CAMERA_ID); + let camera_id = slab.read_unchecked(camera_id_id); + let camera = slab.read_unchecked(camera_id); + let resolution_id = Id::from(GeometryDescriptor::OFFSET_OF_RESOLUTION); + let viewport_size = slab.read_unchecked(resolution_id); - *frag_color = Vec4::ZERO; + *frag_color = Vec4::ZERO; - for i in 0..draw_calls.len() { - let draw_call = unsafe { draw_calls.index_unchecked(i) }; - let renderlet_id = Id::::new(draw_call.first_instance); - let transform_id = slab.read_unchecked(renderlet_id + Renderlet::OFFSET_OF_TRANSFORM_ID); - let mut model = Transform::IDENTITY; - slab.read_into_if_some(transform_id, &mut model); - let bounds = slab.read_unchecked(renderlet_id + Renderlet::OFFSET_OF_BOUNDS); + for i in 0..draw_calls.len() { + let draw_call = unsafe { draw_calls.index_unchecked(i) }; + let primitive_id: Id = + Id::::new(draw_call.first_instance); + let transform_id = + slab.read_unchecked(primitive_id + PrimitiveDescriptor::OFFSET_OF_TRANSFORM_ID); + let mut model = TransformDescriptor::IDENTITY; + slab.read_into_if_some::(transform_id, &mut model); + let bounds = slab.read_unchecked(primitive_id + PrimitiveDescriptor::OFFSET_OF_BOUNDS); - let (_, sphere_in_world_coords) = bounds.is_inside_camera_view(&camera, model); - let sphere_aabb = sphere_in_world_coords.project_onto_viewport( - &camera, - Vec2::new(viewport_size.x as f32, viewport_size.y as f32), - ); + let (_, sphere_in_world_coords) = bounds.is_inside_camera_view(&camera, model); + let sphere_aabb = sphere_in_world_coords.project_onto_viewport( + &camera, + Vec2::new(viewport_size.x as f32, viewport_size.y as f32), + ); - let sdf_circle = sdf::Box { - center: sphere_aabb.center().xy(), - half_extent: (sphere_aabb.max.xy() - sphere_aabb.min.xy()) * 0.5, - }; + let sdf_circle = sdf::Box { + center: sphere_aabb.center().xy(), + half_extent: (sphere_aabb.max.xy() - sphere_aabb.min.xy()) * 0.5, + }; - let distance = sdf_circle.distance(frag_coord.xy() + 0.5); + let distance = sdf_circle.distance(frag_coord.xy() + 0.5); - // Here we use `step_le`, which I have annotated with `#inline(always)`. - // I did this because without it, it seems to do the opposite of expected. - // I found this by inlining by hand. - let alpha = crate::math::step_le(sphere_aabb.max.z, 1.0); - if distance.abs() < 0.5 { - *frag_color = Vec4::new(0.0, 0.0, 0.0, 1.0 * alpha); - } else if distance.abs() <= 2.0 { - *frag_color = Vec4::new(1.0, 1.0, 1.0, 0.5 * alpha); - } else if distance.abs() <= 3.0 { - *frag_color = Vec4::new(0.5, 0.5, 0.5, 1.0 * alpha); + // Here we use `step_le`, which I have annotated with `#inline(always)`. + // I did this because without it, it seems to do the opposite of expected. + // I found this by inlining by hand. + let alpha = crate::math::step_le(sphere_aabb.max.z, 1.0); + if distance.abs() < 0.5 { + *frag_color = Vec4::new(0.0, 0.0, 0.0, 1.0 * alpha); + } else if distance.abs() <= 2.0 { + *frag_color = Vec4::new(1.0, 1.0, 1.0, 0.5 * alpha); + } else if distance.abs() <= 3.0 { + *frag_color = Vec4::new(0.5, 0.5, 0.5, 1.0 * alpha); + } } } } diff --git a/crates/renderling/src/draw.rs b/crates/renderling/src/draw.rs index d2990b7a..781cc0a9 100644 --- a/crates/renderling/src/draw.rs +++ b/crates/renderling/src/draw.rs @@ -1,9 +1,9 @@ //! Handles queueing draw calls. //! //! [`DrawCalls`] is used to maintain the list of all staged -//! [`Renderlet`](crate::prelude::Renderlet)s. +//! [`PrimitiveDescriptor`](crate::primitive::shader::PrimitiveDescriptor)s. //! It also performs frustum culling and issues draw calls during -//! [`Stage::render`](crate::prelude::Stage::render). +//! [`Stage::render`](crate::stage::Stage::render). use crabslab::SlabItem; #[cfg(cpu)] diff --git a/crates/renderling/src/draw/cpu.rs b/crates/renderling/src/draw/cpu.rs index 2ffafe0a..2f840589 100644 --- a/crates/renderling/src/draw/cpu.rs +++ b/crates/renderling/src/draw/cpu.rs @@ -1,39 +1,20 @@ //! CPU-only side of renderling/draw.rs use craballoc::{ - prelude::{Gpu, Hybrid, SlabAllocator, WeakHybrid, WgpuRuntime}, + prelude::{Gpu, SlabAllocator, WgpuRuntime}, slab::SlabBuffer, }; use crabslab::Id; -use rustc_hash::FxHashMap; use crate::{ + context::Context, cull::{ComputeCulling, CullingError}, - stage::Renderlet, + primitive::{shader::PrimitiveDescriptor, Primitive}, texture::Texture, - Context, }; use super::DrawIndirectArgs; -/// Used to track renderlets internally. -#[repr(transparent)] -struct InternalRenderlet { - inner: WeakHybrid, -} - -impl InternalRenderlet { - fn has_external_references(&self) -> bool { - self.inner.strong_count() > 0 - } - - fn from_hybrid_renderlet(hr: &Hybrid) -> Self { - Self { - inner: WeakHybrid::from_hybrid(hr), - } - } -} - /// Issues indirect draw calls. /// /// Issues draw calls and performs culling. @@ -64,6 +45,10 @@ impl IndirectDraws { } } + pub(crate) fn slab_allocator(&self) -> &SlabAllocator { + &self.slab + } + fn invalidate(&mut self) { if !self.draws.is_empty() { log::trace!("draining indirect draws after invalidation"); @@ -71,35 +56,6 @@ impl IndirectDraws { } } - fn get_indirect_buffer(&self) -> SlabBuffer { - self.slab.commit() - } - - fn sync_with_internal_renderlets( - &mut self, - internal_renderlets: &[InternalRenderlet], - redraw_args: bool, - ) -> SlabBuffer { - if redraw_args || self.draws.len() != internal_renderlets.len() { - self.invalidate(); - // Pre-upkeep to reclaim resources - this is necessary because - // the draw buffer has to be contiguous (it can't start with a bunch of trash) - let indirect_buffer = self.slab.commit(); - if indirect_buffer.is_new_this_commit() { - log::warn!("new indirect buffer"); - } - self.draws = internal_renderlets - .iter() - .map(|ir: &InternalRenderlet| { - self.slab - .new_value(DrawIndirectArgs::from(ir.inner.id())) - .into_gpu_only() - }) - .collect(); - } - self.get_indirect_buffer() - } - /// Read the images from the hierarchical z-buffer used for occlusion /// culling. /// @@ -113,8 +69,8 @@ impl IndirectDraws { } } -impl From> for DrawIndirectArgs { - fn from(id: Id) -> Self { +impl From> for DrawIndirectArgs { + fn from(id: Id) -> Self { // This is obviously incomplete, but that's ok because // the rest of this struct is filled out on the GPU during // culling. @@ -139,17 +95,16 @@ pub(crate) struct DrawingStrategy { } impl DrawingStrategy { - #[cfg(test)] - pub fn as_indirect(&self) -> Option<&IndirectDraws> { + pub(crate) fn as_indirect(&self) -> Option<&IndirectDraws> { self.indirect.as_ref() } } /// Used to determine which objects are drawn and maintains the -/// list of all [`Renderlet`]s. +/// list of all [`Primitive`]s. pub struct DrawCalls { /// Internal representation of all staged renderlets. - internal_renderlets: Vec, + renderlets: Vec, pub(crate) drawing_strategy: DrawingStrategy, } @@ -181,7 +136,7 @@ impl DrawCalls { } let can_use_compute_culling = use_compute_culling && can_use_multi_draw_indirect; Self { - internal_renderlets: vec![], + renderlets: vec![], drawing_strategy: DrawingStrategy { indirect: if can_use_compute_culling { log::debug!("Using indirect drawing method and compute culling"); @@ -194,6 +149,10 @@ impl DrawCalls { } } + pub(crate) fn drawing_strategy(&self) -> &DrawingStrategy { + &self.drawing_strategy + } + /// Returns whether compute culling is available. pub fn get_compute_culling_available(&self) -> bool { matches!( @@ -205,93 +164,46 @@ impl DrawCalls { /// Add a renderlet to the drawing queue. /// /// Returns the number of draw calls in the queue. - pub fn add_renderlet(&mut self, renderlet: &Hybrid) -> usize { + pub fn add_primitive(&mut self, renderlet: &Primitive) -> usize { log::trace!("adding renderlet {:?}", renderlet.id()); if let Some(indirect) = &mut self.drawing_strategy.indirect { indirect.invalidate(); } - self.internal_renderlets - .push(InternalRenderlet::from_hybrid_renderlet(renderlet)); - self.internal_renderlets.len() + self.renderlets.push(renderlet.clone()); + self.renderlets.len() } /// Erase the given renderlet from the internal list of renderlets to be /// drawn each frame. /// /// Returns the number of draw calls remaining in the queue. - pub fn remove_renderlet(&mut self, renderlet: &Hybrid) -> usize { + pub fn remove_primitive(&mut self, renderlet: &Primitive) -> usize { let id = renderlet.id(); - self.internal_renderlets.retain(|ir| ir.inner.id() != id); + self.renderlets.retain(|ir| ir.descriptor.id() != id); if let Some(indirect) = &mut self.drawing_strategy.indirect { indirect.invalidate(); } - self.internal_renderlets.len() + self.renderlets.len() } - /// Re-order the renderlets to that of the given list of identifiers. - /// - /// This determines the order in which they are drawn each frame. - /// - /// If the `order` iterator is missing any renderlet ids, those missing - /// renderlets will be drawn _before_ the ordered ones, in no particular - /// order. - pub fn reorder_renderlets(&mut self, order: impl IntoIterator>) { - let mut ordered = vec![]; - let mut m = FxHashMap::from_iter( - std::mem::take(&mut self.internal_renderlets) - .into_iter() - .map(|r| (r.inner.id(), r)), - ); - for id in order.into_iter() { - if let Some(renderlet) = m.remove(&id) { - ordered.push(renderlet); - } - } - self.internal_renderlets.extend(m.into_values()); - self.internal_renderlets.extend(ordered); + /// Sort draw calls using a function compairing [`Primitive`]s. + pub fn sort_primitives(&mut self, f: impl Fn(&Primitive, &Primitive) -> std::cmp::Ordering) { + self.renderlets.sort_by(f); if let Some(indirect) = &mut self.drawing_strategy.indirect { indirect.invalidate(); } } - /// Iterator over all staged [`Renderlet`]s. - pub fn renderlets_iter(&self) -> impl Iterator> + '_ { - self.internal_renderlets.iter().map(|ir| ir.inner.clone()) - } - /// - /// Perform upkeep on queued draw calls and synchronize internal buffers. - pub fn upkeep(&mut self) { - let mut redraw_args = false; - - // Drop any renderlets that have no external references. - self.internal_renderlets.retain_mut(|ir| { - if ir.has_external_references() { - true - } else { - redraw_args = true; - log::trace!("dropping '{:?}' from drawing", ir.inner.id()); - false - } - }); - - if let Some(indirect) = &mut self.drawing_strategy.indirect { - indirect.sync_with_internal_renderlets(&self.internal_renderlets, redraw_args); - } - } - /// Returns the number of draw calls (direct or indirect) that will be /// made during pre-rendering (compute culling) and rendering. pub fn draw_count(&self) -> usize { - self.internal_renderlets.len() + self.renderlets.len() } /// Perform pre-draw steps like frustum and occlusion culling, if available. /// - /// This does not do upkeep, please call [`DrawCalls::upkeep`] before - /// calling this function. - /// /// Returns the indirect draw buffer. pub fn pre_draw( &mut self, @@ -308,20 +220,31 @@ impl DrawCalls { // We can do this without multidraw by running GPU culling and then // copying `indirect_buffer` back to the CPU. if let Some(indirect) = &mut self.drawing_strategy.indirect { - let maybe_buffer = indirect.slab.get_buffer(); - if let Some(indirect_buffer) = maybe_buffer { - log::trace!("performing culling on {num_draw_calls} renderlets"); - indirect - .compute_culling - .run(num_draw_calls as u32, depth_texture); - Ok(Some(indirect_buffer)) - } else { - log::warn!( - "DrawCalls::pre_render called without first calling `upkeep` - no culling \ - was performed" - ); - Ok(None) + if indirect.draws.len() != self.renderlets.len() { + indirect.invalidate(); + // Pre-upkeep to reclaim resources - this is necessary because + // the draw buffer has to be contiguous (it can't start with a bunch of trash) + let indirect_buffer = indirect.slab.commit(); + if indirect_buffer.is_new_this_commit() { + log::warn!("new indirect buffer"); + } + indirect.draws = self + .renderlets + .iter() + .map(|r| { + indirect + .slab + .new_value(DrawIndirectArgs::from(r.descriptor.id())) + .into_gpu_only() + }) + .collect(); } + let indirect_buffer = indirect.slab.commit(); + log::trace!("performing culling on {num_draw_calls} renderlets"); + indirect + .compute_culling + .run(num_draw_calls as u32, depth_texture); + Ok(Some(indirect_buffer)) } else { Ok(None) } @@ -332,18 +255,16 @@ impl DrawCalls { /// Draw into the given `RenderPass` by directly calling each draw. pub fn draw_direct(&self, render_pass: &mut wgpu::RenderPass) { - if self.internal_renderlets.is_empty() { + if self.renderlets.is_empty() { log::warn!("no internal renderlets, nothing to draw"); } - for ir in self.internal_renderlets.iter() { + for ir in self.renderlets.iter() { // UNWRAP: panic on purpose - if let Some(hr) = ir.inner.upgrade() { - let ir = hr.get(); - let vertex_range = 0..ir.get_vertex_count(); - let id = hr.id(); - let instance_range = id.inner()..id.inner() + 1; - render_pass.draw(vertex_range, instance_range); - } + let desc = ir.descriptor.get(); + let vertex_range = 0..desc.get_vertex_count(); + let id = ir.descriptor.id(); + let instance_range = id.inner()..id.inner() + 1; + render_pass.draw(vertex_range, instance_range); } } diff --git a/crates/renderling/src/geometry.rs b/crates/renderling/src/geometry.rs index 722e5d13..585b229b 100644 --- a/crates/renderling/src/geometry.rs +++ b/crates/renderling/src/geometry.rs @@ -1,42 +1,134 @@ -//! Holds geometry on CPU and GPU. -use crabslab::{Id, SlabItem}; +//! Types and functions for staging geometry. +use crate::math::IsVector; +use crabslab::SlabItem; #[cfg(cpu)] mod cpu; #[cfg(cpu)] pub use cpu::*; +use glam::{Vec2, Vec3, Vec4}; -use crate::camera::Camera; +pub mod shader; -/// Holds configuration info for vertex and shading render passes of -/// geometry. +/// A displacement target. /// -/// This descriptor lives at the root (index 0) of the geometry slab. -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, SlabItem)] -#[offsets] -pub struct GeometryDescriptor { - pub camera_id: Id, - pub atlas_size: glam::UVec2, - pub resolution: glam::UVec2, - pub debug_channel: crate::pbr::debug::DebugChannel, - pub has_lighting: bool, - pub has_skinning: bool, - pub perform_frustum_culling: bool, - pub perform_occlusion_culling: bool, +/// Use to displace vertices using weights defined on the mesh. +/// +/// For more info on morph targets in general, see +/// +#[derive(Clone, Copy, Default, PartialEq, SlabItem)] +#[cfg_attr(cpu, derive(Debug))] +pub struct MorphTarget { + pub position: Vec3, + pub normal: Vec3, + pub tangent: Vec3, + // TODO: Extend MorphTargets to include UV and Color. + // I think this would take a contribution to the `gltf` crate. +} + +/// A vertex in a mesh. +#[derive(Clone, Copy, core::fmt::Debug, PartialEq, SlabItem)] +pub struct Vertex { + pub position: Vec3, + pub color: Vec4, + pub uv0: Vec2, + pub uv1: Vec2, + pub normal: Vec3, + pub tangent: Vec4, + // Indices that point to this vertex's 'joint' transforms. + pub joints: [u32; 4], + // The weights of influence that each joint has over this vertex + pub weights: [f32; 4], } -impl Default for GeometryDescriptor { +impl Default for Vertex { fn default() -> Self { Self { - camera_id: Id::NONE, - atlas_size: Default::default(), - resolution: glam::UVec2::ONE, - debug_channel: Default::default(), - has_lighting: true, - has_skinning: true, - perform_frustum_culling: true, - perform_occlusion_culling: false, + position: Default::default(), + color: Vec4::ONE, + uv0: Vec2::ZERO, + uv1: Vec2::ZERO, + normal: Vec3::Z, + tangent: Vec4::Y, + joints: [0; 4], + weights: [0.0; 4], + } + } +} + +impl Vertex { + pub fn with_position(mut self, p: impl Into) -> Self { + self.position = p.into(); + self + } + + pub fn with_color(mut self, c: impl Into) -> Self { + self.color = c.into(); + self + } + + pub fn with_uv0(mut self, uv: impl Into) -> Self { + self.uv0 = uv.into(); + self + } + + pub fn with_uv1(mut self, uv: impl Into) -> Self { + self.uv1 = uv.into(); + self + } + + pub fn with_normal(mut self, n: impl Into) -> Self { + self.normal = n.into(); + self + } + + pub fn with_tangent(mut self, t: impl Into) -> Self { + self.tangent = t.into(); + self + } + + pub fn generate_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 { + let ab = a - b; + let ac = a - c; + ab.cross(ac).normalize() + } + + pub fn generate_tangent(a: Vec3, a_uv: Vec2, b: Vec3, b_uv: Vec2, c: Vec3, c_uv: Vec2) -> Vec4 { + let ab = b - a; + let ac = c - a; + let n = ab.cross(ac); + let d_uv1 = b_uv - a_uv; + let d_uv2 = c_uv - a_uv; + let denom = d_uv1.x * d_uv2.y - d_uv2.x * d_uv1.y; + let denom_sign = if denom >= 0.0 { 1.0 } else { -1.0 }; + let denom = denom.abs().max(f32::EPSILON) * denom_sign; + let f = 1.0 / denom; + let s = f * Vec3::new( + d_uv2.y * ab.x - d_uv1.y * ac.x, + d_uv2.y * ab.y - d_uv1.y * ac.y, + d_uv2.y * ab.z - d_uv1.y * ac.z, + ); + let t = f * Vec3::new( + d_uv1.x * ac.x - d_uv2.x * ab.x, + d_uv1.x * ac.y - d_uv2.x * ab.y, + d_uv1.x * ac.z - d_uv2.x * ab.z, + ); + let n_cross_t_dot_s_sign = if n.cross(t).dot(s) >= 0.0 { 1.0 } else { -1.0 }; + (s - s.dot(n) * n) + .alt_norm_or_zero() + .extend(n_cross_t_dot_s_sign) + } + + #[cfg(cpu)] + /// A triangle list mesh of points. + pub fn cube_mesh() -> [Vertex; 36] { + let mut mesh = [Vertex::default(); 36]; + let unit_cube = crate::math::unit_cube(); + debug_assert_eq!(36, unit_cube.len()); + for (i, (position, normal)) in unit_cube.into_iter().enumerate() { + mesh[i].position = position; + mesh[i].normal = normal; } + mesh } } diff --git a/crates/renderling/src/geometry/cpu.rs b/crates/renderling/src/geometry/cpu.rs index 0402c4aa..f7ad58a3 100644 --- a/crates/renderling/src/geometry/cpu.rs +++ b/crates/renderling/src/geometry/cpu.rs @@ -1,35 +1,318 @@ //! CPU side of the [super::geometry](geometry) module. -//! - use std::sync::{Arc, Mutex}; use craballoc::{ - runtime::WgpuRuntime, + runtime::{IsRuntime, WgpuRuntime}, slab::{SlabAllocator, SlabBuffer}, - value::{Hybrid, HybridArray}, + value::{GpuArray, Hybrid, HybridArray, IsContainer}, }; use crabslab::{Array, Id}; -use glam::{Mat4, UVec2}; +use glam::{Mat4, UVec2, Vec4}; use crate::{ camera::Camera, - geometry::GeometryDescriptor, - prelude::Transform, - stage::{MorphTarget, Renderlet, Skin, Vertex}, + geometry::{ + shader::{GeometryDescriptor, SkinDescriptor}, + MorphTarget, Vertex, + }, + transform::{shader::TransformDescriptor, NestedTransform, Transform}, + types::{GpuCpuArray, GpuOnlyArray}, }; -// TODO: Move `Renderlet` to geometry. +/// A contiguous array of vertices, staged on the GPU. +/// +/// The type variable `Ct` denotes whether the staged data lives on the GPU +/// only, or on the CPU and the GPU. +/// +/// # Note +/// The amount of data staged in `Vertices` can potentially be very large, and +/// it is common to unload the data from the CPU with +/// [`Vertices::into_gpu_only`]. +/// +/// The only reason to keep data on the CPU is if it needs to be inspected and +/// modified _in place_. This type of modification can be done with +/// [`Vertices::modify_vertex`]. +/// +/// After unloading it is still possible to _replace_ a [`Vertex`] at a +/// specific index using [`Vertices::set_vertex`]. +pub struct Vertices { + inner: Ct::Container, +} + +impl Clone for Vertices +where + Ct: IsContainer, + Ct::Container: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl std::fmt::Debug for Vertices +where + Ct: IsContainer = Array>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Vertices") + .field("array", &Ct::get_pointer(&self.inner)) + .finish() + } +} + +impl From for Vertices { + fn from(value: Vertices) -> Self { + value.into_gpu_only() + } +} + +impl From<&Vertices> for Vertices { + fn from(value: &Vertices) -> Self { + value.clone().into_gpu_only() + } +} + +impl From<&Vertices> for Vertices { + fn from(value: &Vertices) -> Self { + value.clone() + } +} + +impl Vertices { + /// Stage a new array of vertex data on the GPU. + /// + /// The resulting `Vertices` will live on the GPU and CPU, allowing for modification. + /// If you would like to unload the CPU side, use [`Vertices::into_gpu_only`]. + pub(crate) fn new( + slab: &SlabAllocator, + vertices: impl IntoIterator, + ) -> Self { + Vertices { + inner: slab.new_array(vertices), + } + } + + /// Unload the CPU side of vertex data. + /// + /// After this operation the data can still be updated using [`Vertices::set_item`], + /// but it cannot be modified in-place. + pub fn into_gpu_only(self) -> Vertices { + Vertices { + inner: self.inner.into_gpu_only(), + } + } + + /// Returns a [`Vertex`] at a specific index, if any. + pub fn get_vertex(&self, index: usize) -> Option { + self.inner.get(index) + } + + /// Return all vertices as a vector. + pub fn get_vec(&self) -> Vec { + self.inner.get_vec() + } + + /// Modify a vertex at a specific index, if it exists. + pub fn modify_vertex( + &self, + index: usize, + f: impl FnOnce(&mut Vertex) -> T, + ) -> Option { + self.inner.modify(index, f) + } +} + +impl Vertices +where + T: IsContainer = Array>, +{ + /// Returns a pointer to the underlying data on the GPU. + pub fn array(&self) -> Array { + T::get_pointer(&self.inner) + } +} + +impl Vertices { + /// Set the [`Vertex`] at the given index to the given value, if the item at + /// the index exists. + pub fn set_vertex(&self, index: usize, value: &Vertex) { + self.inner.set_item(index, value) + } +} + +/// A contiguous array of indices, staged on the GPU. +/// The type variable `Ct` denotes whether the data lives on the GPU only, or on +/// the CPU and the GPU. +pub struct Indices { + inner: Ct::Container, +} + +impl std::fmt::Debug for Indices +where + Ct: IsContainer = Array>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Indices") + .field("array", &Ct::get_pointer(&self.inner)) + .finish() + } +} + +impl Clone for Indices +where + Ct: IsContainer, + Ct::Container: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl From for Indices { + fn from(value: Indices) -> Self { + value.into_gpu_only() + } +} + +impl From<&Indices> for Indices { + fn from(value: &Indices) -> Self { + value.clone().into_gpu_only() + } +} + +impl From<&Indices> for Indices { + fn from(value: &Indices) -> Self { + value.clone() + } +} + +impl Indices +where + T: IsContainer = Array>, +{ + /// Returns a pointer to the underlying data on the GPU. + pub fn array(&self) -> Array { + T::get_pointer(&self.inner) + } +} + +impl Indices { + /// Stage a new array of index data on the GPU. + /// + /// The resulting `Indices` will live on the GPU and CPU, allowing for modification. + /// If you would like to unload the CPU side, use [`Indices::into_gpu_only`]. + pub fn new(geometry: &Geometry, indices: impl IntoIterator) -> Self { + Indices { + inner: geometry.slab.new_array(indices), + } + } + + /// Unload the CPU side of this index data. + pub fn into_gpu_only(self) -> Indices { + Indices { + inner: self.inner.into_gpu_only(), + } + } +} + +/// Holds morph targets for animated nodes. +#[derive(Clone)] +pub struct MorphTargets { + // Held onto so the values don't drop from under us + _targets: Arc>>, + arrays: HybridArray>, +} + +impl From<&MorphTargets> for MorphTargets { + fn from(value: &MorphTargets) -> Self { + value.clone() + } +} + +impl std::fmt::Debug for MorphTargets { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("MorphTargets") + .field("arrays", &self.arrays) + .field("targets", &"...") + .finish() + } +} + +impl MorphTargets { + pub(crate) fn new( + slab: &SlabAllocator, + morph_targets: impl IntoIterator>, + ) -> Self { + let targets = morph_targets + .into_iter() + .map(|verts| slab.new_array(verts).into_gpu_only()) + .collect::>(); + let arrays = slab.new_array(targets.iter().map(|ts| ts.array())); + Self { + _targets: targets.into(), + arrays, + } + } + /// Returns a pointer to the underlying morph targets data on the GPU. + pub fn array(&self) -> Array> { + self.arrays.array() + } +} + +/// Holds morph targets weights for animated nodes. +#[derive(Clone, Debug)] +pub struct MorphTargetWeights { + inner: HybridArray, +} + +impl From<&MorphTargetWeights> for MorphTargetWeights { + fn from(value: &MorphTargetWeights) -> Self { + value.clone() + } +} + +impl MorphTargetWeights { + pub(crate) fn new( + slab: &SlabAllocator, + data: impl IntoIterator, + ) -> Self { + Self { + inner: slab.new_array(data), + } + } + + /// Returns a pointer to the underlying morph targets weights data on the GPU. + pub fn array(&self) -> Array { + self.inner.array() + } + + /// Return the weight at the given index, if any. + pub fn get_item(&self, index: usize) -> Option { + self.inner.get(index) + } + + /// Update the weight at the given index. + pub fn set_item(&self, index: usize, weight: f32) { + self.inner.set_item(index, weight); + } +} /// Wrapper around the geometry slab, which holds mesh data and more. #[derive(Clone)] pub struct Geometry { slab: SlabAllocator, descriptor: Hybrid, + /// A plain white cube to use as default geometry. + default_vertices: Vertices, /// Holds the current camera just in case the user drops it, /// this way we never lose a camera that is in use. Dropping /// the camera would cause a blank screen, which is very confusing /// =( - _camera: Arc>>>, + camera: Arc>>, } impl AsRef for Geometry { @@ -45,7 +328,6 @@ impl AsRef> for Geometry { } impl Geometry { - // TODO: move atlas size into materials. pub fn new(runtime: impl AsRef, resolution: UVec2, atlas_size: UVec2) -> Self { let runtime = runtime.as_ref(); let slab = SlabAllocator::new(runtime, "geometry", wgpu::BufferUsages::empty()); @@ -54,10 +336,20 @@ impl Geometry { resolution, ..Default::default() }); + let default_vertices = Vertices::new( + &slab, + crate::math::unit_cube().into_iter().map(|(p, n)| { + Vertex::default() + .with_position(p) + .with_normal(n) + .with_color(Vec4::ONE) + }), + ); Self { slab, descriptor, - _camera: Default::default(), + default_vertices, + camera: Default::default(), } } @@ -73,6 +365,11 @@ impl Geometry { &self.descriptor } + /// Returns the vertices of a white unit cube. + pub fn default_vertices(&self) -> &Vertices { + &self.default_vertices + } + #[must_use] pub fn commit(&self) -> SlabBuffer { self.slab.commit() @@ -81,8 +378,8 @@ impl Geometry { /// Create a new camera. /// /// If this is the first camera created, it will be automatically used. - pub fn new_camera(&self, camera: Camera) -> Hybrid { - let c = self.slab.new_value(camera); + pub fn new_camera(&self) -> Camera { + let c = Camera::new(&self.slab); if self.descriptor.get().camera_id.is_none() { self.use_camera(&c); } @@ -90,72 +387,163 @@ impl Geometry { } /// Set all geometry to use the given camera. - pub fn use_camera(&self, camera: impl AsRef>) { - let c = camera.as_ref(); - log::info!("using camera: {:?}", c.id()); + pub fn use_camera(&self, camera: &Camera) { + let id = camera.id(); + log::info!("using camera: {id:?}"); // Save a clone so we never lose the active camera, even if the user drops it - *self._camera.lock().unwrap() = Some(c.clone()); - self.descriptor.modify(|cfg| cfg.camera_id = c.id()); + self.descriptor.modify(|cfg| cfg.camera_id = id); + *self.camera.lock().unwrap() = Some(camera.clone()); } - pub fn new_transform(&self, transform: Transform) -> Hybrid { - self.slab.new_value(transform) + /// Stage a new transform. + pub fn new_transform(&self) -> Transform { + Transform::new(&self.slab) } - /// Create new geometry data. - // TODO: Move `Vertex` to geometry. - pub fn new_vertices(&self, data: impl IntoIterator) -> HybridArray { - self.slab.new_array(data) + /// Stage vertex geometry data on the GPU. + pub fn new_vertices(&self, vertices: impl IntoIterator) -> Vertices { + Vertices::new(self.slab_allocator(), vertices) } - /// Create new indices that point to offsets of an array of vertices. - pub fn new_indices(&self, data: impl IntoIterator) -> HybridArray { - self.slab.new_array(data) + /// Stage indices that point to offsets of an array of vertices. + pub fn new_indices(&self, indices: impl IntoIterator) -> Indices { + Indices::new(self, indices) } - /// Create new morph targets. - // TODO: Move `MorphTarget` to geometry. + /// Stage new morph targets on the GPU. pub fn new_morph_targets( &self, - data: impl IntoIterator, - ) -> HybridArray { - self.slab.new_array(data) + data: impl IntoIterator>, + ) -> MorphTargets { + MorphTargets::new(&self.slab, data) } - /// Create an array of morph target arrays. - pub fn new_morph_targets_array( + /// Create new morph target weights. + pub fn new_morph_target_weights( &self, - data: impl IntoIterator>, - ) -> HybridArray> { - let morph_targets = data.into_iter(); - self.slab.new_array(morph_targets) + data: impl IntoIterator, + ) -> MorphTargetWeights { + MorphTargetWeights::new(&self.slab, data) } - /// Create new morph target weights. - pub fn new_weights(&self, data: impl IntoIterator) -> HybridArray { + /// Create a new array of matrices. + pub fn new_matrices(&self, data: impl IntoIterator) -> HybridArray { self.slab.new_array(data) } - /// Create a new array of joint transform ids that each point to a [`Transform`]. - pub fn new_joint_transform_ids( + pub fn new_skin( &self, - data: impl IntoIterator>, - ) -> HybridArray> { - self.slab.new_array(data) + joints: impl IntoIterator>, + inverse_bind_matrices: impl IntoIterator>, + ) -> Skin { + Skin::new(self.slab_allocator(), joints, inverse_bind_matrices) } +} - /// Create a new array of matrices. - pub fn new_matrices(&self, data: impl IntoIterator) -> HybridArray { - self.slab.new_array(data) +/// A vertex skin. +/// +/// For more info on vertex skinning, see +/// +#[derive(Clone)] +pub struct Skin { + descriptor: Hybrid, + joints: HybridArray>, + // Held onto so the transforms don't drop from under us + _skin_joints: Arc>>, + // Contains the 4x4 inverse-bind matrices. + // + // When None, each matrix is assumed to be the 4x4 identity matrix which implies that the + // inverse-bind matrices were pre-applied. + _inverse_bind_matrices: Arc>>>, +} + +impl From<&Skin> for Skin { + fn from(value: &Skin) -> Self { + value.clone() } +} - /// Create a new skin. - // TODO: move `Skin` to geometry. - pub fn new_skin(&self, skin: Skin) -> Hybrid { - self.slab.new_value(skin) +impl core::fmt::Debug for Skin { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Skin") + .field("descriptor", &self.descriptor) + .field("joints", &self.joints) + .field("joint_transforms", &"...") + .field("inverse_bind_matrices", &"...") + .finish() } +} + +impl Skin { + /// Stage a new skin on the GPU. + pub fn new( + slab: &SlabAllocator, + joints: impl IntoIterator>, + inverse_bind_matrices: impl IntoIterator>, + ) -> Self { + let descriptor = slab.new_value(SkinDescriptor::default()); + let skin_joints = joints.into_iter().map(|t| t.into()).collect::>(); + let joints = skin_joints.iter().map(|sj| sj.0.id()).collect::>(); + let inverse_bind_matrices = inverse_bind_matrices + .into_iter() + .map(|m| m.into()) + .collect::>(); + let inverse_bind_matrices = if inverse_bind_matrices.is_empty() { + None + } else { + Some(slab.new_array(inverse_bind_matrices).into_gpu_only()) + }; + + Skin { + descriptor, + joints: slab.new_array(joints), + // We hold on to the transforms so they don't get dropped if the user drops them. + _skin_joints: Arc::new(Mutex::new(skin_joints)), + _inverse_bind_matrices: Arc::new(Mutex::new(inverse_bind_matrices)), + } + } + + /// Return a pointer to the underlying descriptor data on the GPU. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Return a copy of the underlying descriptor. + pub fn descriptor(&self) -> SkinDescriptor { + self.descriptor.get() + } +} + +/// A joint in a skinned rigging. +/// +/// This is a thin wrapper over [`Transform`] and +/// [`NestedTransform`]. You can create a [`SkinJoint`] +/// from either of those types using the [`From`] trait. +/// +/// You can also pass an iterator of either [`Transform`] or [`NestedTransform`] +/// to [`Stage::new_skin`](crate::stage::Stage::new_skin). +pub struct SkinJoint(pub(crate) Transform); + +impl From for SkinJoint { + fn from(transform: Transform) -> Self { + SkinJoint(transform) + } +} + +impl From<&Transform> for SkinJoint { + fn from(transform: &Transform) -> Self { + transform.clone().into() + } +} + +impl From for SkinJoint { + fn from(value: NestedTransform) -> Self { + SkinJoint(value.global_transform) + } +} - pub fn new_renderlet(&self, renderlet: Renderlet) -> Hybrid { - self.slab.new_value(renderlet) +impl From<&NestedTransform> for SkinJoint { + fn from(value: &NestedTransform) -> Self { + value.clone().into() } } diff --git a/crates/renderling/src/geometry/shader.rs b/crates/renderling/src/geometry/shader.rs new file mode 100644 index 00000000..93a11ec3 --- /dev/null +++ b/crates/renderling/src/geometry/shader.rs @@ -0,0 +1,84 @@ +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::Mat4; + +use crate::{ + camera::shader::CameraDescriptor, geometry::Vertex, transform::shader::TransformDescriptor, +}; + +/// A vertex skin descriptor. +/// +/// For more info on vertex skinning, see +/// +#[derive(Clone, Copy, Default, SlabItem)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +pub struct SkinDescriptor { + // Ids of the skeleton nodes' global transforms used as joints in this skin. + pub joints_array: Array>, + // Contains the 4x4 inverse-bind matrices. + // + // When is none, each matrix is assumed to be the 4x4 identity matrix + // which implies that the inverse-bind matrices were pre-applied. + pub inverse_bind_matrices_array: Array, +} + +impl SkinDescriptor { + pub fn get_joint_matrix(&self, i: usize, vertex: Vertex, slab: &[u32]) -> Mat4 { + let joint_index = vertex.joints[i] as usize; + let joint_id = slab.read(self.joints_array.at(joint_index)); + let joint_transform = slab.read(joint_id); + // First apply the inverse bind matrix to bring the vertex into the joint's + // local space, then apply the joint's current transformation to move it + // into world space. + let inverse_bind_matrix = slab.read(self.inverse_bind_matrices_array.at(joint_index)); + Mat4::from(joint_transform) * inverse_bind_matrix + } + + pub fn get_skinning_matrix(&self, vertex: Vertex, slab: &[u32]) -> Mat4 { + let mut skinning_matrix = Mat4::ZERO; + for i in 0..vertex.joints.len() { + let joint_matrix = self.get_joint_matrix(i, vertex, slab); + // Ensure weights are applied correctly to the joint matrix + let weight = vertex.weights[i]; + skinning_matrix += weight * joint_matrix; + } + + if skinning_matrix == Mat4::ZERO { + Mat4::IDENTITY + } else { + skinning_matrix + } + } +} + +/// Holds configuration info for vertex and shading render passes of +/// geometry. +/// +/// This descriptor lives at the root (index 0) of the geometry slab. +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Clone, Copy, PartialEq, SlabItem)] +#[offsets] +pub struct GeometryDescriptor { + pub camera_id: Id, + pub atlas_size: glam::UVec2, + pub resolution: glam::UVec2, + pub debug_channel: crate::pbr::debug::DebugChannel, + pub has_lighting: bool, + pub has_skinning: bool, + pub perform_frustum_culling: bool, + pub perform_occlusion_culling: bool, +} + +impl Default for GeometryDescriptor { + fn default() -> Self { + Self { + camera_id: Id::NONE, + atlas_size: Default::default(), + resolution: glam::UVec2::ONE, + debug_channel: Default::default(), + has_lighting: true, + has_skinning: true, + perform_frustum_culling: true, + perform_occlusion_culling: false, + } + } +} diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/gltf.rs similarity index 73% rename from crates/renderling/src/stage/gltf_support.rs rename to crates/renderling/src/gltf.rs index c7a16f0a..b11bdce9 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/gltf.rs @@ -1,4 +1,9 @@ -//! Gltf support for the [`Stage`](crate::Stage). +//! GLTF support. +//! +//! # Loading GLTF files +//! +//! Loading GLTF files is accomplished through [`Stage::load_gltf_document_from_path`] +//! and [`Stage::load_gltf_document_from_bytes`]. use std::{collections::HashMap, sync::Arc}; use craballoc::prelude::*; @@ -8,16 +13,19 @@ use rustc_hash::{FxHashMap, FxHashSet}; use snafu::{OptionExt, ResultExt, Snafu}; use crate::{ - atlas::{AtlasError, AtlasImage, AtlasTexture, TextureAddressMode, TextureModes}, + atlas::{ + shader::AtlasTextureDescriptor, AtlasError, AtlasImage, AtlasTexture, TextureAddressMode, + TextureModes, + }, bvol::Aabb, camera::Camera, - light::{ - AnalyticalLight, DirectionalLightDescriptor, LightStyle, PointLightDescriptor, - SpotLightDescriptor, - }, - pbr::Material, - stage::{MorphTarget, NestedTransform, Renderlet, Skin, Stage, Vertex}, - transform::Transform, + geometry::{Indices, MorphTarget, MorphTargetWeights, MorphTargets, Skin, Vertex, Vertices}, + light::{shader::LightStyle, AnalyticalLight}, + material::Material, + primitive::Primitive, + stage::{Stage, StageError}, + transform::{shader::TransformDescriptor, NestedTransform}, + types::{GpuCpuArray, GpuOnlyArray}, }; mod anime; @@ -25,11 +33,19 @@ pub use anime::*; #[derive(Debug, Snafu)] pub enum StageGltfError { - #[snafu(display("{source}"))] - Stage { source: crate::stage::StageError }, - - #[snafu(display("{source}"))] - Gltf { source: gltf::Error }, + #[snafu( + display( + "GLTF error with '{}': {source}", + path + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or("".to_string())), + visibility(pub(crate)) + )] + Gltf { + source: gltf::Error, + path: Option, + }, #[snafu(display("{source}"))] Atlas { source: crate::atlas::AtlasError }, @@ -43,7 +59,7 @@ pub enum StageGltfError { #[snafu(display("Missing texture at gltf index {index} slab index {tex_id:?}"))] MissingTexture { index: usize, - tex_id: Id, + tex_id: Id, }, #[snafu(display("Missing material with index {index}"))] @@ -86,22 +102,16 @@ pub enum StageGltfError { Animation { source: anime::AnimationError }, } -impl From for StageGltfError { - fn from(source: gltf::Error) -> Self { - Self::Gltf { source } - } -} - impl From for StageGltfError { fn from(source: AtlasError) -> Self { Self::Atlas { source } } } -impl From for Transform { +impl From for TransformDescriptor { fn from(transform: gltf::scene::Transform) -> Self { let (translation, rotation, scale) = transform.decomposed(); - Transform { + TransformDescriptor { translation: Vec3::from_array(translation), rotation: Quat::from_array(rotation), scale: Vec3::from_array(scale), @@ -198,120 +208,103 @@ impl Material { } pub fn from_gltf( + stage: &Stage, material: gltf::Material, - entries: &[Hybrid], + entries: &[AtlasTexture], ) -> Result { let name = material.name().map(String::from); log::trace!("loading material {:?} {name:?}", material.index()); let pbr = material.pbr_metallic_roughness(); - let material = if material.unlit() { + let builder = stage.new_material(); + if material.unlit() { log::trace!(" is unlit"); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { + builder.set_has_lighting(false); + + if let Some(info) = pbr.base_color_texture() { let texture = info.texture(); let index = texture.index(); - let tex_id = entries.get(index).map(|e| e.id()).unwrap_or_default(); - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; - - Material { - albedo_texture_id: albedo_texture, - albedo_tex_coord, - albedo_factor: pbr.base_color_factor().into(), - ..Default::default() + if let Some(tex) = entries.get(index) { + builder.set_albedo_texture(tex); + builder.set_albedo_tex_coord(info.tex_coord()); + } } + builder.set_albedo_factor(pbr.base_color_factor().into()); } else { log::trace!(" is pbr"); - let albedo_factor: Vec4 = pbr.base_color_factor().into(); - let (albedo_texture, albedo_tex_coord) = if let Some(info) = pbr.base_color_texture() { + builder.set_has_lighting(true); + + if let Some(info) = pbr.base_color_texture() { let texture = info.texture(); let index = texture.index(); - let tex_id = entries.get(index).map(|e| e.id()).unwrap_or_default(); - (tex_id, info.tex_coord()) - } else { - (Id::NONE, 0) - }; + if let Some(tex) = entries.get(index) { + builder.set_albedo_texture(tex); + builder.set_albedo_tex_coord(info.tex_coord()); + } + } + builder.set_albedo_factor(pbr.base_color_factor().into()); - let ( - metallic_factor, - roughness_factor, - metallic_roughness_texture, - metallic_roughness_tex_coord, - ) = if let Some(info) = pbr.metallic_roughness_texture() { + if let Some(info) = pbr.metallic_roughness_texture() { let index = info.texture().index(); - let tex_id = entries.get(index).map(|e| e.id()).unwrap_or_default(); - (1.0, 1.0, tex_id, info.tex_coord()) + if let Some(tex) = entries.get(index) { + builder.set_metallic_roughness_texture(tex); + builder.set_metallic_roughness_tex_coord(info.tex_coord()); + } } else { - (pbr.metallic_factor(), pbr.roughness_factor(), Id::NONE, 0) - }; + builder.set_metallic_factor(pbr.metallic_factor()); + builder.set_roughness_factor(pbr.roughness_factor()); + } - let (normal_texture, normal_tex_coord) = - if let Some(norm_tex) = material.normal_texture() { - let tex_id = entries - .get(norm_tex.texture().index()) - .map(|e| e.id()) - .unwrap_or_default(); - (tex_id, norm_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; + if let Some(norm_tex) = material.normal_texture() { + if let Some(tex) = entries.get(norm_tex.texture().index()) { + builder.set_normal_texture(tex); + builder.set_normal_tex_coord(norm_tex.tex_coord()); + } + } - let (ao_strength, ao_texture, ao_tex_coord) = - if let Some(occlusion_tex) = material.occlusion_texture() { - let tex_id = entries - .get(occlusion_tex.texture().index()) - .map(|e| e.id()) - .unwrap_or_default(); - (occlusion_tex.strength(), tex_id, occlusion_tex.tex_coord()) - } else { - (0.0, Id::NONE, 0) - }; + if let Some(occlusion_tex) = material.occlusion_texture() { + if let Some(tex) = entries.get(occlusion_tex.texture().index()) { + builder.set_ambient_occlusion_texture(tex); + builder.set_ambient_occlusion_tex_coord(occlusion_tex.tex_coord()); + builder.set_ambient_occlusion_strength(occlusion_tex.strength()); + } + } - let (emissive_texture, emissive_tex_coord) = - if let Some(emissive_tex) = material.emissive_texture() { - let texture = emissive_tex.texture(); - let index = texture.index(); - let tex_id = entries.get(index).map(|e| e.id()).unwrap_or_default(); - (tex_id, emissive_tex.tex_coord()) - } else { - (Id::NONE, 0) - }; - let emissive_factor = Vec3::from(material.emissive_factor()); - let emissive_strength_multiplier = material.emissive_strength().unwrap_or(1.0); - - Material { - albedo_factor, - metallic_factor, - roughness_factor, - albedo_texture_id: albedo_texture, - metallic_roughness_texture_id: metallic_roughness_texture, - normal_texture_id: normal_texture, - ao_texture_id: ao_texture, - albedo_tex_coord, - metallic_roughness_tex_coord, - normal_tex_coord, - ao_tex_coord, - ao_strength, - emissive_factor, - emissive_strength_multiplier, - emissive_texture_id: emissive_texture, - emissive_tex_coord, - has_lighting: true, + if let Some(emissive_tex) = material.emissive_texture() { + let texture = emissive_tex.texture(); + let index = texture.index(); + if let Some(tex) = entries.get(index) { + builder.set_emissive_texture(tex); + builder.set_emissive_tex_coord(emissive_tex.tex_coord()); + } } + builder.set_emissive_strength_multiplier(material.emissive_strength().unwrap_or(1.0)); }; - Ok(material) + Ok(builder) } } -#[derive(Debug)] -pub struct GltfPrimitive { - pub indices: HybridArray, - pub vertices: HybridArray, +pub struct GltfPrimitive { + pub indices: Indices, + pub vertices: Vertices, pub bounding_box: (Vec3, Vec3), - pub material: Id, - pub morph_targets: Vec>, - pub morph_targets_array: HybridArray>, + pub material_index: Option, + pub morph_targets: MorphTargets, +} + +impl core::fmt::Debug for GltfPrimitive +where + Ct: IsContainer = Array>, + Ct: IsContainer = Array>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("GltfPrimitive") + .field("indices", &self.indices) + .field("vertices", &self.vertices) + .field("bounding_box", &self.bounding_box) + .field("material_index", &self.material_index) + .field("morph_targets", &self.morph_targets) + .finish() + } } impl GltfPrimitive { @@ -319,13 +312,8 @@ impl GltfPrimitive { stage: &Stage, primitive: gltf::Primitive, buffer_data: &[gltf::buffer::Data], - materials: &HybridArray, ) -> Self { - let material = primitive - .material() - .index() - .map(|index| materials.array().at(index)) - .unwrap_or_default(); + let material_index = primitive.material().index(); let reader = primitive.reader(|buffer| { let data = buffer_data.get(buffer.index())?; @@ -508,13 +496,7 @@ impl GltfPrimitive { morph_targets.len(), morph_targets.iter().map(|mt| mt.len()).collect::>() ); - let morph_targets = morph_targets - .into_iter() - .map(|verts| stage.new_morph_targets(verts)) - .collect::>(); - let morph_targets_array = - stage.new_morph_targets_array(morph_targets.iter().map(HybridArray::array)); - + let morph_targets = stage.new_morph_targets(morph_targets); let vs = joints.into_iter().zip(weights); let vs = colors.zip(vs); let vs = tangents.into_iter().zip(vs); @@ -544,9 +526,13 @@ impl GltfPrimitive { ) .collect::>(); let vertices = stage.new_vertices(vertices); - log::debug!("{} vertices, {:?}", vertices.len(), vertices.array()); + log::debug!( + "{} vertices, {:?}", + vertices.array().len(), + vertices.array() + ); let indices = stage.new_indices(indices); - log::debug!("{} indices, {:?}", indices.len(), indices.array()); + log::debug!("{} indices, {:?}", indices.array().len(), indices.array()); let (bbmin, bbmax) = { let gltf::mesh::Bounds { min, max } = primitive.bounding_box(); (Vec3::from_array(min), Vec3::from_array(max)) @@ -564,39 +550,77 @@ impl GltfPrimitive { Self { vertices, indices, - material, + material_index, + morph_targets, + bounding_box, + } + } + + pub fn into_gpu_only(self) -> GltfPrimitive { + let Self { + indices, + vertices, + bounding_box, + material_index, morph_targets, - morph_targets_array, + } = self; + GltfPrimitive { + indices: indices.into_gpu_only(), + vertices: vertices.into_gpu_only(), bounding_box, + material_index, + morph_targets, } } } -#[derive(Debug)] -pub struct GltfMesh { +pub struct GltfMesh { /// Mesh primitives, aka meshlets - pub primitives: Vec, + pub primitives: Vec>, /// Morph target weights - pub weights: HybridArray, + pub weights: MorphTargetWeights, +} + +impl core::fmt::Debug for GltfMesh +where + Ct: IsContainer = Array>, + Ct: IsContainer = Array>, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("GltfMesh") + .field("primitives", &self.primitives) + .field("weights", &self.weights) + .finish() + } } impl GltfMesh { - fn from_gltf( - stage: &Stage, - buffer_data: &[gltf::buffer::Data], - materials: &HybridArray, - mesh: gltf::Mesh, - ) -> Self { + fn from_gltf(stage: &Stage, buffer_data: &[gltf::buffer::Data], mesh: gltf::Mesh) -> Self { log::debug!("Loading primitives for mesh {}", mesh.index()); let primitives = mesh .primitives() - .map(|prim| GltfPrimitive::from_gltf(stage, prim, buffer_data, materials)) + .map(|prim| GltfPrimitive::from_gltf(stage, prim, buffer_data)) .collect::>(); log::debug!(" loaded {} primitives\n", primitives.len()); let weights = mesh.weights().unwrap_or(&[]).iter().copied(); GltfMesh { primitives, - weights: stage.new_weights(weights), + weights: stage.new_morph_target_weights(weights), + } + } + + pub fn into_gpu_only(self) -> GltfMesh { + let Self { + primitives, + weights, + } = self; + let primitives = primitives + .into_iter() + .map(GltfPrimitive::into_gpu_only) + .collect::>(); + GltfMesh { + primitives, + weights, } } } @@ -606,12 +630,11 @@ pub struct GltfCamera { pub index: usize, pub name: Option, pub node_transform: NestedTransform, - projection: Mat4, - pub camera: Hybrid, + pub camera: Camera, } -impl AsRef> for GltfCamera { - fn as_ref(&self) -> &Hybrid { +impl AsRef for GltfCamera { + fn as_ref(&self) -> &Camera { &self.camera } } @@ -619,7 +642,7 @@ impl AsRef> for GltfCamera { impl GltfCamera { fn new(stage: &Stage, gltf_camera: gltf::Camera<'_>, transform: &NestedTransform) -> Self { log::debug!("camera: {}", gltf_camera.name().unwrap_or("unknown")); - log::debug!(" transform: {:#?}", transform.get_global_transform()); + log::debug!(" transform: {:#?}", transform.global_descriptor()); let projection = match gltf_camera.projection() { gltf::camera::Projection::Orthographic(o) => glam::Mat4::orthographic_rh( -o.xmag(), @@ -643,21 +666,17 @@ impl GltfCamera { } } }; - let view = Mat4::from(transform.get_global_transform()).inverse(); - let camera = stage.new_camera(Camera::new(projection, view)); + let view = Mat4::from(transform.global_descriptor()).inverse(); + let camera = stage + .new_camera() + .with_projection_and_view(projection, view); GltfCamera { index: gltf_camera.index(), name: gltf_camera.name().map(String::from), - projection, node_transform: transform.clone(), camera, } } - - pub fn get_camera(&self) -> Camera { - let view = Mat4::from(self.node_transform.get_global_transform()).inverse(); - Camera::new(self.projection, view) - } } /// A node in a GLTF document, ready to be 'drawn'. @@ -679,15 +698,15 @@ pub struct GltfNode { /// /// Each element indexes into the `GltfDocument`'s `nodes` field. pub children: Vec, - /// Array of morph target weights - pub weights: HybridArray, + /// Morph target weights + pub weights: MorphTargetWeights, /// This node's transform. pub transform: NestedTransform, } impl GltfNode { - pub fn global_transform(&self) -> Transform { - self.transform.get_global_transform() + pub fn global_transform(&self) -> TransformDescriptor { + self.transform.global_descriptor() } } @@ -697,17 +716,11 @@ pub struct GltfSkin { // Indices of the skeleton nodes used as joints in this skin, unused internally // but possibly useful. pub joint_nodes: Vec, - pub joint_transforms: HybridArray>, - // Containins the 4x4 inverse-bind matrices. - // - // When None, each matrix is assumed to be the 4x4 identity matrix which implies that the - // inverse-bind matrices were pre-applied. - pub inverse_bind_matrices: Option>, // Index of the node used as the skeleton root. // When None, joints transforms resolve to scene root. pub skeleton: Option, - // Skin as seen by shaders, on the GPU - pub skin: Hybrid, + // Skin as seen by renderling + pub skin: Skin, } impl GltfSkin { @@ -715,34 +728,33 @@ impl GltfSkin { stage: &Stage, buffer_data: &[gltf::buffer::Data], nodes: &[GltfNode], - skin: gltf::Skin, + gltf_skin: gltf::Skin, ) -> Result { - log::debug!("reading skin {} {:?}", skin.index(), skin.name()); - let joint_nodes = skin.joints().map(|n| n.index()).collect::>(); + log::debug!("reading skin {} {:?}", gltf_skin.index(), gltf_skin.name()); + let joint_nodes = gltf_skin.joints().map(|n| n.index()).collect::>(); log::debug!(" has {} joints", joint_nodes.len()); let mut joint_transforms = vec![]; for node_index in joint_nodes.iter() { let gltf_node: &GltfNode = nodes .get(*node_index) .context(MissingNodeSnafu { index: *node_index })?; - let transform_id = gltf_node.transform.global_transform_id(); + let transform_id = gltf_node.transform.global_descriptor(); log::debug!(" joint node {node_index} is {transform_id:?}"); - joint_transforms.push(transform_id); + joint_transforms.push(gltf_node.transform.clone()); } - let joint_transforms = stage.new_joint_transform_ids(joint_transforms); - let reader = skin.reader(|b| buffer_data.get(b.index()).map(|d| d.0.as_slice())); - let inverse_bind_matrices = if let Some(mats) = reader.read_inverse_bind_matrices() { + let reader = gltf_skin.reader(|b| buffer_data.get(b.index()).map(|d| d.0.as_slice())); + let mut inverse_bind_matrices = vec![]; + if let Some(mats) = reader.read_inverse_bind_matrices() { let invs = mats .into_iter() .map(|m| Mat4::from_cols_array_2d(&m)) .collect::>(); log::debug!(" has {} inverse bind matrices", invs.len()); - Some(stage.new_matrices(invs)) + inverse_bind_matrices = invs; } else { log::debug!(" no inverse bind matrices"); - None - }; - let skeleton = if let Some(n) = skin.skeleton() { + } + let skeleton = if let Some(n) = gltf_skin.skeleton() { let index = n.index(); log::debug!(" skeleton is node {index}, {:?}", n.name()); Some(index) @@ -751,36 +763,40 @@ impl GltfSkin { None }; Ok(GltfSkin { - index: skin.index(), - skin: stage.new_skin(Skin { - joints: joint_transforms.array(), - inverse_bind_matrices: inverse_bind_matrices - .as_ref() - .map(|a| a.array()) - .unwrap_or_default(), - }), + index: gltf_skin.index(), + skin: stage.new_skin(joint_transforms, inverse_bind_matrices), joint_nodes, - joint_transforms, - inverse_bind_matrices, skeleton, }) } } /// A loaded GLTF document. -pub struct GltfDocument { +/// +/// After being loaded, a [`GltfDocument`] is a collection of staged resources. +/// +/// All primitives are automatically added to the [`Stage`] they were loaded +/// from. +/// +/// ## Note +/// +/// After being loaded, the `meshes` field contains [`Vertices`] and [`Indices`] +/// that can be inspected from the CPU. This has memory implications, so if your +/// document contains lots of geometric data it is advised that you unload that +/// data from the CPU using [`GltfDocument::into_gpu_only`]. +pub struct GltfDocument { pub animations: Vec, pub cameras: Vec, - pub default_material: Hybrid, pub default_scene: Option, pub extensions: Option, - pub textures: Vec>, + pub textures: Vec, pub lights: Vec, - pub meshes: Vec, + pub meshes: Vec>, pub nodes: Vec, - pub materials: HybridArray, - // map of node index to renderlets - pub renderlets: FxHashMap>>, + pub default_material: Material, + pub materials: Vec, + // map of node index to primitives + pub primitives: FxHashMap>, /// Vector of scenes - each being a list of nodes. pub scenes: Vec>, pub skins: Vec, @@ -792,7 +808,7 @@ impl GltfDocument { document: &gltf::Document, buffer_data: Vec, images: Vec, - ) -> Result { + ) -> Result { let textures = { let mut images = images.into_iter().map(AtlasImage::from).collect::>(); for gltf_material in document.materials() { @@ -854,10 +870,10 @@ impl GltfDocument { Err(aimg) => aimg.as_ref().clone(), }) .collect(); - let hybrid_textures = stage.add_images(prepared_images).context(StageSnafu)?; - let mut texture_lookup = FxHashMap::>::default(); + let hybrid_textures = stage.add_images(prepared_images)?; + let mut texture_lookup = FxHashMap::::default(); for (hybrid, (tex, refs)) in hybrid_textures.into_iter().zip(deduped_textures) { - hybrid.modify(|t| t.modes = tex.modes); + hybrid.set_modes(tex.modes); for tex_index in refs.into_iter() { texture_lookup.insert(tex_index, hybrid.clone()); } @@ -871,27 +887,26 @@ impl GltfDocument { }; log::debug!("Creating materials"); - let default_material = stage.new_material(Material::default()); + let mut default_material = stage.default_material().clone(); let mut materials = vec![]; for gltf_material in document.materials() { let material_index = gltf_material.index(); - let material = Material::from_gltf(gltf_material, &textures)?; + let material = Material::from_gltf(stage, gltf_material, &textures)?; if let Some(index) = material_index { log::trace!(" created material {index}"); debug_assert_eq!(index, materials.len(), "unexpected material index"); materials.push(material); } else { log::trace!(" created default material"); - default_material.set(material); + default_material = material; } } - let materials = stage.new_materials(materials); log::trace!(" created {} materials", materials.len()); log::debug!("Loading meshes"); let mut meshes = vec![]; for mesh in document.meshes() { - let mesh = GltfMesh::from_gltf(stage, &buffer_data, &materials, mesh); + let mesh = GltfMesh::from_gltf(stage, &buffer_data, mesh); meshes.push(mesh); } log::trace!(" loaded {} meshes", meshes.len()); @@ -912,8 +927,16 @@ impl GltfDocument { let nt = if let Some(nt) = cache.get(&node.index()) { nt.clone() } else { - let transform = stage.new_nested_transform(); - transform.set(node.transform().into()); + let TransformDescriptor { + translation, + rotation, + scale, + } = node.transform().into(); + let transform = stage + .new_nested_transform() + .with_local_translation(translation) + .with_local_rotation(rotation) + .with_local_scale(scale); for node in node.children() { let child_transform = transform_for_node(nesting_level + 1, stage, cache, &node); @@ -922,7 +945,7 @@ impl GltfDocument { cache.insert(node.index(), transform.clone()); transform }; - let t = nt.get(); + let t = nt.local_descriptor(); log::trace!( "{padding}{} {:?} {:?} {:?} {:?}", node.index(), @@ -961,10 +984,10 @@ impl GltfDocument { if let Some(mesh) = node.mesh() { meshes[mesh.index()].weights.clone() } else { - stage.new_weights(weights) + stage.new_morph_target_weights(weights) } } else { - stage.new_weights(weights) + stage.new_morph_target_weights(weights) }; let transform = transform_for_node(0, stage, &mut node_transforms, &node); nodes.push(GltfNode { @@ -1016,9 +1039,10 @@ impl GltfDocument { let intensity = gltf_light.intensity(); let light_bundle = match gltf_light.kind() { gltf::khr_lights_punctual::Kind::Directional => { - stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::NEG_Z, - color, + stage + .new_directional_light() + .with_direction(Vec3::NEG_Z) + .with_color(color) // TODO: Set a unit for lighting. // We don't yet use a unit for our lighting, and we should. // https://www.realtimerendering.com/blog/physical-units-for-lights/ @@ -1030,36 +1054,36 @@ impl GltfDocument { // 1. https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md // 2. https://depts.washington.edu/mictech/optics/me557/Radiometry.pdf // 3. https://projects.blender.org/blender/blender-addons/commit/9d903a93f03b - intensity: intensity / 683.0, - }) + .with_intensity(intensity / 683.0) + .into_generic() } - gltf::khr_lights_punctual::Kind::Point => { - stage.new_analytical_light(PointLightDescriptor { - position: Vec3::ZERO, - color, - intensity: intensity / 683.0, - }) - } + gltf::khr_lights_punctual::Kind::Point => stage + .new_point_light() + .with_position(Vec3::ZERO) + .with_color(color) + .with_intensity(intensity / 683.0) + .into_generic(), gltf::khr_lights_punctual::Kind::Spot { inner_cone_angle, outer_cone_angle, - } => stage.new_analytical_light(SpotLightDescriptor { - position: Vec3::ZERO, - direction: Vec3::NEG_Z, - inner_cutoff: inner_cone_angle, - outer_cutoff: outer_cone_angle, - color, - intensity: intensity / (683.0 * 4.0 * std::f32::consts::PI), - }), + } => stage + .new_spot_light() + .with_position(Vec3::ZERO) + .with_direction(Vec3::NEG_Z) + .with_inner_cutoff(inner_cone_angle) + .with_outer_cutoff(outer_cone_angle) + .with_color(color) + .with_intensity(intensity / (683.0 * 4.0 * std::f32::consts::PI)) + .into_generic(), }; log::trace!( " linking light {:?} with node transform {:?}: {:#?}", - light_bundle.light().id(), - node_transform.global_transform_id(), - node_transform.get_global_transform() + light_bundle.id(), + node_transform.global_id(), + node_transform.global_descriptor() ); light_bundle.link_node_transform(&node_transform); lights.push(light_bundle); @@ -1088,14 +1112,14 @@ impl GltfDocument { let mut renderlets = FxHashMap::default(); for gltf_node in nodes.iter() { let mut node_renderlets = vec![]; - let skin_id = if let Some(skin_index) = gltf_node.skin { + let maybe_skin = if let Some(skin_index) = gltf_node.skin { log::debug!(" node {} {:?} has skin", gltf_node.index, gltf_node.name); let gltf_skin = skins .get(skin_index) .context(MissingSkinSnafu { index: skin_index })?; - gltf_skin.skin.id() + Some(gltf_skin.skin.clone()) } else { - Id::NONE + None }; if let Some(mesh_index) = gltf_node.mesh { @@ -1110,20 +1134,26 @@ impl GltfDocument { let num_prims = mesh.primitives.len(); log::trace!(" has {num_prims} primitives"); for (prim, i) in mesh.primitives.iter().zip(1..) { - let hybrid = stage.new_renderlet(Renderlet { - vertices_array: prim.vertices.array(), - indices_array: prim.indices.array(), - transform_id: gltf_node.transform.global_transform_id(), - material_id: prim.material, - skin_id, - morph_targets: prim.morph_targets_array.array(), - morph_weights: gltf_node.weights.array(), - bounds: prim.bounding_box.into(), - ..Default::default() - }); - log::trace!(" created renderlet {i}/{num_prims}: {:#?}", hybrid.get()); - stage.add_renderlet(&hybrid); - node_renderlets.push(hybrid); + let material = prim + .material_index + .and_then(|index| materials.get(index)) + .unwrap_or(&default_material); + let renderlet = stage + .new_primitive() + .with_vertices(&prim.vertices) + .with_indices(&prim.indices) + .with_transform(&gltf_node.transform) + .with_material(material) + .with_bounds(prim.bounding_box.into()) + .with_morph_targets(&prim.morph_targets, &gltf_node.weights); + if let Some(skin) = maybe_skin.as_ref() { + renderlet.set_skin(skin); + } + log::trace!( + " created renderlet {i}/{num_prims}: {:#?}", + renderlet.id() + ); + node_renderlets.push(renderlet); } } if !node_renderlets.is_empty() { @@ -1147,7 +1177,7 @@ impl GltfDocument { scenes, skins, default_scene: document.default_scene().map(|scene| scene.index()), - renderlets, + primitives: renderlets, extensions: document .extensions() .cloned() @@ -1155,8 +1185,54 @@ impl GltfDocument { }) } - pub fn renderlets_iter(&self) -> impl Iterator> { - self.renderlets.iter().flat_map(|(_, rs)| rs.iter()) + /// Unload vertex and index data from the CPU. + /// + /// The data can still be updated from the CPU, but will not be inspectable. + pub fn into_gpu_only(self) -> GltfDocument { + let Self { + animations, + cameras, + default_scene, + extensions, + textures, + lights, + meshes, + nodes, + default_material, + materials, + primitives, + scenes, + skins, + } = self; + let meshes = meshes + .into_iter() + .map(GltfMesh::into_gpu_only) + .collect::>(); + GltfDocument { + animations, + cameras, + default_scene, + extensions, + textures, + lights, + meshes, + nodes, + default_material, + materials, + primitives, + scenes, + skins, + } + } +} + +impl GltfDocument +where + Ct: IsContainer = Array>, + Ct: IsContainer = Array>, +{ + pub fn renderlets_iter(&self) -> impl Iterator { + self.primitives.iter().flat_map(|(_, rs)| rs.iter()) } pub fn nodes_in_scene(&self, scene_index: usize) -> impl Iterator { @@ -1196,30 +1272,9 @@ impl GltfDocument { } } -impl Stage { - pub fn load_gltf_document_from_path( - &self, - path: impl AsRef, - ) -> Result { - let (document, buffers, images) = gltf::import(path)?; - GltfDocument::from_gltf(self, &document, buffers, images) - } - - pub fn load_gltf_document_from_bytes( - &self, - bytes: impl AsRef<[u8]>, - ) -> Result { - let (document, buffers, images) = gltf::import_slice(bytes)?; - GltfDocument::from_gltf(self, &document, buffers, images) - } -} - #[cfg(test)] mod test { - use crate::{ - camera::Camera, pbr::Material, stage::Vertex, test::BlockOnFuture, transform::Transform, - Context, - }; + use crate::{context::Context, geometry::Vertex, test::BlockOnFuture}; use glam::{Vec3, Vec4}; #[test] @@ -1253,7 +1308,9 @@ mod test { .with_lighting(false) .with_bloom(false) .with_background_color(Vec3::splat(0.0).extend(1.0)); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let _doc = stage .load_gltf_document_from_path("../../gltf/gltfTutorial_008_SimpleMeshes.gltf") .unwrap(); @@ -1277,7 +1334,9 @@ mod test { let projection = crate::camera::perspective(20.0, 20.0); let eye = Vec3::new(0.5, 0.5, 2.0); let view = crate::camera::look_at(eye, Vec3::new(0.5, 0.5, 0.0), Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let _doc = stage .load_gltf_document_from_path("../../gltf/gltfTutorial_003_MinimalGltfFile.gltf") @@ -1300,38 +1359,42 @@ mod test { .with_lighting(false) .with_background_color(Vec4::splat(1.0)); let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let doc = stage .load_gltf_document_from_path("../../gltf/cheetah_cone.glb") .unwrap(); assert!(!doc.textures.is_empty()); - let (material, _vertices, _indices, _transform, _renderlet) = stage - .builder() - .with_material(Material { - albedo_texture_id: doc.textures[0].id(), - has_lighting: false, - ..Default::default() - }) - .with_vertices([ - Vertex::default() - .with_position([0.0, 0.0, 0.0]) - .with_uv0([0.0, 0.0]), - Vertex::default() - .with_position([1.0, 0.0, 0.0]) - .with_uv0([1.0, 0.0]), - Vertex::default() - .with_position([1.0, 1.0, 0.0]) - .with_uv0([1.0, 1.0]), - Vertex::default() - .with_position([0.0, 1.0, 0.0]) - .with_uv0([0.0, 1.0]), - ]) - .with_indices([0u32, 3, 2, 0, 2, 1]) - .with_transform(Transform { - scale: Vec3::new(100.0, 100.0, 1.0), - ..Default::default() - }) - .build(); + let material = stage + .new_material() + .with_albedo_texture(&doc.textures[0]) + .with_has_lighting(false); + let _rez = stage + .new_primitive() + .with_material(&material) + .with_vertices( + stage.new_vertices([ + Vertex::default() + .with_position([0.0, 0.0, 0.0]) + .with_uv0([0.0, 0.0]), + Vertex::default() + .with_position([1.0, 0.0, 0.0]) + .with_uv0([1.0, 0.0]), + Vertex::default() + .with_position([1.0, 1.0, 0.0]) + .with_uv0([1.0, 1.0]), + Vertex::default() + .with_position([0.0, 1.0, 0.0]) + .with_uv0([0.0, 1.0]), + ]), + ) + .with_indices(stage.new_indices([0u32, 3, 2, 0, 2, 1])) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(100.0, 100.0, 1.0)), + ); println!("material_id: {:#?}", material.id()); let frame = ctx.get_next_frame().unwrap(); @@ -1354,7 +1417,9 @@ mod test { let projection = crate::camera::perspective(size as f32, size as f32); let view = crate::camera::look_at(Vec3::new(0.5, 0.5, 1.25), Vec3::new(0.5, 0.5, 0.0), Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let _doc = stage .load_gltf_document_from_path("../../gltf/gltfTutorial_013_SimpleTexture.gltf") @@ -1407,11 +1472,12 @@ mod test { let up = Vec3::Y; let view = glam::Mat4::look_at_rh(eye, target, up); - let _camera = stage.new_camera(Camera::new(projection, view)); - let doc = stage + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + let _doc = stage .load_gltf_document_from_path("../../gltf/Fox.glb") .unwrap(); - log::info!("renderlets: {:#?}", doc.renderlets); // render a frame without vertex skinning as a baseline let frame = ctx.get_next_frame().unwrap(); @@ -1483,10 +1549,19 @@ mod test { .unwrap(); let camera_a = doc.cameras.first().unwrap(); - let eq = |p: Vec3| p.distance(camera_a.get_camera().position()) <= 10e-6; - let either_y_up_or_z_up = eq(Vec3::new(14.699949, 4.958309, 12.676651)) - || eq(Vec3::new(14.699949, -12.676651, 4.958309)); - assert!(either_y_up_or_z_up); + let desc = camera_a.camera.descriptor(); + const THRESHOLD: f32 = 10e-6; + let a = Vec3::new(14.699949, 4.958309, 12.676651); + let b = Vec3::new(14.699949, -12.676651, 4.958309); + let distance_a = a.distance(desc.position()); + let distance_b = b.distance(desc.position()); + if distance_a > THRESHOLD && distance_b > THRESHOLD { + println!("desc: {desc:#?}"); + println!("distance_a: {distance_a}"); + println!("distance_b: {distance_b}"); + println!("threshold: {THRESHOLD}"); + panic!("distance greater than threshold"); + } let doc = stage .load_gltf_document_from_path( @@ -1505,8 +1580,8 @@ mod test { a.distance(b) <= 10e-6 || c.distance(c) <= 10e-6 }; assert!(eq( - camera_a.get_camera().position(), - camera_b.get_camera().position() + camera_a.camera.descriptor().position(), + camera_b.camera.descriptor().position() )); } } diff --git a/crates/renderling/src/stage/gltf_support/anime.rs b/crates/renderling/src/gltf/anime.rs similarity index 97% rename from crates/renderling/src/stage/gltf_support/anime.rs rename to crates/renderling/src/gltf/anime.rs index 72958a41..3d750e80 100644 --- a/crates/renderling/src/stage/gltf_support/anime.rs +++ b/crates/renderling/src/gltf/anime.rs @@ -1,9 +1,8 @@ //! Animation helpers for gltf. -use craballoc::prelude::HybridArray; use glam::{Quat, Vec3}; use snafu::prelude::*; -use crate::stage::{gltf_support::GltfNode, NestedTransform}; +use crate::{geometry::MorphTargetWeights, gltf::GltfNode, transform::NestedTransform}; #[derive(Debug, Snafu)] pub enum InterpolationError { @@ -546,7 +545,7 @@ impl Tween { #[derive(Clone, Debug)] pub struct AnimationNode { pub transform: NestedTransform, - pub morph_weights: HybridArray, + pub morph_weights: MorphTargetWeights, } impl From<&GltfNode> for (usize, AnimationNode) { @@ -737,26 +736,20 @@ impl Animator { if let Some(node) = self.nodes.get(&node_index) { match property { TweenProperty::Translation(translation) => { - node.transform.modify(|t| { - t.translation = translation; - }); + node.transform.set_local_translation(translation); } TweenProperty::Rotation(rotation) => { - node.transform.modify(|t| { - t.rotation = rotation; - }); + node.transform.set_local_rotation(rotation); } TweenProperty::Scale(scale) => { - node.transform.modify(|t| { - t.scale = scale; - }); + node.transform.set_local_scale(scale); } TweenProperty::MorphTargetWeights(new_weights) => { - if node.morph_weights.is_empty() { + if node.morph_weights.array().is_empty() { log::error!("animation is applied to morph targets but node {node_index} is missing weights"); } else { for (i, w) in new_weights.into_iter().enumerate() { - let _ = node.morph_weights.set_item(i, w); + node.morph_weights.set_item(i, w); } } } @@ -771,7 +764,7 @@ impl Animator { #[cfg(test)] mod test { - use crate::{camera::Camera, stage::Animator, test::BlockOnFuture, Context}; + use crate::{context::Context, gltf::Animator, test::BlockOnFuture}; use glam::Vec3; #[test] @@ -783,7 +776,9 @@ mod test { .with_background_color(Vec3::ZERO.extend(1.0)); let projection = crate::camera::perspective(50.0, 50.0); let view = crate::camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let doc = stage .load_gltf_document_from_path("../../gltf/animated_triangle.gltf") diff --git a/crates/renderling/src/ibl/diffuse_irradiance/cpu.rs b/crates/renderling/src/ibl/diffuse_irradiance/cpu.rs deleted file mode 100644 index 141cdb9c..00000000 --- a/crates/renderling/src/ibl/diffuse_irradiance/cpu.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Pipeline and bindings for for diffuse irradiance convolution shaders. - -pub fn diffuse_irradiance_convolution_bindgroup_layout( - device: &wgpu::Device, -) -> wgpu::BindGroupLayout { - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("convolution bindgroup"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::Cube, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }) -} - -pub fn diffuse_irradiance_convolution_bindgroup( - device: &wgpu::Device, - label: Option<&str>, - buffer: &wgpu::Buffer, - // The texture to sample the environment from - texture: &crate::texture::Texture, -) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label, - layout: &diffuse_irradiance_convolution_bindgroup_layout(device), - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&texture.view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&texture.sampler), - }, - ], - }) -} - -pub struct DiffuseIrradianceConvolutionRenderPipeline(pub wgpu::RenderPipeline); - -impl DiffuseIrradianceConvolutionRenderPipeline { - /// Create the rendering pipeline that performs a convolution. - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { - log::trace!("creating convolution render pipeline with format '{format:?}'"); - let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device); - let fragment_linkage = crate::linkage::di_convolution_fragment::linkage(device); - // let fragment_shader = device.create_shader_module(wgpu::include_wgsl!( - // // TODO: rewrite this shader in Rust after atomics are added to naga spv - // "../../wgsl/diffuse_irradiance_convolution.wgsl" - // )); - log::trace!(" done."); - let bg_layout = diffuse_irradiance_convolution_bindgroup_layout(device); - let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("convolution pipeline layout"), - bind_group_layouts: &[&bg_layout], - push_constant_ranges: &[], - }); - // TODO: merge irradiance pipeline with cubemap - let pipeline = DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( - &wgpu::RenderPipelineDescriptor { - label: Some("convolution pipeline"), - layout: Some(&pp_layout), - vertex: wgpu::VertexState { - module: &vertex_linkage.module, - entry_point: Some(vertex_linkage.entry_point), - buffers: &[], - compilation_options: Default::default(), - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_linkage.module, - entry_point: Some(fragment_linkage.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - multiview: None, - cache: None, - }, - )); - log::trace!(" completed pipeline creation"); - pipeline - } -} diff --git a/crates/renderling/src/ibl/mod.rs b/crates/renderling/src/ibl/mod.rs deleted file mode 100644 index fe2a39a6..00000000 --- a/crates/renderling/src/ibl/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Resources for image based lighting. - -pub mod diffuse_irradiance; -#[cfg(not(target_arch = "spirv"))] -pub mod prefiltered_environment; diff --git a/crates/renderling/src/ibl/prefiltered_environment.rs b/crates/renderling/src/ibl/prefiltered_environment.rs deleted file mode 100644 index 353922c0..00000000 --- a/crates/renderling/src/ibl/prefiltered_environment.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Pipeline for creating a prefiltered environment map from an existing -//! environment cubemap. - -pub fn create_pipeline_and_bindgroup( - device: &wgpu::Device, - buffer: &wgpu::Buffer, - environment_texture: &crate::texture::Texture, -) -> (wgpu::RenderPipeline, wgpu::BindGroup) { - let label = Some("prefiltered environment"); - let bindgroup_layout_desc = wgpu::BindGroupLayoutDescriptor { - label, - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::Cube, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }; - let bg_layout = device.create_bind_group_layout(&bindgroup_layout_desc); - let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { - label, - layout: &bg_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&environment_texture.view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&environment_texture.sampler), - }, - ], - }); - let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label, - bind_group_layouts: &[&bg_layout], - push_constant_ranges: &[], - }); - let vertex_linkage = crate::linkage::prefilter_environment_cubemap_vertex::linkage(device); - let fragment_linkage = crate::linkage::prefilter_environment_cubemap_fragment::linkage(device); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("prefiltered environment"), - layout: Some(&pp_layout), - vertex: wgpu::VertexState { - module: &vertex_linkage.module, - entry_point: Some(vertex_linkage.entry_point), - buffers: &[], - compilation_options: Default::default(), - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_linkage.module, - entry_point: Some(fragment_linkage.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - multiview: None, - cache: None, - }); - (pipeline, bindgroup) -} diff --git a/crates/renderling/src/internal.rs b/crates/renderling/src/internal.rs index 512ecbf2..8cbf38b1 100644 --- a/crates/renderling/src/internal.rs +++ b/crates/renderling/src/internal.rs @@ -7,180 +7,7 @@ //! They are public here because they are needed for integration tests, and //! on the off-chance that somebody wants to build something with them. -use std::sync::Arc; - -use snafu::{OptionExt, ResultExt}; - -use crate::{ - CannotCreateAdaptorSnafu, CannotRequestDeviceSnafu, ContextError, IncompatibleSurfaceSnafu, - RenderTarget, RenderTargetInner, -}; - -/// Create a new [`wgpu::Adapter`]. -pub async fn adapter( - instance: &wgpu::Instance, - compatible_surface: Option<&wgpu::Surface<'_>>, -) -> Result { - log::trace!( - "creating adapter for a {} context", - if compatible_surface.is_none() { - "headless" - } else { - "surface-based" - } - ); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface, - force_fallback_adapter: false, - }) - .await - .context(CannotCreateAdaptorSnafu)?; - - log::info!("Adapter selected: {:?}", adapter.get_info()); - let info = adapter.get_info(); - log::info!( - "using adapter: '{}' backend:{:?} driver:'{}'", - info.name, - info.backend, - info.driver - ); - Ok(adapter) -} - -/// Create a new [`wgpu::Device`]. -pub async fn device( - adapter: &wgpu::Adapter, -) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { - let wanted_features = wgpu::Features::INDIRECT_FIRST_INSTANCE - | wgpu::Features::MULTI_DRAW_INDIRECT - //// when debugging rust-gpu shader miscompilation it's nice to have this - //| wgpu::Features::SPIRV_SHADER_PASSTHROUGH - // this one is a funny requirement, it seems it is needed if using storage buffers in - // vertex shaders, even if those shaders are read-only - | wgpu::Features::VERTEX_WRITABLE_STORAGE - | wgpu::Features::CLEAR_TEXTURE; - let supported_features = adapter.features(); - let required_features = wanted_features.intersection(supported_features); - let unsupported_features = wanted_features.difference(supported_features); - if !unsupported_features.is_empty() { - log::error!("requested but unsupported features: {unsupported_features:#?}"); - log::warn!("requested and supported features: {supported_features:#?}"); - } - let limits = adapter.limits(); - log::info!("adapter limits: {limits:#?}"); - adapter - .request_device(&wgpu::DeviceDescriptor { - required_features, - required_limits: adapter.limits(), - label: None, - memory_hints: wgpu::MemoryHints::default(), - trace: wgpu::Trace::Off, - }) - .await -} - -/// Create a new instance. -/// -/// This is for internal use. It is not necessary to create your own `wgpu` -/// instance to use this library. -pub fn new_instance(backends: Option) -> wgpu::Instance { - log::info!( - "creating instance - available backends: {:#?}", - wgpu::Instance::enabled_backend_features() - ); - // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU - let backends = backends.unwrap_or(wgpu::Backends::PRIMARY); - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends, - ..Default::default() - }); - - #[cfg(not(target_arch = "wasm32"))] - { - let adapters = instance.enumerate_adapters(backends); - log::trace!("available adapters: {adapters:#?}"); - } - - instance -} - -/// Create a new suite of `wgpu` machinery using a window or canvas. -/// -/// ## Note -/// This function is used internally. -pub async fn new_windowed_adapter_device_queue( - width: u32, - height: u32, - instance: &wgpu::Instance, - window: impl Into>, -) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { - let surface = instance - .create_surface(window) - .map_err(|e| ContextError::CreateSurface { source: e })?; - let adapter = adapter(instance, Some(&surface)).await?; - let surface_caps = surface.get_capabilities(&adapter); - let fmt = if surface_caps - .formats - .contains(&wgpu::TextureFormat::Rgba8UnormSrgb) - { - wgpu::TextureFormat::Rgba8UnormSrgb - } else { - surface_caps - .formats - .iter() - .copied() - .find(|f| f.is_srgb()) - .unwrap_or(surface_caps.formats[0]) - }; - let view_fmts = if fmt.is_srgb() { - vec![] - } else { - vec![fmt.add_srgb_suffix()] - }; - log::info!("surface capabilities: {surface_caps:#?}"); - let mut surface_config = surface - .get_default_config(&adapter, width, height) - .context(IncompatibleSurfaceSnafu)?; - surface_config.view_formats = view_fmts; - let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; - surface.configure(&device, &surface_config); - let target = RenderTarget(RenderTargetInner::Surface { - surface, - surface_config, - }); - Ok((adapter, device, queue, target)) -} - -/// Create a new suite of `wgpu` machinery that renders to a texture. -/// -/// ## Note -/// This function is used internally. -pub async fn new_headless_device_queue_and_target( - width: u32, - height: u32, - instance: &wgpu::Instance, -) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { - let adapter = adapter(instance, None).await?; - let texture_desc = wgpu::TextureDescriptor { - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - label: None, - view_formats: &[], - }; - let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; - let texture = Arc::new(device.create_texture(&texture_desc)); - let target = RenderTarget(RenderTargetInner::Texture { texture }); - Ok((adapter, device, queue, target)) -} +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; diff --git a/crates/renderling/src/internal/cpu.rs b/crates/renderling/src/internal/cpu.rs new file mode 100644 index 00000000..c0566de0 --- /dev/null +++ b/crates/renderling/src/internal/cpu.rs @@ -0,0 +1,178 @@ +//! Internal CPU utilities and stuff. +use std::sync::Arc; + +use snafu::{OptionExt, ResultExt}; + +use crate::context::{ + CannotCreateAdaptorSnafu, CannotRequestDeviceSnafu, ContextError, IncompatibleSurfaceSnafu, + RenderTarget, RenderTargetInner, +}; + +/// Create a new [`wgpu::Adapter`]. +pub async fn adapter( + instance: &wgpu::Instance, + compatible_surface: Option<&wgpu::Surface<'_>>, +) -> Result { + log::trace!( + "creating adapter for a {} context", + if compatible_surface.is_none() { + "headless" + } else { + "surface-based" + } + ); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface, + force_fallback_adapter: false, + }) + .await + .context(CannotCreateAdaptorSnafu)?; + + log::info!("Adapter selected: {:?}", adapter.get_info()); + let info = adapter.get_info(); + log::info!( + "using adapter: '{}' backend:{:?} driver:'{}'", + info.name, + info.backend, + info.driver + ); + Ok(adapter) +} + +/// Create a new [`wgpu::Device`]. +pub async fn device( + adapter: &wgpu::Adapter, +) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { + let wanted_features = wgpu::Features::INDIRECT_FIRST_INSTANCE + | wgpu::Features::MULTI_DRAW_INDIRECT + //// when debugging rust-gpu shader miscompilation it's nice to have this + //| wgpu::Features::SPIRV_SHADER_PASSTHROUGH + // this one is a funny requirement, it seems it is needed if using storage buffers in + // vertex shaders, even if those shaders are read-only + | wgpu::Features::VERTEX_WRITABLE_STORAGE + | wgpu::Features::CLEAR_TEXTURE; + let supported_features = adapter.features(); + let required_features = wanted_features.intersection(supported_features); + let unsupported_features = wanted_features.difference(supported_features); + if !unsupported_features.is_empty() { + log::error!("requested but unsupported features: {unsupported_features:#?}"); + log::warn!("requested and supported features: {supported_features:#?}"); + } + let limits = adapter.limits(); + log::info!("adapter limits: {limits:#?}"); + adapter + .request_device(&wgpu::DeviceDescriptor { + required_features, + required_limits: adapter.limits(), + label: None, + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::Off, + }) + .await +} + +/// Create a new instance. +/// +/// This is for internal use. It is not necessary to create your own `wgpu` +/// instance to use this library. +pub fn new_instance(backends: Option) -> wgpu::Instance { + log::info!( + "creating instance - available backends: {:#?}", + wgpu::Instance::enabled_backend_features() + ); + // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU + let backends = backends.unwrap_or(wgpu::Backends::PRIMARY); + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends, + ..Default::default() + }); + + #[cfg(not(target_arch = "wasm32"))] + { + let adapters = instance.enumerate_adapters(backends); + log::trace!("available adapters: {adapters:#?}"); + } + + instance +} + +/// Create a new suite of `wgpu` machinery using a window or canvas. +/// +/// ## Note +/// This function is used internally. +pub async fn new_windowed_adapter_device_queue( + width: u32, + height: u32, + instance: &wgpu::Instance, + window: impl Into>, +) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { + let surface = instance + .create_surface(window) + .map_err(|e| ContextError::CreateSurface { source: e })?; + let adapter = adapter(instance, Some(&surface)).await?; + let surface_caps = surface.get_capabilities(&adapter); + let fmt = if surface_caps + .formats + .contains(&wgpu::TextureFormat::Rgba8UnormSrgb) + { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(surface_caps.formats[0]) + }; + let view_fmts = if fmt.is_srgb() { + vec![] + } else { + vec![fmt.add_srgb_suffix()] + }; + log::info!("surface capabilities: {surface_caps:#?}"); + let mut surface_config = surface + .get_default_config(&adapter, width, height) + .context(IncompatibleSurfaceSnafu)?; + surface_config.view_formats = view_fmts; + let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; + surface.configure(&device, &surface_config); + let target = RenderTarget(RenderTargetInner::Surface { + surface, + surface_config, + }); + Ok((adapter, device, queue, target)) +} + +/// Create a new suite of `wgpu` machinery that renders to a texture. +/// +/// ## Note +/// This function is used internally. +pub async fn new_headless_device_queue_and_target( + width: u32, + height: u32, + instance: &wgpu::Instance, +) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { + let adapter = adapter(instance, None).await?; + let texture_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + label: None, + view_formats: &[], + }; + let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; + let texture = Arc::new(device.create_texture(&texture_desc)); + let target = RenderTarget(RenderTargetInner::Texture { texture }); + Ok((adapter, device, queue, target)) +} diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 0c7a9b91..472dff5a 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -1,9 +1,15 @@ -//! A "GPU driven" renderer with a focus on simplicity and ease of use. -//! Backed by WebGPU. +//!

+//! renderling mascot +//!
//! -//! Shaders are written in Rust using [`rust-gpu`](https://rust-gpu.github.io/). +//! `renderling` is a "GPU driven" renderer with a focus on simplicity and ease +//! of use, targeting WebGPU. //! -//! All data is staged on the GPU using a [slab allocator](https://crates.io/crates/craballoc). +//! Shaders are written in Rust using [`rust-gpu`](https://rust-gpu.github.io/). //! //! ## Hello triangle //! @@ -13,10 +19,11 @@ //! ### Context creation //! //! First you must create a [`Context`]. -//! The `Context` holds the render target - either a window or a texture. +//! The `Context` holds the render target - either a native window, an HTML +//! canvas or a texture. //! //! ``` -//! use renderling::prelude::*; +//! use renderling::{context::Context, stage::Stage, geometry::Vertex}; //! //! // create a headless context with dimensions 100, 100. //! let ctx = futures_lite::future::block_on(Context::headless(100, 100)); @@ -24,12 +31,21 @@ //! //! [`Context::headless`] creates a `Context` that renders to a texture. //! -//! ### Staging +//! [`Context::from_winit_window`] creates a `Context` that renders to a native +//! window. +//! +//! [`Context::try_new_with_surface`] creates a `Context` that renders to any +//! [`wgpu::SurfaceTarget`]. +//! +//! See the [`renderling::context`](context) module documentation for +//! more info. +//! +//! ### Staging resources //! //! We then create a "stage" to place the camera, geometry, materials and lights. //! //! ``` -//! # use renderling::prelude::*; +//! # use renderling::{context::Context, stage::Stage}; //! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! let stage: Stage = ctx //! .new_stage() @@ -38,70 +54,62 @@ //! .with_lighting(false); //! ``` //! -//! The [`Stage`](crate::stage::Stage) is neat in that it allows you to "stage" data +//! The [`Stage`] is neat in that it allows you to "stage" data //! directly onto the GPU. Those values can be modified on the CPU and -//! synchronization will happen during -//! [`Stage::render`](crate::stage::Stage::render). -//! -//! When "staging" some data, you receive [`Hybrid`](crate::prelude::Hybrid)s and -//! [`HybridArray`](crate::prelude::HybridArray)s in return. +//! synchronization will happen during [`Stage::render`]. //! -//! These types come from the [`craballoc`] library, which is re-exported -//! from [the prelude](crate::prelude). +//! Use one of the many `Stage::new_*` functions to stage data on the GPU: +//! * [`Stage::new_camera`] +//! * [`Stage::new_vertices`] +//! * [`Stage::new_indices`] +//! * [`Stage::new_material`] +//! * [`Stage::new_primitive`] +//! * ...and more //! //! In order to render, we need to "stage" a -//! [`Renderlet`](crate::stage::Renderlet), which is a bundle of rendering +//! [`Primitive`], which is a bundle of rendering //! resources, roughly representing a singular mesh. //! -//! But first we'll need a [`Camera`](crate::camera::Camera) so we can see -//! what's on the stage, and then we'll need a list -//! of [`Vertex`](crate::stage::Vertex) organized as triangles with -//! counter-clockwise winding. Here we'll use the builder pattern to create a -//! staged [`Renderlet`](crate::stage::Renderlet) using our vertices. +//! But first we'll need a list of [`Vertex`] organized +//! as triangles with counter-clockwise winding. Here we'll use the builder +//! pattern to create a staged [`Primitive`] using our vertices. +//! +//! We'll also create a [`Camera`] so we can see the stage. //! //! ``` -//! # use renderling::prelude::*; +//! # use renderling::{context::Context, geometry::Vertex, stage::Stage}; //! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! # let stage: Stage = ctx.new_stage(); +//! let vertices = stage.new_vertices([ +//! Vertex::default() +//! .with_position([0.0, 0.0, 0.0]) +//! .with_color([0.0, 1.0, 1.0, 1.0]), +//! Vertex::default() +//! .with_position([0.0, 100.0, 0.0]) +//! .with_color([1.0, 1.0, 0.0, 1.0]), +//! Vertex::default() +//! .with_position([100.0, 0.0, 0.0]) +//! .with_color([1.0, 0.0, 1.0, 1.0]), +//! ]); +//! let triangle_prim = stage +//! .new_primitive() +//! .with_vertices(vertices); //! -//! let camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); -//! let (vertices, triangle_renderlet) = stage -//! .builder() -//! .with_vertices([ -//! Vertex::default() -//! .with_position([0.0, 0.0, 0.0]) -//! .with_color([0.0, 1.0, 1.0, 1.0]), -//! Vertex::default() -//! .with_position([0.0, 100.0, 0.0]) -//! .with_color([1.0, 1.0, 0.0, 1.0]), -//! Vertex::default() -//! .with_position([100.0, 0.0, 0.0]) -//! .with_color([1.0, 0.0, 1.0, 1.0]), -//! ]) -//! .build(); +//! let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0); //! ``` //! -//! The builder is of the type [`RenderletBuilder`](crate::stage::RenderletBuilder) -//! and after building, it leaves you with all the resources that have been staged, -//! including the `Renderlet`. -//! The return type of [`RenderletBuilder::build`](crate::stage::RenderletBuilder::build) -//! is special in that it depends on the new resources that have been staged. -//! The type will be a tuple of all the newly staged resources that have been added. -//! In this case it's our mesh data and the `Renderlet`. -//! //! ### Rendering //! //! Finally, we get the next frame from the context with -//! [`Context::get_next_frame`], render to it using -//! [`Stage::render`](crate::stage::Stage::render) and then present the -//! frame with [`Frame::present`]. +//! [`Context::get_next_frame`]. Then we render to it using [`Stage::render`] +//! and then present the frame with [`Frame::present`]. //! //! ``` -//! # use renderling::prelude::*; +//! # use renderling::{context::Context, geometry::Vertex, stage::Stage}; //! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! # let stage = ctx.new_stage(); -//! # let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); -//! # let _rez = stage.builder().with_vertices([ +//! # let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0); +//! # let vertices = stage.new_vertices([ //! # Vertex::default() //! # .with_position([0.0, 0.0, 0.0]) //! # .with_color([0.0, 1.0, 1.0, 1.0]), @@ -111,24 +119,68 @@ //! # Vertex::default() //! # .with_position([100.0, 0.0, 0.0]) //! # .with_color([1.0, 0.0, 1.0, 1.0]), -//! # ]).build(); -//! +//! # ]); +//! # let triangle_prim = stage +//! # .new_primitive() +//! # .with_vertices(vertices); //! let frame = ctx.get_next_frame().unwrap(); //! stage.render(&frame.view()); //! let img = futures_lite::future::block_on(frame.read_image()).unwrap(); //! frame.present(); //! ``` //! +//! Here for our purposes we also read the rendered frame as an image. //! Saving `img` should give us this: //! -//! ![renderling hello triangle](https://github.com/schell/renderling/blob/main/test_img/cmy_triangle.png?raw=true) +//! ![renderling hello triangle](https://github.com/schell/renderling/blob/main/test_img/cmy_triangle/hdr.png?raw=true) //! -//! ### Modifying +//! ### Modifying resources //! //! Later, if we want to modify any of the staged values, we can do so through -//! [`Hybrid`](crate::prelude::Hybrid) and [`HybridArray`](crate::prelude::HybridArray). +//! each resource's struct, using `set_*`, `modify_*` and `with_*` functions. +//! //! The changes made will be synchronized to the GPU at the beginning of the -//! [`Stage::render`](crate::prelude::Stage::render) function. +//! next [`Stage::render`] function. +//! +//! ### Removing and hiding primitives +//! +//! To remove primitives from the stage, use [`Stage::remove_primitive`]. +//! This will remove the primitive from rendering entirely, but the GPU +//! resources will not be released until all clones have been dropped. +//! +//! If you just want to mark a [`Primitive`] invisible, use +//! [`Primitive::set_visible`]. +//! +//! ### Releasing resources +//! +//! GPU resources are automatically released when all clones are dropped. +//! The data they occupy on the GPU is reclaimed during calls to +//! [`Stage::render`]. +//! If you would like to manually reclaim the resources of fully dropped +//! resources without rendering, you can do so with +//! [`Stage::commit`]. +//! +//! #### Ensuring resources are released +//! +//! Keep in mind that many resource functions (like [`Primitive::set_material`] +//! for example) take another resource as a parameter. In these functions the +//! parameter resource is cloned and held internally. This is done to keep +//! resources that are in use from being released. Therefore if you want a +//! resource to be released, you must ensure that all references to it are +//! removed. You can use the `remove_*` functions on many resources for this +//! purpose, like [`Primitive::remove_material`], for example, which would +//! remove the material from the primitive. After that call, if no other +//! primitives are using that material and the material is dropped from +//! user code, the next call to [`Stage::render`] or [`Stage::commit`] will +//! reclaim the GPU resources of the material to be re-used. +//! +//! Other resources like [`Vertices`], [`Indices`], [`Transform`], +//! [`NestedTransform`] and others can simply be dropped. +//! +//! # Next steps +//! +//! For further introduction to what renderling can do, take a tour of the +//! [`Stage`] type, or get started with [the manual](#todo). //! //! # WARNING //! @@ -136,30 +188,34 @@ //! //! Your mileage may vary, but I hope you get good use out of this library. //! -//! PRs, criticisms and ideas are all very much welcomed [at the repo](https://github.com/schell/renderling). +//! PRs, criticisms and ideas are all very much welcomed [at the +//! repo](https://github.com/schell/renderling). //! //! 😀☕ #![allow(unexpected_cfgs)] #![cfg_attr(target_arch = "spirv", no_std)] #![deny(clippy::disallowed_methods)] +#[cfg(doc)] +use crate::{camera::Camera, geometry::*, primitive::Primitive, stage::Stage, transform::*}; + pub mod atlas; #[cfg(cpu)] -pub mod bindgroup; -pub mod bits; +pub(crate) mod bindgroup; pub mod bloom; pub mod bvol; pub mod camera; pub mod color; #[cfg(cpu)] -mod context; +pub mod context; pub mod convolution; pub mod cubemap; pub mod cull; pub mod debug; pub mod draw; pub mod geometry; -pub mod ibl; +#[cfg(all(cpu, gltf))] +pub mod gltf; #[cfg(cpu)] pub mod internal; pub mod light; @@ -168,6 +224,7 @@ pub mod linkage; pub mod material; pub mod math; pub mod pbr; +pub mod primitive; pub mod sdf; pub mod skybox; pub mod stage; @@ -176,30 +233,13 @@ pub mod sync; pub mod texture; pub mod tonemapping; pub mod transform; -pub mod tuple; pub mod tutorial; +#[cfg(cpu)] +pub mod types; #[cfg(feature = "ui")] pub mod ui; -#[cfg(cpu)] -pub use context::*; - -pub mod prelude { - //! A prelude, meant to be glob-imported. - - #[cfg(cpu)] - pub extern crate craballoc; - pub extern crate glam; - - #[cfg(cpu)] - pub use craballoc::prelude::*; - pub use crabslab::{Array, Id}; - - pub use crate::{camera::*, light::*, pbr::Material, stage::*, transform::Transform}; - - #[cfg(cpu)] - pub use crate::context::*; -} +pub extern crate glam; #[macro_export] /// A wrapper around `std::println` that is a noop on the GPU. @@ -212,32 +252,83 @@ macro_rules! println { } } +#[cfg(all(cpu, any(test, feature = "test-utils")))] +#[allow(unused, reason = "Used in debugging on macos")] +pub fn capture_gpu_frame( + ctx: &crate::context::Context, + path: impl AsRef, + f: impl FnOnce() -> T, +) -> T { + let path = path.as_ref(); + let parent = path.parent().unwrap(); + std::fs::create_dir_all(parent).unwrap(); + + #[cfg(target_os = "macos")] + { + if path.exists() { + log::info!( + "deleting {} before writing gpu frame capture", + path.display() + ); + std::fs::remove_dir_all(path).unwrap(); + } + + if std::env::var("METAL_CAPTURE_ENABLED").is_err() { + log::error!("Env var METAL_CAPTURE_ENABLED must be set"); + panic!("missing METAL_CAPTURE_ENABLED=1"); + } + + let m = metal::CaptureManager::shared(); + let desc = metal::CaptureDescriptor::new(); + + desc.set_destination(metal::MTLCaptureDestination::GpuTraceDocument); + desc.set_output_url(path); + let maybe_metal_device = unsafe { ctx.get_device().as_hal::() }; + if let Some(metal_device) = maybe_metal_device { + desc.set_capture_device(metal_device.raw_device().try_lock().unwrap().as_ref()); + } else { + panic!("not a capturable device") + } + m.start_capture(&desc).unwrap(); + let t = f(); + m.stop_capture(); + t + } + #[cfg(not(target_os = "macos"))] + { + log::warn!("capturing a GPU frame is only supported on macos"); + f() + } +} + #[cfg(test)] mod test { use super::*; - use crate::{ - atlas::AtlasImage, camera::Camera, pbr::Material, stage::Vertex, transform::Transform, - }; + use crate::{atlas::AtlasImage, context::Context, geometry::Vertex}; use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4}; use img_diff::DiffCfg; - use light::{AnalyticalLight, DirectionalLightDescriptor}; + use light::AnalyticalLight; use pretty_assertions::assert_eq; use stage::Stage; + #[allow(unused_imports)] + pub use renderling_build::{test_output_dir, workspace_dir}; + #[cfg_attr(not(target_arch = "wasm32"), ctor::ctor)] fn init_logging() { let _ = env_logger::builder().is_test(true).try_init(); log::info!("logging is on"); } - pub fn workspace_dir() -> std::path::PathBuf { - std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")) - } - - #[allow(dead_code)] - pub fn test_output_dir() -> std::path::PathBuf { - workspace_dir().join("test_output") + #[allow(unused, reason = "Used in debugging on macos")] + pub fn capture_gpu_frame( + ctx: &Context, + path: impl AsRef, + f: impl FnOnce() -> T, + ) -> T { + let path = workspace_dir().join("test_output").join(path); + super::capture_gpu_frame(ctx, path, f) } /// Marker trait to block on futures in synchronous code. @@ -263,64 +354,17 @@ mod test { } pub fn make_two_directional_light_setup(stage: &Stage) -> (AnalyticalLight, AnalyticalLight) { - let sunlight_a = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::new(-0.8, -1.0, 0.5).normalize(), - color: Vec4::ONE, - intensity: 100.0, - }); - let sunlight_b = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::new(1.0, 1.0, -0.1).normalize(), - color: Vec4::ONE, - intensity: 10.0, - }); - (sunlight_a, sunlight_b) - } - - #[allow(unused, reason = "Used in debugging on macos")] - pub fn capture_gpu_frame( - ctx: &Context, - path: impl AsRef, - f: impl FnOnce() -> T, - ) -> T { - let path = workspace_dir().join("test_output").join(path); - let parent = path.parent().unwrap(); - std::fs::create_dir_all(parent).unwrap(); - - #[cfg(target_os = "macos")] - { - if path.exists() { - log::info!( - "deleting {} before writing gpu frame capture", - path.display() - ); - std::fs::remove_dir_all(&path).unwrap(); - } - - if std::env::var("METAL_CAPTURE_ENABLED").is_err() { - log::error!("Env var METAL_CAPTURE_ENABLED must be set"); - panic!("missing METAL_CAPTURE_ENABLED=1"); - } - - let m = metal::CaptureManager::shared(); - let desc = metal::CaptureDescriptor::new(); - - desc.set_destination(metal::MTLCaptureDestination::GpuTraceDocument); - desc.set_output_url(path); - let maybe_metal_device = unsafe { ctx.get_device().as_hal::() }; - if let Some(metal_device) = maybe_metal_device { - desc.set_capture_device(metal_device.raw_device().try_lock().unwrap().as_ref()); - } else { - panic!("not a capturable device") - } - m.start_capture(&desc).unwrap(); - let t = f(); - m.stop_capture(); - t - } - #[cfg(not(target_os = "macos"))] - { - f() - } + let sunlight_a = stage + .new_directional_light() + .with_direction(Vec3::new(-0.8, -1.0, 0.5).normalize()) + .with_color(Vec4::ONE) + .with_intensity(100.0); + let sunlight_b = stage + .new_directional_light() + .with_direction(Vec3::new(1.0, 1.0, -0.1).normalize()) + .with_color(Vec4::ONE) + .with_intensity(10.0); + (sunlight_a.into_generic(), sunlight_b.into_generic()) } #[test] @@ -360,8 +404,12 @@ mod test { fn cmy_triangle_sanity() { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); - let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); - let _rez = stage.builder().with_vertices(right_tri_vertices()).build(); + let (p, v) = crate::camera::default_ortho2d(100.0, 100.0); + let _camera = stage.new_camera().with_projection_and_view(p, v); + + let _prim = stage + .new_primitive() + .with_vertices(stage.new_vertices(right_tri_vertices())); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -397,15 +445,13 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); - let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); - let _rez = stage - .builder() - .with_vertices({ - let mut vs = right_tri_vertices(); - vs.reverse(); - vs - }) - .build(); + let (p, v) = crate::camera::default_ortho2d(100.0, 100.0); + let _camera = stage.new_camera().with_projection_and_view(p, v); + let _rez = stage.new_primitive().with_vertices(stage.new_vertices({ + let mut vs = right_tri_vertices(); + vs.reverse(); + vs + })); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -427,21 +473,21 @@ mod test { fn cmy_triangle_update_transform() { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); - let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); - let (_vertices, transform, _renderlet) = stage - .builder() - .with_vertices(right_tri_vertices()) - .with_transform(Transform::default()) - .build(); + let (p, v) = crate::camera::default_ortho2d(100.0, 100.0); + let _camera = stage.new_camera().with_projection_and_view(p, v); + let transform = stage.new_transform(); + let _renderlet = stage + .new_primitive() + .with_vertices(stage.new_vertices(right_tri_vertices())) + .with_transform(&transform); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - transform.set(Transform { - translation: Vec3::new(100.0, 0.0, 0.0), - rotation: Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2), - scale: Vec3::new(0.5, 0.5, 1.0), - }); + transform + .set_translation(Vec3::new(100.0, 0.0, 0.0)) + .set_rotation(Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2)) + .set_scale(Vec3::new(0.5, 0.5, 1.0)); stage.render(&frame.view()); let img = frame.read_linear_image().block().unwrap(); @@ -493,19 +539,19 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 12.0, 20.0); - let _camera = stage.new_camera(Camera::new( + let _camera = stage.new_camera().with_projection_and_view( Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), - )); + ); let _rez = stage - .builder() - .with_vertices(gpu_cube_vertices()) - .with_transform(Transform { - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(stage.new_vertices(gpu_cube_vertices())) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -519,20 +565,21 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 12.0, 20.0); - let _camera = stage.new_camera(Camera::new( + let _camera = stage.new_camera().with_projection_and_view( Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), - )); + ); + let _rez = stage - .builder() - .with_vertices(math::UNIT_POINTS.map(cmy_gpu_vertex)) - .with_indices(math::UNIT_INDICES.map(|i| i as u32)) - .with_transform(Transform { - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(stage.new_vertices(math::UNIT_POINTS.map(cmy_gpu_vertex))) + .with_indices(stage.new_indices(math::UNIT_INDICES.map(|i| i as u32))) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -554,26 +601,31 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let (projection, view) = camera::default_perspective(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); - let (geometry, _cube_one_transform, _cube_one) = stage - .builder() - .with_vertices(gpu_cube_vertices()) - .with_transform(Transform { - translation: Vec3::new(-4.5, 0.0, 0.0), - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - }) - .build(); - - let (_cube_two_transform, cube_two) = stage - .builder() - .with_vertices_array(geometry.array()) - .with_transform(Transform { - translation: Vec3::new(4.5, 0.0, 0.0), - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4), - }) - .build(); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + let geometry = stage.new_vertices(gpu_cube_vertices()); + let _cube_one = stage + .new_primitive() + .with_vertices(&geometry) + .with_transform( + stage + .new_transform() + .with_translation(Vec3::new(-4.5, 0.0, 0.0)) + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); + + let cube_two = stage + .new_primitive() + .with_vertices(&geometry) + .with_transform( + stage + .new_transform() + .with_translation(Vec3::new(4.5, 0.0, 0.0)) + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4)), + ); // we should see two colored cubes let frame = ctx.get_next_frame().unwrap(); @@ -584,7 +636,7 @@ mod test { frame.present(); // update cube two making it invisible - cube_two.modify(|r| r.visible = false); + cube_two.set_visible(false); // we should see only one colored cube let frame = ctx.get_next_frame().unwrap(); @@ -594,7 +646,7 @@ mod test { frame.present(); // update cube two making in visible again - cube_two.modify(|r| r.visible = true); + cube_two.set_visible(true); // we should see two colored cubes again let frame = ctx.get_next_frame().unwrap(); @@ -613,15 +665,20 @@ mod test { .with_lighting(false) .with_background_color(Vec4::splat(1.0)); let (projection, view) = camera::default_perspective(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); - let (_cube_geometry, _transform, cube) = stage - .builder() - .with_vertices(math::UNIT_INDICES.map(|i| cmy_gpu_vertex(math::UNIT_POINTS[i]))) - .with_transform(Transform { - scale: Vec3::new(10.0, 10.0, 10.0), - ..Default::default() - }) - .build(); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + let cube = stage + .new_primitive() + .with_vertices( + stage + .new_vertices(math::UNIT_INDICES.map(|i| cmy_gpu_vertex(math::UNIT_POINTS[i]))), + ) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(10.0, 10.0, 10.0)), + ); // we should see a cube (in sRGB color space) let frame = ctx.get_next_frame().unwrap(); @@ -635,7 +692,7 @@ mod test { let pyramid_points = pyramid_points(); let pyramid_geometry = stage .new_vertices(pyramid_indices().map(|i| cmy_gpu_vertex(pyramid_points[i as usize]))); - cube.modify(|r| r.vertices_array = pyramid_geometry.array()); + cube.set_vertices(pyramid_geometry); // we should see a pyramid (in sRGB color space) let frame = ctx.get_next_frame().unwrap(); @@ -697,26 +754,28 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0)); let (projection, view) = camera::default_perspective(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let sandstone = AtlasImage::from(image::open("../../img/sandstone.png").unwrap()); let dirt = AtlasImage::from(image::open("../../img/dirt.jpg").unwrap()); let entries = stage.set_images([sandstone, dirt]).unwrap(); - let (material, _geometry, _transform, cube) = stage - .builder() - .with_material(Material { - albedo_texture_id: entries[0].id(), - has_lighting: false, - ..Default::default() - }) - .with_vertices(gpu_uv_unit_cube()) - .with_transform(Transform { - scale: Vec3::new(10.0, 10.0, 10.0), - ..Default::default() - }) - .build(); - println!("cube: {cube:?}"); + let material = stage + .new_material() + .with_albedo_texture(&entries[0]) + .with_has_lighting(false); + let cube = stage + .new_primitive() + .with_vertices(stage.new_vertices(gpu_uv_unit_cube())) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(10.0, 10.0, 10.0)), + ) + .with_material(&material); + println!("cube: {:?}", cube.descriptor()); // we should see a cube with a stoney texture let frame = ctx.get_next_frame().unwrap(); @@ -726,7 +785,7 @@ mod test { frame.present(); // update the material's texture on the GPU - material.modify(|m| m.albedo_texture_id = entries[1].id()); + material.set_albedo_texture(&entries[1]); // we should see a cube with a dirty texture let frame = ctx.get_next_frame().unwrap(); @@ -754,53 +813,52 @@ mod test { .with_background_color(Vec3::splat(0.0).extend(1.0)); let (projection, view) = camera::default_ortho2d(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); // now test the textures functionality let img = AtlasImage::from_path("../../img/cheetah.jpg").unwrap(); let entries = stage.set_images([img]).unwrap(); - let (geometry, _color_prim) = stage - .builder() - .with_vertices([ - Vertex { - position: Vec3::new(0.0, 0.0, 0.0), - color: Vec4::new(1.0, 1.0, 0.0, 1.0), - uv0: Vec2::new(0.0, 0.0), - uv1: Vec2::new(0.0, 0.0), - ..Default::default() - }, - Vertex { - position: Vec3::new(100.0, 100.0, 0.0), - color: Vec4::new(0.0, 1.0, 1.0, 1.0), - uv0: Vec2::new(1.0, 1.0), - uv1: Vec2::new(1.0, 1.0), - ..Default::default() - }, - Vertex { - position: Vec3::new(100.0, 0.0, 0.0), - color: Vec4::new(1.0, 0.0, 1.0, 1.0), - uv0: Vec2::new(1.0, 0.0), - uv1: Vec2::new(1.0, 0.0), - ..Default::default() - }, - ]) - .build(); - - let _rez = stage - .builder() - .with_vertices_array(geometry.array()) - .with_material(Material { - albedo_texture_id: entries[0].id(), - has_lighting: false, + let geometry = stage.new_vertices([ + Vertex { + position: Vec3::new(0.0, 0.0, 0.0), + color: Vec4::new(1.0, 1.0, 0.0, 1.0), + uv0: Vec2::new(0.0, 0.0), + uv1: Vec2::new(0.0, 0.0), + ..Default::default() + }, + Vertex { + position: Vec3::new(100.0, 100.0, 0.0), + color: Vec4::new(0.0, 1.0, 1.0, 1.0), + uv0: Vec2::new(1.0, 1.0), + uv1: Vec2::new(1.0, 1.0), ..Default::default() - }) - .with_transform(Transform { - translation: Vec3::new(15.0, 35.0, 0.5), - scale: Vec3::new(0.5, 0.5, 1.0), + }, + Vertex { + position: Vec3::new(100.0, 0.0, 0.0), + color: Vec4::new(1.0, 0.0, 1.0, 1.0), + uv0: Vec2::new(1.0, 0.0), + uv1: Vec2::new(1.0, 0.0), ..Default::default() - }) - .build(); + }, + ]); + let _color_prim = stage.new_primitive().with_vertices(&geometry); + + let material = stage + .new_material() + .with_albedo_texture(&entries[0]) + .with_has_lighting(false); + let transform = stage + .new_transform() + .with_translation(Vec3::new(15.0, 35.0, 0.5)) + .with_scale(Vec3::new(0.5, 0.5, 1.0)); + let _rez = stage + .new_primitive() + .with_vertices(&geometry) + .with_material(material) + .with_transform(transform); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -811,8 +869,6 @@ mod test { #[test] /// Tests shading with directional light. fn scene_cube_directional() { - use crate::light::{DirectionalLightDescriptor, Light, LightStyle}; - let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() @@ -821,58 +877,39 @@ mod test { let (projection, _) = camera::default_perspective(100.0, 100.0); let view = Mat4::look_at_rh(Vec3::new(1.8, 1.8, 1.8), Vec3::ZERO, Vec3::Y); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let red = Vec3::X.extend(1.0); let green = Vec3::Y.extend(1.0); let blue = Vec3::Z.extend(1.0); - let dir_red = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::NEG_Y, - color: red, - intensity: 10.0, - }); - let _dir_green = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::NEG_X, - color: green, - intensity: 10.0, - }); - let _dir_blue = stage.new_analytical_light(DirectionalLightDescriptor { - direction: Vec3::NEG_Z, - color: blue, - intensity: 10.0, - }); - assert_eq!( - Light { - light_type: LightStyle::Directional, - index: dir_red - .light_details() - .as_directional() - .unwrap() - .id() - .inner(), - ..Default::default() - }, - Light::from(dir_red.light_details().as_directional().unwrap().id()) - ); + let _dir_red = stage + .new_directional_light() + .with_direction(Vec3::NEG_Y) + .with_color(red) + .with_intensity(10.0); + let _dir_green = stage + .new_directional_light() + .with_direction(Vec3::NEG_X) + .with_color(green) + .with_intensity(10.0); + let _dir_blue = stage + .new_directional_light() + .with_direction(Vec3::NEG_Z) + .with_color(blue) + .with_intensity(10.0); let _rez = stage - .builder() - .with_material(Material::default()) - .with_vertices( - math::unit_cube() - .into_iter() - .map(|(p, n)| Vertex { - position: p, - normal: n, - color: Vec4::ONE, - ..Default::default() - }) - .collect::>(), - ) - .build(); + .new_primitive() + .with_material(stage.default_material()); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); + println!( + "lighting_descriptor: {:#?}", + stage.lighting.lighting_descriptor.get() + ); let img = frame.read_image().block().unwrap(); let depth_texture = stage.get_depth_texture(); let depth_img = depth_texture.read_image().block().unwrap().unwrap(); @@ -924,76 +961,71 @@ mod test { let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0)); let (projection, view) = camera::default_ortho2d(100.0, 100.0); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); - let root_node = stage.new_nested_transform(); - root_node.set(Transform { - scale: Vec3::new(25.0, 25.0, 1.0), - ..Default::default() - }); - println!("root_node: {:#?}", root_node.get_global_transform()); + let root_node = stage + .new_nested_transform() + .with_local_scale(Vec3::new(25.0, 25.0, 1.0)); + println!("root_node: {:#?}", root_node.global_descriptor()); - let offset = Transform { - translation: Vec3::new(1.0, 1.0, 0.0), - ..Default::default() - }; + let offset = Vec3::new(1.0, 1.0, 0.0); - let cyan_node = stage.new_nested_transform(); - cyan_node.set(offset); - println!("cyan_node: {:#?}", cyan_node.get_global_transform()); + let cyan_node = stage.new_nested_transform().with_local_translation(offset); + println!("cyan_node: {:#?}", cyan_node.global_descriptor()); - let yellow_node = stage.new_nested_transform(); - yellow_node.set(offset); - println!("yellow_node: {:#?}", yellow_node.get_global_transform()); + let yellow_node = stage.new_nested_transform().with_local_translation(offset); + println!("yellow_node: {:#?}", yellow_node.global_descriptor()); - let red_node = stage.new_nested_transform(); - red_node.set(offset); - println!("red_node: {:#?}", red_node.get_global_transform()); + let red_node = stage.new_nested_transform().with_local_translation(offset); + println!("red_node: {:#?}", red_node.global_descriptor()); root_node.add_child(&cyan_node); - println!("cyan_node: {:#?}", cyan_node.get_global_transform()); + println!("cyan_node: {:#?}", cyan_node.global_descriptor()); cyan_node.add_child(&yellow_node); - println!("yellow_node: {:#?}", yellow_node.get_global_transform()); + println!("yellow_node: {:#?}", yellow_node.global_descriptor()); yellow_node.add_child(&red_node); - println!("red_node: {:#?}", red_node.get_global_transform()); - - let (geometry, _cyan_material, _cyan_primitive) = stage - .builder() - .with_vertices({ - let size = 1.0; - [ - Vertex::default().with_position([0.0, 0.0, 0.0]), - Vertex::default().with_position([size, size, 0.0]), - Vertex::default().with_position([size, 0.0, 0.0]), - ] - }) - .with_material(Material { - albedo_factor: Vec4::new(0.0, 1.0, 1.0, 1.0), - has_lighting: false, - ..Default::default() - }) - .with_nested_transform(&cyan_node) - .build(); - let _yellow = stage - .builder() - .with_vertices_array(geometry.array()) - .with_material(Material { - albedo_factor: Vec4::new(1.0, 1.0, 0.0, 1.0), - has_lighting: false, - ..Default::default() - }) - .with_nested_transform(&yellow_node) - .build(); - let _red = stage - .builder() - .with_vertices_array(geometry.array()) - .with_material(Material { - albedo_factor: Vec4::new(1.0, 0.0, 0.0, 1.0), - has_lighting: false, - ..Default::default() - }) - .with_nested_transform(&red_node) - .build(); + println!("red_node: {:#?}", red_node.global_descriptor()); + + let geometry = stage.new_vertices({ + let size = 1.0; + [ + Vertex::default().with_position([0.0, 0.0, 0.0]), + Vertex::default().with_position([size, size, 0.0]), + Vertex::default().with_position([size, 0.0, 0.0]), + ] + }); + let _cyan_primitive = stage + .new_primitive() + .with_vertices(&geometry) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(0.0, 1.0, 1.0, 1.0)) + .with_has_lighting(false), + ) + .with_transform(&cyan_node); + let _yellow_primitive = stage + .new_primitive() + .with_vertices(&geometry) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(1.0, 1.0, 0.0, 1.0)) + .with_has_lighting(false), + ) + .with_transform(&yellow_node); + let _red_primitive = stage + .new_primitive() + .with_vertices(&geometry) + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0)) + .with_has_lighting(false), + ) + .with_transform(&red_node); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -1019,19 +1051,19 @@ mod test { // create the CMY cube let camera_position = Vec3::new(0.0, 12.0, 20.0); - let _camera = stage.new_camera(Camera::new( + let _camera = stage.new_camera().with_projection_and_view( Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), - )); + ); let _rez = stage - .builder() - .with_vertices(gpu_cube_vertices()) - .with_transform(Transform { - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(stage.new_vertices(gpu_cube_vertices())) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -1062,19 +1094,19 @@ mod test { // create the CMY cube let camera_position = Vec3::new(0.0, 12.0, 20.0); - let _camera = stage.new_camera(Camera::new( + let _camera = stage.new_camera().with_projection_and_view( Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0), Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y), - )); + ); let _rez = stage - .builder() - .with_vertices(gpu_cube_vertices()) - .with_transform(Transform { - scale: Vec3::new(6.0, 6.0, 6.0), - rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), - ..Default::default() - }) - .build(); + .new_primitive() + .with_vertices(stage.new_vertices(gpu_cube_vertices())) + .with_transform( + stage + .new_transform() + .with_scale(Vec3::new(6.0, 6.0, 6.0)) + .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)), + ); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); diff --git a/crates/renderling/src/light.rs b/crates/renderling/src/light.rs index 694902e2..e1b2f1ad 100644 --- a/crates/renderling/src/light.rs +++ b/crates/renderling/src/light.rs @@ -1,23 +1,81 @@ -//! Lighting. +//! Lighting effects. //! -//! Directional, point and spot lights. +//! This module includes support for various types of lights such as +//! directional, point, and spot lights. //! -//! Shadow mapping. +//! Additionally, the module provides shadow mapping to create realistic shadows. //! -//! Tiling. -use crabslab::{Array, Id, Slab, SlabItem}; -use glam::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; -#[cfg(gpu)] -use spirv_std::num_traits::Float; -use spirv_std::{spirv, Image}; +//! Also provided is an implementation of light tiling, a technique that optimizes +//! the rendering of thousands of analytical lights. If you find your scene performing +//! poorly under the load of very many lights, [`LightTiling`] can speed things up. +//! +//! ## Analytical lights +//! +//! Analytical lights are a fundamental lighting effect in a scene. +//! These lights can be created directly from the [`Stage`] using the methods +//! * [`Stage::new_directional_light`] +//! * [`Stage::new_point_light`] +//! * [`Stage::new_spot_light`] +//! +//! Each of these methods returns an [`AnalyticalLight`] instance that can be +//! manipulated to simulate different lighting conditions. +//! +//! Once created, these lights can be positioned and oriented using +//! [`Transform`] or [`NestedTransform`] objects. The [`Transform`] allows you +//! to set the position, rotation, and scale of the light, while +//! [`NestedTransform`] enables hierarchical transformations, which is useful +//! for complex scenes where lights need to follow specific objects or structures. +//! +//! By adjusting the properties of these lights, such as intensity, color, and +//! direction, you can achieve a wide range of lighting effects, from simulating +//! sunlight with directional lights to creating focused spotlights or ambient +//! point lights. These lights can also be combined with shadow mapping +//! techniques to enhance the realism of shadows in the scene. +//! +//! ## Shadow mapping +//! +//! Shadow mapping is a technique used to add realistic shadows to a scene by +//! simulating the way light interacts with objects. +//! +//! To create a [`ShadowMap`], use the [`Stage::new_shadow_map`] method, passing in +//! the light source and desired parameters such as the size of the shadow map +//! and the near and far planes of the light's frustum. Once created, the +//! shadow map needs to be updated each frame (or as needed) using the +//! [`ShadowMap::update`] method, which renders the scene from the light's +//! perspective to determine which areas are in shadow. +//! +//! This technique allows for dynamic shadows that change with the movement of +//! lights and objects, enhancing the realism of the scene. Proper +//! configuration of shadow map parameters, such as bias and resolution, is +//! crucial to achieving high-quality shadows without artifacts, and varies +//! with each scene. +//! +//! ## Light tiling +//! +//! Light tiling is a technique used to optimize the rendering of scenes with a +//! large number of lights. +//! +//! It divides the rendering surface into a grid of tiles, allowing for +//! efficient computation of lighting effects by processing each tile +//! independently and cutting down on the overall lighting calculations. +//! +//! To create a [`LightTiling`], use the [`Stage::new_light_tiling`] method, +//! providing a [`LightTilingConfig`] to specify parameters such as tile size +//! and maximum lights per tile. +//! +//! Once created, the [`LightTiling`] instance should be kept in sync with the +//! scene by calling the [`LightTiling::run`] method each frame, or however you +//! see fit. This method updates the lighting calculations for each tile based +//! on the current scene configuration, ensuring optimal performance even with +//! many lights. +//! +//! By using light tiling, you can significantly improve the performance of your +//! rendering pipeline, especially in complex scenes with numerous light sources. +#[cfg(doc)] use crate::{ - atlas::{AtlasDescriptor, AtlasTexture}, - cubemap::{CubemapDescriptor, CubemapFaceDirection}, - geometry::GeometryDescriptor, - math::{Fetch, IsSampler, IsVector, Sample2dArray}, - stage::{Renderlet, VertexInfo}, - transform::Transform, + stage::Stage, + transform::{NestedTransform, Transform}, }; #[cfg(cpu)] @@ -35,1217 +93,16 @@ mod tiling; #[cfg(cpu)] pub use tiling::*; -/// Root descriptor of the lighting system. -#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] -#[offsets] -pub struct LightingDescriptor { - /// List of all analytical lights in the scene. - pub analytical_lights_array: Array>, - /// Shadow mapping atlas info. - pub shadow_map_atlas_descriptor_id: Id, - /// `Id` of the [`ShadowMapDescriptor`] to use when updating - /// a shadow map. - /// - /// This changes from each run of the `shadow_mapping_vertex`. - pub update_shadow_map_id: Id, - /// The index of the shadow map atlas texture to update. - pub update_shadow_map_texture_index: u32, - /// `Id` of the [`LightTilingDescriptor`] to use when performing - /// light tiling. - pub light_tiling_descriptor_id: Id, -} - -#[derive(Clone, Copy, SlabItem, core::fmt::Debug)] -pub struct ShadowMapDescriptor { - pub light_space_transforms_array: Array, - /// Near plane of the projection matrix - pub z_near: f32, - /// Far plane of the projection matrix - pub z_far: f32, - /// Pointers to the atlas textures where the shadow map depth - /// data is stored. - /// - /// This will be an array of one `Id` for directional and spot lights, - /// and an array of four `Id`s for a point light. - pub atlas_textures_array: Array>, - pub bias_min: f32, - pub bias_max: f32, - pub pcf_samples: u32, -} - -impl Default for ShadowMapDescriptor { - fn default() -> Self { - Self { - light_space_transforms_array: Default::default(), - z_near: Default::default(), - z_far: Default::default(), - atlas_textures_array: Default::default(), - bias_min: 0.0005, - bias_max: 0.005, - pcf_samples: 4, - } - } -} - -#[cfg(test)] -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct ShadowMappingVertexInfo { - pub renderlet_id: Id, - pub vertex_index: u32, - pub vertex: crate::stage::Vertex, - pub transform: Transform, - pub model_matrix: Mat4, - pub world_pos: Vec3, - pub view_projection: Mat4, - pub clip_pos: Vec4, -} - -/// Shadow mapping vertex shader. -/// -/// It is assumed that a [`LightingDescriptor`] is stored at `Id(0)` of the -/// `light_slab`. -/// -/// This shader reads the [`LightingDescriptor`] to find the shadow map to -/// be updated, then determines the clip positions to emit based on the -/// shadow map's atlas texture. -/// -/// It then renders the renderlet into the designated atlas frame. -// Note: -// If this is taking too long to render for each renderlet, think about -// a frustum and occlusion culling pass to generate the list of renderlets. -#[spirv(vertex)] -#[allow(clippy::too_many_arguments)] -pub fn shadow_mapping_vertex( - // Points at a `Renderlet` - #[spirv(instance_index)] renderlet_id: Id, - // Which vertex within the renderlet are we rendering - #[spirv(vertex_index)] vertex_index: u32, - // The slab where the renderlet's geometry is staged - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], - // The slab where the scene's lighting data is staged - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] light_slab: &[u32], - - #[spirv(position)] out_clip_pos: &mut Vec4, - #[cfg(test)] out_comparison_info: &mut ShadowMappingVertexInfo, -) { - let renderlet = geometry_slab.read_unchecked(renderlet_id); - if !renderlet.visible { - // put it outside the clipping frustum - *out_clip_pos = Vec4::new(100.0, 100.0, 100.0, 1.0); - return; - } - - let VertexInfo { - world_pos, - vertex: _vertex, - transform: _transform, - model_matrix: _model_matrix, - } = renderlet.get_vertex_info(vertex_index, geometry_slab); - - let lighting_desc = light_slab.read_unchecked(Id::::new(0)); - let shadow_desc = light_slab.read_unchecked(lighting_desc.update_shadow_map_id); - let light_space_transform_id = shadow_desc - .light_space_transforms_array - .at(lighting_desc.update_shadow_map_texture_index as usize); - let light_space_transform = light_slab.read_unchecked(light_space_transform_id); - let clip_pos = light_space_transform * world_pos.extend(1.0); - #[cfg(test)] - { - *out_comparison_info = ShadowMappingVertexInfo { - renderlet_id, - vertex_index, - vertex: _vertex, - transform: _transform, - model_matrix: _model_matrix, - world_pos, - view_projection: light_space_transform, - clip_pos, - }; - } - *out_clip_pos = clip_pos; -} - -#[spirv(fragment)] -pub fn shadow_mapping_fragment(clip_pos: Vec4, frag_color: &mut Vec4) { - *frag_color = (clip_pos.xyz() / clip_pos.w).extend(1.0); -} - -/// Contains values needed to determine the outgoing radiance of a fragment. -/// -/// For more info, see the **Spotlight** section of the -/// [learnopengl](https://learnopengl.com/Lighting/Light-casters) -/// article. -#[derive(Clone, Copy, Default, core::fmt::Debug)] -pub struct SpotLightCalculation { - /// Position of the light in world space - pub light_position: Vec3, - /// Position of the fragment in world space - pub frag_position: Vec3, - /// Unit vector (LightDir) pointing from the fragment to the light - pub frag_to_light: Vec3, - /// Distance from the fragment to the light - pub frag_to_light_distance: f32, - /// Unit vector (SpotDir) direction that the light is pointing in - pub light_direction: Vec3, - /// The cosine of the cutoff angle (Phi ϕ) that specifies the spotlight's radius. - /// - /// Everything inside this angle is lit by the spotlight. - pub cos_inner_cutoff: f32, - /// The cosine of the cutoff angle (Gamma γ) that specifies the spotlight's outer radius. - /// - /// Everything outside this angle is not lit by the spotlight. - /// - /// Fragments between `inner_cutoff` and `outer_cutoff` have an intensity - /// between `1.0` and `0.0`. - pub cos_outer_cutoff: f32, - /// Whether the fragment is inside the `inner_cutoff` cone. - pub fragment_is_inside_inner_cone: bool, - /// Whether the fragment is inside the `outer_cutoff` cone. - pub fragment_is_inside_outer_cone: bool, - /// `outer_cutoff` - `inner_cutoff` - pub epsilon: f32, - /// Cosine of the angle (Theta θ) between `frag_to_light` (LightDir) vector and the - /// `light_direction` (SpotDir) vector. - /// - /// θ should be smaller than `outer_cutoff` (Gamma γ) to be - /// inside the spotlight, but since these are all cosines of angles, we actually - /// compare using `>`. - pub cos_theta: f32, - pub contribution_unclamped: f32, - /// The intensity level between `0.0` and `1.0` that should be used to determine - /// outgoing radiance. - pub contribution: f32, -} - -impl SpotLightCalculation { - /// Calculate the values required to determine outgoing radiance of a spot light. - pub fn new( - spot_light_descriptor: SpotLightDescriptor, - node_transform: Mat4, - fragment_world_position: Vec3, - ) -> Self { - let light_position = node_transform.transform_point3(spot_light_descriptor.position); - let frag_position = fragment_world_position; - let frag_to_light = light_position - frag_position; - let frag_to_light_distance = frag_to_light.length(); - if frag_to_light_distance == 0.0 { - crate::println!("frag_to_light_distance: {frag_to_light_distance}"); - return Self::default(); - } - let frag_to_light = frag_to_light.alt_norm_or_zero(); - let light_direction = node_transform - .transform_vector3(spot_light_descriptor.direction) - .alt_norm_or_zero(); - let cos_inner_cutoff = spot_light_descriptor.inner_cutoff.cos(); - let cos_outer_cutoff = spot_light_descriptor.outer_cutoff.cos(); - let epsilon = cos_inner_cutoff - cos_outer_cutoff; - let cos_theta = frag_to_light.dot(-light_direction); - let fragment_is_inside_inner_cone = cos_theta > cos_inner_cutoff; - let fragment_is_inside_outer_cone = cos_theta > cos_outer_cutoff; - let contribution_unclamped = (cos_theta - cos_outer_cutoff) / epsilon; - let contribution = contribution_unclamped.clamp(0.0, 1.0); - Self { - light_position, - frag_position, - frag_to_light, - frag_to_light_distance, - light_direction, - cos_inner_cutoff, - cos_outer_cutoff, - fragment_is_inside_inner_cone, - fragment_is_inside_outer_cone, - epsilon, - cos_theta, - contribution_unclamped, - contribution, - } - } -} - -/// Description of a spot light. -/// -/// ## Tips -/// -/// If your spotlight is not illuminating your scenery, ensure that the -/// `inner_cutoff` and `outer_cutoff` values are "correct". `outer_cutoff` -/// should be _greater than_ `inner_cutoff` and the values should be a large -/// enough to cover at least one pixel at the distance between the light and -/// the scenery. -#[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, SlabItem)] -pub struct SpotLightDescriptor { - pub position: Vec3, - pub direction: Vec3, - pub inner_cutoff: f32, - pub outer_cutoff: f32, - pub color: Vec4, - pub intensity: f32, -} - -impl Default for SpotLightDescriptor { - fn default() -> Self { - let white = Vec4::splat(1.0); - let inner_cutoff = 0.077143565; - let outer_cutoff = 0.09075713; - let direction = Vec3::new(0.0, -1.0, 0.0); - let color = white; - let intensity = 1.0; - - Self { - position: Default::default(), - direction, - inner_cutoff, - outer_cutoff, - color, - intensity, - } - } -} - -impl SpotLightDescriptor { - pub fn shadow_mapping_projection_and_view( - &self, - parent_light_transform: &Mat4, - z_near: f32, - z_far: f32, - ) -> (Mat4, Mat4) { - let fovy = 2.0 * self.outer_cutoff; - let aspect = 1.0; - let projection = Mat4::perspective_rh(fovy, aspect, z_near, z_far); - let direction = parent_light_transform - .transform_vector3(self.direction) - .alt_norm_or_zero(); - let position = parent_light_transform.transform_point3(self.position); - let up = direction.orthonormal_vectors()[0]; - let view = Mat4::look_to_rh(position, direction, up); - (projection, view) - } -} - -#[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, SlabItem)] -pub struct DirectionalLightDescriptor { - pub direction: Vec3, - pub color: Vec4, - pub intensity: f32, -} - -impl Default for DirectionalLightDescriptor { - fn default() -> Self { - let direction = Vec3::new(0.0, -1.0, 0.0); - let color = Vec4::splat(1.0); - let intensity = 1.0; - - Self { - direction, - color, - intensity, - } - } -} - -impl DirectionalLightDescriptor { - pub fn shadow_mapping_projection_and_view( - &self, - parent_light_transform: &Mat4, - // Near limits of the light's reach - // - // The maximum should be the `Camera`'s `Frustum::depth()`. - // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum - // as a parameter and then figure out the minimal view projection that includes that frustum - z_near: f32, - // Far limits of the light's reach - z_far: f32, - ) -> (Mat4, Mat4) { - crate::println!("descriptor: {self:#?}"); - let depth = (z_far - z_near).abs(); - let hd = depth * 0.5; - let projection = Mat4::orthographic_rh(-hd, hd, -hd, hd, z_near, z_far); - let direction = parent_light_transform - .transform_vector3(self.direction) - .alt_norm_or_zero(); - let position = -direction * depth * 0.5; - crate::println!("direction: {direction}"); - crate::println!("position: {position}"); - let up = direction.orthonormal_vectors()[0]; - let view = Mat4::look_to_rh(position, direction, up); - (projection, view) - } -} - -#[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, SlabItem)] -pub struct PointLightDescriptor { - pub position: Vec3, - pub color: Vec4, - /// Expressed as candelas. - pub intensity: f32, -} - -impl Default for PointLightDescriptor { - fn default() -> Self { - let color = Vec4::splat(1.0); - let intensity = 1.0; - - Self { - position: Default::default(), - color, - intensity, - } - } -} - -impl PointLightDescriptor { - pub fn shadow_mapping_view_matrix( - &self, - face_index: usize, - parent_light_transform: &Mat4, - ) -> Mat4 { - let eye = parent_light_transform.transform_point3(self.position); - let mut face = CubemapFaceDirection::FACES[face_index]; - face.eye = eye; - face.view() - } - - pub fn shadow_mapping_projection_matrix(z_near: f32, z_far: f32) -> Mat4 { - Mat4::perspective_lh(core::f32::consts::FRAC_PI_2, 1.0, z_near, z_far) - } - - pub fn shadow_mapping_projection_and_view_matrices( - &self, - parent_light_transform: &Mat4, - z_near: f32, - z_far: f32, - ) -> (Mat4, [Mat4; 6]) { - let p = Self::shadow_mapping_projection_matrix(z_near, z_far); - let eye = parent_light_transform.transform_point3(self.position); - ( - p, - CubemapFaceDirection::FACES.map(|mut face| { - face.eye = eye; - face.view() - }), - ) - } -} - -/// Returns the radius of illumination in meters. -/// -/// * Moonlight: < 1 lux. -/// - Full moon on a clear night: 0.25 lux. -/// - Quarter moon: 0.01 lux -/// - Starlight overcast moonless night sky: 0.0001 lux. -/// * General indoor lighting: Around 100 to 300 lux. -/// * Office lighting: Typically around 300 to 500 lux. -/// * Reading or task lighting: Around 500 to 750 lux. -/// * Detailed work (e.g., drafting, surgery): 1000 lux or more. -pub fn radius_of_illumination(intensity_candelas: f32, minimum_illuminance_lux: f32) -> f32 { - (intensity_candelas / minimum_illuminance_lux).sqrt() -} - -#[repr(u32)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, PartialEq)] -pub enum LightStyle { - Directional = 0, - Point = 1, - Spot = 2, -} - -impl core::fmt::Display for LightStyle { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - LightStyle::Directional => f.write_str("directional"), - LightStyle::Point => f.write_str("point"), - LightStyle::Spot => f.write_str("spot"), - } - } -} - -impl SlabItem for LightStyle { - const SLAB_SIZE: usize = { 1 }; - - fn read_slab(index: usize, slab: &[u32]) -> Self { - let proxy = u32::read_slab(index, slab); - match proxy { - 0 => LightStyle::Directional, - 1 => LightStyle::Point, - 2 => LightStyle::Spot, - _ => LightStyle::Directional, - } - } - - fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { - let proxy = *self as u32; - proxy.write_slab(index, slab) - } -} - -/// A generic light that is used as a slab pointer to a -/// specific light type. -// TODO: rename to `LightDescriptor` -#[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Copy, Clone, PartialEq, SlabItem)] -pub struct Light { - /// The type of the light - pub light_type: LightStyle, - /// The index of the light in the lighting slab - pub index: u32, - /// The id of a transform to apply to the position and direction of the light. - /// - /// This `Id` points to a transform on the geometry slab. - pub transform_id: Id, - /// The id of the shadow map in use by this light. - pub shadow_map_desc_id: Id, -} - -impl Default for Light { - fn default() -> Self { - Self { - light_type: LightStyle::Directional, - index: Id::<()>::NONE.inner(), - transform_id: Id::NONE, - shadow_map_desc_id: Id::NONE, - } - } -} - -impl From> for Light { - fn from(id: Id) -> Self { - Self { - light_type: LightStyle::Directional, - index: id.inner(), - transform_id: Id::NONE, - shadow_map_desc_id: Id::NONE, - } - } -} - -impl From> for Light { - fn from(id: Id) -> Self { - Self { - light_type: LightStyle::Spot, - index: id.inner(), - transform_id: Id::NONE, - shadow_map_desc_id: Id::NONE, - } - } -} - -impl From> for Light { - fn from(id: Id) -> Self { - Self { - light_type: LightStyle::Point, - index: id.inner(), - transform_id: Id::NONE, - shadow_map_desc_id: Id::NONE, - } - } -} - -impl Light { - pub fn into_directional_id(self) -> Id { - Id::from(self.index) - } - - pub fn into_spot_id(self) -> Id { - Id::from(self.index) - } - - pub fn into_point_id(self) -> Id { - Id::from(self.index) - } -} - -/// Parameters to the shadow mapping calculation function. -/// -/// This is mostly just to appease clippy. -pub struct ShadowCalculation { - pub shadow_map_desc: ShadowMapDescriptor, - pub shadow_map_atlas_size: UVec2, - pub surface_normal_in_world_space: Vec3, - pub frag_pos_in_world_space: Vec3, - pub frag_to_light_in_world_space: Vec3, - pub bias_min: f32, - pub bias_max: f32, - pub pcf_samples: u32, -} - -impl ShadowCalculation { - /// Reads various required parameters from the slab and creates a `ShadowCalculation`. - pub fn new( - light_slab: &[u32], - light: crate::prelude::Light, - in_pos: Vec3, - surface_normal: Vec3, - light_direction: Vec3, - ) -> Self { - let shadow_map_desc = light_slab.read_unchecked(light.shadow_map_desc_id); - let atlas_size = { - let lighting_desc_id = Id::::new(0); - let atlas_desc_id = light_slab.read_unchecked( - lighting_desc_id + LightingDescriptor::OFFSET_OF_SHADOW_MAP_ATLAS_DESCRIPTOR_ID, - ); - let atlas_desc = light_slab.read_unchecked(atlas_desc_id); - atlas_desc.size - }; - - ShadowCalculation { - shadow_map_desc, - shadow_map_atlas_size: atlas_size.xy(), - surface_normal_in_world_space: surface_normal, - frag_pos_in_world_space: in_pos, - frag_to_light_in_world_space: light_direction, - bias_min: shadow_map_desc.bias_min, - bias_max: shadow_map_desc.bias_max, - pcf_samples: shadow_map_desc.pcf_samples, - } - } - - fn get_atlas_texture_at(&self, light_slab: &[u32], index: usize) -> AtlasTexture { - let atlas_texture_id = - light_slab.read_unchecked(self.shadow_map_desc.atlas_textures_array.at(index)); - light_slab.read_unchecked(atlas_texture_id) - } - - fn get_frag_pos_in_light_space(&self, light_slab: &[u32], index: usize) -> Vec3 { - let light_space_transform_id = self.shadow_map_desc.light_space_transforms_array.at(index); - let light_space_transform = light_slab.read_unchecked(light_space_transform_id); - light_space_transform.project_point3(self.frag_pos_in_world_space) - } - - /// Returns shadow _intensity_ for directional and spot lights. - /// - /// Returns `0.0` when the fragment is in full light. - /// Returns `1.0` when the fragment is in full shadow. - pub fn run_directional_or_spot( - &self, - light_slab: &[u32], - shadow_map: &T, - shadow_map_sampler: &S, - ) -> f32 - where - S: IsSampler, - T: Sample2dArray, - { - let ShadowCalculation { - shadow_map_desc: _, - shadow_map_atlas_size, - frag_pos_in_world_space: _, - surface_normal_in_world_space: surface_normal, - frag_to_light_in_world_space: light_direction, - bias_min, - bias_max, - pcf_samples, - } = self; - let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, 0); - crate::println!("frag_pos_in_light_space: {frag_pos_in_light_space}"); - if !crate::math::is_inside_clip_space(frag_pos_in_light_space.xyz()) { - return 0.0; - } - // The range of coordinates in the light's clip space is -1.0 to 1.0 for x and y, - // but the texture space is [0, 1], and Y increases downward, so we do this - // conversion to flip Y and also normalize to the range [0.0, 1.0]. - // Z should already be 0.0 to 1.0. - let proj_coords_uv = (frag_pos_in_light_space.xy() * Vec2::new(1.0, -1.0) - + Vec2::splat(1.0)) - * Vec2::splat(0.5); - crate::println!("proj_coords_uv: {proj_coords_uv}"); - - let shadow_map_atlas_texture = self.get_atlas_texture_at(light_slab, 0); - // With these projected coordinates we can sample the depth map as the - // resulting [0,1] coordinates from proj_coords directly correspond to - // the transformed NDC coordinates from the `ShadowMap::update` render pass. - // This gives us the closest depth from the light's point of view: - let pcf_samples_2 = *pcf_samples as i32 / 2; - let texel_size = 1.0 - / Vec2::new( - shadow_map_atlas_texture.size_px.x as f32, - shadow_map_atlas_texture.size_px.y as f32, - ); - let mut shadow = 0.0f32; - let mut total = 0.0f32; - for x in -pcf_samples_2..=pcf_samples_2 { - for y in -pcf_samples_2..=pcf_samples_2 { - let proj_coords = shadow_map_atlas_texture.uv( - proj_coords_uv + Vec2::new(x as f32, y as f32) * texel_size, - *shadow_map_atlas_size, - ); - let shadow_map_depth = shadow_map - .sample_by_lod(*shadow_map_sampler, proj_coords, 0.0) - .x; - // To get the current depth at this fragment we simply retrieve the projected vector's z - // coordinate which equals the depth of this fragment from the light's perspective. - let fragment_depth = frag_pos_in_light_space.z; - - // If the `current_depth`, which is the depth of the fragment from the lights POV, is - // greater than the `closest_depth` of the shadow map at that fragment, the fragment - // is in shadow - crate::println!("current_depth: {fragment_depth}"); - crate::println!("closest_depth: {shadow_map_depth}"); - let bias = (bias_max * (1.0 - surface_normal.dot(*light_direction))).max(*bias_min); - - if (fragment_depth - bias) >= shadow_map_depth { - shadow += 1.0 - } - total += 1.0; - } - } - shadow / total.max(1.0) - } - - pub const POINT_SAMPLE_OFFSET_DIRECTIONS: [Vec3; 21] = [ - Vec3::ZERO, - Vec3::new(1.0, 1.0, 1.0), - Vec3::new(1.0, -1.0, 1.0), - Vec3::new(-1.0, -1.0, 1.0), - Vec3::new(-1.0, 1.0, 1.0), - Vec3::new(1.0, 1.0, -1.0), - Vec3::new(1.0, -1.0, -1.0), - Vec3::new(-1.0, -1.0, -1.0), - Vec3::new(-1.0, 1.0, -1.0), - Vec3::new(1.0, 1.0, 0.0), - Vec3::new(1.0, -1.0, 0.0), - Vec3::new(-1.0, -1.0, 0.0), - Vec3::new(-1.0, 1.0, 0.0), - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(-1.0, 0.0, 1.0), - Vec3::new(1.0, 0.0, -1.0), - Vec3::new(-1.0, 0.0, -1.0), - Vec3::new(0.0, 1.0, 1.0), - Vec3::new(0.0, -1.0, 1.0), - Vec3::new(0.0, -1.0, -1.0), - Vec3::new(0.0, 1.0, -1.0), - ]; - /// Returns shadow _intensity_ for point lights. - /// - /// Returns `0.0` when the fragment is in full light. - /// Returns `1.0` when the fragment is in full shadow. - pub fn run_point( - &self, - light_slab: &[u32], - shadow_map: &T, - shadow_map_sampler: &S, - light_pos_in_world_space: Vec3, - ) -> f32 - where - S: IsSampler, - T: Sample2dArray, - { - let ShadowCalculation { - shadow_map_desc, - shadow_map_atlas_size, - frag_pos_in_world_space, - surface_normal_in_world_space: surface_normal, - frag_to_light_in_world_space: frag_to_light, - bias_min, - bias_max, - pcf_samples, - } = self; - - let light_to_frag_dir = frag_pos_in_world_space - light_pos_in_world_space; - crate::println!("light_to_frag_dir: {light_to_frag_dir}"); - - let pcf_samplesf = (*pcf_samples as f32) - .max(1.0) - .min(Self::POINT_SAMPLE_OFFSET_DIRECTIONS.len() as f32); - let pcf_samples = pcf_samplesf as usize; - let view_distance = light_to_frag_dir.length(); - let disk_radius = (1.0 + view_distance / shadow_map_desc.z_far) / 25.0; - let mut shadow = 0.0f32; - for i in 0..pcf_samples { - let sample_offset = Self::POINT_SAMPLE_OFFSET_DIRECTIONS[i] * disk_radius; - crate::println!("sample_offset: {sample_offset}"); - let sample_dir = (light_to_frag_dir + sample_offset).alt_norm_or_zero(); - let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(sample_dir); - crate::println!("face_index: {face_index}",); - crate::println!("uv: {uv}"); - let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, face_index); - let face_texture = self.get_atlas_texture_at(light_slab, face_index); - let uv_tex = face_texture.uv(uv, *shadow_map_atlas_size); - let shadow_map_depth = shadow_map.sample_by_lod(*shadow_map_sampler, uv_tex, 0.0).x; - let fragment_depth = frag_pos_in_light_space.z; - let bias = (bias_max * (1.0 - surface_normal.dot(*frag_to_light))).max(*bias_min); - if (fragment_depth - bias) > shadow_map_depth { - shadow += 1.0 - } - } - - shadow / pcf_samplesf - } -} - -/// Depth pre-pass for the light tiling feature. -/// -/// This shader writes all staged [`Renderlet`]'s depth into a buffer. -/// -/// This shader is very much like [`shadow_mapping_vertex`], except that -/// shader gets its projection+view matrix from the light stored in a -/// `ShadowMapDescriptor`. -/// -/// Here we want to render as normal forward pass would, with the `Renderlet`'s view -/// and the [`Camera`]'s projection. -/// -/// ## Note -/// This shader will likely be expanded to include parts of occlusion culling and order -/// independent transparency. -#[spirv(vertex)] -pub fn light_tiling_depth_pre_pass( - // Points at a `Renderlet`. - #[spirv(instance_index)] renderlet_id: Id, - // Which vertex within the renderlet are we rendering? - #[spirv(vertex_index)] vertex_index: u32, - // The slab where the renderlet's geometry is staged - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], - // Output clip coords - #[spirv(position)] out_clip_pos: &mut Vec4, -) { - let renderlet = geometry_slab.read_unchecked(renderlet_id); - if !renderlet.visible { - // put it outside the clipping frustum - *out_clip_pos = Vec3::splat(100.0).extend(1.0); - return; - } - - let camera_id = geometry_slab - .read_unchecked(Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID); - let camera = geometry_slab.read_unchecked(camera_id); - - let VertexInfo { world_pos, .. } = renderlet.get_vertex_info(vertex_index, geometry_slab); - - *out_clip_pos = camera.view_projection() * world_pos.extend(1.0); -} - -pub type DepthImage2d = Image!(2D, type=f32, sampled, depth); - -pub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled=true); - -/// A tile of screen space used to cull lights. -#[derive(Clone, Copy, Default, SlabItem)] -#[offsets] -pub struct LightTile { - /// Minimum depth of objects found within the frustum of the tile. - pub depth_min: u32, - /// Maximum depth of objects foudn within the frustum of the tile. - pub depth_max: u32, - /// The count of lights in this tile. - /// - /// Also, the next available light index. - pub next_light_index: u32, - /// List of light ids that intersect this tile's frustum. - pub lights_array: Array>, -} - -impl core::fmt::Debug for LightTile { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("LightTile") - .field("depth_min", &dequantize_depth_u32_to_f32(self.depth_min)) - .field("depth_max", &dequantize_depth_u32_to_f32(self.depth_max)) - .field("next_light_index", &self.next_light_index) - .field("lights_array", &self.lights_array) - .finish() - } -} - -/// Descriptor of the light tiling operation, which culls lights by accumulating -/// them into lists that illuminate tiles of the screen. -#[derive(Clone, Copy, SlabItem, core::fmt::Debug)] -pub struct LightTilingDescriptor { - /// Size of the [`Stage`]'s depth texture. - pub depth_texture_size: UVec2, - /// Configurable tile size. - pub tile_size: u32, - /// Array pointing to the lighting "tiles". - pub tiles_array: Array, - /// Minimum illuminance. - /// - /// Used to determine whether a light illuminates a tile. - pub minimum_illuminance_lux: f32, -} - -impl Default for LightTilingDescriptor { - fn default() -> Self { - Self { - depth_texture_size: Default::default(), - tile_size: 16, - tiles_array: Default::default(), - minimum_illuminance_lux: 0.1, - } - } -} - -impl LightTilingDescriptor { - /// Returns the dimensions of the grid of tiles. - pub fn tile_grid_size(&self) -> UVec2 { - let dims_f32 = self.depth_texture_size.as_vec2() / self.tile_size as f32; - dims_f32.ceil().as_uvec2() - } - - pub fn tile_coord_for_fragment(&self, frag_coord: Vec2) -> UVec2 { - let frag_coord = frag_coord.as_uvec2(); - frag_coord / self.tile_size - } - - pub fn tile_index_for_fragment(&self, frag_coord: Vec2) -> usize { - let tile_coord = self.tile_coord_for_fragment(frag_coord); - let tile_dimensions = self.tile_grid_size(); - (tile_coord.y * tile_dimensions.x + tile_coord.x) as usize - } -} - -/// Quantizes a fragment depth from `f32` to `u32`. -pub fn quantize_depth_f32_to_u32(depth: f32) -> u32 { - (u32::MAX as f32 * depth).round() as u32 -} - -/// Reconstructs a previously quantized depth from a `u32`. -pub fn dequantize_depth_u32_to_f32(depth: u32) -> f32 { - depth as f32 / u32::MAX as f32 -} - -/// Helper for determining the next light to check during an -/// invocation of the light list computation. -struct NextLightIndex { - current_step: usize, - tile_size: u32, - lights: Array>, - global_id: UVec3, -} - -impl Iterator for NextLightIndex { - type Item = Id>; - - fn next(&mut self) -> Option { - let next_index = self.next_index(); - self.current_step += 1; - if next_index < self.lights.len() { - Some(self.lights.at(next_index)) - } else { - None - } - } -} - -impl NextLightIndex { - pub fn new( - global_id: UVec3, - tile_size: u32, - analytical_lights_array: Array>, - ) -> Self { - Self { - current_step: 0, - tile_size, - lights: analytical_lights_array, - global_id, - } - } - - pub fn next_index(&self) -> usize { - // Determine the xy coord of this invocation within the _tile_ - let frag_tile_xy = self.global_id.xy() % self.tile_size; - // Determine the index of this invocation within the _tile_ - let offset = frag_tile_xy.y * self.tile_size + frag_tile_xy.x; - let stride = (self.tile_size * self.tile_size) as usize; - self.current_step * stride + offset as usize - } -} - -struct LightTilingInvocation { - global_id: UVec3, - descriptor: LightTilingDescriptor, -} - -impl LightTilingInvocation { - fn new(global_id: UVec3, descriptor: LightTilingDescriptor) -> Self { - Self { - global_id, - descriptor, - } - } - - /// The fragment's position. - /// - /// X range is 0 to (width - 1), Y range is 0 to (height - 1). - fn frag_pos(&self) -> UVec2 { - self.global_id.xy() - } - - /// The number of tiles in X and Y within the depth texture. - fn tile_grid_size(&self) -> UVec2 { - self.descriptor.tile_grid_size() - } - - /// The tile's coordinate among all tiles in the tile grid. - /// - /// The units are in tile x y. - fn tile_coord(&self) -> UVec2 { - self.global_id.xy() / self.descriptor.tile_size - } - - /// The tile's index in all the [`LightTilingDescriptor`]'s `tile_array`. - fn tile_index(&self) -> usize { - let tile_pos = self.tile_coord(); - let tile_dimensions = self.tile_grid_size(); - (tile_pos.y * tile_dimensions.x + tile_pos.x) as usize - } - - /// The tile's normalized midpoint. - fn tile_ndc_midpoint(&self) -> Vec2 { - let min_coord = self.tile_coord().as_vec2(); - let mid_coord = min_coord + 0.5; - crate::math::convert_pixel_to_ndc(mid_coord, self.tile_grid_size()) - } - - /// Compute the min and max depth of one fragment/invocation for light tiling. - /// - /// The min and max is stored in a tile on lighting slab. - fn compute_min_and_max_depth( - &self, - depth_texture: &impl Fetch, - lighting_slab: &mut [u32], - ) { - let frag_pos = self.frag_pos(); - let depth_texture_size = self.descriptor.depth_texture_size; - if frag_pos.x >= depth_texture_size.x || frag_pos.y >= depth_texture_size.y { - return; - } - // Depth frag value at the fragment position - let frag_depth: f32 = depth_texture.fetch(frag_pos).x; - // Fragment depth scaled to min/max of u32 values - // - // This is so we can compare with normal atomic ops instead of using the float extension - let frag_depth_u32 = quantize_depth_f32_to_u32(frag_depth); - - // The tile's index in all the tiles - let tile_index = self.tile_index(); - let lighting_desc = lighting_slab.read_unchecked(Id::::new(0)); - let tiling_desc = lighting_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id); - // index of the tile's min depth atomic value in the lighting slab - let tile_id = tiling_desc.tiles_array.at(tile_index); - let min_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MIN; - // index of the tile's max depth atomic value in the lighting slab - let max_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MAX; - - let _prev_min_depth = crate::sync::atomic_u_min::< - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, - >(lighting_slab, min_depth_index, frag_depth_u32); - let _prev_max_depth = crate::sync::atomic_u_max::< - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, - >(lighting_slab, max_depth_index, frag_depth_u32); - } - - /// Determine whether this invocation should run. - fn should_invoke(&self) -> bool { - self.global_id.x < self.descriptor.depth_texture_size.x - && self.global_id.y < self.descriptor.depth_texture_size.y - } - - /// Clears one tile. - /// - /// ## Note - /// This is only valid to call from the [`light_tiling_clear_tiles`] shader. - fn clear_tile(&self, lighting_slab: &mut [u32]) { - let dimensions = self.tile_grid_size(); - let index = (self.global_id.y * dimensions.x + self.global_id.x) as usize; - if index < self.descriptor.tiles_array.len() { - let tile_id = self.descriptor.tiles_array.at(index); - let mut tile = lighting_slab.read(tile_id); - tile.depth_min = u32::MAX; - tile.depth_max = 0; - tile.next_light_index = 0; - lighting_slab.write(tile_id, &tile); - // Zero out the light list and the ratings - for id in tile.lights_array.iter() { - lighting_slab.write(id, &Id::NONE); - } - } - } - - // The difficulty here is that in SPIRV we can access `lighting_slab` atomically without wrapping it - // in a type, but on CPU we must pass an array of (something like) `AtomicU32`. I'm not sure how to - // model this interaction to test it on the CPU. - fn compute_light_lists(&self, geometry_slab: &[u32], lighting_slab: &mut [u32]) { - let index = self.tile_index(); - let tile_id = self.descriptor.tiles_array.at(index); - // Construct the tile's frustum in clip space. - let depth_min_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MIN); - let depth_max_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MAX); - let depth_min = dequantize_depth_u32_to_f32(depth_min_u32); - let depth_max = dequantize_depth_u32_to_f32(depth_max_u32); - - if depth_min == depth_max { - // If we would construct a frustum with zero volume, abort. - // - // See - // for more info. - return; - } - - let camera_id = geometry_slab.read_unchecked( - Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID, - ); - let camera = geometry_slab.read_unchecked(camera_id); - - // let (ndc_tile_min, ndc_tile_max) = self.tile_ndc_min_max(); - // // This is the AABB frustum, in NDC coords - // let ndc_tile_aabb = Aabb::new( - // ndc_tile_min.extend(depth_min), - // ndc_tile_max.extend(depth_max), - // ); - - // Get the frustum (here simplified to a line) in world coords, since we'll be - // using it to compare against the radius of illumination of each light - let tile_ndc_midpoint = self.tile_ndc_midpoint(); - let tile_line_ndc = ( - tile_ndc_midpoint.extend(depth_min), - tile_ndc_midpoint.extend(depth_max), - ); - let inverse_viewproj = camera.view_projection().inverse(); - let tile_line = ( - inverse_viewproj.project_point3(tile_line_ndc.0), - inverse_viewproj.project_point3(tile_line_ndc.1), - ); - - let tile_index = self.tile_index(); - let tile_id = self.descriptor.tiles_array.at(tile_index); - let tile_lights_array = lighting_slab.read(tile_id + LightTile::OFFSET_OF_LIGHTS_ARRAY); - let next_light_id = tile_id + LightTile::OFFSET_OF_NEXT_LIGHT_INDEX; - - // List of all analytical lights in the scene - let analytical_lights_array = lighting_slab.read_unchecked( - Id::::new(0) - + LightingDescriptor::OFFSET_OF_ANALYTICAL_LIGHTS_ARRAY, - ); - - // Each invocation will calculate a few lights' contribution to the tile, until all lights - // have been visited - let next_light = NextLightIndex::new( - self.global_id, - self.descriptor.tile_size, - analytical_lights_array, - ); - for id_of_light_id in next_light { - let light_id = lighting_slab.read_unchecked(id_of_light_id); - let light = lighting_slab.read_unchecked(light_id); - let transform = lighting_slab.read(light.transform_id); - // Get the distance to the light in world coords, and the - // intensity of the light. - let (distance, intensity_candelas) = match light.light_type { - LightStyle::Directional => { - let directional_light = lighting_slab.read(light.into_directional_id()); - (0.0, directional_light.intensity) - } - LightStyle::Point => { - let point_light = lighting_slab.read(light.into_point_id()); - let center = Mat4::from(transform).transform_point3(point_light.position); - let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1); - (distance, point_light.intensity) - } - LightStyle::Spot => { - // TODO: take into consideration the direction the spot light is pointing - let spot_light = lighting_slab.read(light.into_spot_id()); - let center = Mat4::from(transform).transform_point3(spot_light.position); - let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1); - (distance, spot_light.intensity) - } - }; - - let radius = - radius_of_illumination(intensity_candelas, self.descriptor.minimum_illuminance_lux); - let should_add = radius >= distance; - if should_add { - // If the light should be added to the bin, get the next available index in the bin, - // then write the id of the light into that index. - let next_index = crate::sync::atomic_i_increment::< - { spirv_std::memory::Scope::Workgroup as u32 }, - { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, - >(lighting_slab, next_light_id); - if next_index as usize >= tile_lights_array.len() { - // We've already filled the bin, so abort. - // - // TODO: Figure out a better way to handle light tile list overrun. - break; - } else { - // Get the id that corresponds to the next available index in the ratings bin - let binned_light_id = tile_lights_array.at(next_index as usize); - // Write to that location - lighting_slab.write(binned_light_id, &light_id); - } - } - } - } -} - -#[spirv(compute(threads(16, 16, 1)))] -pub fn light_tiling_clear_tiles( - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let lighting_descriptor = lighting_slab.read(Id::::new(0)); - let light_tiling_descriptor = - lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); - let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); - invocation.clear_tile(lighting_slab); -} - -/// Compute the min and max depth value for a tile. -/// -/// This shader must be called **once for each fragment in the depth texture**. -#[spirv(compute(threads(16, 16, 1)))] -pub fn light_tiling_compute_tile_min_and_max_depth( - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], - #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2d, - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let lighting_descriptor = lighting_slab.read(Id::::new(0)); - let light_tiling_descriptor = - lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); - let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); - invocation.compute_min_and_max_depth(depth_texture, lighting_slab); -} - -/// Compute the min and max depth value for a tile, multisampled. -/// -/// This shader must be called **once for each fragment in the depth texture**. -#[spirv(compute(threads(16, 16, 1)))] -pub fn light_tiling_compute_tile_min_and_max_depth_multisampled( - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], - #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2dMultisampled, - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let lighting_descriptor = lighting_slab.read(Id::::new(0)); - let light_tiling_descriptor = - lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); - let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); - invocation.compute_min_and_max_depth(depth_texture, lighting_slab); -} - -#[spirv(compute(threads(16, 16, 1)))] -pub fn light_tiling_bin_lights( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let lighting_descriptor = lighting_slab.read(Id::::new(0)); - let light_tiling_descriptor = - lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); - let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); - if invocation.should_invoke() { - invocation.compute_light_lists(geometry_slab, lighting_slab); - } -} +pub mod shader; #[cfg(test)] mod test { - use crate::math::GpuRng; + use crabslab::Array; + use glam::{UVec2, UVec3, Vec2, Vec3}; + + use crate::math::{GpuRng, IsVector}; - use super::*; + use super::shader::*; #[cfg(feature = "gltf")] #[test] @@ -1258,6 +115,8 @@ mod test { println!("{:#?}", std::env::current_dir()); let (document, _buffers, _images) = gltf::import("../../gltf/four_spotlights.glb").unwrap(); for node in document.nodes() { + use glam::Vec3; + println!("node: {} {:?}", node.index(), node.name()); let gltf_transform = node.transform(); diff --git a/crates/renderling/src/light/cpu.rs b/crates/renderling/src/light/cpu.rs index 4fdbfb09..0dca1ca3 100644 --- a/crates/renderling/src/light/cpu.rs +++ b/crates/renderling/src/light/cpu.rs @@ -1,25 +1,26 @@ //! CPU-only lighting and shadows. -use std::sync::{Arc, RwLock, RwLockReadGuard}; +use std::sync::{Arc, RwLock}; +#[cfg(doc)] +use crate::stage::Stage; use craballoc::{ prelude::{Hybrid, SlabAllocator, WgpuRuntime}, slab::SlabBuffer, - value::{HybridArray, HybridContainer, IsContainer, WeakContainer, WeakHybrid}, + value::HybridArray, }; -use crabslab::{Id, SlabItem}; -use glam::{Mat4, UVec2}; +use crabslab::Id; +use glam::{Mat4, UVec2, Vec3, Vec4}; use snafu::prelude::*; use crate::{ atlas::{Atlas, AtlasBlitter, AtlasError}, geometry::Geometry, - prelude::Transform, - stage::NestedTransform, + transform::{shader::TransformDescriptor, NestedTransform, Transform}, }; -use super::{ - DirectionalLightDescriptor, Light, LightStyle, LightingDescriptor, PointLightDescriptor, - SpotLightDescriptor, +use super::shader::{ + DirectionalLightDescriptor, LightDescriptor, LightStyle, LightingDescriptor, + PointLightDescriptor, SpotLightDescriptor, }; pub use super::shadow_map::ShadowMap; @@ -30,9 +31,6 @@ pub enum LightingError { #[snafu(display("{source}"))] Atlas { source: AtlasError }, - #[snafu(display("AnalyticalLightBundle attached to this ShadowMap was dropped"))] - DroppedAnalyticalLightBundle, - #[snafu(display("Driver poll error: {source}"))] Poll { source: wgpu::PollError }, } @@ -43,203 +41,608 @@ impl From for LightingError { } } -/// A wrapper around all types of analytical lighting. +/// Describes shared behaviour between all analytical lights. +pub trait IsLight: Clone { + /// Return the style of this light. + fn style(&self) -> LightStyle; + + fn light_space_transforms( + &self, + // Another transform applied to the light. + parent_transform: &TransformDescriptor, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum + // as a parameter and then figure out the minimal view projection that includes that frustum + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> Vec; +} + +/// A directional light. +/// +/// An analitical light that casts light in parallel, infinitely. #[derive(Clone, Debug)] -pub enum LightDetails { - Directional(C::Container), - Point(C::Container), - Spot(C::Container), +pub struct DirectionalLight { + descriptor: Hybrid, } -impl From> for LightDetails { - fn from(value: Hybrid) -> Self { - LightDetails::Directional(value) +impl IsLight for DirectionalLight { + fn style(&self) -> LightStyle { + LightStyle::Directional + } + + fn light_space_transforms( + &self, + parent_transform: &TransformDescriptor, + z_near: f32, + z_far: f32, + ) -> Vec { + let m = Mat4::from(*parent_transform); + vec![{ + let (p, v) = self + .descriptor() + .shadow_mapping_projection_and_view(&m, z_near, z_far); + p * v + }] } } -impl From> for LightDetails { - fn from(value: Hybrid) -> Self { - LightDetails::Spot(value) +impl DirectionalLight { + /// Returns a pointer to the descriptor data on the GPU slab. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Returns the a copy of the descriptor. + pub fn descriptor(&self) -> DirectionalLightDescriptor { + self.descriptor.get() } } -impl From> for LightDetails { - fn from(value: Hybrid) -> Self { - LightDetails::Point(value) +/// A [`DirectionalLight`] comes wrapped in [`AnalyticalLight`], giving the +/// [`AnalyticalLight`] the ability to simulate sunlight or other lights that +/// are "infinitely" far away. +impl AnalyticalLight { + /// Set the direction of the directional light. + pub fn set_direction(&self, direction: Vec3) -> &Self { + self.inner.descriptor.modify(|d| d.direction = direction); + self + } + + /// Set the direction and return the directional light. + pub fn with_direction(self, direction: Vec3) -> Self { + self.set_direction(direction); + self + } + + /// Modify the direction of the directional light. + pub fn modify_direction(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.direction)) + } + + /// Get the direction of the directional light. + pub fn direction(&self) -> Vec3 { + self.inner.descriptor.get().direction + } + + /// Set the color of the directional light. + pub fn set_color(&self, color: Vec4) -> &Self { + self.inner.descriptor.modify(|d| d.color = color); + self + } + + /// Set the color and return the directional light. + pub fn with_color(self, color: Vec4) -> Self { + self.set_color(color); + self + } + + /// Modify the color of the directional light. + pub fn modify_color(&self, f: impl FnOnce(&mut Vec4) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.color)) + } + + /// Get the color of the directional light. + pub fn color(&self) -> Vec4 { + self.inner.descriptor.get().color + } + + /// Set the intensity of the directional light. + pub fn set_intensity(&self, intensity: f32) -> &Self { + self.inner.descriptor.modify(|d| d.intensity = intensity); + self + } + + /// Set the intensity and return the directional light. + pub fn with_intensity(self, intensity: f32) -> Self { + self.set_intensity(intensity); + self + } + + /// Modify the intensity of the directional light. + pub fn modify_intensity(&self, f: impl FnOnce(&mut f32) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.intensity)) + } + + /// Get the intensity of the directional light. + pub fn intensity(&self) -> f32 { + self.inner.descriptor.get().intensity } } -impl LightDetails { - pub fn as_directional(&self) -> Option<&C::Container> { - if let LightDetails::Directional(d) = self { - Some(d) - } else { - None - } +/// A point light. +/// +/// An analytical light that emits light in all directions from a single point. +#[derive(Clone, Debug)] +pub struct PointLight { + descriptor: Hybrid, +} + +impl IsLight for PointLight { + fn style(&self) -> LightStyle { + LightStyle::Point } - pub fn as_spot(&self) -> Option<&C::Container> { - if let LightDetails::Spot(s) = self { - Some(s) - } else { - None - } + fn light_space_transforms( + &self, + t: &TransformDescriptor, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> Vec { + let m = Mat4::from(*t); + let (p, vs) = self + .descriptor() + .shadow_mapping_projection_and_view_matrices(&m, z_near, z_far); + vs.into_iter().map(|v| p * v).collect() } +} - pub fn as_point(&self) -> Option<&C::Container> { - if let LightDetails::Point(p) = self { - Some(p) - } else { - None - } +impl PointLight { + /// Returns a pointer to the descriptor data on the GPU slab. + pub fn id(&self) -> Id { + self.descriptor.id() } - pub fn style(&self) -> LightStyle { - match self { - LightDetails::Directional(_) => LightStyle::Directional, - LightDetails::Point(_) => LightStyle::Point, - LightDetails::Spot(_) => LightStyle::Spot, - } + /// Returns a copy of the descriptor. + pub fn descriptor(&self) -> PointLightDescriptor { + self.descriptor.get() + } +} + +/// A [`PointLight`] comes wrapped in [`AnalyticalLight`], giving the +/// [`AnalyticalLight`] the ability to simulate lights that +/// emit from a single point in space and attenuate exponentially with +/// distance. +impl AnalyticalLight { + /// Set the position of the point light. + pub fn set_position(&self, position: Vec3) -> &Self { + self.inner.descriptor.modify(|d| d.position = position); + self + } + + /// Set the position and return the point light. + pub fn with_position(self, position: Vec3) -> Self { + self.set_position(position); + self + } + + /// Modify the position of the point light. + pub fn modify_position(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.position)) + } + + /// Get the position of the point light. + pub fn position(&self) -> Vec3 { + self.inner.descriptor.get().position + } + + /// Set the color of the point light. + pub fn set_color(&self, color: Vec4) -> &Self { + self.inner.descriptor.modify(|d| d.color = color); + self + } + + /// Set the color and return the point light. + pub fn with_color(self, color: Vec4) -> Self { + self.set_color(color); + self + } + + /// Modify the color of the point light. + pub fn modify_color(&self, f: impl FnOnce(&mut Vec4) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.color)) + } + + /// Get the color of the point light. + pub fn color(&self) -> Vec4 { + self.inner.descriptor.get().color + } + + /// Set the intensity of the point light. + pub fn set_intensity(&self, intensity: f32) -> &Self { + self.inner.descriptor.modify(|d| d.intensity = intensity); + self + } + + /// Set the intensity and return the point light. + pub fn with_intensity(self, intensity: f32) -> Self { + self.set_intensity(intensity); + self + } + + /// Modify the intensity of the point light. + pub fn modify_intensity(&self, f: impl FnOnce(&mut f32) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.intensity)) + } + + /// Get the intensity of the point light. + pub fn intensity(&self) -> f32 { + self.inner.descriptor.get().intensity + } +} + +/// A spot light. +/// +/// An analytical light that emits light in a cone shape. +#[derive(Clone, Debug)] +pub struct SpotLight { + descriptor: Hybrid, +} + +impl IsLight for SpotLight { + fn style(&self) -> LightStyle { + LightStyle::Spot + } + + fn light_space_transforms( + &self, + t: &TransformDescriptor, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> Vec { + let m = Mat4::from(*t); + vec![{ + let (p, v) = self + .descriptor() + .shadow_mapping_projection_and_view(&m, z_near, z_far); + p * v + }] + } +} + +impl SpotLight { + /// Returns a pointer to the descriptor data on the GPU slab. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Returns a copy of the descriptor. + pub fn descriptor(&self) -> SpotLightDescriptor { + self.descriptor.get() + } +} + +/// A [`SpotLight`] comes wrapped in [`AnalyticalLight`], giving the +/// [`AnalyticalLight`] the ability to simulate lights that +/// emit from a single point in space in a specific direction, with +/// a specific spread. +impl AnalyticalLight { + /// Set the position of the spot light. + pub fn set_position(&self, position: Vec3) -> &Self { + self.inner.descriptor.modify(|d| d.position = position); + self + } + + /// Set the position and return the spot light. + pub fn with_position(self, position: Vec3) -> Self { + self.set_position(position); + self + } + + /// Modify the position of the spot light. + pub fn modify_position(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.position)) + } + + /// Get the position of the spot light. + pub fn position(&self) -> Vec3 { + self.inner.descriptor.get().position + } + + /// Set the direction of the spot light. + pub fn set_direction(&self, direction: Vec3) -> &Self { + self.inner.descriptor.modify(|d| d.direction = direction); + self + } + + /// Set the direction and return the spot light. + pub fn with_direction(self, direction: Vec3) -> Self { + self.set_direction(direction); + self + } + + /// Modify the direction of the spot light. + pub fn modify_direction(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.direction)) + } + + /// Get the direction of the spot light. + pub fn direction(&self) -> Vec3 { + self.inner.descriptor.get().direction + } + + /// Set the inner cutoff of the spot light. + pub fn set_inner_cutoff(&self, inner_cutoff: f32) -> &Self { + self.inner + .descriptor + .modify(|d| d.inner_cutoff = inner_cutoff); + self + } + + /// Set the inner cutoff and return the spot light. + pub fn with_inner_cutoff(self, inner_cutoff: f32) -> Self { + self.set_inner_cutoff(inner_cutoff); + self + } + + /// Modify the inner cutoff of the spot light. + pub fn modify_inner_cutoff(&self, f: impl FnOnce(&mut f32) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.inner_cutoff)) + } + + /// Get the inner cutoff of the spot light. + pub fn inner_cutoff(&self) -> f32 { + self.inner.descriptor.get().inner_cutoff + } + + /// Set the outer cutoff of the spot light. + pub fn set_outer_cutoff(&self, outer_cutoff: f32) -> &Self { + self.inner + .descriptor + .modify(|d| d.outer_cutoff = outer_cutoff); + self + } + + /// Set the outer cutoff and return the spot light. + pub fn with_outer_cutoff(self, outer_cutoff: f32) -> Self { + self.set_outer_cutoff(outer_cutoff); + self + } + + /// Modify the outer cutoff of the spot light. + pub fn modify_outer_cutoff(&self, f: impl FnOnce(&mut f32) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.outer_cutoff)) + } + + /// Get the outer cutoff of the spot light. + pub fn outer_cutoff(&self) -> f32 { + self.inner.descriptor.get().outer_cutoff + } + + /// Set the color of the spot light. + pub fn set_color(&self, color: Vec4) -> &Self { + self.inner.descriptor.modify(|d| d.color = color); + self + } + + /// Set the color and return the spot light. + pub fn with_color(self, color: Vec4) -> Self { + self.set_color(color); + self + } + + /// Modify the color of the spot light. + pub fn modify_color(&self, f: impl FnOnce(&mut Vec4) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.color)) + } + + /// Get the color of the spot light. + pub fn color(&self) -> Vec4 { + self.inner.descriptor.get().color + } + + /// Set the intensity of the spot light. + pub fn set_intensity(&self, intensity: f32) -> &Self { + self.inner.descriptor.modify(|d| d.intensity = intensity); + self + } + + /// Set the intensity and return the spot light. + pub fn with_intensity(self, intensity: f32) -> Self { + self.set_intensity(intensity); + self + } + + /// Modify the intensity of the spot light. + pub fn modify_intensity(&self, f: impl FnOnce(&mut f32) -> T) -> T { + self.inner.descriptor.modify(|d| f(&mut d.intensity)) + } + + /// Get the intensity of the spot light. + pub fn intensity(&self) -> f32 { + self.inner.descriptor.get().intensity } } -impl LightDetails { - pub(crate) fn from_hybrid(hybrid: &LightDetails) -> Self { - match hybrid { - LightDetails::Directional(d) => LightDetails::Directional(WeakHybrid::from_hybrid(d)), - LightDetails::Point(p) => LightDetails::Point(WeakHybrid::from_hybrid(p)), - LightDetails::Spot(s) => LightDetails::Spot(WeakHybrid::from_hybrid(s)), +#[derive(Clone)] +pub enum Light { + Directional(DirectionalLight), + Point(PointLight), + Spot(SpotLight), +} + +impl From for Light { + fn from(light: DirectionalLight) -> Self { + Light::Directional(light) + } +} + +impl From for Light { + fn from(light: PointLight) -> Self { + Light::Point(light) + } +} + +impl From for Light { + fn from(light: SpotLight) -> Self { + Light::Spot(light) + } +} + +impl IsLight for Light { + fn style(&self) -> LightStyle { + match self { + Light::Directional(light) => light.style(), + Light::Point(light) => light.style(), + Light::Spot(light) => light.style(), } } - pub(crate) fn upgrade(&self) -> Option { - Some(match self { - LightDetails::Directional(d) => LightDetails::Directional(d.upgrade()?), - LightDetails::Point(p) => LightDetails::Point(p.upgrade()?), - LightDetails::Spot(s) => LightDetails::Spot(s.upgrade()?), - }) + fn light_space_transforms( + &self, + // Another transform applied to the light. + parent_transform: &TransformDescriptor, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> Vec { + match self { + Light::Directional(light) => { + light.light_space_transforms(parent_transform, z_near, z_far) + } + Light::Point(light) => light.light_space_transforms(parent_transform, z_near, z_far), + Light::Spot(light) => light.light_space_transforms(parent_transform, z_near, z_far), + } } } /// A bundle of lighting resources representing one analytical light in a scene. /// -/// Create an `AnalyticalLightBundle` with the `Lighting::new_analytical_light`, -/// or from `Stage::new_analytical_light`. -pub struct AnalyticalLight { +/// Create an [`AnalyticalLight`] with: +/// * [`Stage::new_directional_light`] +/// * [`Stage::new_point_light`] +/// * [`Stage::new_spot_light`]. +/// +/// Lights may be added and removed from rendering with [`Stage::add_light`] and +/// [`Stage::remove_light`]. +/// The GPU resources a light uses will not be released until [`Stage::remove_light`] +/// is called _and_ the light is dropped. +#[derive(Clone)] +pub struct AnalyticalLight { /// The generic light descriptor. - light: Ct::Container, - /// The specific light descriptor. - light_details: LightDetails, + pub(crate) light_descriptor: Hybrid, + /// The specific light. + inner: T, /// The light's global transform. /// /// This value lives in the lighting slab. - transform: Ct::Container, + transform: Transform, /// The light's nested transform. /// /// This value comes from the light's node, if it belongs to one. /// This may have been set if this light originated from a GLTF file. /// This value lives on the geometry slab and must be referenced here /// to keep the two in sync, which is required to animate lights. - node_transform: Arc>>>, + node_transform: Arc>>, } -impl core::fmt::Display for AnalyticalLight { +impl core::fmt::Display for AnalyticalLight { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( - "AnalyticalLightBundle type={} light-id={:?} node-nested-transform-global-id:{:?}", - self.light_details.style(), - self.light.id(), - self.node_transform.read().unwrap().as_ref().and_then(|wh| { - let h: NestedTransform = wh.upgrade()?; - Some(h.global_transform_id()) - }) + "AnalyticalLight type={} light-id={:?} node-nested-transform-global-id:{:?}", + self.inner.style(), + self.light_descriptor.id(), + self.node_transform + .read() + .unwrap() + .as_ref() + .map(|h| h.global_id()) )) } } -impl Clone for AnalyticalLight -where - Ct::Container: Clone, - Ct::Container: Clone, - LightDetails: Clone, - NestedTransform: Clone, -{ - fn clone(&self) -> Self { - Self { - light: self.light.clone(), - light_details: self.light_details.clone(), - transform: self.transform.clone(), - node_transform: self.node_transform.clone(), - } +impl IsLight for AnalyticalLight { + fn style(&self) -> LightStyle { + self.inner.style() + } + + fn light_space_transforms( + &self, + // Another transform applied to the light. + parent_transform: &TransformDescriptor, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum + // as a parameter and then figure out the minimal view projection that includes that frustum + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> Vec { + self.inner + .light_space_transforms(parent_transform, z_near, z_far) } } -impl AnalyticalLight { - pub(crate) fn from_hybrid(light: &AnalyticalLight) -> Self { - AnalyticalLight { - light: WeakHybrid::from_hybrid(&light.light), - light_details: LightDetails::from_hybrid(&light.light_details), - transform: WeakHybrid::from_hybrid(&light.transform), - node_transform: light.node_transform.clone(), +impl AnalyticalLight { + /// Returns a reference to the inner `DirectionalLight`, if this light is directional. + pub fn as_directional(&self) -> Option<&DirectionalLight> { + match &self.inner { + Light::Directional(light) => Some(light), + _ => None, } } - pub(crate) fn upgrade(&self) -> Option { - Some(AnalyticalLight { - light: self.light.upgrade()?, - light_details: self.light_details.upgrade()?, - transform: self.transform.upgrade()?, - node_transform: self.node_transform.clone(), - }) + /// Returns a reference to the inner `PointLight`, if this light is a point light. + pub fn as_point(&self) -> Option<&PointLight> { + match &self.inner { + Light::Point(light) => Some(light), + _ => None, + } } -} -impl AnalyticalLight { - pub fn weak(&self) -> AnalyticalLight { - AnalyticalLight::from_hybrid(self) - } - - pub fn light_space_transforms(&self, z_near: f32, z_far: f32) -> Vec { - let t = self.transform.get(); - let m = Mat4::from(t); - match &self.light_details { - LightDetails::Directional(d) => vec![{ - let (p, v) = d - .get() - .shadow_mapping_projection_and_view(&m, z_near, z_far); - p * v - }], - LightDetails::Point(point) => { - let (p, vs) = point - .get() - .shadow_mapping_projection_and_view_matrices(&m, z_near, z_far); - vs.into_iter().map(|v| p * v).collect() - } - LightDetails::Spot(spot) => vec![{ - let (p, v) = spot - .get() - .shadow_mapping_projection_and_view(&m, z_near, z_far); - p * v - }], + /// Returns a reference to the inner `SpotLight`, if this light is a spot light. + pub fn as_spot(&self) -> Option<&SpotLight> { + match &self.inner { + Light::Spot(light) => Some(light), + _ => None, } } } -impl AnalyticalLight { - /// Link this light to a node's `NestedTransform`. - pub fn link_node_transform(&self, transform: &NestedTransform) { - *self.node_transform.write().unwrap() = - Some(NestedTransform::::from_hybrid(transform)); +impl AnalyticalLight { + /// Returns a pointer to this light on the GPU + pub fn id(&self) -> Id { + self.light_descriptor.id() } - /// Get a reference to the generic light descriptor. - pub fn light(&self) -> &Ct::Container { - &self.light + /// Returns a copy of the descriptor on the GPU. + pub fn descriptor(&self) -> LightDescriptor { + self.light_descriptor.get() } - /// Get a reference to the specific light descriptor. - pub fn light_details(&self) -> &LightDetails { - &self.light_details + /// Link this light to a node's `NestedTransform`. + pub fn link_node_transform(&self, transform: &NestedTransform) { + *self.node_transform.write().unwrap() = Some(transform.clone()); + } + + /// Get a reference to the inner light. + pub fn inner(&self) -> &T { + &self.inner } /// Get a reference to the light's global transform. @@ -247,10 +650,10 @@ impl AnalyticalLight { /// This value lives in the lighting slab. /// /// ## Note - /// If a `NestedTransform` has been linked to this light by using [`Self::link_node_transform`], + /// If a [`NestedTransform`] has been linked to this light by using [`Self::link_node_transform`], /// the transform returned by this function may be overwritten at any point by the given - /// `NestedTransform`. - pub fn transform(&self) -> &Ct::Container { + /// [`NestedTransform`]. + pub fn transform(&self) -> &Transform { &self.transform } @@ -261,23 +664,33 @@ impl AnalyticalLight { /// To change this value, you should do so through the `NestedTransform`, which is likely /// held in the pub fn linked_node_transform(&self) -> Option { - let guard = self.node_transform.read().unwrap(); - let weak = guard.as_ref()?; - weak.upgrade() + self.node_transform + .read() + .unwrap() + .as_ref() + .map(|t| t.clone()) } -} - -struct AnalyticalLightIterator<'a> { - inner: RwLockReadGuard<'a, Vec>>, - index: usize, -} -impl Iterator for AnalyticalLightIterator<'_> { - type Item = AnalyticalLight; - - fn next(&mut self) -> Option { - let item = self.inner.get(self.index)?; - item.upgrade() + /// Convert this light into a generic light, hiding the specific light type that it is. + /// + /// This is useful if you want to store your lights together. + pub fn into_generic(self) -> AnalyticalLight + where + Light: From, + { + let AnalyticalLight { + light_descriptor, + inner, + transform, + node_transform, + } = self; + let inner = Light::from(inner); + AnalyticalLight { + light_descriptor, + inner, + transform, + node_transform, + } } } @@ -289,8 +702,8 @@ pub struct Lighting { pub(crate) light_slab_buffer: Arc>>, pub(crate) geometry_slab_buffer: Arc>>, pub(crate) lighting_descriptor: Hybrid, - pub(crate) analytical_lights: Arc>>>, - pub(crate) analytical_lights_array: Arc>>>>, + pub(crate) analytical_lights: Arc>>, + pub(crate) analytical_lights_array: Arc>>>>, pub(crate) shadow_map_update_pipeline: Arc, pub(crate) shadow_map_update_bindgroup_layout: Arc, pub(crate) shadow_map_update_blitter: AtlasBlitter, @@ -388,92 +801,135 @@ impl Lighting { &self.light_slab } - /// Add an [`AnalyticalLightBundle`] to the internal list of lights. + /// Add an [`AnalyticalLight`] to the internal list of lights. + /// + /// This is called implicitly by: /// - /// This is called implicitly by [`Lighting::new_analytical_light`]. + /// * [`Lighting::new_directional_light`]. + /// * [`Lighting::new_point_light`]. + /// * [`Lighting::new_spot_light`]. /// /// This can be used to add the light back to the scene after using /// [`Lighting::remove_light`]. - pub fn add_light(&self, bundle: &AnalyticalLight) { + pub fn add_light(&self, bundle: &AnalyticalLight) + where + T: IsLight, + Light: From, + { log::trace!( "adding light {:?} ({})", - bundle.light.id(), - bundle.light_details.style() + bundle.light_descriptor.id(), + bundle.inner.style() ); // Update our list of weakly ref'd light bundles self.analytical_lights .write() .unwrap() - .push(AnalyticalLight::::from_hybrid(bundle)); + .push(bundle.clone().into_generic()); // Invalidate the array of lights *self.analytical_lights_array.write().unwrap() = None; } - /// Remove an [`AnalyticalLightBundle`] from the internal list of lights. + /// Remove an [`AnalyticalLight`] from the internal list of lights. /// /// Use this to exclude a light from rendering, without dropping the light. /// /// After calling this function you can include the light again using [`Lighting::add_light`]. - pub fn remove_light(&self, bundle: &AnalyticalLight) { + pub fn remove_light(&self, bundle: &AnalyticalLight) { log::trace!( "removing light {:?} ({})", - bundle.light.id(), - bundle.light_details.style() + bundle.light_descriptor.id(), + bundle.inner.style() ); // Remove the light from the list of weakly ref'd light bundles let mut guard = self.analytical_lights.write().unwrap(); - guard.retain(|stored_light| stored_light.light.id() != bundle.light.id()); + guard.retain(|stored_light| { + stored_light.light_descriptor.id() != bundle.light_descriptor.id() + }); *self.analytical_lights_array.write().unwrap() = None; } /// Return an iterator over all lights. - pub fn lights(&self) -> impl Iterator + '_ { - let inner = self.analytical_lights.read().unwrap(); - AnalyticalLightIterator { inner, index: 0 } + pub fn lights(&self) -> Vec { + self.analytical_lights.read().unwrap().clone() } - /// Create a new [`AnalyticalLightBundle`] for the given descriptor `T`. + /// Create a new [`AnalyticalLight`]. /// - /// `T` must be one of: - /// - [`DirectionalLightDescriptor`] - /// - [`SpotLightDescriptor`] - /// - [`PointLightDescriptor`] - pub fn new_analytical_light(&self, light_descriptor: T) -> AnalyticalLight - where - T: Clone + Copy + SlabItem + Send + Sync, - Light: From>, - LightDetails: From>, - { - let transform = self.light_slab.new_value(Transform::default()); - let light_inner = self.light_slab.new_value(light_descriptor); - let light = self.light_slab.new_value({ - let mut light = Light::from(light_inner.id()); + /// The light is automatically added with [`Lighting::add_light`]. + pub fn new_directional_light(&self) -> AnalyticalLight { + let descriptor = self + .light_slab + .new_value(DirectionalLightDescriptor::default()); + let transform = Transform::new(&self.light_slab); + let light_descriptor = self.light_slab.new_value({ + let mut light = LightDescriptor::from(descriptor.id()); light.transform_id = transform.id(); light }); - let light_details = LightDetails::from(light_inner); + let bundle = AnalyticalLight { - light, - light_details, + light_descriptor, + inner: DirectionalLight { descriptor }, transform, node_transform: Default::default(), }; - log::trace!( - "created light {:?} ({})", - bundle.light.id(), - bundle.light_details.style() - ); + self.add_light(&bundle); + + bundle + } + + /// Create a new [`AnalyticalLight`]. + /// + /// The light is automatically added with [`Lighting::add_light`]. + pub fn new_point_light(&self) -> AnalyticalLight { + let descriptor = self.light_slab.new_value(PointLightDescriptor::default()); + let transform = Transform::new(&self.light_slab); + let light_descriptor = self.light_slab.new_value({ + let mut light = LightDescriptor::from(descriptor.id()); + light.transform_id = transform.id(); + light + }); + + let bundle = AnalyticalLight { + light_descriptor, + inner: PointLight { descriptor }, + transform, + node_transform: Default::default(), + }; + self.add_light(&bundle); + + bundle + } + + /// Create a new [`AnalyticalLight`]. + /// + /// The light is automatically added with [`Lighting::add_light`]. + pub fn new_spot_light(&self) -> AnalyticalLight { + let descriptor = self.light_slab.new_value(SpotLightDescriptor::default()); + let transform = Transform::new(&self.light_slab); + let light_descriptor = self.light_slab.new_value({ + let mut light = LightDescriptor::from(descriptor.id()); + light.transform_id = transform.id(); + light + }); + let bundle = AnalyticalLight { + light_descriptor, + inner: SpotLight { descriptor }, + transform, + node_transform: Default::default(), + }; self.add_light(&bundle); bundle } - /// Enable shadow mapping for the given [`AnalyticalLightBundle`], creating + /// Enable shadow mapping for the given [`AnalyticalLight`], creating /// a new [`ShadowMap`]. - pub fn new_shadow_map( + pub fn new_shadow_map( &self, - analytical_light_bundle: &AnalyticalLight, + analytical_light_bundle: &AnalyticalLight, // Size of the shadow map size: UVec2, // Distance to the near plane of the shadow map's frustum. @@ -484,62 +940,40 @@ impl Lighting { // // Only objects within the shadow map's frustum will cast shadows. z_far: f32, - ) -> Result { + ) -> Result + where + T: IsLight, + Light: From, + { ShadowMap::new(self, analytical_light_bundle, size, z_near, z_far) } #[must_use] pub fn commit(&self) -> SlabBuffer { log::trace!("committing lights"); - let lights_array = { - let mut lights_guard = self.analytical_lights.write().unwrap(); - // Update the list of analytical lights to only reference lights that are still - // held somewhere in the outside program. - let mut analytical_lights_dropped = false; - lights_guard.retain_mut(|light_bundle| { - let has_refs = light_bundle.light.has_external_references(); - if has_refs { - let mut node_transform_guard = light_bundle.node_transform.write().unwrap(); - // References to this light still exist, so we'll check to see - // if we need to update the values of linked node transforms. - if let Some(weak_node_transform) = node_transform_guard.take() { - if let Some(node_transform) = weak_node_transform.upgrade() { - // If we can upgrade the node transform, something is holding onto - // it and may updated it in the future, so put it back. - *node_transform_guard = Some(weak_node_transform); - // Get on with checking the update. - let node_global_transform_value = node_transform.get_global_transform(); - // UNWRAP: Safe because we checked that the light has external references - let light_global_transform = light_bundle.transform.upgrade().unwrap(); - let global_transform_value = light_global_transform.get(); - if global_transform_value != node_global_transform_value { - // TODO: write a test that animates a light using GLTF to ensure - // that this is working correctly - light_global_transform.set(node_global_transform_value); - } - } - } + + // Sync any lights whose node transforms have changed + for light in self.analytical_lights.read().unwrap().iter() { + if let Some(node_transform) = light.node_transform.read().unwrap().as_ref() { + let global_node_transform = node_transform.global_descriptor(); + if node_transform.global_descriptor() != light.transform.descriptor() { + light.transform.set_descriptor(global_node_transform); } - analytical_lights_dropped = analytical_lights_dropped || !has_refs; - has_refs - }); + } + } - // If lights have been dropped, invalidate the array + let lights_array = { let mut array_guard = self.analytical_lights_array.write().unwrap(); - if analytical_lights_dropped { - array_guard.take(); - } - // If lights have been invalidated (either by some being dropped or if - // it was previously invalidated by `Lighting::add_light` or `Lighting::remove_light`), - // create a new array + // Create a new array if lights have been invalidated by + // `Lighting::add_light` or `Lighting::remove_light` array_guard .get_or_insert_with(|| { log::trace!(" analytical lights array was invalidated"); + let lights_guard = self.analytical_lights.read().unwrap(); let new_lights = lights_guard .iter() - .map(|bundle| bundle.light.id()) - .collect::>(); + .map(|bundle| bundle.light_descriptor.id()); let array = self.light_slab.new_array(new_lights); log::trace!(" lights array is now: {:?}", array.array()); array diff --git a/crates/renderling/src/light/cpu/test.rs b/crates/renderling/src/light/cpu/test.rs index 76a2fb44..0ac1668d 100644 --- a/crates/renderling/src/light/cpu/test.rs +++ b/crates/renderling/src/light/cpu/test.rs @@ -2,18 +2,20 @@ use glam::{Vec3, Vec4, Vec4Swizzles}; - use spirv_std::num_traits::Zero; use crate::{ bvol::BoundingBox, camera::Camera, color::linear_xfer_vec4, - light::{LightTiling, LightTilingConfig, SpotLightCalculation}, + context::Context, + geometry::Vertex, + light::{shader::SpotLightCalculation, LightTiling, LightTilingConfig}, math::GpuRng, - pbr::Material, - prelude::Transform, - stage::{Renderlet, RenderletPbrVertexInfo, Stage, Vertex}, test::BlockOnFuture, + primitive::{shader::PrimitivePbrVertexInfo, Primitive}, + stage::Stage, + test::BlockOnFuture, + transform::shader::TransformDescriptor, }; use super::*; @@ -41,7 +43,7 @@ fn spot_one_calc() { log::info!("spot: {spot:#?}"); let light_node = doc.nodes().find(|node| node.light().is_some()).unwrap(); - let parent_transform = Transform::from(light_node.transform()); + let parent_transform = TransformDescriptor::from(light_node.transform()); log::info!("parent_transform: {parent_transform:#?}"); let spot_descriptor = SpotLightDescriptor { @@ -84,7 +86,7 @@ fn spot_one_calc() { fn spot_one_frame() { let m = 32.0; let (w, h) = (16.0f32 * m, 9.0 * m); - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx.new_stage().with_msaa_sample_count(4); let doc = stage .load_gltf_document_from_path( @@ -96,7 +98,7 @@ fn spot_one_frame() { let camera = doc.cameras.first().unwrap(); camera .as_ref() - .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let frame = ctx.get_next_frame().unwrap(); @@ -114,7 +116,7 @@ fn spot_one_frame() { fn spot_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -130,14 +132,11 @@ fn spot_lights() { let camera = doc.cameras.first().unwrap(); camera .as_ref() - .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let down_light = doc.lights.first().unwrap(); - log::info!( - "down_light: {:#?}", - down_light.light_details.as_spot().unwrap().get() - ); + log::info!("down_light: {:#?}", down_light.as_spot().unwrap()); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -151,7 +150,7 @@ fn light_tiling_light_bounds() { let magnification = 8; let w = 16.0 * 2.0f32.powi(magnification); let h = 9.0 * 2.0f32.powi(magnification); - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx.new_stage().with_msaa_sample_count(4); let doc = stage .load_gltf_document_from_path( @@ -168,20 +167,15 @@ fn light_tiling_light_bounds() { // Here we only want to render the bounding boxes of the renderlets, // so mark the renderlets themeselves invisible - doc.renderlets_iter().for_each(|hy_rend| { - hy_rend.modify(|r| { - r.visible = false; - }); + doc.renderlets_iter().for_each(|r| { + r.set_visible(false); }); let colors = [0x6DE1D2FF, 0xFFD63AFF, 0x6DE1D2FF, 0xF75A5AFF].map(|albedo_factor| { - stage.new_material(Material { - albedo_factor: { - let mut color = crate::math::hex_to_vec4(albedo_factor); - linear_xfer_vec4(&mut color); - color - }, - ..Default::default() + stage.new_material().with_albedo_factor({ + let mut color = crate::math::hex_to_vec4(albedo_factor); + linear_xfer_vec4(&mut color); + color }) }); let mut resources = vec![]; @@ -189,7 +183,7 @@ fn light_tiling_light_bounds() { if node.mesh.is_none() { continue; } - let transform = Mat4::from(node.transform.get_global_transform()); + let transform = Mat4::from(node.transform.global_descriptor()); if let Some(mesh_index) = node.mesh { log::info!("mesh: {}", node.name.as_deref().unwrap_or("unknown")); let mesh = &doc.meshes[mesh_index]; @@ -205,13 +199,15 @@ fn light_tiling_light_bounds() { log::info!("min: {min}, max: {max}"); resources.push( stage - .builder() - .with_vertices({ - bb.get_mesh() - .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)) - }) - .with_material_id(colors[i % colors.len()].id()) - .build(), + .new_primitive() + .with_vertices( + stage.new_vertices( + bb.get_mesh().map(|(p, n)| { + Vertex::default().with_position(p).with_normal(n) + }), + ), + ) + .with_material(&colors[i % colors.len()]), ); } } @@ -232,11 +228,9 @@ fn gen_vec3(prng: &mut GpuRng) -> Vec3 { } struct GeneratedLight { - _unused_transform: Hybrid, - _mesh_geometry: HybridArray, - _mesh_material: Hybrid, - _light: AnalyticalLight, - mesh_renderlet: Hybrid, + _unused_transform: Transform, + _light: AnalyticalLight, + mesh_renderlet: Primitive, } fn gen_light(stage: &Stage, prng: &mut GpuRng, bounding_boxes: &[BoundingBox]) -> GeneratedLight { @@ -262,51 +256,36 @@ fn gen_light(stage: &Stage, prng: &mut GpuRng, bounding_boxes: &[BoundingBox]) - half_extent: Vec3::new(scale, scale, scale) * 0.5, }; - // Also make a renderlet for the light, so we can see where it is. - // let transform = stage.new_nested_transform(); - // transform.modify(|t| { - // if transform.global_transform_id().inner() == 5676 { - // println!("generated position: {position}"); - // } - // t.translation = position; - // }); - let (a, b, c, d, e) = stage - .builder() - .with_transform(Transform { - translation: position, - ..Default::default() - }) - .with_vertices( - light_bb - .get_mesh() - .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), - ) - .with_material(Material { - albedo_factor: color, - has_lighting: false, - emissive_factor: color.xyz(), - emissive_strength_multiplier: 100.0, - ..Default::default() - }) - .suffix({ - // suffix the actual analytical light - let intensity = scale * 100.0; - - let light_descriptor = PointLightDescriptor { - position, - color, - intensity, - }; + let _unused_transform = stage.new_transform().with_translation(position); + let vertices = stage.new_vertices( + light_bb + .get_mesh() + .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)), + ); + let material = stage + .new_material() + .with_albedo_factor(color) + .with_has_lighting(false) + .with_emissive_factor(color.xyz()) + .with_emissive_strength_multiplier(100.0); + let mesh_renderlet = stage + .new_primitive() + .with_vertices(vertices) + .with_material(material); + let _light = { + // suffix the actual analytical light + let intensity = scale * 100.0; + stage + .new_point_light() + .with_position(position) + .with_color(color) + .with_intensity(intensity) + }; - stage.new_analytical_light(light_descriptor) - }) - .build(); GeneratedLight { - _unused_transform: a, - _mesh_geometry: b, - _mesh_material: c, - _light: d, - mesh_renderlet: e, + _unused_transform, + _light, + mesh_renderlet, } } @@ -317,12 +296,12 @@ fn size() -> UVec2 { ) } -fn make_camera() -> Camera { +fn make_camera(stage: &Stage) -> Camera { let size = size(); let eye = Vec3::new(250.0, 200.0, 250.0); let target = Vec3::ZERO; log::info!("make_camera: forward {}", (target - eye).normalize()); - Camera::new( + stage.new_camera().with_projection_and_view( Mat4::perspective_rh( std::f32::consts::FRAC_PI_4, size.x as f32 / size.y as f32, @@ -339,7 +318,7 @@ fn clear_tiles_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s).block(); + let ctx = Context::headless(s, s).block(); let stage = ctx.new_stage(); let lighting: &Lighting = stage.as_ref(); let tiling_config = LightTilingConfig::default(); @@ -363,8 +342,8 @@ fn clear_tiles_sanity() { // This should produce an image where pixels get darker towards the upper left corner. let max = 1.0 - distance / max_distance; - item.depth_min = crate::light::quantize_depth_f32_to_u32(min); - item.depth_max = crate::light::quantize_depth_f32_to_u32(max); + item.depth_min = crate::light::shader::quantize_depth_f32_to_u32(min); + item.depth_max = crate::light::shader::quantize_depth_f32_to_u32(max); // This should produce an image that looks like noise item.next_light_index = rng.gen_u32(0, 32); @@ -410,7 +389,7 @@ fn min_max_depth_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s).block(); + let ctx = Context::headless(s, s).block(); let stage = ctx.new_stage(); let _doc = stage .load_gltf_document_from_path( @@ -419,7 +398,7 @@ fn min_max_depth_sanity() { .join("light_tiling_test.glb"), ) .unwrap(); - let camera = stage.new_camera(make_camera()); + let camera = make_camera(&stage); stage.use_camera(camera); snapshot( &ctx, @@ -462,7 +441,7 @@ fn light_bins_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s).block(); + let ctx = Context::headless(s, s).block(); let stage = ctx.new_stage(); let doc = stage .load_gltf_document_from_path( @@ -471,7 +450,7 @@ fn light_bins_sanity() { .join("light_tiling_test.glb"), ) .unwrap(); - let camera = stage.new_camera(make_camera()); + let camera = make_camera(&stage); stage.use_camera(camera); snapshot(&ctx, &stage, "light/tiling/bins/1-scene.png", false); @@ -520,7 +499,7 @@ fn light_bins_sanity() { // Assert either the light is the correct one, or we're using the zero frustum optimization // discussed in if tile.depth_min != tile.depth_max { - assert_eq!(light_bin[0], directional_light.light.id()); + assert_eq!(light_bin[0], directional_light.id()); assert_eq!(light_bin[1], Id::NONE); } else { assert_eq!(0, tile.next_light_index); @@ -532,41 +511,36 @@ fn light_bins_sanity() { // Ensures point lights are being binned properly. #[test] fn light_bins_point() { - let ctx = crate::Context::headless(256, 256).block(); + let ctx = Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_msaa_sample_count(1) .with_bloom_mix_strength(0.08); - let doc = stage + let mut doc = stage .load_gltf_document_from_path( crate::test::workspace_dir() .join("gltf") .join("pedestal.glb"), ) .unwrap(); - let materials = doc.materials.get_vec(); - log::info!("materials: {materials:#?}"); - doc.materials.set_item( - 0, - Material { - albedo_factor: Vec4::ONE, - roughness_factor: 1.0, - metallic_factor: 0.0, - ..Default::default() - }, - ); - let camera = doc.cameras.first().unwrap(); - camera.camera.modify(|cam| { - let view = Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y); - let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0); - cam.set_projection_and_view(proj, view); - }); - let _point_light = stage.new_analytical_light(PointLightDescriptor { - position: Vec3::new(1.1, 1.0, 1.1), - color: Vec4::ONE, - intensity: 5.0, - }); + doc.materials + .get_mut(0) + .unwrap() + .set_albedo_factor(Vec4::ONE) + .set_roughness_factor(1.0) + .set_metallic_factor(0.0); + + let camera = doc.cameras.first().unwrap(); + let view = Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y); + let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0); + camera.camera.set_projection_and_view(proj, view); + + let _point_light = stage + .new_point_light() + .with_position(Vec3::new(1.1, 1.0, 1.1)) + .with_color(Vec4::ONE) + .with_intensity(5.0); snapshot( &ctx, &stage, @@ -605,7 +579,7 @@ fn tiling_e2e_sanity_with( minimum_illuminance: {minimum_illuminance}" ); let size = size(); - let ctx = crate::Context::headless(size.x, size.y).block(); + let ctx = Context::headless(size.x, size.y).block(); let stage = ctx .new_stage() .with_bloom(true) @@ -620,7 +594,7 @@ fn tiling_e2e_sanity_with( ) .unwrap(); - let camera = stage.new_camera(make_camera()); + let camera = make_camera(&stage); stage.use_camera(camera); let _ = stage.lighting.commit(); @@ -644,7 +618,7 @@ fn tiling_e2e_sanity_with( if node.mesh.is_none() { continue; } - let transform = Mat4::from(node.transform.get_global_transform()); + let transform = Mat4::from(node.transform.global_descriptor()); if let Some(mesh_index) = node.mesh { let mesh = &doc.meshes[mesh_index]; for prim in mesh.primitives.iter() { @@ -670,7 +644,7 @@ fn tiling_e2e_sanity_with( // Remove the light meshes for generated_light in lights.iter() { - stage.remove_renderlet(&generated_light.mesh_renderlet); + stage.remove_primitive(&generated_light.mesh_renderlet); } snapshot( &ctx, @@ -691,7 +665,7 @@ fn tiling_e2e_sanity_with( &ctx, &stage, &format!("light/tiling/e2e/6-scene-{tile_size}-{max_lights_per_tile}-lights-{i}-{minimum_illuminance}-min-lux.png"), - save_images + save_images ); #[cfg(feature = "light-tiling-stats")] @@ -769,7 +743,7 @@ fn tiling_e2e_sanity() { max_lights_per_tile, i as u32, *minimum_illuminance, - true + true, ) } } @@ -777,11 +751,11 @@ fn tiling_e2e_sanity() { } } -fn snapshot(ctx: &crate::Context, stage: &Stage, path: &str, save: bool) { +fn snapshot(ctx: &crate::context::Context, stage: &Stage, path: &str, save: bool) { let frame = ctx.get_next_frame().unwrap(); let start = std::time::Instant::now(); stage.render(&frame.view()); - let elapsed = start.elapsed(); + let elapsed = start.elapsed(); log::info!("shapshot: {}s '{path}'", elapsed.as_secs_f32()); let img = frame.read_image().block().unwrap(); if save { @@ -834,8 +808,8 @@ mod stats { } pub fn plot(stats: LightTilingStats, filename: &str) { - let path = - crate::test::workspace_dir().join(format!("test_output/light/tiling/e2e/{filename}.png")); + let path = crate::test::workspace_dir() + .join(format!("test_output/light/tiling/e2e/{filename}.png")); let root_drawing_area = BitMapBackend::new(&path, (800, 600)).into_drawing_area(); root_drawing_area.fill(&plotters::style::WHITE).unwrap(); @@ -904,7 +878,8 @@ mod stats { 5, ShapeStyle::from(&plotters::style::RED).filled(), &|(num_lights, seconds_per_frame), size, style| { - EmptyElement::at((num_lights, seconds_per_frame)) + Circle::new((0, 0), size, style) + EmptyElement::at((num_lights, seconds_per_frame)) + + Circle::new((0, 0), size, style) }, )) .unwrap(); @@ -917,7 +892,8 @@ mod stats { 5, ShapeStyle::from(&plotters::style::BLUE).filled(), &|(num_lights, seconds_per_frame), size, style| { - EmptyElement::at((num_lights, seconds_per_frame)) + Circle::new((0, 0), size, style) + EmptyElement::at((num_lights, seconds_per_frame)) + + Circle::new((0, 0), size, style) }, )) .unwrap(); @@ -943,36 +919,32 @@ mod stats { /// In other words, light w/ nested transform is the same as light with /// that same transform pre-applied. fn pedestal() { - let ctx = crate::Context::headless(256, 256).block(); + let ctx = crate::context::Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_lighting(false) .with_msaa_sample_count(4) .with_bloom_mix_strength(0.08); - let doc = stage + let mut doc = stage .load_gltf_document_from_path( crate::test::workspace_dir() .join("gltf") .join("pedestal.glb"), ) .unwrap(); - let materials = doc.materials.get_vec(); - log::info!("materials: {materials:#?}"); - doc.materials.set_item( - 0, - Material { - albedo_factor: Vec4::ONE, - roughness_factor: 1.0, - metallic_factor: 0.0, - ..Default::default() - }, - ); + + doc.materials + .get_mut(0) + .unwrap() + .set_albedo_factor(Vec4::ONE) + .set_roughness_factor(1.0) + .set_metallic_factor(0.0); + let camera = doc.cameras.first().unwrap(); - camera.camera.modify(|cam| { - let view = Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y); - let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0); - cam.set_projection_and_view(proj, view); - }); + camera.camera.set_projection_and_view( + Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0), + Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y), + ); let color = { // let mut c = hex_to_vec4(0xEEDF7AFF); @@ -981,30 +953,27 @@ fn pedestal() { Vec4::ONE }; let position = Vec3::new(1.1, 1.0, 1.1); - let transform = stage.new_nested_transform(); - transform.modify(|t| t.translation = position); - stage.set_has_lighting(true); let mut dir_infos = vec![]; { log::info!("adding dir light"); - let _dir_light = stage.new_analytical_light(DirectionalLightDescriptor { - direction: -position, - color, - intensity: 5.0, - }); + let dir_light = stage + .new_directional_light() + .with_direction(-position) + .with_color(color) + .with_intensity(5.0); snapshot(&ctx, &stage, "light/pedestal/directional.png", false); let geometry_slab = futures_lite::future::block_on(stage.geometry.slab_allocator().read(..)).unwrap(); let renderlet = doc.renderlets_iter().next().unwrap(); - log::info!("renderlet: {renderlet:#?}"); + log::info!("renderlet: {:#?}", renderlet.descriptor()); - for vertex_index in 0..renderlet.get().vertices_array.len() { - let mut info = RenderletPbrVertexInfo::default(); - crate::stage::renderlet_vertex( + for vertex_index in 0..renderlet.descriptor().vertices_array.len() { + let mut info = PrimitivePbrVertexInfo::default(); + crate::primitive::shader::primitive_vertex( renderlet.id(), vertex_index as u32, &geometry_slab, @@ -1022,62 +991,60 @@ fn pedestal() { dir_infos.push(info); } - log::info!("dropping dir light"); + stage.remove_light(&dir_light); } - assert_eq!(0, stage.lighting.lights().count()); + assert_eq!(0, stage.lighting.lights().len()); // Point lights { log::info!("adding point light with pre-applied position"); - let _point_light = stage.new_analytical_light(PointLightDescriptor { - position, - color, - intensity: 5.0, - }); + let point_light = stage + .new_point_light() + .with_position(position) + .with_color(color) + .with_intensity(5.0); snapshot(&ctx, &stage, "light/pedestal/point.png", false); - log::info!("dropping point light"); + stage.remove_light(&point_light); } { log::info!("adding point light with nested transform"); let transform = stage.new_nested_transform(); - transform.modify(|t| t.translation = position); + transform.set_local_translation(position); - let point_light = stage.new_analytical_light(PointLightDescriptor { - position: Vec3::ZERO, - color, - intensity: 5.0, - }); + let point_light = stage + .new_point_light() + .with_position(Vec3::ZERO) + .with_color(color) + .with_intensity(5.0); point_light.link_node_transform(&transform); snapshot(&ctx, &stage, "light/pedestal/point.png", false); - log::info!("dropping point light"); + stage.remove_light(&point_light); } { log::info!("adding spot light with pre-applied position"); - let spot_desc = SpotLightDescriptor { - position, - direction: -position, - color, - intensity: 5.0, - inner_cutoff: core::f32::consts::PI / 5.0, - outer_cutoff: core::f32::consts::PI / 4.0, - // ..Default::default() - }; - let _spot = stage.new_analytical_light(spot_desc); + let spot = stage + .new_spot_light() + .with_position(position) + .with_direction(-position) + .with_color(color) + .with_intensity(5.0) + .with_inner_cutoff(core::f32::consts::PI / 5.0) + .with_outer_cutoff(core::f32::consts::PI / 4.0); snapshot(&ctx, &stage, "light/pedestal/spot.png", false); let geometry_slab = futures_lite::future::block_on(stage.geometry.slab_allocator().read(..)).unwrap(); let renderlet = doc.renderlets_iter().next().unwrap(); - log::info!("renderlet: {renderlet:#?}"); + log::info!("renderlet: {:#?}", renderlet.descriptor()); let mut spot_infos = vec![]; - for vertex_index in 0..renderlet.get().vertices_array.len() { - let mut info = RenderletPbrVertexInfo::default(); - crate::stage::renderlet_vertex( + for vertex_index in 0..renderlet.descriptor().vertices_array.len() { + let mut info = PrimitivePbrVertexInfo::default(); + crate::primitive::shader::primitive_vertex( renderlet.id(), vertex_index as u32, &geometry_slab, @@ -1098,24 +1065,23 @@ fn pedestal() { // assert that the output of the vertex shader is the same for the first renderlet, // regardless of the lighting pretty_assertions::assert_eq!(dir_infos, spot_infos); + stage.remove_light(&spot); } { log::info!("adding spot light with node position"); let node_transform = stage.new_nested_transform(); - node_transform.modify(|t| t.translation = position); - - let spot_desc = SpotLightDescriptor { - position: Vec3::ZERO, - direction: -position, - color, - intensity: 5.0, - inner_cutoff: core::f32::consts::PI / 5.0, - outer_cutoff: core::f32::consts::PI / 4.0, - // ..Default::default() - }; - let spot = stage.new_analytical_light(spot_desc); + node_transform.set_local_translation(position); + + let spot = stage + .new_spot_light() + .with_position(Vec3::ZERO) + .with_direction(-position) + .with_color(color) + .with_intensity(5.0) + .with_inner_cutoff(core::f32::consts::PI / 5.0) + .with_outer_cutoff(core::f32::consts::PI / 4.0); spot.link_node_transform(&node_transform); snapshot(&ctx, &stage, "light/pedestal/spot.png", false); } diff --git a/crates/renderling/src/light/shader.rs b/crates/renderling/src/light/shader.rs new file mode 100644 index 00000000..afbae486 --- /dev/null +++ b/crates/renderling/src/light/shader.rs @@ -0,0 +1,1222 @@ +/// Root descriptor of the lighting system. +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +#[cfg(gpu)] +use spirv_std::num_traits::Float; +use spirv_std::{spirv, Image}; + +use crate::{ + atlas::shader::{AtlasDescriptor, AtlasTextureDescriptor}, + cubemap::shader::{CubemapDescriptor, CubemapFaceDirection}, + geometry::shader::GeometryDescriptor, + math::{Fetch, IsSampler, IsVector, Sample2dArray}, + primitive::shader::{PrimitiveDescriptor, VertexInfo}, + transform::shader::TransformDescriptor, +}; + +#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)] +#[offsets] +pub struct LightingDescriptor { + /// List of all analytical lights in the scene. + pub analytical_lights_array: Array>, + /// Shadow mapping atlas info. + pub shadow_map_atlas_descriptor_id: Id, + /// `Id` of the [`ShadowMapDescriptor`] to use when updating + /// a shadow map. + /// + /// This changes from each run of the `shadow_mapping_vertex`. + pub update_shadow_map_id: Id, + /// The index of the shadow map atlas texture to update. + pub update_shadow_map_texture_index: u32, + /// `Id` of the [`LightTilingDescriptor`] to use when performing + /// light tiling. + pub light_tiling_descriptor_id: Id, +} + +#[derive(Clone, Copy, SlabItem, core::fmt::Debug)] +pub struct ShadowMapDescriptor { + pub light_space_transforms_array: Array, + /// Near plane of the projection matrix + pub z_near: f32, + /// Far plane of the projection matrix + pub z_far: f32, + /// Pointers to the atlas textures where the shadow map depth + /// data is stored. + /// + /// This will be an array of one `Id` for directional and spot lights, + /// and an array of four `Id`s for a point light. + pub atlas_textures_array: Array>, + pub bias_min: f32, + pub bias_max: f32, + pub pcf_samples: u32, +} + +impl Default for ShadowMapDescriptor { + fn default() -> Self { + Self { + light_space_transforms_array: Default::default(), + z_near: Default::default(), + z_far: Default::default(), + atlas_textures_array: Default::default(), + bias_min: 0.0005, + bias_max: 0.005, + pcf_samples: 4, + } + } +} + +#[cfg(test)] +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct ShadowMappingVertexInfo { + pub renderlet_id: Id, + pub vertex_index: u32, + pub vertex: crate::geometry::Vertex, + pub transform: TransformDescriptor, + pub model_matrix: Mat4, + pub world_pos: Vec3, + pub view_projection: Mat4, + pub clip_pos: Vec4, +} + +/// Shadow mapping vertex shader. +/// +/// It is assumed that a [`LightingDescriptor`] is stored at `Id(0)` of the +/// `light_slab`. +/// +/// This shader reads the [`LightingDescriptor`] to find the shadow map to +/// be updated, then determines the clip positions to emit based on the +/// shadow map's atlas texture. +/// +/// It then renders the renderlet into the designated atlas frame. +// Note: +// If this is taking too long to render for each renderlet, think about +// a frustum and occlusion culling pass to generate the list of renderlets. +#[spirv(vertex)] +#[allow(clippy::too_many_arguments)] +pub fn shadow_mapping_vertex( + // Points at a `Renderlet` + #[spirv(instance_index)] renderlet_id: Id, + // Which vertex within the renderlet are we rendering + #[spirv(vertex_index)] vertex_index: u32, + // The slab where the renderlet's geometry is staged + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + // The slab where the scene's lighting data is staged + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] light_slab: &[u32], + + #[spirv(position)] out_clip_pos: &mut Vec4, + #[cfg(test)] out_comparison_info: &mut ShadowMappingVertexInfo, +) { + let renderlet = geometry_slab.read_unchecked(renderlet_id); + if !renderlet.visible { + // put it outside the clipping frustum + *out_clip_pos = Vec4::new(100.0, 100.0, 100.0, 1.0); + return; + } + + let VertexInfo { + world_pos, + vertex: _vertex, + transform: _transform, + model_matrix: _model_matrix, + } = renderlet.get_vertex_info(vertex_index, geometry_slab); + + let lighting_desc = light_slab.read_unchecked(Id::::new(0)); + let shadow_desc = light_slab.read_unchecked(lighting_desc.update_shadow_map_id); + let light_space_transform_id = shadow_desc + .light_space_transforms_array + .at(lighting_desc.update_shadow_map_texture_index as usize); + let light_space_transform = light_slab.read_unchecked(light_space_transform_id); + let clip_pos = light_space_transform * world_pos.extend(1.0); + #[cfg(test)] + { + *out_comparison_info = ShadowMappingVertexInfo { + renderlet_id, + vertex_index, + vertex: _vertex, + transform: _transform, + model_matrix: _model_matrix, + world_pos, + view_projection: light_space_transform, + clip_pos, + }; + } + *out_clip_pos = clip_pos; +} + +#[spirv(fragment)] +pub fn shadow_mapping_fragment(clip_pos: Vec4, frag_color: &mut Vec4) { + *frag_color = (clip_pos.xyz() / clip_pos.w).extend(1.0); +} + +/// Contains values needed to determine the outgoing radiance of a fragment. +/// +/// For more info, see the **Spotlight** section of the +/// [learnopengl](https://learnopengl.com/Lighting/Light-casters) +/// article. +#[derive(Clone, Copy, Default, core::fmt::Debug)] +pub struct SpotLightCalculation { + /// Position of the light in world space + pub light_position: Vec3, + /// Position of the fragment in world space + pub frag_position: Vec3, + /// Unit vector (LightDir) pointing from the fragment to the light + pub frag_to_light: Vec3, + /// Distance from the fragment to the light + pub frag_to_light_distance: f32, + /// Unit vector (SpotDir) direction that the light is pointing in + pub light_direction: Vec3, + /// The cosine of the cutoff angle (Phi ϕ) that specifies the spotlight's radius. + /// + /// Everything inside this angle is lit by the spotlight. + pub cos_inner_cutoff: f32, + /// The cosine of the cutoff angle (Gamma γ) that specifies the spotlight's outer radius. + /// + /// Everything outside this angle is not lit by the spotlight. + /// + /// Fragments between `inner_cutoff` and `outer_cutoff` have an intensity + /// between `1.0` and `0.0`. + pub cos_outer_cutoff: f32, + /// Whether the fragment is inside the `inner_cutoff` cone. + pub fragment_is_inside_inner_cone: bool, + /// Whether the fragment is inside the `outer_cutoff` cone. + pub fragment_is_inside_outer_cone: bool, + /// `outer_cutoff` - `inner_cutoff` + pub epsilon: f32, + /// Cosine of the angle (Theta θ) between `frag_to_light` (LightDir) vector and the + /// `light_direction` (SpotDir) vector. + /// + /// θ should be smaller than `outer_cutoff` (Gamma γ) to be + /// inside the spotlight, but since these are all cosines of angles, we actually + /// compare using `>`. + pub cos_theta: f32, + pub contribution_unclamped: f32, + /// The intensity level between `0.0` and `1.0` that should be used to determine + /// outgoing radiance. + pub contribution: f32, +} + +impl SpotLightCalculation { + /// Calculate the values required to determine outgoing radiance of a spot light. + pub fn new( + spot_light_descriptor: SpotLightDescriptor, + node_transform: Mat4, + fragment_world_position: Vec3, + ) -> Self { + let light_position = node_transform.transform_point3(spot_light_descriptor.position); + let frag_position = fragment_world_position; + let frag_to_light = light_position - frag_position; + let frag_to_light_distance = frag_to_light.length(); + if frag_to_light_distance == 0.0 { + crate::println!("frag_to_light_distance: {frag_to_light_distance}"); + return Self::default(); + } + let frag_to_light = frag_to_light.alt_norm_or_zero(); + let light_direction = node_transform + .transform_vector3(spot_light_descriptor.direction) + .alt_norm_or_zero(); + let cos_inner_cutoff = spot_light_descriptor.inner_cutoff.cos(); + let cos_outer_cutoff = spot_light_descriptor.outer_cutoff.cos(); + let epsilon = cos_inner_cutoff - cos_outer_cutoff; + let cos_theta = frag_to_light.dot(-light_direction); + let fragment_is_inside_inner_cone = cos_theta > cos_inner_cutoff; + let fragment_is_inside_outer_cone = cos_theta > cos_outer_cutoff; + let contribution_unclamped = (cos_theta - cos_outer_cutoff) / epsilon; + let contribution = contribution_unclamped.clamp(0.0, 1.0); + Self { + light_position, + frag_position, + frag_to_light, + frag_to_light_distance, + light_direction, + cos_inner_cutoff, + cos_outer_cutoff, + fragment_is_inside_inner_cone, + fragment_is_inside_outer_cone, + epsilon, + cos_theta, + contribution_unclamped, + contribution, + } + } +} + +/// Description of a spot light. +/// +/// ## Tips +/// +/// If your spotlight is not illuminating your scenery, ensure that the +/// `inner_cutoff` and `outer_cutoff` values are "correct". `outer_cutoff` +/// should be _greater than_ `inner_cutoff` and the values should be a large +/// enough to cover at least one pixel at the distance between the light and +/// the scenery. +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct SpotLightDescriptor { + pub position: Vec3, + pub direction: Vec3, + pub inner_cutoff: f32, + pub outer_cutoff: f32, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for SpotLightDescriptor { + fn default() -> Self { + let white = Vec4::splat(1.0); + let inner_cutoff = 0.077143565; + let outer_cutoff = 0.09075713; + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = white; + let intensity = 1.0; + + Self { + position: Default::default(), + direction, + inner_cutoff, + outer_cutoff, + color, + intensity, + } + } +} + +impl SpotLightDescriptor { + pub fn shadow_mapping_projection_and_view( + &self, + parent_light_transform: &Mat4, + z_near: f32, + z_far: f32, + ) -> (Mat4, Mat4) { + let fovy = 2.0 * self.outer_cutoff; + let aspect = 1.0; + let projection = Mat4::perspective_rh(fovy, aspect, z_near, z_far); + let direction = parent_light_transform + .transform_vector3(self.direction) + .alt_norm_or_zero(); + let position = parent_light_transform.transform_point3(self.position); + let up = direction.orthonormal_vectors()[0]; + let view = Mat4::look_to_rh(position, direction, up); + (projection, view) + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct DirectionalLightDescriptor { + pub direction: Vec3, + pub color: Vec4, + pub intensity: f32, +} + +impl Default for DirectionalLightDescriptor { + fn default() -> Self { + let direction = Vec3::new(0.0, -1.0, 0.0); + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + direction, + color, + intensity, + } + } +} + +impl DirectionalLightDescriptor { + pub fn shadow_mapping_projection_and_view( + &self, + parent_light_transform: &Mat4, + // Near limits of the light's reach + // + // The maximum should be the `Camera`'s `Frustum::depth()`. + // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum + // as a parameter and then figure out the minimal view projection that includes that frustum + z_near: f32, + // Far limits of the light's reach + z_far: f32, + ) -> (Mat4, Mat4) { + crate::println!("descriptor: {self:#?}"); + let depth = (z_far - z_near).abs(); + let hd = depth * 0.5; + let projection = Mat4::orthographic_rh(-hd, hd, -hd, hd, z_near, z_far); + let direction = parent_light_transform + .transform_vector3(self.direction) + .alt_norm_or_zero(); + let position = -direction * depth * 0.5; + crate::println!("direction: {direction}"); + crate::println!("position: {position}"); + let up = direction.orthonormal_vectors()[0]; + let view = Mat4::look_to_rh(position, direction, up); + (projection, view) + } +} + +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, SlabItem)] +pub struct PointLightDescriptor { + pub position: Vec3, + pub color: Vec4, + /// Expressed as candelas. + pub intensity: f32, +} + +impl Default for PointLightDescriptor { + fn default() -> Self { + let color = Vec4::splat(1.0); + let intensity = 1.0; + + Self { + position: Default::default(), + color, + intensity, + } + } +} + +impl PointLightDescriptor { + pub fn shadow_mapping_view_matrix( + &self, + face_index: usize, + parent_light_transform: &Mat4, + ) -> Mat4 { + let eye = parent_light_transform.transform_point3(self.position); + let mut face = CubemapFaceDirection::FACES[face_index]; + face.eye = eye; + face.view() + } + + pub fn shadow_mapping_projection_matrix(z_near: f32, z_far: f32) -> Mat4 { + Mat4::perspective_lh(core::f32::consts::FRAC_PI_2, 1.0, z_near, z_far) + } + + pub fn shadow_mapping_projection_and_view_matrices( + &self, + parent_light_transform: &Mat4, + z_near: f32, + z_far: f32, + ) -> (Mat4, [Mat4; 6]) { + let p = Self::shadow_mapping_projection_matrix(z_near, z_far); + let eye = parent_light_transform.transform_point3(self.position); + ( + p, + CubemapFaceDirection::FACES.map(|mut face| { + face.eye = eye; + face.view() + }), + ) + } +} + +/// Returns the radius of illumination in meters. +/// +/// * Moonlight: < 1 lux. +/// - Full moon on a clear night: 0.25 lux. +/// - Quarter moon: 0.01 lux +/// - Starlight overcast moonless night sky: 0.0001 lux. +/// * General indoor lighting: Around 100 to 300 lux. +/// * Office lighting: Typically around 300 to 500 lux. +/// * Reading or task lighting: Around 500 to 750 lux. +/// * Detailed work (e.g., drafting, surgery): 1000 lux or more. +pub fn radius_of_illumination(intensity_candelas: f32, minimum_illuminance_lux: f32) -> f32 { + (intensity_candelas / minimum_illuminance_lux).sqrt() +} + +#[repr(u32)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, PartialEq)] +pub enum LightStyle { + Directional = 0, + Point = 1, + Spot = 2, +} + +impl core::fmt::Display for LightStyle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + LightStyle::Directional => f.write_str("directional"), + LightStyle::Point => f.write_str("point"), + LightStyle::Spot => f.write_str("spot"), + } + } +} + +impl SlabItem for LightStyle { + const SLAB_SIZE: usize = { 1 }; + + fn read_slab(index: usize, slab: &[u32]) -> Self { + let proxy = u32::read_slab(index, slab); + match proxy { + 0 => LightStyle::Directional, + 1 => LightStyle::Point, + 2 => LightStyle::Spot, + _ => LightStyle::Directional, + } + } + + fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize { + let proxy = *self as u32; + proxy.write_slab(index, slab) + } +} + +/// A generic light that is used as a slab pointer to a +/// specific light type. +#[repr(C)] +#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] +#[derive(Copy, Clone, PartialEq, SlabItem)] +pub struct LightDescriptor { + /// The type of the light + pub light_type: LightStyle, + /// The index of the light in the lighting slab + pub index: u32, + /// The id of a transform to apply to the position and direction of the light. + /// + /// This `Id` points to a transform on the lighting slab. + /// + /// The value of this descriptor can be synchronized with that of a node + /// transform on the geometry slab from + /// [`crate::light::AnalyticalLight::link_node_transform`]. + pub transform_id: Id, + /// The id of the shadow map in use by this light. + pub shadow_map_desc_id: Id, +} + +impl Default for LightDescriptor { + fn default() -> Self { + Self { + light_type: LightStyle::Directional, + index: Id::<()>::NONE.inner(), + transform_id: Id::NONE, + shadow_map_desc_id: Id::NONE, + } + } +} + +impl From> for LightDescriptor { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Directional, + index: id.inner(), + transform_id: Id::NONE, + shadow_map_desc_id: Id::NONE, + } + } +} + +impl From> for LightDescriptor { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Spot, + index: id.inner(), + transform_id: Id::NONE, + shadow_map_desc_id: Id::NONE, + } + } +} + +impl From> for LightDescriptor { + fn from(id: Id) -> Self { + Self { + light_type: LightStyle::Point, + index: id.inner(), + transform_id: Id::NONE, + shadow_map_desc_id: Id::NONE, + } + } +} + +impl LightDescriptor { + pub fn into_directional_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_spot_id(self) -> Id { + Id::from(self.index) + } + + pub fn into_point_id(self) -> Id { + Id::from(self.index) + } +} + +/// Parameters to the shadow mapping calculation function. +/// +/// This is mostly just to appease clippy. +pub struct ShadowCalculation { + pub shadow_map_desc: ShadowMapDescriptor, + pub shadow_map_atlas_size: UVec2, + pub surface_normal_in_world_space: Vec3, + pub frag_pos_in_world_space: Vec3, + pub frag_to_light_in_world_space: Vec3, + pub bias_min: f32, + pub bias_max: f32, + pub pcf_samples: u32, +} + +impl ShadowCalculation { + /// Reads various required parameters from the slab and creates a `ShadowCalculation`. + pub fn new( + light_slab: &[u32], + light: LightDescriptor, + in_pos: Vec3, + surface_normal: Vec3, + light_direction: Vec3, + ) -> Self { + let shadow_map_desc = light_slab.read_unchecked(light.shadow_map_desc_id); + let atlas_size = { + let lighting_desc_id = Id::::new(0); + let atlas_desc_id = light_slab.read_unchecked( + lighting_desc_id + LightingDescriptor::OFFSET_OF_SHADOW_MAP_ATLAS_DESCRIPTOR_ID, + ); + let atlas_desc = light_slab.read_unchecked(atlas_desc_id); + atlas_desc.size + }; + + ShadowCalculation { + shadow_map_desc, + shadow_map_atlas_size: atlas_size.xy(), + surface_normal_in_world_space: surface_normal, + frag_pos_in_world_space: in_pos, + frag_to_light_in_world_space: light_direction, + bias_min: shadow_map_desc.bias_min, + bias_max: shadow_map_desc.bias_max, + pcf_samples: shadow_map_desc.pcf_samples, + } + } + + fn get_atlas_texture_at(&self, light_slab: &[u32], index: usize) -> AtlasTextureDescriptor { + let atlas_texture_id = + light_slab.read_unchecked(self.shadow_map_desc.atlas_textures_array.at(index)); + light_slab.read_unchecked(atlas_texture_id) + } + + fn get_frag_pos_in_light_space(&self, light_slab: &[u32], index: usize) -> Vec3 { + let light_space_transform_id = self.shadow_map_desc.light_space_transforms_array.at(index); + let light_space_transform = light_slab.read_unchecked(light_space_transform_id); + light_space_transform.project_point3(self.frag_pos_in_world_space) + } + + /// Returns shadow _intensity_ for directional and spot lights. + /// + /// Returns `0.0` when the fragment is in full light. + /// Returns `1.0` when the fragment is in full shadow. + pub fn run_directional_or_spot( + &self, + light_slab: &[u32], + shadow_map: &T, + shadow_map_sampler: &S, + ) -> f32 + where + S: IsSampler, + T: Sample2dArray, + { + let ShadowCalculation { + shadow_map_desc: _, + shadow_map_atlas_size, + frag_pos_in_world_space: _, + surface_normal_in_world_space: surface_normal, + frag_to_light_in_world_space: light_direction, + bias_min, + bias_max, + pcf_samples, + } = self; + let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, 0); + crate::println!("frag_pos_in_light_space: {frag_pos_in_light_space}"); + if !crate::math::is_inside_clip_space(frag_pos_in_light_space.xyz()) { + return 0.0; + } + // The range of coordinates in the light's clip space is -1.0 to 1.0 for x and y, + // but the texture space is [0, 1], and Y increases downward, so we do this + // conversion to flip Y and also normalize to the range [0.0, 1.0]. + // Z should already be 0.0 to 1.0. + let proj_coords_uv = (frag_pos_in_light_space.xy() * Vec2::new(1.0, -1.0) + + Vec2::splat(1.0)) + * Vec2::splat(0.5); + crate::println!("proj_coords_uv: {proj_coords_uv}"); + + let shadow_map_atlas_texture = self.get_atlas_texture_at(light_slab, 0); + // With these projected coordinates we can sample the depth map as the + // resulting [0,1] coordinates from proj_coords directly correspond to + // the transformed NDC coordinates from the `ShadowMap::update` render pass. + // This gives us the closest depth from the light's point of view: + let pcf_samples_2 = *pcf_samples as i32 / 2; + let texel_size = 1.0 + / Vec2::new( + shadow_map_atlas_texture.size_px.x as f32, + shadow_map_atlas_texture.size_px.y as f32, + ); + let mut shadow = 0.0f32; + let mut total = 0.0f32; + for x in -pcf_samples_2..=pcf_samples_2 { + for y in -pcf_samples_2..=pcf_samples_2 { + let proj_coords = shadow_map_atlas_texture.uv( + proj_coords_uv + Vec2::new(x as f32, y as f32) * texel_size, + *shadow_map_atlas_size, + ); + let shadow_map_depth = shadow_map + .sample_by_lod(*shadow_map_sampler, proj_coords, 0.0) + .x; + // To get the current depth at this fragment we simply retrieve the projected vector's z + // coordinate which equals the depth of this fragment from the light's perspective. + let fragment_depth = frag_pos_in_light_space.z; + + // If the `current_depth`, which is the depth of the fragment from the lights POV, is + // greater than the `closest_depth` of the shadow map at that fragment, the fragment + // is in shadow + crate::println!("current_depth: {fragment_depth}"); + crate::println!("closest_depth: {shadow_map_depth}"); + let bias = (bias_max * (1.0 - surface_normal.dot(*light_direction))).max(*bias_min); + + if (fragment_depth - bias) >= shadow_map_depth { + shadow += 1.0 + } + total += 1.0; + } + } + shadow / total.max(1.0) + } + + pub const POINT_SAMPLE_OFFSET_DIRECTIONS: [Vec3; 21] = [ + Vec3::ZERO, + Vec3::new(1.0, 1.0, 1.0), + Vec3::new(1.0, -1.0, 1.0), + Vec3::new(-1.0, -1.0, 1.0), + Vec3::new(-1.0, 1.0, 1.0), + Vec3::new(1.0, 1.0, -1.0), + Vec3::new(1.0, -1.0, -1.0), + Vec3::new(-1.0, -1.0, -1.0), + Vec3::new(-1.0, 1.0, -1.0), + Vec3::new(1.0, 1.0, 0.0), + Vec3::new(1.0, -1.0, 0.0), + Vec3::new(-1.0, -1.0, 0.0), + Vec3::new(-1.0, 1.0, 0.0), + Vec3::new(1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, 1.0), + Vec3::new(1.0, 0.0, -1.0), + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(0.0, 1.0, 1.0), + Vec3::new(0.0, -1.0, 1.0), + Vec3::new(0.0, -1.0, -1.0), + Vec3::new(0.0, 1.0, -1.0), + ]; + /// Returns shadow _intensity_ for point lights. + /// + /// Returns `0.0` when the fragment is in full light. + /// Returns `1.0` when the fragment is in full shadow. + pub fn run_point( + &self, + light_slab: &[u32], + shadow_map: &T, + shadow_map_sampler: &S, + light_pos_in_world_space: Vec3, + ) -> f32 + where + S: IsSampler, + T: Sample2dArray, + { + let ShadowCalculation { + shadow_map_desc, + shadow_map_atlas_size, + frag_pos_in_world_space, + surface_normal_in_world_space: surface_normal, + frag_to_light_in_world_space: frag_to_light, + bias_min, + bias_max, + pcf_samples, + } = self; + + let light_to_frag_dir = frag_pos_in_world_space - light_pos_in_world_space; + crate::println!("light_to_frag_dir: {light_to_frag_dir}"); + + let pcf_samplesf = (*pcf_samples as f32) + .max(1.0) + .min(Self::POINT_SAMPLE_OFFSET_DIRECTIONS.len() as f32); + let pcf_samples = pcf_samplesf as usize; + let view_distance = light_to_frag_dir.length(); + let disk_radius = (1.0 + view_distance / shadow_map_desc.z_far) / 25.0; + let mut shadow = 0.0f32; + for i in 0..pcf_samples { + let sample_offset = Self::POINT_SAMPLE_OFFSET_DIRECTIONS[i] * disk_radius; + crate::println!("sample_offset: {sample_offset}"); + let sample_dir = (light_to_frag_dir + sample_offset).alt_norm_or_zero(); + let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(sample_dir); + crate::println!("face_index: {face_index}",); + crate::println!("uv: {uv}"); + let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, face_index); + let face_texture = self.get_atlas_texture_at(light_slab, face_index); + let uv_tex = face_texture.uv(uv, *shadow_map_atlas_size); + let shadow_map_depth = shadow_map.sample_by_lod(*shadow_map_sampler, uv_tex, 0.0).x; + let fragment_depth = frag_pos_in_light_space.z; + let bias = (bias_max * (1.0 - surface_normal.dot(*frag_to_light))).max(*bias_min); + if (fragment_depth - bias) > shadow_map_depth { + shadow += 1.0 + } + } + + shadow / pcf_samplesf + } +} + +/// Depth pre-pass for the light tiling feature. +/// +/// This shader writes all staged [`PrimitiveDescriptor`]'s depth into a buffer. +/// +/// This shader is very much like [`shadow_mapping_vertex`], except that +/// shader gets its projection+view matrix from the light stored in a +/// `ShadowMapDescriptor`. +/// +/// Here we want to render as normal forward pass would, with the `PrimitiveDescriptor` +/// and the `Camera`'s view projection matrix. +/// ## Note +/// This shader will likely be expanded to include parts of occlusion culling and order +/// independent transparency. +#[spirv(vertex)] +pub fn light_tiling_depth_pre_pass( + // Points at a `Renderlet`. + #[spirv(instance_index)] renderlet_id: Id, + // Which vertex within the renderlet are we rendering? + #[spirv(vertex_index)] vertex_index: u32, + // The slab where the renderlet's geometry is staged + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + // Output clip coords + #[spirv(position)] out_clip_pos: &mut Vec4, +) { + let renderlet = geometry_slab.read_unchecked(renderlet_id); + if !renderlet.visible { + // put it outside the clipping frustum + *out_clip_pos = Vec3::splat(100.0).extend(1.0); + return; + } + + let camera_id = geometry_slab + .read_unchecked(Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID); + let camera = geometry_slab.read_unchecked(camera_id); + + let VertexInfo { world_pos, .. } = renderlet.get_vertex_info(vertex_index, geometry_slab); + + *out_clip_pos = camera.view_projection() * world_pos.extend(1.0); +} + +pub type DepthImage2d = Image!(2D, type=f32, sampled, depth); + +pub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled=true); + +/// A tile of screen space used to cull lights. +#[derive(Clone, Copy, Default, SlabItem)] +#[offsets] +pub struct LightTile { + /// Minimum depth of objects found within the frustum of the tile. + pub depth_min: u32, + /// Maximum depth of objects foudn within the frustum of the tile. + pub depth_max: u32, + /// The count of lights in this tile. + /// + /// Also, the next available light index. + pub next_light_index: u32, + /// List of light ids that intersect this tile's frustum. + pub lights_array: Array>, +} + +impl core::fmt::Debug for LightTile { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LightTile") + .field("depth_min", &dequantize_depth_u32_to_f32(self.depth_min)) + .field("depth_max", &dequantize_depth_u32_to_f32(self.depth_max)) + .field("next_light_index", &self.next_light_index) + .field("lights_array", &self.lights_array) + .finish() + } +} + +/// Descriptor of the light tiling operation, which culls lights by accumulating +/// them into lists that illuminate tiles of the screen. +#[derive(Clone, Copy, SlabItem, core::fmt::Debug)] +pub struct LightTilingDescriptor { + /// Size of the [`Stage`](crate::stage::Stage)'s depth texture. + pub depth_texture_size: UVec2, + /// Configurable tile size. + pub tile_size: u32, + /// Array pointing to the lighting "tiles". + pub tiles_array: Array, + /// Minimum illuminance. + /// + /// Used to determine whether a light illuminates a tile. + pub minimum_illuminance_lux: f32, +} + +impl Default for LightTilingDescriptor { + fn default() -> Self { + Self { + depth_texture_size: Default::default(), + tile_size: 16, + tiles_array: Default::default(), + minimum_illuminance_lux: 0.1, + } + } +} + +impl LightTilingDescriptor { + /// Returns the dimensions of the grid of tiles. + pub fn tile_grid_size(&self) -> UVec2 { + let dims_f32 = self.depth_texture_size.as_vec2() / self.tile_size as f32; + dims_f32.ceil().as_uvec2() + } + + pub fn tile_coord_for_fragment(&self, frag_coord: Vec2) -> UVec2 { + let frag_coord = frag_coord.as_uvec2(); + frag_coord / self.tile_size + } + + pub fn tile_index_for_fragment(&self, frag_coord: Vec2) -> usize { + let tile_coord = self.tile_coord_for_fragment(frag_coord); + let tile_dimensions = self.tile_grid_size(); + (tile_coord.y * tile_dimensions.x + tile_coord.x) as usize + } +} + +/// Quantizes a fragment depth from `f32` to `u32`. +pub fn quantize_depth_f32_to_u32(depth: f32) -> u32 { + (u32::MAX as f32 * depth).round() as u32 +} + +/// Reconstructs a previously quantized depth from a `u32`. +pub fn dequantize_depth_u32_to_f32(depth: u32) -> f32 { + depth as f32 / u32::MAX as f32 +} + +/// Helper for determining the next light to check during an +/// invocation of the light list computation. +pub(crate) struct NextLightIndex { + current_step: usize, + tile_size: u32, + lights: Array>, + global_id: UVec3, +} + +impl Iterator for NextLightIndex { + type Item = Id>; + + fn next(&mut self) -> Option { + let next_index = self.next_index(); + self.current_step += 1; + if next_index < self.lights.len() { + Some(self.lights.at(next_index)) + } else { + None + } + } +} + +impl NextLightIndex { + pub fn new( + global_id: UVec3, + tile_size: u32, + analytical_lights_array: Array>, + ) -> Self { + Self { + current_step: 0, + tile_size, + lights: analytical_lights_array, + global_id, + } + } + + pub fn next_index(&self) -> usize { + // Determine the xy coord of this invocation within the _tile_ + let frag_tile_xy = self.global_id.xy() % self.tile_size; + // Determine the index of this invocation within the _tile_ + let offset = frag_tile_xy.y * self.tile_size + frag_tile_xy.x; + let stride = (self.tile_size * self.tile_size) as usize; + self.current_step * stride + offset as usize + } +} + +struct LightTilingInvocation { + global_id: UVec3, + descriptor: LightTilingDescriptor, +} + +impl LightTilingInvocation { + fn new(global_id: UVec3, descriptor: LightTilingDescriptor) -> Self { + Self { + global_id, + descriptor, + } + } + + /// The fragment's position. + /// + /// X range is 0 to (width - 1), Y range is 0 to (height - 1). + fn frag_pos(&self) -> UVec2 { + self.global_id.xy() + } + + /// The number of tiles in X and Y within the depth texture. + fn tile_grid_size(&self) -> UVec2 { + self.descriptor.tile_grid_size() + } + + /// The tile's coordinate among all tiles in the tile grid. + /// + /// The units are in tile x y. + fn tile_coord(&self) -> UVec2 { + self.global_id.xy() / self.descriptor.tile_size + } + + /// The tile's index in all the [`LightTilingDescriptor`]'s `tile_array`. + fn tile_index(&self) -> usize { + let tile_pos = self.tile_coord(); + let tile_dimensions = self.tile_grid_size(); + (tile_pos.y * tile_dimensions.x + tile_pos.x) as usize + } + + /// The tile's normalized midpoint. + fn tile_ndc_midpoint(&self) -> Vec2 { + let min_coord = self.tile_coord().as_vec2(); + let mid_coord = min_coord + 0.5; + crate::math::convert_pixel_to_ndc(mid_coord, self.tile_grid_size()) + } + + /// Compute the min and max depth of one fragment/invocation for light tiling. + /// + /// The min and max is stored in a tile on lighting slab. + fn compute_min_and_max_depth( + &self, + depth_texture: &impl Fetch, + lighting_slab: &mut [u32], + ) { + let frag_pos = self.frag_pos(); + let depth_texture_size = self.descriptor.depth_texture_size; + if frag_pos.x >= depth_texture_size.x || frag_pos.y >= depth_texture_size.y { + return; + } + // Depth frag value at the fragment position + let frag_depth: f32 = depth_texture.fetch(frag_pos).x; + // Fragment depth scaled to min/max of u32 values + // + // This is so we can compare with normal atomic ops instead of using the float extension + let frag_depth_u32 = quantize_depth_f32_to_u32(frag_depth); + + // The tile's index in all the tiles + let tile_index = self.tile_index(); + let lighting_desc = lighting_slab.read_unchecked(Id::::new(0)); + let tiling_desc = lighting_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id); + // index of the tile's min depth atomic value in the lighting slab + let tile_id = tiling_desc.tiles_array.at(tile_index); + let min_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MIN; + // index of the tile's max depth atomic value in the lighting slab + let max_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MAX; + + let _prev_min_depth = crate::sync::atomic_u_min::< + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(lighting_slab, min_depth_index, frag_depth_u32); + let _prev_max_depth = crate::sync::atomic_u_max::< + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(lighting_slab, max_depth_index, frag_depth_u32); + } + + /// Determine whether this invocation should run. + fn should_invoke(&self) -> bool { + self.global_id.x < self.descriptor.depth_texture_size.x + && self.global_id.y < self.descriptor.depth_texture_size.y + } + + /// Clears one tile. + /// + /// ## Note + /// This is only valid to call from the [`light_tiling_clear_tiles`] shader. + fn clear_tile(&self, lighting_slab: &mut [u32]) { + let dimensions = self.tile_grid_size(); + let index = (self.global_id.y * dimensions.x + self.global_id.x) as usize; + if index < self.descriptor.tiles_array.len() { + let tile_id = self.descriptor.tiles_array.at(index); + let mut tile = lighting_slab.read(tile_id); + tile.depth_min = u32::MAX; + tile.depth_max = 0; + tile.next_light_index = 0; + lighting_slab.write(tile_id, &tile); + // Zero out the light list and the ratings + for id in tile.lights_array.iter() { + lighting_slab.write(id, &Id::NONE); + } + } + } + + // The difficulty here is that in SPIRV we can access `lighting_slab` atomically without wrapping it + // in a type, but on CPU we must pass an array of (something like) `AtomicU32`. I'm not sure how to + // model this interaction to test it on the CPU. + fn compute_light_lists(&self, geometry_slab: &[u32], lighting_slab: &mut [u32]) { + let index = self.tile_index(); + let tile_id = self.descriptor.tiles_array.at(index); + // Construct the tile's frustum in clip space. + let depth_min_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MIN); + let depth_max_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MAX); + let depth_min = dequantize_depth_u32_to_f32(depth_min_u32); + let depth_max = dequantize_depth_u32_to_f32(depth_max_u32); + + if depth_min == depth_max { + // If we would construct a frustum with zero volume, abort. + // + // See + // for more info. + return; + } + + let camera_id = geometry_slab.read_unchecked( + Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID, + ); + let camera = geometry_slab.read_unchecked(camera_id); + + // let (ndc_tile_min, ndc_tile_max) = self.tile_ndc_min_max(); + // // This is the AABB frustum, in NDC coords + // let ndc_tile_aabb = Aabb::new( + // ndc_tile_min.extend(depth_min), + // ndc_tile_max.extend(depth_max), + // ); + + // Get the frustum (here simplified to a line) in world coords, since we'll be + // using it to compare against the radius of illumination of each light + let tile_ndc_midpoint = self.tile_ndc_midpoint(); + let tile_line_ndc = ( + tile_ndc_midpoint.extend(depth_min), + tile_ndc_midpoint.extend(depth_max), + ); + let inverse_viewproj = camera.view_projection().inverse(); + let tile_line = ( + inverse_viewproj.project_point3(tile_line_ndc.0), + inverse_viewproj.project_point3(tile_line_ndc.1), + ); + + let tile_index = self.tile_index(); + let tile_id = self.descriptor.tiles_array.at(tile_index); + let tile_lights_array = lighting_slab.read(tile_id + LightTile::OFFSET_OF_LIGHTS_ARRAY); + let next_light_id = tile_id + LightTile::OFFSET_OF_NEXT_LIGHT_INDEX; + + // List of all analytical lights in the scene + let analytical_lights_array = lighting_slab.read_unchecked( + Id::::new(0) + + LightingDescriptor::OFFSET_OF_ANALYTICAL_LIGHTS_ARRAY, + ); + + // Each invocation will calculate a few lights' contribution to the tile, until all lights + // have been visited + let next_light = NextLightIndex::new( + self.global_id, + self.descriptor.tile_size, + analytical_lights_array, + ); + for id_of_light_id in next_light { + let light_id = lighting_slab.read_unchecked(id_of_light_id); + let light = lighting_slab.read_unchecked(light_id); + let transform = lighting_slab.read(light.transform_id); + // Get the distance to the light in world coords, and the + // intensity of the light. + let (distance, intensity_candelas) = match light.light_type { + LightStyle::Directional => { + let directional_light = lighting_slab.read(light.into_directional_id()); + (0.0, directional_light.intensity) + } + LightStyle::Point => { + let point_light = lighting_slab.read(light.into_point_id()); + let center = Mat4::from(transform).transform_point3(point_light.position); + let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1); + (distance, point_light.intensity) + } + LightStyle::Spot => { + // TODO: take into consideration the direction the spot light is pointing + let spot_light = lighting_slab.read(light.into_spot_id()); + let center = Mat4::from(transform).transform_point3(spot_light.position); + let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1); + (distance, spot_light.intensity) + } + }; + + let radius = + radius_of_illumination(intensity_candelas, self.descriptor.minimum_illuminance_lux); + let should_add = radius >= distance; + if should_add { + // If the light should be added to the bin, get the next available index in the bin, + // then write the id of the light into that index. + let next_index = crate::sync::atomic_i_increment::< + { spirv_std::memory::Scope::Workgroup as u32 }, + { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() }, + >(lighting_slab, next_light_id); + if next_index as usize >= tile_lights_array.len() { + // We've already filled the bin, so abort. + // + // TODO: Figure out a better way to handle light tile list overrun. + break; + } else { + // Get the id that corresponds to the next available index in the ratings bin + let binned_light_id = tile_lights_array.at(next_index as usize); + // Write to that location + lighting_slab.write(binned_light_id, &light_id); + } + } + } + } +} + +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_clear_tiles( + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let lighting_descriptor = lighting_slab.read(Id::::new(0)); + let light_tiling_descriptor = + lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); + let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); + invocation.clear_tile(lighting_slab); +} + +/// Compute the min and max depth value for a tile. +/// +/// This shader must be called **once for each fragment in the depth texture**. +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_compute_tile_min_and_max_depth( + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2d, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let lighting_descriptor = lighting_slab.read(Id::::new(0)); + let light_tiling_descriptor = + lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); + let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); + invocation.compute_min_and_max_depth(depth_texture, lighting_slab); +} + +/// Compute the min and max depth value for a tile, multisampled. +/// +/// This shader must be called **once for each fragment in the depth texture**. +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_compute_tile_min_and_max_depth_multisampled( + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], + #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2dMultisampled, + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let lighting_descriptor = lighting_slab.read(Id::::new(0)); + let light_tiling_descriptor = + lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); + let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); + invocation.compute_min_and_max_depth(depth_texture, lighting_slab); +} + +#[spirv(compute(threads(16, 16, 1)))] +pub fn light_tiling_bin_lights( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32], + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let lighting_descriptor = lighting_slab.read(Id::::new(0)); + let light_tiling_descriptor = + lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id); + let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor); + if invocation.should_invoke() { + invocation.compute_light_lists(geometry_slab, lighting_slab); + } +} diff --git a/crates/renderling/src/light/shadow_map.rs b/crates/renderling/src/light/shadow_map.rs index 1e24e9c0..c520ee71 100644 --- a/crates/renderling/src/light/shadow_map.rs +++ b/crates/renderling/src/light/shadow_map.rs @@ -5,26 +5,32 @@ use std::sync::Arc; use craballoc::{ prelude::Hybrid, - value::{HybridArray, HybridWriteGuard, WeakContainer}, + value::{HybridArray, HybridWriteGuard}, }; use crabslab::Id; use glam::{Mat4, UVec2}; -use snafu::{OptionExt, ResultExt}; +use snafu::ResultExt; use crate::{ - atlas::{AtlasBlittingOperation, AtlasImage, AtlasTexture}, + atlas::{shader::AtlasTextureDescriptor, AtlasBlittingOperation, AtlasImage, AtlasTexture}, bindgroup::ManagedBindGroup, - stage::Renderlet, + light::{IsLight, Light}, + primitive::Primitive, }; use super::{ - AnalyticalLight, DroppedAnalyticalLightBundleSnafu, Lighting, LightingError, PollSnafu, - ShadowMapDescriptor, + shader::{LightStyle, ShadowMapDescriptor}, + AnalyticalLight, Lighting, LightingError, PollSnafu, }; -/// A depth map rendering of the scene from a light's point of view. +/// Projects shadows from a single light source for specific objects. /// -/// Used to project shadows from one light source for specific objects. +/// A `ShadowMap` is essentially a depth map rendering of the scene from one +/// light's point of view. We use this rendering of the scene to determine if +/// an object lies in shadow. +/// +/// To create a new [`ShadowMap`], use +/// [`Stage::new_shadow_map`](crate::stage::Stage::new_shadow_map). #[derive(Clone)] pub struct ShadowMap { /// Last time the stage slab was bound. @@ -40,11 +46,11 @@ pub struct ShadowMap { pub(crate) light_space_transforms: HybridArray, /// Bindgroup for the shadow map update shader pub(crate) update_bindgroup: ManagedBindGroup, - pub(crate) atlas_textures: Vec>, - pub(crate) _atlas_textures_array: HybridArray>, + pub(crate) atlas_textures: Vec, + pub(crate) _atlas_textures_array: HybridArray>, pub(crate) update_texture: crate::texture::Texture, pub(crate) blitting_op: AtlasBlittingOperation, - pub(crate) light_bundle: AnalyticalLight, + pub(crate) light_bundle: AnalyticalLight, } impl ShadowMap { @@ -160,21 +166,24 @@ impl ShadowMap { self.shadowmap_descriptor.lock() } - /// Enable shadow mapping for the given [`AnalyticalLightBundle`], creating + /// Enable shadow mapping for the given [`AnalyticalLight`], creating /// a new [`ShadowMap`]. - pub fn new( + pub fn new( lighting: &Lighting, - analytical_light_bundle: &AnalyticalLight, + analytical_light_bundle: &AnalyticalLight, // Size of the shadow map size: UVec2, // Distance to the shadow map frustum's near plane z_near: f32, // Distance to the shadow map frustum's far plane z_far: f32, - ) -> Result { + ) -> Result + where + T: IsLight, + Light: From, + { let stage_slab_buffer = lighting.geometry_slab_buffer.read().unwrap(); - let is_point_light = - analytical_light_bundle.light_details().style() == super::LightStyle::Point; + let is_point_light = analytical_light_bundle.style() == LightStyle::Point; let count = if is_point_light { 6 } else { 1 }; let atlas = &lighting.shadow_map_atlas; let image = AtlasImage::new(size, crate::atlas::AtlasImageFormat::R32FLOAT); @@ -195,12 +204,19 @@ impl ShadowMap { let atlas_textures_array = lighting .light_slab .new_array(atlas_textures.iter().map(|t| t.id())); - let blitting_op = lighting - .shadow_map_update_blitter - .new_blitting_operation(atlas, if is_point_light { 6 } else { 1 }); - let light_space_transforms = lighting - .light_slab - .new_array(analytical_light_bundle.light_space_transforms(z_near, z_far)); + let blitting_op = AtlasBlittingOperation::new( + &lighting.shadow_map_update_blitter, + atlas, + if is_point_light { 6 } else { 1 }, + ); + let light_space_transforms = + lighting + .light_slab + .new_array(analytical_light_bundle.light_space_transforms( + &analytical_light_bundle.transform().descriptor(), + z_near, + z_far, + )); let shadowmap_descriptor = lighting.light_slab.new_value(ShadowMapDescriptor { light_space_transforms_array: light_space_transforms.array(), z_near, @@ -211,7 +227,7 @@ impl ShadowMap { pcf_samples: 4, }); // Set the descriptor in the light, so the shader knows to use it - analytical_light_bundle.light().modify(|light| { + analytical_light_bundle.light_descriptor.modify(|light| { light.shadow_map_desc_id = shadowmap_descriptor.id(); }); let light_slab_buffer = lighting.commit(); @@ -232,32 +248,32 @@ impl ShadowMap { _atlas_textures_array: atlas_textures_array, update_texture, blitting_op, - light_bundle: analytical_light_bundle.weak(), + light_bundle: analytical_light_bundle.clone().into_generic(), }) } - /// Update the `ShadowMap`, rendering the given [`Renderlet`]s to the map as shadow casters. + /// Update the `ShadowMap`, rendering the given [`Primitive`]s to the map as + /// shadow casters. /// - /// The `ShadowMap` contains a weak reference to the [`AnalyticalLightBundle`] used to create - /// it. Updates made to this `AnalyticalLightBundle` will automatically propogate to this + /// The `ShadowMap` contains a weak reference to the [`AnalyticalLight`] used to create + /// it. Updates made to this `AnalyticalLight` will automatically propogate to this /// `ShadowMap`. /// /// ## Errors - /// If the `AnalyticalLightBundle` used to create this `ShadowMap` has been + /// If the `AnalyticalLight` used to create this `ShadowMap` has been /// dropped, calling this function will err. pub fn update<'a>( &self, lighting: impl AsRef, - renderlets: impl IntoIterator>, + renderlets: impl IntoIterator, ) -> Result<(), LightingError> { let lighting = lighting.as_ref(); - let light_bundle = self - .light_bundle - .upgrade() - .context(DroppedAnalyticalLightBundleSnafu)?; let shadow_desc = self.shadowmap_descriptor.get(); - let new_transforms = - light_bundle.light_space_transforms(shadow_desc.z_near, shadow_desc.z_far); + let new_transforms = self.light_bundle.light_space_transforms( + &self.light_bundle.transform().descriptor(), + shadow_desc.z_near, + shadow_desc.z_far, + ); for (i, t) in (0..self.light_space_transforms.len()).zip(new_transforms) { self.light_space_transforms.set_item(i, t); } @@ -339,7 +355,7 @@ impl ShadowMap { let mut count = 0; for rlet in renderlets.iter() { let id = rlet.id(); - let rlet = rlet.get(); + let rlet = rlet.descriptor(); let vertex_range = 0..rlet.get_vertex_count(); let instance_range = id.inner()..id.inner() + 1; render_pass.draw(vertex_range, instance_range); @@ -368,15 +384,15 @@ impl ShadowMap { #[cfg(test)] #[allow(clippy::unused_enumerate_index)] mod test { - use crate::{camera::Camera, test::BlockOnFuture}; + use glam::{UVec2, Vec3}; - use super::super::*; + use crate::{context::Context, test::BlockOnFuture}; #[test] fn shadow_mapping_just_cuboid() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -398,7 +414,7 @@ mod test { let camera = doc.cameras.first().unwrap(); camera .as_ref() - .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let frame = ctx.get_next_frame().unwrap(); @@ -430,7 +446,7 @@ mod test { fn shadow_mapping_just_cuboid_red_and_blue() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -446,7 +462,7 @@ mod test { let camera = doc.cameras.first().unwrap(); camera .as_ref() - .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let gltf_light_a = doc.lights.first().unwrap(); @@ -483,7 +499,7 @@ mod test { fn shadow_mapping_sanity() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32) + let ctx = Context::headless(w as u32, h as u32) .block() .with_shadow_mapping_atlas_texture_size([1024, 1024, 2]); let stage = ctx.new_stage().with_lighting(true); @@ -498,7 +514,7 @@ mod test { let camera = doc.cameras.first().unwrap(); camera .as_ref() - .modify(|cam| cam.set_projection(crate::camera::perspective(w, h))); + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let frame = ctx.get_next_frame().unwrap(); @@ -511,7 +527,7 @@ mod test { let gltf_light = doc.lights.first().unwrap(); assert_eq!( - gltf_light.light().get().transform_id, + gltf_light.descriptor().transform_id, gltf_light.transform().id(), "light's global transform id is different from its transform_id" ); @@ -570,7 +586,7 @@ mod test { fn shadow_mapping_spot_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -584,10 +600,9 @@ mod test { ) .unwrap(); let camera = doc.cameras.first().unwrap(); - let original_camera = camera.as_ref().modify(|cam| { - cam.set_projection(crate::camera::perspective(w, h)); - *cam - }); + camera + .as_ref() + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let mut shadow_maps = vec![]; @@ -595,13 +610,14 @@ mod test { let z_far = 100.0; for (_i, light_bundle) in doc.lights.iter().enumerate() { { - let desc = light_bundle.light_details().as_spot().unwrap().get(); + let desc = light_bundle.as_spot().unwrap().descriptor(); let (p, v) = desc.shadow_mapping_projection_and_view( - &light_bundle.transform().get().into(), + &light_bundle.transform().as_mat4(), z_near, z_far, ); - camera.as_ref().set(Camera::new(p, v)); + let temp_camera = stage.new_camera().with_projection_and_view(p, v); + stage.use_camera(temp_camera); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let _img = frame.read_image().block().unwrap(); @@ -622,7 +638,8 @@ mod test { shadow.update(&stage, doc.renderlets_iter()).unwrap(); shadow_maps.push(shadow); } - camera.as_ref().set(original_camera); + + stage.use_camera(camera); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -635,7 +652,7 @@ mod test { fn shadow_mapping_point_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32).block(); + let ctx = Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -649,10 +666,9 @@ mod test { ) .unwrap(); let camera = doc.cameras.first().unwrap(); - let c = camera.as_ref().modify(|cam| { - cam.set_projection(crate::camera::perspective(w, h)); - *cam - }); + camera + .as_ref() + .set_projection(crate::camera::perspective(w, h)); stage.use_camera(camera); let mut shadows = vec![]; @@ -660,15 +676,15 @@ mod test { let z_far = 100.0; for (i, light_bundle) in doc.lights.iter().enumerate() { { - let desc = light_bundle.light_details().as_point().unwrap().get(); + let desc = light_bundle.as_point().unwrap().descriptor(); println!("point light {i}: {desc:?}"); let (p, vs) = desc.shadow_mapping_projection_and_view_matrices( - &light_bundle.transform().get().into(), + &light_bundle.transform().as_mat4(), z_near, z_far, ); for (_j, v) in vs.into_iter().enumerate() { - camera.as_ref().set(Camera::new(p, v)); + stage.use_camera(stage.new_camera().with_projection_and_view(p, v)); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let _img = frame.read_image().block().unwrap(); @@ -690,7 +706,8 @@ mod test { shadow.update(&stage, doc.renderlets_iter()).unwrap(); shadows.push(shadow); } - camera.as_ref().set(c); + + stage.use_camera(camera); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); diff --git a/crates/renderling/src/light/tiling.rs b/crates/renderling/src/light/tiling.rs index 0a1f20c5..3834933e 100644 --- a/crates/renderling/src/light/tiling.rs +++ b/crates/renderling/src/light/tiling.rs @@ -1,7 +1,10 @@ -//! Implementation of light tiling. +//! This module implements light tiling, a technique used in rendering to efficiently manage and apply lighting effects across a scene. //! -//! For more info on what light tiling is, see -//! [this blog post](https://renderling.xyz/articles/live/light_tiling.html). +//! Light tiling divides the rendering surface into a grid of tiles, allowing for the efficient computation of lighting effects by processing each tile independently. This approach helps in optimizing the rendering pipeline by reducing the number of lighting calculations needed, especially in complex scenes with multiple light sources. +//! +//! The `LightTiling` struct and its associated methods provide the necessary functionality to set up and execute light tiling operations. It includes the creation of compute pipelines for clearing tiles, computing minimum and maximum depths, and binning lights into tiles. +//! +//! For more detailed information on light tiling and its implementation, refer to [this blog post](https://renderling.xyz/articles/live/light_tiling.html). use core::sync::atomic::AtomicUsize; use std::sync::Arc; @@ -15,7 +18,10 @@ use glam::{UVec2, UVec3}; use crate::{ bindgroup::ManagedBindGroup, - light::{LightTile, LightTilingDescriptor, Lighting}, + light::{ + shader::{LightTile, LightTilingDescriptor}, + Lighting, + }, stage::Stage, }; @@ -28,6 +34,8 @@ use crate::{ /// . pub struct LightTiling { pub(crate) tiling_descriptor: Hybrid, + /// Container is a type variable for testing, as we have to load + /// the tiles with known values from the CPU. tiles: Ct::Container, /// Cache of the id of the Stage's depth texture. /// @@ -334,7 +342,7 @@ impl LightTiling { ) -> (image::GrayImage, image::GrayImage, image::GrayImage) { use crabslab::Slab; - use crate::light::dequantize_depth_u32_to_f32; + use crate::light::shader::dequantize_depth_u32_to_f32; let tile_dimensions = self.tiling_descriptor.get().tile_grid_size(); let slab = lighting.light_slab.read(..).await.unwrap(); @@ -422,8 +430,8 @@ impl Default for LightTilingConfig { } impl LightTiling { - /// Creates a new [`LightTiling`] struct with a [`HybridArray`] of tiles. - pub fn new_hybrid( + /// Creates a new [`LightTiling`] struct with a `HybridArray` of tiles. + pub(crate) fn new_hybrid( lighting: &Lighting, multisampled: bool, depth_texture_size: UVec2, diff --git a/crates/renderling/src/linkage.rs b/crates/renderling/src/linkage.rs index 55941dd6..f12c7a3e 100644 --- a/crates/renderling/src/linkage.rs +++ b/crates/renderling/src/linkage.rs @@ -33,8 +33,8 @@ pub mod light_tiling_compute_tile_min_and_max_depth_multisampled; pub mod light_tiling_depth_pre_pass; pub mod prefilter_environment_cubemap_fragment; pub mod prefilter_environment_cubemap_vertex; -pub mod renderlet_fragment; -pub mod renderlet_vertex; +pub mod primitive_fragment; +pub mod primitive_vertex; pub mod shadow_mapping_fragment; pub mod shadow_mapping_vertex; pub mod skybox_cubemap_fragment; diff --git a/crates/renderling/src/linkage/atlas_blit_fragment.rs b/crates/renderling/src/linkage/atlas_blit_fragment.rs index f4ce010d..43d02289 100644 --- a/crates/renderling/src/linkage/atlas_blit_fragment.rs +++ b/crates/renderling/src/linkage/atlas_blit_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "atlas::atlas_blit_fragment"; + pub const ENTRY_POINT: &str = "atlas::shader::atlas_blit_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/atlas-atlas_blit_fragment.spv") + wgpu::include_spirv!("../../shaders/atlas-shader-atlas_blit_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "atlas_blit_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "atlasatlas_blit_fragment"; + pub const ENTRY_POINT: &str = "atlasshaderatlas_blit_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/atlas-atlas_blit_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/atlas-shader-atlas_blit_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "atlas_blit_fragment"); diff --git a/crates/renderling/src/linkage/atlas_blit_vertex.rs b/crates/renderling/src/linkage/atlas_blit_vertex.rs index e3cfab71..d78fd25b 100644 --- a/crates/renderling/src/linkage/atlas_blit_vertex.rs +++ b/crates/renderling/src/linkage/atlas_blit_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "atlas::atlas_blit_vertex"; + pub const ENTRY_POINT: &str = "atlas::shader::atlas_blit_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/atlas-atlas_blit_vertex.spv") + wgpu::include_spirv!("../../shaders/atlas-shader-atlas_blit_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "atlas_blit_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "atlasatlas_blit_vertex"; + pub const ENTRY_POINT: &str = "atlasshaderatlas_blit_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/atlas-atlas_blit_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/atlas-shader-atlas_blit_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "atlas_blit_vertex"); diff --git a/crates/renderling/src/linkage/bloom_downsample_fragment.rs b/crates/renderling/src/linkage/bloom_downsample_fragment.rs index 124add10..6960adc6 100644 --- a/crates/renderling/src/linkage/bloom_downsample_fragment.rs +++ b/crates/renderling/src/linkage/bloom_downsample_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "bloom::bloom_downsample_fragment"; + pub const ENTRY_POINT: &str = "bloom::shader::bloom_downsample_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/bloom-bloom_downsample_fragment.spv") + wgpu::include_spirv!("../../shaders/bloom-shader-bloom_downsample_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "bloombloom_downsample_fragment"; + pub const ENTRY_POINT: &str = "bloomshaderbloom_downsample_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/bloom-bloom_downsample_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/bloom-shader-bloom_downsample_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "bloom_downsample_fragment"); diff --git a/crates/renderling/src/linkage/bloom_mix_fragment.rs b/crates/renderling/src/linkage/bloom_mix_fragment.rs index 9bcd9325..fe362a45 100644 --- a/crates/renderling/src/linkage/bloom_mix_fragment.rs +++ b/crates/renderling/src/linkage/bloom_mix_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "bloom::bloom_mix_fragment"; + pub const ENTRY_POINT: &str = "bloom::shader::bloom_mix_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/bloom-bloom_mix_fragment.spv") + wgpu::include_spirv!("../../shaders/bloom-shader-bloom_mix_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "bloom_mix_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "bloombloom_mix_fragment"; + pub const ENTRY_POINT: &str = "bloomshaderbloom_mix_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/bloom-bloom_mix_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/bloom-shader-bloom_mix_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "bloom_mix_fragment"); diff --git a/crates/renderling/src/linkage/bloom_upsample_fragment.rs b/crates/renderling/src/linkage/bloom_upsample_fragment.rs index 5884452b..74fcc4b8 100644 --- a/crates/renderling/src/linkage/bloom_upsample_fragment.rs +++ b/crates/renderling/src/linkage/bloom_upsample_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "bloom::bloom_upsample_fragment"; + pub const ENTRY_POINT: &str = "bloom::shader::bloom_upsample_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/bloom-bloom_upsample_fragment.spv") + wgpu::include_spirv!("../../shaders/bloom-shader-bloom_upsample_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "bloom_upsample_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "bloombloom_upsample_fragment"; + pub const ENTRY_POINT: &str = "bloomshaderbloom_upsample_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/bloom-bloom_upsample_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/bloom-shader-bloom_upsample_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "bloom_upsample_fragment"); diff --git a/crates/renderling/src/linkage/bloom_vertex.rs b/crates/renderling/src/linkage/bloom_vertex.rs index 20417c46..532f4dcd 100644 --- a/crates/renderling/src/linkage/bloom_vertex.rs +++ b/crates/renderling/src/linkage/bloom_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "bloom::bloom_vertex"; + pub const ENTRY_POINT: &str = "bloom::shader::bloom_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/bloom-bloom_vertex.spv") + wgpu::include_spirv!("../../shaders/bloom-shader-bloom_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "bloom_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "bloombloom_vertex"; + pub const ENTRY_POINT: &str = "bloomshaderbloom_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/bloom-bloom_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/bloom-shader-bloom_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "bloom_vertex"); diff --git a/crates/renderling/src/linkage/brdf_lut_convolution_fragment.rs b/crates/renderling/src/linkage/brdf_lut_convolution_fragment.rs index 47cf8a95..810a04e4 100644 --- a/crates/renderling/src/linkage/brdf_lut_convolution_fragment.rs +++ b/crates/renderling/src/linkage/brdf_lut_convolution_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::brdf_lut_convolution_fragment"; + pub const ENTRY_POINT: &str = "convolution::shader::brdf_lut_convolution_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-brdf_lut_convolution_fragment.spv") + wgpu::include_spirv!("../../shaders/convolution-shader-brdf_lut_convolution_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutionbrdf_lut_convolution_fragment"; + pub const ENTRY_POINT: &str = "convolutionshaderbrdf_lut_convolution_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-brdf_lut_convolution_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/convolution-shader-brdf_lut_convolution_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/brdf_lut_convolution_vertex.rs b/crates/renderling/src/linkage/brdf_lut_convolution_vertex.rs index 2ada2506..85504cba 100644 --- a/crates/renderling/src/linkage/brdf_lut_convolution_vertex.rs +++ b/crates/renderling/src/linkage/brdf_lut_convolution_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::brdf_lut_convolution_vertex"; + pub const ENTRY_POINT: &str = "convolution::shader::brdf_lut_convolution_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-brdf_lut_convolution_vertex.spv") + wgpu::include_spirv!("../../shaders/convolution-shader-brdf_lut_convolution_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutionbrdf_lut_convolution_vertex"; + pub const ENTRY_POINT: &str = "convolutionshaderbrdf_lut_convolution_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-brdf_lut_convolution_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/convolution-shader-brdf_lut_convolution_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "brdf_lut_convolution_vertex"); diff --git a/crates/renderling/src/linkage/compute_copy_depth_to_pyramid.rs b/crates/renderling/src/linkage/compute_copy_depth_to_pyramid.rs index fa419158..471ed73a 100644 --- a/crates/renderling/src/linkage/compute_copy_depth_to_pyramid.rs +++ b/crates/renderling/src/linkage/compute_copy_depth_to_pyramid.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cull::compute_copy_depth_to_pyramid"; + pub const ENTRY_POINT: &str = "cull::shader::compute_copy_depth_to_pyramid"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cull-compute_copy_depth_to_pyramid.spv") + wgpu::include_spirv!("../../shaders/cull-shader-compute_copy_depth_to_pyramid.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cullcompute_copy_depth_to_pyramid"; + pub const ENTRY_POINT: &str = "cullshadercompute_copy_depth_to_pyramid"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cull-compute_copy_depth_to_pyramid.wgsl") + wgpu::include_wgsl!("../../shaders/cull-shader-compute_copy_depth_to_pyramid.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/compute_copy_depth_to_pyramid_multisampled.rs b/crates/renderling/src/linkage/compute_copy_depth_to_pyramid_multisampled.rs index 707b4fc0..4eb2118f 100644 --- a/crates/renderling/src/linkage/compute_copy_depth_to_pyramid_multisampled.rs +++ b/crates/renderling/src/linkage/compute_copy_depth_to_pyramid_multisampled.rs @@ -3,9 +3,11 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cull::compute_copy_depth_to_pyramid_multisampled"; + pub const ENTRY_POINT: &str = "cull::shader::compute_copy_depth_to_pyramid_multisampled"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cull-compute_copy_depth_to_pyramid_multisampled.spv") + wgpu::include_spirv!( + "../../shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +22,11 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cullcompute_copy_depth_to_pyramid_multisampled"; + pub const ENTRY_POINT: &str = "cullshadercompute_copy_depth_to_pyramid_multisampled"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cull-compute_copy_depth_to_pyramid_multisampled.wgsl") + wgpu::include_wgsl!( + "../../shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.wgsl" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/compute_culling.rs b/crates/renderling/src/linkage/compute_culling.rs index 09a45604..d70aed56 100644 --- a/crates/renderling/src/linkage/compute_culling.rs +++ b/crates/renderling/src/linkage/compute_culling.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cull::compute_culling"; + pub const ENTRY_POINT: &str = "cull::shader::compute_culling"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cull-compute_culling.spv") + wgpu::include_spirv!("../../shaders/cull-shader-compute_culling.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "compute_culling"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cullcompute_culling"; + pub const ENTRY_POINT: &str = "cullshadercompute_culling"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cull-compute_culling.wgsl") + wgpu::include_wgsl!("../../shaders/cull-shader-compute_culling.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "compute_culling"); diff --git a/crates/renderling/src/linkage/compute_downsample_depth_pyramid.rs b/crates/renderling/src/linkage/compute_downsample_depth_pyramid.rs index 4c65701f..af7a2f06 100644 --- a/crates/renderling/src/linkage/compute_downsample_depth_pyramid.rs +++ b/crates/renderling/src/linkage/compute_downsample_depth_pyramid.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cull::compute_downsample_depth_pyramid"; + pub const ENTRY_POINT: &str = "cull::shader::compute_downsample_depth_pyramid"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cull-compute_downsample_depth_pyramid.spv") + wgpu::include_spirv!("../../shaders/cull-shader-compute_downsample_depth_pyramid.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cullcompute_downsample_depth_pyramid"; + pub const ENTRY_POINT: &str = "cullshadercompute_downsample_depth_pyramid"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cull-compute_downsample_depth_pyramid.wgsl") + wgpu::include_wgsl!("../../shaders/cull-shader-compute_downsample_depth_pyramid.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs b/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs index 8783ec44..6a015c8f 100644 --- a/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs +++ b/crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cubemap::cubemap_sampling_test_fragment"; + pub const ENTRY_POINT: &str = "cubemap::shader::cubemap_sampling_test_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cubemap-cubemap_sampling_test_fragment.spv") + wgpu::include_spirv!("../../shaders/cubemap-shader-cubemap_sampling_test_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cubemapcubemap_sampling_test_fragment"; + pub const ENTRY_POINT: &str = "cubemapshadercubemap_sampling_test_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cubemap-cubemap_sampling_test_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/cubemap-shader-cubemap_sampling_test_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs b/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs index 40c8ba5f..bb5528ad 100644 --- a/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs +++ b/crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "cubemap::cubemap_sampling_test_vertex"; + pub const ENTRY_POINT: &str = "cubemap::shader::cubemap_sampling_test_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/cubemap-cubemap_sampling_test_vertex.spv") + wgpu::include_spirv!("../../shaders/cubemap-shader-cubemap_sampling_test_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "cubemapcubemap_sampling_test_vertex"; + pub const ENTRY_POINT: &str = "cubemapshadercubemap_sampling_test_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/cubemap-cubemap_sampling_test_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/cubemap-shader-cubemap_sampling_test_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/debug_overlay_fragment.rs b/crates/renderling/src/linkage/debug_overlay_fragment.rs index f365ed3d..3612aa13 100644 --- a/crates/renderling/src/linkage/debug_overlay_fragment.rs +++ b/crates/renderling/src/linkage/debug_overlay_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "debug::debug_overlay_fragment"; + pub const ENTRY_POINT: &str = "debug::shader::debug_overlay_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/debug-debug_overlay_fragment.spv") + wgpu::include_spirv!("../../shaders/debug-shader-debug_overlay_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "debug_overlay_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "debugdebug_overlay_fragment"; + pub const ENTRY_POINT: &str = "debugshaderdebug_overlay_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/debug-debug_overlay_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/debug-shader-debug_overlay_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "debug_overlay_fragment"); diff --git a/crates/renderling/src/linkage/debug_overlay_vertex.rs b/crates/renderling/src/linkage/debug_overlay_vertex.rs index b6e66b69..c3614af8 100644 --- a/crates/renderling/src/linkage/debug_overlay_vertex.rs +++ b/crates/renderling/src/linkage/debug_overlay_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "debug::debug_overlay_vertex"; + pub const ENTRY_POINT: &str = "debug::shader::debug_overlay_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/debug-debug_overlay_vertex.spv") + wgpu::include_spirv!("../../shaders/debug-shader-debug_overlay_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "debug_overlay_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "debugdebug_overlay_vertex"; + pub const ENTRY_POINT: &str = "debugshaderdebug_overlay_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/debug-debug_overlay_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/debug-shader-debug_overlay_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "debug_overlay_vertex"); diff --git a/crates/renderling/src/linkage/di_convolution_fragment.rs b/crates/renderling/src/linkage/di_convolution_fragment.rs index 8d26d94f..bc8f7165 100644 --- a/crates/renderling/src/linkage/di_convolution_fragment.rs +++ b/crates/renderling/src/linkage/di_convolution_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "ibl::diffuse_irradiance::di_convolution_fragment"; + pub const ENTRY_POINT: &str = "pbr::ibl::shader::di_convolution_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/ibl-diffuse_irradiance-di_convolution_fragment.spv") + wgpu::include_spirv!("../../shaders/pbr-ibl-shader-di_convolution_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "di_convolution_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "ibldiffuse_irradiancedi_convolution_fragment"; + pub const ENTRY_POINT: &str = "pbriblshaderdi_convolution_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/ibl-diffuse_irradiance-di_convolution_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/pbr-ibl-shader-di_convolution_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "di_convolution_fragment"); diff --git a/crates/renderling/src/linkage/generate_mipmap_fragment.rs b/crates/renderling/src/linkage/generate_mipmap_fragment.rs index eec4a4ae..e72fa2ad 100644 --- a/crates/renderling/src/linkage/generate_mipmap_fragment.rs +++ b/crates/renderling/src/linkage/generate_mipmap_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::generate_mipmap_fragment"; + pub const ENTRY_POINT: &str = "convolution::shader::generate_mipmap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-generate_mipmap_fragment.spv") + wgpu::include_spirv!("../../shaders/convolution-shader-generate_mipmap_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "generate_mipmap_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutiongenerate_mipmap_fragment"; + pub const ENTRY_POINT: &str = "convolutionshadergenerate_mipmap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-generate_mipmap_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/convolution-shader-generate_mipmap_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "generate_mipmap_fragment"); diff --git a/crates/renderling/src/linkage/generate_mipmap_vertex.rs b/crates/renderling/src/linkage/generate_mipmap_vertex.rs index cdeab9dd..38b11edd 100644 --- a/crates/renderling/src/linkage/generate_mipmap_vertex.rs +++ b/crates/renderling/src/linkage/generate_mipmap_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::generate_mipmap_vertex"; + pub const ENTRY_POINT: &str = "convolution::shader::generate_mipmap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-generate_mipmap_vertex.spv") + wgpu::include_spirv!("../../shaders/convolution-shader-generate_mipmap_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "generate_mipmap_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutiongenerate_mipmap_vertex"; + pub const ENTRY_POINT: &str = "convolutionshadergenerate_mipmap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-generate_mipmap_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/convolution-shader-generate_mipmap_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "generate_mipmap_vertex"); diff --git a/crates/renderling/src/linkage/light_tiling_bin_lights.rs b/crates/renderling/src/linkage/light_tiling_bin_lights.rs index 6db4017f..f5cd5b20 100644 --- a/crates/renderling/src/linkage/light_tiling_bin_lights.rs +++ b/crates/renderling/src/linkage/light_tiling_bin_lights.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::light_tiling_bin_lights"; + pub const ENTRY_POINT: &str = "light::shader::light_tiling_bin_lights"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-light_tiling_bin_lights.spv") + wgpu::include_spirv!("../../shaders/light-shader-light_tiling_bin_lights.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "light_tiling_bin_lights"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightlight_tiling_bin_lights"; + pub const ENTRY_POINT: &str = "lightshaderlight_tiling_bin_lights"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-light_tiling_bin_lights.wgsl") + wgpu::include_wgsl!("../../shaders/light-shader-light_tiling_bin_lights.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "light_tiling_bin_lights"); diff --git a/crates/renderling/src/linkage/light_tiling_clear_tiles.rs b/crates/renderling/src/linkage/light_tiling_clear_tiles.rs index dcd8723b..6f858507 100644 --- a/crates/renderling/src/linkage/light_tiling_clear_tiles.rs +++ b/crates/renderling/src/linkage/light_tiling_clear_tiles.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::light_tiling_clear_tiles"; + pub const ENTRY_POINT: &str = "light::shader::light_tiling_clear_tiles"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-light_tiling_clear_tiles.spv") + wgpu::include_spirv!("../../shaders/light-shader-light_tiling_clear_tiles.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "light_tiling_clear_tiles"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightlight_tiling_clear_tiles"; + pub const ENTRY_POINT: &str = "lightshaderlight_tiling_clear_tiles"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-light_tiling_clear_tiles.wgsl") + wgpu::include_wgsl!("../../shaders/light-shader-light_tiling_clear_tiles.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "light_tiling_clear_tiles"); diff --git a/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth.rs b/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth.rs index 9067d0d8..66ad2071 100644 --- a/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth.rs +++ b/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth.rs @@ -3,9 +3,11 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::light_tiling_compute_tile_min_and_max_depth"; + pub const ENTRY_POINT: &str = "light::shader::light_tiling_compute_tile_min_and_max_depth"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-light_tiling_compute_tile_min_and_max_depth.spv") + wgpu::include_spirv!( + "../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +22,11 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightlight_tiling_compute_tile_min_and_max_depth"; + pub const ENTRY_POINT: &str = "lightshaderlight_tiling_compute_tile_min_and_max_depth"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-light_tiling_compute_tile_min_and_max_depth.wgsl") + wgpu::include_wgsl!( + "../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.wgsl" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs b/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs index 22f3b90c..9c1af43d 100644 --- a/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs +++ b/crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs @@ -3,11 +3,10 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::light_tiling_compute_tile_min_and_max_depth_multisampled"; + pub const ENTRY_POINT: &str = + "light::shader::light_tiling_compute_tile_min_and_max_depth_multisampled"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!( - "../../shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.spv" - ) + wgpu :: include_spirv ! ("../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -22,11 +21,10 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightlight_tiling_compute_tile_min_and_max_depth_multisampled"; + pub const ENTRY_POINT: &str = + "lightshaderlight_tiling_compute_tile_min_and_max_depth_multisampled"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!( - "../../shaders/light-light_tiling_compute_tile_min_and_max_depth_multisampled.wgsl" - ) + wgpu :: include_wgsl ! ("../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs b/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs index 0423c693..8b11c26d 100644 --- a/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs +++ b/crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::light_tiling_depth_pre_pass"; + pub const ENTRY_POINT: &str = "light::shader::light_tiling_depth_pre_pass"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-light_tiling_depth_pre_pass.spv") + wgpu::include_spirv!("../../shaders/light-shader-light_tiling_depth_pre_pass.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightlight_tiling_depth_pre_pass"; + pub const ENTRY_POINT: &str = "lightshaderlight_tiling_depth_pre_pass"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-light_tiling_depth_pre_pass.wgsl") + wgpu::include_wgsl!("../../shaders/light-shader-light_tiling_depth_pre_pass.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "light_tiling_depth_pre_pass"); diff --git a/crates/renderling/src/linkage/prefilter_environment_cubemap_fragment.rs b/crates/renderling/src/linkage/prefilter_environment_cubemap_fragment.rs index 0cc0ce40..e559d794 100644 --- a/crates/renderling/src/linkage/prefilter_environment_cubemap_fragment.rs +++ b/crates/renderling/src/linkage/prefilter_environment_cubemap_fragment.rs @@ -3,9 +3,11 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::prefilter_environment_cubemap_fragment"; + pub const ENTRY_POINT: &str = "convolution::shader::prefilter_environment_cubemap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-prefilter_environment_cubemap_fragment.spv") + wgpu::include_spirv!( + "../../shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +22,11 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutionprefilter_environment_cubemap_fragment"; + pub const ENTRY_POINT: &str = "convolutionshaderprefilter_environment_cubemap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-prefilter_environment_cubemap_fragment.wgsl") + wgpu::include_wgsl!( + "../../shaders/convolution-shader-prefilter_environment_cubemap_fragment.wgsl" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/prefilter_environment_cubemap_vertex.rs b/crates/renderling/src/linkage/prefilter_environment_cubemap_vertex.rs index 31878097..052672ff 100644 --- a/crates/renderling/src/linkage/prefilter_environment_cubemap_vertex.rs +++ b/crates/renderling/src/linkage/prefilter_environment_cubemap_vertex.rs @@ -3,9 +3,11 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "convolution::prefilter_environment_cubemap_vertex"; + pub const ENTRY_POINT: &str = "convolution::shader::prefilter_environment_cubemap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/convolution-prefilter_environment_cubemap_vertex.spv") + wgpu::include_spirv!( + "../../shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +22,11 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "convolutionprefilter_environment_cubemap_vertex"; + pub const ENTRY_POINT: &str = "convolutionshaderprefilter_environment_cubemap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/convolution-prefilter_environment_cubemap_vertex.wgsl") + wgpu::include_wgsl!( + "../../shaders/convolution-shader-prefilter_environment_cubemap_vertex.wgsl" + ) } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/renderlet_fragment.rs b/crates/renderling/src/linkage/primitive_fragment.rs similarity index 67% rename from crates/renderling/src/linkage/renderlet_fragment.rs rename to crates/renderling/src/linkage/primitive_fragment.rs index 5984c350..3975086d 100644 --- a/crates/renderling/src/linkage/renderlet_fragment.rs +++ b/crates/renderling/src/linkage/primitive_fragment.rs @@ -3,12 +3,12 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "stage::renderlet_fragment"; + pub const ENTRY_POINT: &str = "primitive::shader::primitive_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/stage-renderlet_fragment.spv") + wgpu::include_spirv!("../../shaders/primitive-shader-primitive_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { - log::debug!("creating native linkage for {}", "renderlet_fragment"); + log::debug!("creating native linkage for {}", "primitive_fragment"); super::ShaderLinkage { entry_point: ENTRY_POINT, module: device.create_shader_module(descriptor()).into(), @@ -17,12 +17,12 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "stagerenderlet_fragment"; + pub const ENTRY_POINT: &str = "primitiveshaderprimitive_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/stage-renderlet_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/primitive-shader-primitive_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { - log::debug!("creating web linkage for {}", "renderlet_fragment"); + log::debug!("creating web linkage for {}", "primitive_fragment"); super::ShaderLinkage { entry_point: ENTRY_POINT, module: device.create_shader_module(descriptor()).into(), diff --git a/crates/renderling/src/linkage/renderlet_vertex.rs b/crates/renderling/src/linkage/primitive_vertex.rs similarity index 67% rename from crates/renderling/src/linkage/renderlet_vertex.rs rename to crates/renderling/src/linkage/primitive_vertex.rs index 11af60ab..40c032a3 100644 --- a/crates/renderling/src/linkage/renderlet_vertex.rs +++ b/crates/renderling/src/linkage/primitive_vertex.rs @@ -3,12 +3,12 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "stage::renderlet_vertex"; + pub const ENTRY_POINT: &str = "primitive::shader::primitive_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/stage-renderlet_vertex.spv") + wgpu::include_spirv!("../../shaders/primitive-shader-primitive_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { - log::debug!("creating native linkage for {}", "renderlet_vertex"); + log::debug!("creating native linkage for {}", "primitive_vertex"); super::ShaderLinkage { entry_point: ENTRY_POINT, module: device.create_shader_module(descriptor()).into(), @@ -17,12 +17,12 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "stagerenderlet_vertex"; + pub const ENTRY_POINT: &str = "primitiveshaderprimitive_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/stage-renderlet_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/primitive-shader-primitive_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { - log::debug!("creating web linkage for {}", "renderlet_vertex"); + log::debug!("creating web linkage for {}", "primitive_vertex"); super::ShaderLinkage { entry_point: ENTRY_POINT, module: device.create_shader_module(descriptor()).into(), diff --git a/crates/renderling/src/linkage/shadow_mapping_fragment.rs b/crates/renderling/src/linkage/shadow_mapping_fragment.rs index a730025a..5f92e841 100644 --- a/crates/renderling/src/linkage/shadow_mapping_fragment.rs +++ b/crates/renderling/src/linkage/shadow_mapping_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::shadow_mapping_fragment"; + pub const ENTRY_POINT: &str = "light::shader::shadow_mapping_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-shadow_mapping_fragment.spv") + wgpu::include_spirv!("../../shaders/light-shader-shadow_mapping_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "shadow_mapping_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightshadow_mapping_fragment"; + pub const ENTRY_POINT: &str = "lightshadershadow_mapping_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-shadow_mapping_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/light-shader-shadow_mapping_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "shadow_mapping_fragment"); diff --git a/crates/renderling/src/linkage/shadow_mapping_vertex.rs b/crates/renderling/src/linkage/shadow_mapping_vertex.rs index bb5ece96..2b2bc317 100644 --- a/crates/renderling/src/linkage/shadow_mapping_vertex.rs +++ b/crates/renderling/src/linkage/shadow_mapping_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "light::shadow_mapping_vertex"; + pub const ENTRY_POINT: &str = "light::shader::shadow_mapping_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/light-shadow_mapping_vertex.spv") + wgpu::include_spirv!("../../shaders/light-shader-shadow_mapping_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "shadow_mapping_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "lightshadow_mapping_vertex"; + pub const ENTRY_POINT: &str = "lightshadershadow_mapping_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/light-shadow_mapping_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/light-shader-shadow_mapping_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "shadow_mapping_vertex"); diff --git a/crates/renderling/src/linkage/skybox_cubemap_fragment.rs b/crates/renderling/src/linkage/skybox_cubemap_fragment.rs index 611d7cbb..46367957 100644 --- a/crates/renderling/src/linkage/skybox_cubemap_fragment.rs +++ b/crates/renderling/src/linkage/skybox_cubemap_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "skybox::skybox_cubemap_fragment"; + pub const ENTRY_POINT: &str = "skybox::shader::skybox_cubemap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/skybox-skybox_cubemap_fragment.spv") + wgpu::include_spirv!("../../shaders/skybox-shader-skybox_cubemap_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "skybox_cubemap_fragment"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "skyboxskybox_cubemap_fragment"; + pub const ENTRY_POINT: &str = "skyboxshaderskybox_cubemap_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/skybox-skybox_cubemap_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/skybox-shader-skybox_cubemap_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "skybox_cubemap_fragment"); diff --git a/crates/renderling/src/linkage/skybox_cubemap_vertex.rs b/crates/renderling/src/linkage/skybox_cubemap_vertex.rs index f6e073c0..5a9a1890 100644 --- a/crates/renderling/src/linkage/skybox_cubemap_vertex.rs +++ b/crates/renderling/src/linkage/skybox_cubemap_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "skybox::skybox_cubemap_vertex"; + pub const ENTRY_POINT: &str = "skybox::shader::skybox_cubemap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/skybox-skybox_cubemap_vertex.spv") + wgpu::include_spirv!("../../shaders/skybox-shader-skybox_cubemap_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "skybox_cubemap_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "skyboxskybox_cubemap_vertex"; + pub const ENTRY_POINT: &str = "skyboxshaderskybox_cubemap_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/skybox-skybox_cubemap_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/skybox-shader-skybox_cubemap_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "skybox_cubemap_vertex"); diff --git a/crates/renderling/src/linkage/skybox_equirectangular_fragment.rs b/crates/renderling/src/linkage/skybox_equirectangular_fragment.rs index 55d579c7..532a4706 100644 --- a/crates/renderling/src/linkage/skybox_equirectangular_fragment.rs +++ b/crates/renderling/src/linkage/skybox_equirectangular_fragment.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "skybox::skybox_equirectangular_fragment"; + pub const ENTRY_POINT: &str = "skybox::shader::skybox_equirectangular_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/skybox-skybox_equirectangular_fragment.spv") + wgpu::include_spirv!("../../shaders/skybox-shader-skybox_equirectangular_fragment.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( @@ -20,9 +20,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "skyboxskybox_equirectangular_fragment"; + pub const ENTRY_POINT: &str = "skyboxshaderskybox_equirectangular_fragment"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/skybox-skybox_equirectangular_fragment.wgsl") + wgpu::include_wgsl!("../../shaders/skybox-shader-skybox_equirectangular_fragment.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!( diff --git a/crates/renderling/src/linkage/skybox_vertex.rs b/crates/renderling/src/linkage/skybox_vertex.rs index 97f15663..81ac3605 100644 --- a/crates/renderling/src/linkage/skybox_vertex.rs +++ b/crates/renderling/src/linkage/skybox_vertex.rs @@ -3,9 +3,9 @@ use crate::linkage::ShaderLinkage; #[cfg(not(target_arch = "wasm32"))] mod target { - pub const ENTRY_POINT: &str = "skybox::skybox_vertex"; + pub const ENTRY_POINT: &str = "skybox::shader::skybox_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_spirv!("../../shaders/skybox-skybox_vertex.spv") + wgpu::include_spirv!("../../shaders/skybox-shader-skybox_vertex.spv") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating native linkage for {}", "skybox_vertex"); @@ -17,9 +17,9 @@ mod target { } #[cfg(target_arch = "wasm32")] mod target { - pub const ENTRY_POINT: &str = "skyboxskybox_vertex"; + pub const ENTRY_POINT: &str = "skyboxshaderskybox_vertex"; pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { - wgpu::include_wgsl!("../../shaders/skybox-skybox_vertex.wgsl") + wgpu::include_wgsl!("../../shaders/skybox-shader-skybox_vertex.wgsl") } pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { log::debug!("creating web linkage for {}", "skybox_vertex"); diff --git a/crates/renderling/src/material.rs b/crates/renderling/src/material.rs index d15e21e8..92117897 100644 --- a/crates/renderling/src/material.rs +++ b/crates/renderling/src/material.rs @@ -4,3 +4,62 @@ mod cpu; #[cfg(cpu)] pub use cpu::*; + +pub mod shader { + //! Material shader types. + + use crabslab::{Id, SlabItem}; + use glam::{Vec3, Vec4}; + + use crate::atlas::shader::AtlasTextureDescriptor; + + /// Represents a material on the GPU. + #[repr(C)] + #[derive(Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)] + pub struct MaterialDescriptor { + pub emissive_factor: Vec3, + pub emissive_strength_multiplier: f32, + pub albedo_factor: Vec4, + pub metallic_factor: f32, + pub roughness_factor: f32, + + pub albedo_texture_id: Id, + pub metallic_roughness_texture_id: Id, + pub normal_texture_id: Id, + pub ao_texture_id: Id, + pub emissive_texture_id: Id, + + pub albedo_tex_coord: u32, + pub metallic_roughness_tex_coord: u32, + pub normal_tex_coord: u32, + pub ao_tex_coord: u32, + pub emissive_tex_coord: u32, + + pub has_lighting: bool, + pub ao_strength: f32, + } + + impl Default for MaterialDescriptor { + fn default() -> Self { + Self { + emissive_factor: Vec3::ZERO, + emissive_strength_multiplier: 1.0, + albedo_factor: Vec4::ONE, + metallic_factor: 1.0, + roughness_factor: 1.0, + albedo_texture_id: Id::NONE, + metallic_roughness_texture_id: Id::NONE, + normal_texture_id: Id::NONE, + ao_texture_id: Id::NONE, + albedo_tex_coord: 0, + metallic_roughness_tex_coord: 0, + normal_tex_coord: 0, + ao_tex_coord: 0, + has_lighting: true, + ao_strength: 0.0, + emissive_texture_id: Id::NONE, + emissive_tex_coord: 0, + } + } + } +} diff --git a/crates/renderling/src/material/cpu.rs b/crates/renderling/src/material/cpu.rs index a8ef1a17..818304a7 100644 --- a/crates/renderling/src/material/cpu.rs +++ b/crates/renderling/src/material/cpu.rs @@ -1,18 +1,26 @@ //! CPU side of materials. +use std::sync::{Arc, Mutex}; use craballoc::{ + // Craballoc is used for memory allocation and management. runtime::WgpuRuntime, slab::{SlabAllocator, SlabBuffer}, - value::{Hybrid, HybridArray}, + value::Hybrid, }; +use crabslab::Id; +use glam::{Vec3, Vec4}; -use crate::{atlas::Atlas, pbr::Material}; +use crate::{ + atlas::{Atlas, AtlasTexture}, + material::shader::MaterialDescriptor, +}; /// Wrapper around the materials slab, which holds material textures in an atlas. #[derive(Clone)] pub struct Materials { slab: SlabAllocator, atlas: Atlas, + default_material: Material, } impl AsRef for Materials { @@ -22,24 +30,50 @@ impl AsRef for Materials { } impl Materials { + /// Creates a new `Materials` instance with the specified runtime and atlas size. + /// + /// # Arguments + /// + /// * `runtime` - A reference to the WgpuRuntime. + /// * `atlas_size` - The size of the atlas texture. pub fn new(runtime: impl AsRef, atlas_size: wgpu::Extent3d) -> Self { let slab = SlabAllocator::new(runtime, "materials", wgpu::BufferUsages::empty()); let atlas = Atlas::new(&slab, atlas_size, None, Some("materials-atlas"), None); - Self { slab, atlas } + let default_material = Material { + descriptor: slab.new_value(Default::default()), + albedo_texture: Default::default(), + metallic_roughness_texture: Default::default(), + normal_mapping_texture: Default::default(), + ao_texture: Default::default(), + emissive_texture: Default::default(), + }; + Self { + slab, + atlas, + default_material, + } } + /// Returns a reference to the WgpuRuntime. pub fn runtime(&self) -> &WgpuRuntime { self.as_ref() } + /// Returns a reference to the slab allocator. pub fn slab_allocator(&self) -> &SlabAllocator { &self.slab } + /// Returns a reference to the atlas. pub fn atlas(&self) -> &Atlas { &self.atlas } + /// Returns the default material. + pub fn default_material(&self) -> &Material { + &self.default_material + } + /// Runs atlas upkeep and commits all changes to the GPU. /// /// Returns `true` if the atlas texture was recreated. @@ -49,14 +83,428 @@ impl Materials { (self.atlas.upkeep(self.runtime()), self.slab.commit()) } - /// Create a new material. - // TODO: move `Material` to material - pub fn new_material(&self, material: Material) -> Hybrid { - self.slab.new_value(material) + /// Stage a new [`Material`] on the materials slab. + pub fn new_material(&self) -> Material { + let descriptor = self.slab.new_value(MaterialDescriptor::default()); + Material { + descriptor, + albedo_texture: Default::default(), + metallic_roughness_texture: Default::default(), + normal_mapping_texture: Default::default(), + ao_texture: Default::default(), + emissive_texture: Default::default(), + } + } +} + +/// A material staged on the GPU. +/// +/// Internally a `Material` holds references to: +/// * its descriptor, [`MaterialDescriptor`], which lives on the GPU +/// * [`AtlasTexture`]s that determine how the material presents: +/// * albedo color +/// * metallic roughness +/// * normal mapping +/// * ambient occlusion +/// * emissive +/// +/// ## Note +/// +/// Clones of `Material` all point to the same underlying data. +/// Changing a value on one `Material` will change that value for all clones as well. +#[derive(Clone)] +pub struct Material { + descriptor: Hybrid, + + albedo_texture: Arc>>, + metallic_roughness_texture: Arc>>, + normal_mapping_texture: Arc>>, + ao_texture: Arc>>, + emissive_texture: Arc>>, +} + +impl From<&Material> for Material { + fn from(value: &Material) -> Self { + value.clone() + } +} + +impl Material { + /// Returns the unique identifier of the material descriptor. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Returns a copy of the underlying descriptor. + pub fn descriptor(&self) -> MaterialDescriptor { + self.descriptor.get() } - /// Create an array of materials, stored contiguously. - pub fn new_materials(&self, data: impl IntoIterator) -> HybridArray { - self.slab.new_array(data) + /// Sets the emissive factor of the material. + /// + /// # Arguments + /// + /// * `param` - The emissive factor as a `Vec3`. + pub fn set_emissive_factor(&self, param: Vec3) -> &Self { + self.descriptor.modify(|d| d.emissive_factor = param); + self + } + /// Sets the emissive factor. + /// + /// # Arguments + /// + /// * `param` - The emissive factor as a `Vec3`. + pub fn with_emissive_factor(self, param: Vec3) -> Self { + self.set_emissive_factor(param); + self + } + /// Sets the emissive strength multiplier of the material. + /// + /// # Arguments + /// + /// * `param` - The emissive strength multiplier as a `f32`. + pub fn set_emissive_strength_multiplier(&self, param: f32) -> &Self { + self.descriptor + .modify(|d| d.emissive_strength_multiplier = param); + self + } + /// Sets the emissive strength multiplier. + /// + /// # Arguments + /// + /// * `param` - The emissive strength multiplier as a `f32`. + pub fn with_emissive_strength_multiplier(self, param: f32) -> Self { + self.set_emissive_strength_multiplier(param); + self + } + /// Sets the albedo factor of the material. + /// + /// # Arguments + /// + /// * `param` - The albedo factor as a `Vec4`. + pub fn set_albedo_factor(&self, param: Vec4) -> &Self { + self.descriptor.modify(|d| d.albedo_factor = param); + self + } + /// Sets the albedo factor. + /// + /// # Arguments + /// + /// * `param` - The albedo factor as a `Vec4`. + pub fn with_albedo_factor(self, param: Vec4) -> Self { + self.set_albedo_factor(param); + self + } + /// Sets the metallic factor of the material. + /// + /// # Arguments + /// + /// * `param` - The metallic factor as a `f32`. + pub fn set_metallic_factor(&self, param: f32) -> &Self { + self.descriptor.modify(|d| d.metallic_factor = param); + self + } + /// Sets the metallic factor. + /// + /// # Arguments + /// + /// * `param` - The metallic factor as a `f32`. + pub fn with_metallic_factor(self, param: f32) -> Self { + self.set_metallic_factor(param); + self + } + /// Sets the roughness factor of the material. + /// + /// # Arguments + /// + /// * `param` - The roughness factor as a `f32`. + pub fn set_roughness_factor(&self, param: f32) -> &Self { + self.descriptor.modify(|d| d.roughness_factor = param); + self + } + /// Sets the roughness factor. + /// + /// # Arguments + /// + /// * `param` - The roughness factor as a `f32`. + pub fn with_roughness_factor(self, param: f32) -> Self { + self.set_roughness_factor(param); + self + } + /// Sets the albedo texture coordinate of the material. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn set_albedo_tex_coord(&self, param: u32) -> &Self { + self.descriptor.modify(|d| d.albedo_tex_coord = param); + self + } + /// Sets the albedo texture coordinate. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn with_albedo_tex_coord(self, param: u32) -> Self { + self.set_albedo_tex_coord(param); + self + } + /// Sets the metallic roughness texture coordinate of the material. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn set_metallic_roughness_tex_coord(&self, param: u32) -> &Self { + self.descriptor + .modify(|d| d.metallic_roughness_tex_coord = param); + self + } + /// Sets the metallic roughness texture coordinate. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn with_metallic_roughness_tex_coord(self, param: u32) -> Self { + self.set_metallic_roughness_tex_coord(param); + self + } + /// Sets the normal texture coordinate of the material. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn set_normal_tex_coord(&self, param: u32) -> &Self { + self.descriptor.modify(|d| d.normal_tex_coord = param); + self + } + /// Sets the normal texture coordinate. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn with_normal_tex_coord(self, param: u32) -> Self { + self.set_normal_tex_coord(param); + self + } + /// Sets the ambient occlusion texture coordinate of the material. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn set_ambient_occlusion_tex_coord(&self, param: u32) -> &Self { + self.descriptor.modify(|d| d.ao_tex_coord = param); + self + } + /// Sets the ambient occlusion texture coordinate. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn with_ambient_occlusion_tex_coord(self, param: u32) -> Self { + self.set_ambient_occlusion_tex_coord(param); + self + } + /// Sets the emissive texture coordinate of the material. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn set_emissive_tex_coord(&self, param: u32) -> &Self { + self.descriptor.modify(|d| d.emissive_tex_coord = param); + self + } + /// Sets the emissive texture coordinate. + /// + /// # Arguments + /// + /// * `param` - The texture coordinate as a `u32`. + pub fn with_emissive_tex_coord(self, param: u32) -> Self { + self.set_emissive_tex_coord(param); + self + } + /// Sets whether the material has lighting. + /// + /// # Arguments + /// + /// * `param` - A boolean indicating if the material has lighting. + pub fn set_has_lighting(&self, param: bool) -> &Self { + self.descriptor.modify(|d| d.has_lighting = param); + self + } + /// Sets whether the material has lighting. + /// + /// # Arguments + /// + /// * `param` - A boolean indicating if the material has lighting. + pub fn with_has_lighting(self, param: bool) -> Self { + self.set_has_lighting(param); + self + } + /// Sets the ambient occlusion strength of the material. + /// + /// # Arguments + /// + /// * `param` - The ambient occlusion strength as a `f32`. + pub fn set_ambient_occlusion_strength(&self, param: f32) -> &Self { + self.descriptor.modify(|d| d.ao_strength = param); + self + } + /// Sets the ambient occlusion strength. + /// + /// # Arguments + /// + /// * `param` - The ambient occlusion strength as a `f32`. + pub fn with_ambient_occlusion_strength(self, param: f32) -> Self { + self.set_ambient_occlusion_strength(param); + self + } + + /// Remove the albedo texture. + /// + /// This causes any `[Primitive]` that references this material to fall back to + /// using the albedo factor for color. + pub fn remove_albedo_texture(&self) { + self.descriptor.modify(|d| d.albedo_texture_id = Id::NONE); + self.albedo_texture.lock().unwrap().take(); + } + + /// Sets the albedo color texture. + pub fn set_albedo_texture(&self, texture: &AtlasTexture) -> &Self { + self.descriptor + .modify(|d| d.albedo_texture_id = texture.id()); + *self.albedo_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Replace the albedo texture. + pub fn with_albedo_texture(self, texture: &AtlasTexture) -> Self { + self.descriptor + .modify(|d| d.albedo_texture_id = texture.id()); + *self.albedo_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Remove the metallic roughness texture. + /// + /// This causes any `[Renderlet]` that references this material to fall back to + /// using the metallic and roughness factors for appearance. + pub fn remove_metallic_roughness_texture(&self) { + self.descriptor + .modify(|d| d.metallic_roughness_texture_id = Id::NONE); + self.metallic_roughness_texture.lock().unwrap().take(); + } + + /// Sets the metallic roughness texture of the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the metallic roughness `AtlasTexture`. + pub fn set_metallic_roughness_texture(&self, texture: &AtlasTexture) -> &Self { + self.descriptor + .modify(|d| d.metallic_roughness_texture_id = texture.id()); + *self.metallic_roughness_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Sets the metallic roughness texture and returns the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the metallic roughness `AtlasTexture`. + pub fn with_metallic_roughness_texture(self, texture: &AtlasTexture) -> Self { + self.set_metallic_roughness_texture(texture); + self + } + + /// Remove the normal texture. + /// + /// This causes any `[Renderlet]` that references this material to fall back to + /// using the default normal mapping. + pub fn remove_normal_texture(&self) { + self.descriptor.modify(|d| d.normal_texture_id = Id::NONE); + self.normal_mapping_texture.lock().unwrap().take(); + } + + /// Sets the normal texture of the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the normal `AtlasTexture`. + pub fn set_normal_texture(&self, texture: &AtlasTexture) -> &Self { + self.descriptor + .modify(|d| d.normal_texture_id = texture.id()); + *self.normal_mapping_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Sets the normal texture and returns the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the normal `AtlasTexture`. + pub fn with_normal_texture(self, texture: &AtlasTexture) -> Self { + self.set_normal_texture(texture); + self + } + + /// Remove the ambient occlusion texture. + /// + /// This causes any `[Renderlet]` that references this material to fall back to + /// using the default ambient occlusion. + pub fn remove_ambient_occlusion_texture(&self) { + self.descriptor.modify(|d| d.ao_texture_id = Id::NONE); + self.ao_texture.lock().unwrap().take(); + } + + /// Sets the ambient occlusion texture of the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the ambient occlusion `AtlasTexture`. + pub fn set_ambient_occlusion_texture(&self, texture: &AtlasTexture) -> &Self { + self.descriptor.modify(|d| d.ao_texture_id = texture.id()); + *self.ao_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Sets the ambient occlusion texture and returns the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the ambient occlusion `AtlasTexture`. + pub fn with_ambient_occlusion_texture(self, texture: &AtlasTexture) -> Self { + self.set_ambient_occlusion_texture(texture); + self + } + + /// Remove the emissive texture. + /// + /// This causes any `[Renderlet]` that references this material to fall back to + /// using the emissive factor for appearance. + pub fn remove_emissive_texture(&self) { + self.descriptor.modify(|d| d.emissive_texture_id = Id::NONE); + self.emissive_texture.lock().unwrap().take(); + } + + /// Sets the emissive texture of the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the emissive `AtlasTexture`. + pub fn set_emissive_texture(&self, texture: &AtlasTexture) -> &Self { + self.descriptor + .modify(|d| d.emissive_texture_id = texture.id()); + *self.emissive_texture.lock().unwrap() = Some(texture.clone()); + self + } + + /// Sets the emissive texture and returns the material. + /// + /// # Arguments + /// + /// * `texture` - A reference to the emissive `AtlasTexture`. + pub fn with_emissive_texture(self, texture: &AtlasTexture) -> Self { + self.set_emissive_texture(texture); + self } } diff --git a/crates/renderling/src/math.rs b/crates/renderling/src/math.rs index 25029b75..c6ad4246 100644 --- a/crates/renderling/src/math.rs +++ b/crates/renderling/src/math.rs @@ -1,11 +1,10 @@ //! Mathematical helper types and functions. //! -//! Primarily this module re-exports types from `glam`. It also adds -//! some traits to help using `glam` types on the GPU without panicking, -//! as well as a few traits to aid in writing generic shader code that can be -//! run on the CPU. +//! Primarily this module adds some traits to help using `glam` types on the GPU +//! without panicking, as well as a few traits to aid in writing generic shader +//! code that can be run on the CPU. //! -//! Lastly, it provides some constant geometry used in many shaders. +//! Lastly, it provides some common geometry and constants used in many shaders. use core::ops::Mul; use spirv_std::{ image::{sample_with, Cubemap, Image2d, Image2dArray, ImageWithMethods}, @@ -690,8 +689,8 @@ pub const fn convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]: [Vec3; 8]) -> [Vec3; /// An PCG PRNG that is optimized for GPUs, in that it is fast to evaluate and accepts /// sequential ids as it's initial state without sacrificing on RNG quality. /// -/// https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/ -/// https://jcgt.org/published/0009/03/02/ +/// * +/// * /// /// Thanks to Firestar99 at /// diff --git a/crates/renderling/src/pbr.rs b/crates/renderling/src/pbr.rs index 26d6d038..5c3b3543 100644 --- a/crates/renderling/src/pbr.rs +++ b/crates/renderling/src/pbr.rs @@ -4,734 +4,31 @@ //! * //! * //! * -use crabslab::{Id, Slab, SlabItem}; -use glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; - -#[allow(unused)] -use spirv_std::num_traits::{Float, Zero}; - -use crate::{ - atlas::AtlasTexture, - geometry::GeometryDescriptor, - light::{ - DirectionalLightDescriptor, LightStyle, LightingDescriptor, PointLightDescriptor, - ShadowCalculation, SpotLightCalculation, - }, - math::{self, IsSampler, IsVector, Sample2d, Sample2dArray, SampleCube}, - println as my_println, - stage::Renderlet, -}; +pub mod brdf; pub mod debug; -use debug::DebugChannel; - -/// Represents a material on the GPU. -#[repr(C)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, SlabItem)] -pub struct Material { - pub emissive_factor: Vec3, - pub emissive_strength_multiplier: f32, - pub albedo_factor: Vec4, - pub metallic_factor: f32, - pub roughness_factor: f32, - - pub albedo_texture_id: Id, - pub metallic_roughness_texture_id: Id, - pub normal_texture_id: Id, - pub ao_texture_id: Id, - pub emissive_texture_id: Id, - - pub albedo_tex_coord: u32, - pub metallic_roughness_tex_coord: u32, - pub normal_tex_coord: u32, - pub ao_tex_coord: u32, - pub emissive_tex_coord: u32, - - pub has_lighting: bool, - pub ao_strength: f32, -} - -impl Default for Material { - fn default() -> Self { - Self { - emissive_factor: Vec3::ZERO, - emissive_strength_multiplier: 1.0, - albedo_factor: Vec4::ONE, - metallic_factor: 1.0, - roughness_factor: 1.0, - albedo_texture_id: Id::NONE, - metallic_roughness_texture_id: Id::NONE, - normal_texture_id: Id::NONE, - ao_texture_id: Id::NONE, - albedo_tex_coord: 0, - metallic_roughness_tex_coord: 0, - normal_tex_coord: 0, - ao_tex_coord: 0, - has_lighting: true, - ao_strength: 0.0, - emissive_texture_id: Id::NONE, - emissive_tex_coord: 0, - } - } -} - -/// Trowbridge-Reitz GGX normal distribution function (NDF). -/// -/// The normal distribution function D statistically approximates the relative -/// surface area of microfacets exactly aligned to the (halfway) vector h. -pub fn normal_distribution_ggx(n: Vec3, h: Vec3, roughness: f32) -> f32 { - let a = roughness * roughness; - let a2 = a * a; - let ndot_h = n.dot(h).max(0.0); - let ndot_h2 = ndot_h * ndot_h; - - let num = a2; - let denom = (ndot_h2 * (a2 - 1.0) + 1.0).powf(2.0) * core::f32::consts::PI; - - num / denom -} - -fn geometry_schlick_ggx(ndot_v: f32, roughness: f32) -> f32 { - let r = roughness + 1.0; - let k = (r * r) / 8.0; - let num = ndot_v; - let denom = ndot_v * (1.0 - k) + k; - - num / denom -} - -/// The geometry function statistically approximates the relative surface area -/// where its micro surface-details overshadow each other, causing light rays to -/// be occluded. -fn geometry_smith(n: Vec3, v: Vec3, l: Vec3, roughness: f32) -> f32 { - let ndot_v = n.dot(v).max(0.0); - let ndot_l = n.dot(l).max(0.0); - let ggx1 = geometry_schlick_ggx(ndot_v, roughness); - let ggx2 = geometry_schlick_ggx(ndot_l, roughness); - - ggx1 * ggx2 -} - -/// Fresnel-Schlick approximation function. -/// -/// The Fresnel equation describes the ratio of light that gets reflected over -/// the light that gets refracted, which varies over the angle we're looking at -/// a surface. The moment light hits a surface, based on the surface-to-view -/// angle, the Fresnel equation tells us the percentage of light that gets -/// reflected. From this ratio of reflection and the energy conservation -/// principle we can directly obtain the refracted portion of light. -fn fresnel_schlick( - // dot product result between the surface's normal n and the halfway h (or view v) direction. - cos_theta: f32, - // surface reflection at zero incidence (how much the surface reflects if looking directly at - // the surface) - f0: Vec3, -) -> Vec3 { - f0 + (1.0 - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0) -} - -fn fresnel_schlick_roughness(cos_theta: f32, f0: Vec3, roughness: f32) -> Vec3 { - f0 + (Vec3::splat(1.0 - roughness).max(f0) - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0) -} - -#[allow(clippy::too_many_arguments)] -pub fn outgoing_radiance( - light_color: Vec4, - albedo: Vec3, - attenuation: f32, - v: Vec3, - l: Vec3, - n: Vec3, - metalness: f32, - roughness: f32, -) -> Vec3 { - my_println!("outgoing_radiance"); - my_println!(" light_color: {light_color:?}"); - my_println!(" albedo: {albedo:?}"); - my_println!(" attenuation: {attenuation:?}"); - my_println!(" v: {v:?}"); - my_println!(" l: {l:?}"); - my_println!(" n: {n:?}"); - my_println!(" metalness: {metalness:?}"); - my_println!(" roughness: {roughness:?}"); - - let f0 = Vec3::splat(0.4).lerp(albedo, metalness); - my_println!(" f0: {f0:?}"); - let radiance = light_color.xyz() * attenuation; - my_println!(" radiance: {radiance:?}"); - let h = (v + l).alt_norm_or_zero(); - my_println!(" h: {h:?}"); - // cook-torrance brdf - let ndf: f32 = normal_distribution_ggx(n, h, roughness); - my_println!(" ndf: {ndf:?}"); - let g: f32 = geometry_smith(n, v, l, roughness); - my_println!(" g: {g:?}"); - let f: Vec3 = fresnel_schlick(h.dot(v).max(0.0), f0); - my_println!(" f: {f:?}"); +pub mod ibl; - let k_s = f; - let k_d = (Vec3::splat(1.0) - k_s) * (1.0 - metalness); - my_println!(" k_s: {k_s:?}"); - - let numerator: Vec3 = ndf * g * f; - my_println!(" numerator: {numerator:?}"); - let n_dot_l = n.dot(l).max(0.0); - my_println!(" n_dot_l: {n_dot_l:?}"); - let denominator: f32 = 4.0 * n.dot(v).max(0.0) * n_dot_l + 0.0001; - my_println!(" denominator: {denominator:?}"); - let specular: Vec3 = numerator / denominator; - my_println!(" specular: {specular:?}"); - - (k_d * albedo / core::f32::consts::PI + specular) * radiance * n_dot_l -} - -pub fn sample_irradiance, S: IsSampler>( - irradiance: &T, - irradiance_sampler: &S, - // Normal vector - n: Vec3, -) -> Vec3 { - irradiance.sample_by_lod(*irradiance_sampler, n, 0.0).xyz() -} - -pub fn sample_specular_reflection, S: IsSampler>( - prefiltered: &T, - prefiltered_sampler: &S, - // camera position in world space - camera_pos: Vec3, - // fragment position in world space - in_pos: Vec3, - // normal vector - n: Vec3, - roughness: f32, -) -> Vec3 { - let v = (camera_pos - in_pos).alt_norm_or_zero(); - let reflect_dir = math::reflect(-v, n); - prefiltered - .sample_by_lod(*prefiltered_sampler, reflect_dir, roughness * 4.0) - .xyz() -} - -pub fn sample_brdf, S: IsSampler>( - brdf: &T, - brdf_sampler: &S, - // camera position in world space - camera_pos: Vec3, - // fragment position in world space - in_pos: Vec3, - // normal vector - n: Vec3, - roughness: f32, -) -> Vec2 { - let v = (camera_pos - in_pos).alt_norm_or_zero(); - brdf.sample_by_lod(*brdf_sampler, Vec2::new(n.dot(v).max(0.0), roughness), 0.0) - .xy() -} - -/// Returns the `Material` from the stage's slab. -pub fn get_material( - material_id: Id, - has_lighting: bool, - material_slab: &[u32], -) -> Material { - if material_id.is_none() { - // without an explicit material (or if the entire render has no lighting) - // the entity will not participate in any lighting calculations - Material { - has_lighting: false, - ..Default::default() - } - } else { - let mut material = material_slab.read_unchecked(material_id); - material.has_lighting &= has_lighting; - material - } -} - -pub fn texture_color, S: IsSampler>( - texture_id: Id, - uv: Vec2, - atlas: &A, - sampler: &S, - atlas_size: glam::UVec2, - material_slab: &[u32], -) -> Vec4 { - let texture = material_slab.read(texture_id); - // uv is [0, 0] when texture_id is Id::NONE - let uv = texture.uv(uv, atlas_size); - crate::println!("uv: {uv}"); - let mut color: Vec4 = atlas.sample_by_lod(*sampler, uv, 0.0); - if texture_id.is_none() { - color = Vec4::splat(1.0); - } - color -} - -/// PBR fragment shader capable of being run on CPU or GPU. -#[allow(clippy::too_many_arguments)] -pub fn fragment_impl( - atlas: &A, - atlas_sampler: &S, - irradiance: &C, - irradiance_sampler: &S, - prefiltered: &C, - prefiltered_sampler: &S, - brdf: &T, - brdf_sampler: &S, - shadow_map: &DtA, - shadow_map_sampler: &S, - - geometry_slab: &[u32], - material_slab: &[u32], - lighting_slab: &[u32], - - renderlet_id: Id, - - frag_coord: Vec4, - in_color: Vec4, - in_uv0: Vec2, - in_uv1: Vec2, - in_norm: Vec3, - in_tangent: Vec3, - in_bitangent: Vec3, - in_pos: Vec3, - - output: &mut Vec4, -) where - A: Sample2dArray, - T: Sample2d, - DtA: Sample2dArray, - C: SampleCube, - S: IsSampler, -{ - let renderlet = geometry_slab.read_unchecked(renderlet_id); - let geom_desc = geometry_slab.read_unchecked(renderlet.geometry_descriptor_id); - crate::println!("pbr_desc_id: {:?}", renderlet.geometry_descriptor_id); - crate::println!("pbr_desc: {geom_desc:#?}"); - let GeometryDescriptor { - camera_id, - atlas_size, - resolution: _, - debug_channel, - has_lighting, - has_skinning: _, - perform_frustum_culling: _, - perform_occlusion_culling: _, - } = geom_desc; - - let material = get_material(renderlet.material_id, has_lighting, material_slab); - crate::println!("material: {:#?}", material); - - let albedo_tex_uv = if material.albedo_tex_coord == 0 { - in_uv0 - } else { - in_uv1 - }; - let albedo_tex_color = texture_color( - material.albedo_texture_id, - albedo_tex_uv, - atlas, - atlas_sampler, - atlas_size, - material_slab, - ); - my_println!("albedo_tex_color: {:?}", albedo_tex_color); - - let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 { - in_uv0 - } else { - in_uv1 - }; - let metallic_roughness_tex_color = texture_color( - material.metallic_roughness_texture_id, - metallic_roughness_uv, - atlas, - atlas_sampler, - atlas_size, - material_slab, - ); - my_println!( - "metallic_roughness_tex_color: {:?}", - metallic_roughness_tex_color - ); - - let normal_tex_uv = if material.normal_tex_coord == 0 { - in_uv0 - } else { - in_uv1 - }; - let normal_tex_color = texture_color( - material.normal_texture_id, - normal_tex_uv, - atlas, - atlas_sampler, - atlas_size, - material_slab, - ); - my_println!("normal_tex_color: {:?}", normal_tex_color); - - let ao_tex_uv = if material.ao_tex_coord == 0 { - in_uv0 - } else { - in_uv1 - }; - let ao_tex_color = texture_color( - material.ao_texture_id, - ao_tex_uv, - atlas, - atlas_sampler, - atlas_size, - material_slab, - ); - - let emissive_tex_uv = if material.emissive_tex_coord == 0 { - in_uv0 - } else { - in_uv1 - }; - let emissive_tex_color = texture_color( - material.emissive_texture_id, - emissive_tex_uv, - atlas, - atlas_sampler, - atlas_size, - material_slab, - ); - - let (norm, uv_norm) = if material.normal_texture_id.is_none() { - // there is no normal map, use the normal normal ;) - (in_norm, Vec3::ZERO) - } else { - // convert the normal from color coords to tangent space -1,1 - let sampled_norm = (normal_tex_color.xyz() * 2.0 - Vec3::splat(1.0)).alt_norm_or_zero(); - let tbn = glam::mat3( - in_tangent.alt_norm_or_zero(), - in_bitangent.alt_norm_or_zero(), - in_norm.alt_norm_or_zero(), - ); - // convert the normal from tangent space to world space - let norm = (tbn * sampled_norm).alt_norm_or_zero(); - (norm, sampled_norm) - }; - - let n = norm; - let albedo = albedo_tex_color * material.albedo_factor * in_color; - let roughness = metallic_roughness_tex_color.y * material.roughness_factor; - let metallic = metallic_roughness_tex_color.z * material.metallic_factor; - let ao = 1.0 + material.ao_strength * (ao_tex_color.x - 1.0); - let emissive = - emissive_tex_color.xyz() * material.emissive_factor * material.emissive_strength_multiplier; - let irradiance = sample_irradiance(irradiance, irradiance_sampler, n); - let camera = geometry_slab.read(camera_id); - let specular = sample_specular_reflection( - prefiltered, - prefiltered_sampler, - camera.position(), - in_pos, - n, - roughness, - ); - let brdf = sample_brdf(brdf, brdf_sampler, camera.position(), in_pos, n, roughness); - - fn colorize(u: Vec3) -> Vec4 { - ((u.alt_norm_or_zero() + Vec3::splat(1.0)) / 2.0).extend(1.0) - } - - crate::println!("debug_mode: {debug_channel:?}"); - match debug_channel { - DebugChannel::None => {} - DebugChannel::UvCoords0 => { - *output = colorize(Vec3::new(in_uv0.x, in_uv0.y, 0.0)); - return; - } - DebugChannel::UvCoords1 => { - *output = colorize(Vec3::new(in_uv1.x, in_uv1.y, 0.0)); - return; - } - DebugChannel::Normals => { - *output = colorize(norm); - return; - } - DebugChannel::VertexColor => { - *output = in_color; - return; - } - DebugChannel::VertexNormals => { - *output = colorize(in_norm); - return; - } - DebugChannel::UvNormals => { - *output = colorize(uv_norm); - return; - } - DebugChannel::Tangents => { - *output = colorize(in_tangent); - return; - } - DebugChannel::Bitangents => { - *output = colorize(in_bitangent); - return; - } - DebugChannel::DiffuseIrradiance => { - *output = irradiance.extend(1.0); - return; - } - DebugChannel::SpecularReflection => { - *output = specular.extend(1.0); - return; - } - DebugChannel::Brdf => { - *output = brdf.extend(1.0).extend(1.0); - return; - } - DebugChannel::Roughness => { - *output = Vec3::splat(roughness).extend(1.0); - return; - } - DebugChannel::Metallic => { - *output = Vec3::splat(metallic).extend(1.0); - return; - } - DebugChannel::Albedo => { - *output = albedo; - return; - } - DebugChannel::Occlusion => { - *output = Vec3::splat(ao).extend(1.0); - return; - } - DebugChannel::Emissive => { - *output = emissive.extend(1.0); - return; - } - DebugChannel::UvEmissive => { - *output = emissive_tex_color.xyz().extend(1.0); - return; - } - DebugChannel::EmissiveFactor => { - *output = material.emissive_factor.extend(1.0); - return; - } - DebugChannel::EmissiveStrength => { - *output = Vec3::splat(material.emissive_strength_multiplier).extend(1.0); - return; - } - } - - *output = if material.has_lighting { - shade_fragment( - shadow_map, - shadow_map_sampler, - camera.position(), - n, - in_pos, - albedo.xyz(), - metallic, - roughness, - ao, - emissive, - irradiance, - specular, - brdf, - lighting_slab, - frag_coord, - ) - } else { - crate::println!("no shading!"); - in_color * albedo_tex_color * material.albedo_factor - }; -} - -#[allow(clippy::too_many_arguments)] -pub fn shade_fragment( - shadow_map: &T, - shadow_map_sampler: &S, - // camera's position in world space - camera_pos: Vec3, - // normal of the fragment - in_norm: Vec3, - // position of the fragment in world space - in_pos: Vec3, - // base color of the fragment - albedo: Vec3, - metallic: f32, - roughness: f32, - ao: f32, - emissive: Vec3, - irradiance: Vec3, - prefiltered: Vec3, - brdf: Vec2, - - light_slab: &[u32], - frag_coord: Vec4, -) -> Vec4 -where - S: IsSampler, - T: Sample2dArray, -{ - let n = in_norm.alt_norm_or_zero(); - let v = (camera_pos - in_pos).alt_norm_or_zero(); - // There is always a `LightingDescriptor` stored at index `0` of the - // light slab. - let lighting_desc = light_slab.read_unchecked(Id::::new(0)); - // If light tiling is enabled, use the pre-computed tile's light list - let analytical_lights_array = if lighting_desc.light_tiling_descriptor_id.is_none() { - lighting_desc.analytical_lights_array - } else { - let tiling_descriptor = light_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id); - let tile_index = tiling_descriptor.tile_index_for_fragment(frag_coord.xy()); - let tile = light_slab.read_unchecked(tiling_descriptor.tiles_array.at(tile_index)); - tile.lights_array - }; - my_println!("lights: {analytical_lights_array:?}"); - my_println!("surface normal: {n:?}"); - my_println!("vector from surface to camera: {v:?}"); - - // accumulated outgoing radiance - let mut lo = Vec3::ZERO; - for light_id_id in analytical_lights_array.iter() { - // calculate per-light radiance - let light_id = light_slab.read(light_id_id); - if light_id.is_none() { - break; - } - let light = light_slab.read(light_id); - let transform = light_slab.read(light.transform_id); - crate::println!("transform: {transform:?}"); - let transform = Mat4::from(transform); - - // determine the light ray and the radiance - let (radiance, shadow) = match light.light_type { - LightStyle::Point => { - let PointLightDescriptor { - position, - color, - intensity, - } = light_slab.read(light.into_point_id()); - let position = transform.transform_point3(position); - // This definitely is the direction pointing from fragment to the light. - // It needs to stay this way. - // For more info, see - // - let frag_to_light = position - in_pos; - let distance = frag_to_light.length(); - if distance == 0.0 { - crate::println!("distance between point light and surface is zero"); - continue; - } - let l = frag_to_light.alt_norm_or_zero(); - let attenuation = intensity / (distance * distance); - let radiance = - outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness); - let shadow = if light.shadow_map_desc_id.is_some() { - // Shadow is 1.0 when the fragment is in the shadow of this light, - // and 0.0 in darkness - ShadowCalculation::new(light_slab, light, in_pos, n, l).run_point( - light_slab, - shadow_map, - shadow_map_sampler, - position, - ) - } else { - 0.0 - }; - (radiance, shadow) - } - - LightStyle::Spot => { - let spot_light_descriptor = light_slab.read(light.into_spot_id()); - let calculation = - SpotLightCalculation::new(spot_light_descriptor, transform, in_pos); - crate::println!("calculation: {calculation:#?}"); - if calculation.frag_to_light_distance == 0.0 { - continue; - } - let attenuation: f32 = spot_light_descriptor.intensity * calculation.contribution; - let radiance = outgoing_radiance( - spot_light_descriptor.color, - albedo, - attenuation, - v, - calculation.frag_to_light, - n, - metallic, - roughness, - ); - let shadow = if light.shadow_map_desc_id.is_some() { - // Shadow is 1.0 when the fragment is in the shadow of this light, - // and 0.0 in darkness - ShadowCalculation::new(light_slab, light, in_pos, n, calculation.frag_to_light) - .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler) - } else { - 0.0 - }; - (radiance, shadow) - } - - LightStyle::Directional => { - let DirectionalLightDescriptor { - direction, - color, - intensity, - } = light_slab.read(light.into_directional_id()); - let direction = transform.transform_vector3(direction); - let l = -direction.alt_norm_or_zero(); - let attenuation = intensity; - let radiance = - outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness); - let shadow = - if light.shadow_map_desc_id.is_some() { - // Shadow is 1.0 when the fragment is in the shadow of this light, - // and 0.0 in darkness - ShadowCalculation::new(light_slab, light, in_pos, n, l) - .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler) - } else { - 0.0 - }; - (radiance, shadow) - } - }; - crate::println!("radiance: {radiance}"); - crate::println!("shadow: {shadow}"); - lo += radiance * (1.0 - shadow); - } - - my_println!("lo: {lo:?}"); - // calculate reflectance at normal incidence; if dia-electric (like plastic) use - // F0 of 0.04 and if it's a metal, use the albedo color as F0 (metallic - // workflow) - let f0: Vec3 = Vec3::splat(0.04).lerp(albedo, metallic); - let cos_theta = n.dot(v).max(0.0); - let fresnel = fresnel_schlick_roughness(cos_theta, f0, roughness); - let ks = fresnel; - let kd = (1.0 - ks) * (1.0 - metallic); - let diffuse = irradiance * albedo; - let specular = prefiltered * (fresnel * brdf.x + brdf.y); - let color = (kd * diffuse + specular) * ao + lo + emissive; - color.extend(1.0) -} +pub mod shader; #[cfg(test)] mod test { use crate::{ atlas::AtlasImage, - camera::Camera, - pbr::Material, - prelude::glam::{Vec3, Vec4}, - stage::Vertex, + geometry::Vertex, + glam::{Vec3, Vec4}, test::BlockOnFuture, - transform::Transform, }; #[test] + // TODO: Move this over to a manual example // Tests the initial implementation of pbr metallic roughness on an array of // spheres with different metallic roughnesses lit by an environment map. // // see https://learnopengl.com/PBR/Lighting fn pbr_metallic_roughness_spheres() { let ss = 600; - let ctx = crate::Context::headless(ss, ss).block(); + let ctx = crate::context::Context::headless(ss, ss).block(); let stage = ctx.new_stage(); let radius = 0.5; @@ -747,7 +44,9 @@ mod test { Vec3::new(half, half, 0.0), Vec3::Y, ); - let _camera = stage.new_camera(Camera::new(projection, view)); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); let geometry = stage.new_vertices({ let mut icosphere = icosahedron::Polyhedron::new_isocahedron(radius, 5); @@ -784,27 +83,26 @@ mod test { let y = (diameter + spacing) * j as f32; let rez = stage - .builder() - .with_material(Material { - albedo_factor: Vec4::new(1.0, 1.0, 1.0, 1.0), - metallic_factor: metallic, - roughness_factor: roughness, - ..Default::default() - }) - .with_transform(Transform { - translation: Vec3::new(x, y, 0.0), - ..Default::default() - }) - .with_vertices_array(geometry.array()) - .build(); - + .new_primitive() + .with_material( + stage + .new_material() + .with_albedo_factor(Vec4::new(1.0, 1.0, 1.0, 1.0)) + .with_metallic_factor(metallic) + .with_roughness_factor(roughness), + ) + .with_transform(stage.new_transform().with_translation(Vec3::new(x, y, 0.0))) + .with_vertices(&geometry); spheres.push(rez); } } let hdr_image = AtlasImage::from_hdr_path("../../img/hdr/resting_place.hdr").unwrap(); let skybox = crate::skybox::Skybox::new(&ctx, hdr_image); - stage.set_skybox(skybox); + stage.use_skybox(&skybox); + + let ibl = stage.new_ibl(&skybox); + stage.use_ibl(&ibl); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); diff --git a/crates/renderling/src/pbr/brdf.rs b/crates/renderling/src/pbr/brdf.rs new file mode 100644 index 00000000..c51e8bdd --- /dev/null +++ b/crates/renderling/src/pbr/brdf.rs @@ -0,0 +1,10 @@ +//! BRDF computation. +//! +//! Helpers for computing (and holding onto) a Bidirectional Reflectance Distribution Function. + +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; + +pub mod shader; diff --git a/crates/renderling/src/pbr/brdf/cpu.rs b/crates/renderling/src/pbr/brdf/cpu.rs new file mode 100644 index 00000000..b041f1eb --- /dev/null +++ b/crates/renderling/src/pbr/brdf/cpu.rs @@ -0,0 +1,108 @@ +//! CPU side of BRDF stuff. +use craballoc::runtime::WgpuRuntime; + +use crate::texture; + +/// Pre-computed texture of the brdf integration. +#[derive(Clone)] +pub struct BrdfLut { + pub(crate) inner: texture::Texture, +} + +impl BrdfLut { + /// Create a new pre-computed BRDF look-up texture. + pub fn new(runtime: impl AsRef) -> Self { + let runtime = runtime.as_ref(); + let device = &runtime.device; + let queue = &runtime.queue; + let vertex_linkage = crate::linkage::brdf_lut_convolution_vertex::linkage(device); + let fragment_linkage = crate::linkage::brdf_lut_convolution_fragment::linkage(device); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("brdf_lut_convolution"), + layout: None, + vertex: wgpu::VertexState { + module: &vertex_linkage.module, + entry_point: Some(vertex_linkage.entry_point), + buffers: &[], + compilation_options: Default::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_linkage.module, + entry_point: Some(fragment_linkage.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rg16Float, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + multiview: None, + cache: None, + }); + + let framebuffer = texture::Texture::new_with( + runtime, + Some("brdf_lut"), + Some( + wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + ), + None, + wgpu::TextureFormat::Rg16Float, + 2, + 2, + 512, + 512, + 1, + &[], + ); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("brdf_lut_convolution"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &framebuffer.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::RED), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + + render_pass.set_pipeline(&pipeline); + render_pass.draw(0..6, 0..1); + } + queue.submit([encoder.finish()]); + + BrdfLut { inner: framebuffer } + } + + /// Return the underlying [`Texture`](crate::texture::Texture). + pub fn texture(&self) -> &texture::Texture { + &self.inner + } +} diff --git a/crates/renderling/src/pbr/brdf/shader.rs b/crates/renderling/src/pbr/brdf/shader.rs new file mode 100644 index 00000000..8249f674 --- /dev/null +++ b/crates/renderling/src/pbr/brdf/shader.rs @@ -0,0 +1,21 @@ +//! Shader side of BRDF stuff. + +use glam::{Vec2, Vec3, Vec4Swizzles}; + +use crate::math::{IsSampler, IsVector, Sample2d}; + +pub fn sample_brdf, S: IsSampler>( + brdf: &T, + brdf_sampler: &S, + // camera position in world space + camera_pos: Vec3, + // fragment position in world space + in_pos: Vec3, + // normal vector + n: Vec3, + roughness: f32, +) -> Vec2 { + let v = (camera_pos - in_pos).alt_norm_or_zero(); + brdf.sample_by_lod(*brdf_sampler, Vec2::new(n.dot(v).max(0.0), roughness), 0.0) + .xy() +} diff --git a/crates/renderling/src/pbr/ibl.rs b/crates/renderling/src/pbr/ibl.rs new file mode 100644 index 00000000..a082400d --- /dev/null +++ b/crates/renderling/src/pbr/ibl.rs @@ -0,0 +1,10 @@ +//! Image based lighting +//! +//! For more info on image based lighting, see . + +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; + +pub mod shader; diff --git a/crates/renderling/src/pbr/ibl/cpu.rs b/crates/renderling/src/pbr/ibl/cpu.rs new file mode 100644 index 00000000..c79e0002 --- /dev/null +++ b/crates/renderling/src/pbr/ibl/cpu.rs @@ -0,0 +1,546 @@ +//! CPU side of IBL + +use core::sync::atomic::AtomicBool; +use std::sync::Arc; + +use craballoc::{runtime::WgpuRuntime, slab::SlabAllocator, value::Hybrid}; +use crabslab::Id; +use glam::{Mat4, Vec3}; + +use crate::{ + camera::Camera, convolution::shader::VertexPrefilterEnvironmentCubemapIds, skybox::Skybox, + texture, +}; + +/// Image based lighting resources. +#[derive(Clone)] +pub struct Ibl { + is_empty: Arc, + // Cubemap texture of the pre-computed irradiance cubemap + pub(crate) irradiance_cubemap: texture::Texture, + // Cubemap texture and mip maps of the specular highlights, + // where each mip level is a different roughness. + pub(crate) prefiltered_environment_cubemap: texture::Texture, +} + +impl Ibl { + pub fn create_irradiance_and_prefilters( + runtime: impl AsRef, + skybox: &Skybox, + ) -> Self { + let runtime = runtime.as_ref(); + let slab = SlabAllocator::new(runtime, "ibl", wgpu::BufferUsages::VERTEX); + let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); + let camera = Camera::new(&slab).with_projection(proj); + let roughness = slab.new_value(0.0f32); + let prefilter_ids = slab.new_value(VertexPrefilterEnvironmentCubemapIds { + camera: camera.id(), + roughness: roughness.id(), + }); + + let buffer = slab.commit(); + let mut buffer_upkeep = || { + let possibly_new_buffer = slab.commit(); + debug_assert!(!possibly_new_buffer.is_new_this_commit()); + }; + + let views = [ + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(-1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + ]; + + let environment_cubemap = skybox.environment_cubemap_texture(); + + // Convolve the environment map. + let irradiance_cubemap = create_irradiance_map( + runtime, + &buffer, + &mut buffer_upkeep, + environment_cubemap, + &camera, + views, + ); + + // Generate specular IBL pre-filtered environment map. + let prefiltered_environment_cubemap = create_prefiltered_environment_map( + runtime, + &buffer, + &mut buffer_upkeep, + &camera, + &roughness, + prefilter_ids.id(), + environment_cubemap, + views, + ); + + Self { + is_empty: Arc::new(skybox.is_empty().into()), + irradiance_cubemap, + prefiltered_environment_cubemap, + } + } + /// Create a new [`Ibl`] resource. + pub fn new(runtime: impl AsRef, skybox: &Skybox) -> Self { + let runtime = runtime.as_ref(); + let slab = SlabAllocator::new(runtime, "ibl", wgpu::BufferUsages::VERTEX); + let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); + let camera = Camera::new(&slab).with_projection(proj); + let roughness = slab.new_value(0.0f32); + let prefilter_ids = slab.new_value(VertexPrefilterEnvironmentCubemapIds { + camera: camera.id(), + roughness: roughness.id(), + }); + + let buffer = slab.commit(); + let mut buffer_upkeep = || { + let possibly_new_buffer = slab.commit(); + debug_assert!(!possibly_new_buffer.is_new_this_commit()); + }; + + let views = [ + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(-1.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, -1.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + Mat4::look_at_rh( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(0.0, 0.0, -1.0), + Vec3::new(0.0, -1.0, 0.0), + ), + ]; + + let environment_cubemap = skybox.environment_cubemap_texture(); + + // Convolve the environment map. + let irradiance_cubemap = create_irradiance_map( + runtime, + &buffer, + &mut buffer_upkeep, + environment_cubemap, + &camera, + views, + ); + + // Generate specular IBL pre-filtered environment map. + let prefiltered_environment_cubemap = create_prefiltered_environment_map( + runtime, + &buffer, + &mut buffer_upkeep, + &camera, + &roughness, + prefilter_ids.id(), + environment_cubemap, + views, + ); + + Self { + is_empty: Arc::new(skybox.is_empty().into()), + irradiance_cubemap, + prefiltered_environment_cubemap, + } + } + + /// Returns whether this [`Ibl`] is empty. + /// + /// An [`Ibl`] is empty if it was created from an empty [`Skybox`]. + pub fn is_empty(&self) -> bool { + self.is_empty.load(std::sync::atomic::Ordering::Relaxed) + } +} + +fn create_irradiance_map( + runtime: impl AsRef, + buffer: &wgpu::Buffer, + buffer_upkeep: impl FnMut(), + environment_texture: &texture::Texture, + camera: &Camera, + views: [Mat4; 6], +) -> texture::Texture { + let runtime = runtime.as_ref(); + let device = &runtime.device; + let pipeline = crate::pbr::ibl::DiffuseIrradianceConvolutionRenderPipeline::new( + device, + wgpu::TextureFormat::Rgba16Float, + ); + + let bindgroup = crate::pbr::ibl::diffuse_irradiance_convolution_bindgroup( + device, + Some("irradiance"), + buffer, + environment_texture, + ); + + texture::Texture::render_cubemap( + runtime, + &pipeline.0, + buffer_upkeep, + camera, + &bindgroup, + views, + 32, + None, + ) +} + +/// Pipeline for creating a prefiltered environment map from an existing +/// environment cubemap. +pub(crate) fn create_prefiltered_environment_pipeline_and_bindgroup( + device: &wgpu::Device, + buffer: &wgpu::Buffer, + environment_texture: &crate::texture::Texture, +) -> (wgpu::RenderPipeline, wgpu::BindGroup) { + let label = Some("prefiltered environment"); + let bindgroup_layout_desc = wgpu::BindGroupLayoutDescriptor { + label, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }; + let bg_layout = device.create_bind_group_layout(&bindgroup_layout_desc); + let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &bg_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&environment_texture.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&environment_texture.sampler), + }, + ], + }); + let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label, + bind_group_layouts: &[&bg_layout], + push_constant_ranges: &[], + }); + let vertex_linkage = crate::linkage::prefilter_environment_cubemap_vertex::linkage(device); + let fragment_linkage = crate::linkage::prefilter_environment_cubemap_fragment::linkage(device); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("prefiltered environment"), + layout: Some(&pp_layout), + vertex: wgpu::VertexState { + module: &vertex_linkage.module, + entry_point: Some(vertex_linkage.entry_point), + buffers: &[], + compilation_options: Default::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_linkage.module, + entry_point: Some(fragment_linkage.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba16Float, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + multiview: None, + cache: None, + }); + (pipeline, bindgroup) +} + +#[allow(clippy::too_many_arguments)] +fn create_prefiltered_environment_map( + runtime: impl AsRef, + buffer: &wgpu::Buffer, + mut buffer_upkeep: impl FnMut(), + camera: &Camera, + roughness: &Hybrid, + prefilter_id: Id, + environment_texture: &texture::Texture, + views: [Mat4; 6], +) -> texture::Texture { + let (pipeline, bindgroup) = + crate::pbr::ibl::create_prefiltered_environment_pipeline_and_bindgroup( + &runtime.as_ref().device, + buffer, + environment_texture, + ); + let mut cubemap_faces = Vec::new(); + + for (i, view) in views.iter().enumerate() { + for mip_level in 0..5 { + let mip_width: u32 = 128 >> mip_level; + let mip_height: u32 = 128 >> mip_level; + + let mut encoder = + runtime + .as_ref() + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("specular convolution"), + }); + + let cubemap_face = texture::Texture::new_with( + runtime.as_ref(), + Some(&format!("cubemap{i}{mip_level}prefiltered_environment")), + Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC), + None, + wgpu::TextureFormat::Rgba16Float, + 4, + 2, + mip_width, + mip_height, + 1, + &[], + ); + + // update the roughness for these mips + roughness.set(mip_level as f32 / 4.0); + // update the view to point at one of the cube faces + camera.set_view(*view); + buffer_upkeep(); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&format!("cubemap{i}")), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &cubemap_face.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, Some(&bindgroup), &[]); + render_pass.draw(0..36, prefilter_id.inner()..prefilter_id.inner() + 1); + } + + runtime.as_ref().queue.submit([encoder.finish()]); + cubemap_faces.push(cubemap_face); + } + } + + texture::Texture::new_cubemap_texture( + runtime, + Some("prefiltered environment cubemap"), + 128, + cubemap_faces.as_slice(), + wgpu::TextureFormat::Rgba16Float, + 5, + ) +} + +pub fn diffuse_irradiance_convolution_bindgroup_layout( + device: &wgpu::Device, +) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("convolution bindgroup"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }) +} + +pub fn diffuse_irradiance_convolution_bindgroup( + device: &wgpu::Device, + label: Option<&str>, + buffer: &wgpu::Buffer, + // The texture to sample the environment from + texture: &crate::texture::Texture, +) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &diffuse_irradiance_convolution_bindgroup_layout(device), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&texture.view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&texture.sampler), + }, + ], + }) +} + +pub struct DiffuseIrradianceConvolutionRenderPipeline(pub wgpu::RenderPipeline); + +impl DiffuseIrradianceConvolutionRenderPipeline { + /// Create the rendering pipeline that performs a convolution. + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device); + let fragment_linkage = crate::linkage::di_convolution_fragment::linkage(device); + let bg_layout = diffuse_irradiance_convolution_bindgroup_layout(device); + let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("convolution pipeline layout"), + bind_group_layouts: &[&bg_layout], + push_constant_ranges: &[], + }); + + DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("convolution pipeline"), + layout: Some(&pp_layout), + vertex: wgpu::VertexState { + module: &vertex_linkage.module, + entry_point: Some(vertex_linkage.entry_point), + buffers: &[], + compilation_options: Default::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_linkage.module, + entry_point: Some(fragment_linkage.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + multiview: None, + cache: None, + }, + )) + } +} diff --git a/crates/renderling/src/pbr/ibl/diffuse_irradiance.rs b/crates/renderling/src/pbr/ibl/diffuse_irradiance.rs new file mode 100644 index 00000000..2fd2bc4a --- /dev/null +++ b/crates/renderling/src/pbr/ibl/diffuse_irradiance.rs @@ -0,0 +1,13 @@ +//! Diffuse irradiance convolution. + +use glam::{Vec3, Vec4, Vec4Swizzles}; +#[cfg(target_arch = "spirv")] +use spirv_std::num_traits::Float; +use spirv_std::{image::Cubemap, spirv, Sampler}; + +use crate::math::IsVector; + +#[cfg(not(target_arch = "spirv"))] +mod cpu; +#[cfg(not(target_arch = "spirv"))] +pub use cpu::*; diff --git a/crates/renderling/src/ibl/diffuse_irradiance.rs b/crates/renderling/src/pbr/ibl/shader.rs similarity index 91% rename from crates/renderling/src/ibl/diffuse_irradiance.rs rename to crates/renderling/src/pbr/ibl/shader.rs index df73f386..d9a8eab3 100644 --- a/crates/renderling/src/ibl/diffuse_irradiance.rs +++ b/crates/renderling/src/pbr/ibl/shader.rs @@ -1,17 +1,11 @@ -//! Diffuse irradiance convolution. - +//! Shader side of IBL use glam::{Vec3, Vec4, Vec4Swizzles}; -#[cfg(target_arch = "spirv")] +#[cfg(gpu)] use spirv_std::num_traits::Float; use spirv_std::{image::Cubemap, spirv, Sampler}; use crate::math::IsVector; -#[cfg(not(target_arch = "spirv"))] -mod cpu; -#[cfg(not(target_arch = "spirv"))] -pub use cpu::*; - /// Diffuse irradiance convolution. #[spirv(fragment)] pub fn di_convolution_fragment( diff --git a/crates/renderling/src/pbr/shader.rs b/crates/renderling/src/pbr/shader.rs new file mode 100644 index 00000000..bc033894 --- /dev/null +++ b/crates/renderling/src/pbr/shader.rs @@ -0,0 +1,643 @@ +//! Physically based shader code. +use crabslab::{Id, Slab}; +use glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; + +#[allow(unused)] +use spirv_std::num_traits::{Float, Zero}; + +use crate::{ + atlas::shader::AtlasTextureDescriptor, + geometry::shader::GeometryDescriptor, + light::shader::{ + DirectionalLightDescriptor, LightStyle, LightingDescriptor, PointLightDescriptor, + ShadowCalculation, SpotLightCalculation, + }, + material::shader::MaterialDescriptor, + math::{self, IsSampler, IsVector, Sample2d, Sample2dArray, SampleCube}, + primitive::shader::PrimitiveDescriptor, + println as my_println, +}; + +use super::{brdf, debug}; + +/// Trowbridge-Reitz GGX normal distribution function (NDF). +/// +/// The normal distribution function D statistically approximates the relative +/// surface area of microfacets exactly aligned to the (halfway) vector h. +pub fn normal_distribution_ggx(n: Vec3, h: Vec3, roughness: f32) -> f32 { + let a = roughness * roughness; + let a2 = a * a; + let ndot_h = n.dot(h).max(0.0); + let ndot_h2 = ndot_h * ndot_h; + + let num = a2; + let denom = (ndot_h2 * (a2 - 1.0) + 1.0).powf(2.0) * core::f32::consts::PI; + + num / denom +} + +fn geometry_schlick_ggx(ndot_v: f32, roughness: f32) -> f32 { + let r = roughness + 1.0; + let k = (r * r) / 8.0; + let num = ndot_v; + let denom = ndot_v * (1.0 - k) + k; + + num / denom +} + +/// The geometry function statistically approximates the relative surface area +/// where its micro surface-details overshadow each other, causing light rays to +/// be occluded. +fn geometry_smith(n: Vec3, v: Vec3, l: Vec3, roughness: f32) -> f32 { + let ndot_v = n.dot(v).max(0.0); + let ndot_l = n.dot(l).max(0.0); + let ggx1 = geometry_schlick_ggx(ndot_v, roughness); + let ggx2 = geometry_schlick_ggx(ndot_l, roughness); + + ggx1 * ggx2 +} + +/// Fresnel-Schlick approximation function. +/// +/// The Fresnel equation describes the ratio of light that gets reflected over +/// the light that gets refracted, which varies over the angle we're looking at +/// a surface. The moment light hits a surface, based on the surface-to-view +/// angle, the Fresnel equation tells us the percentage of light that gets +/// reflected. From this ratio of reflection and the energy conservation +/// principle we can directly obtain the refracted portion of light. +fn fresnel_schlick( + // dot product result between the surface's normal n and the halfway h (or view v) direction. + cos_theta: f32, + // surface reflection at zero incidence (how much the surface reflects if looking directly at + // the surface) + f0: Vec3, +) -> Vec3 { + f0 + (1.0 - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0) +} + +fn fresnel_schlick_roughness(cos_theta: f32, f0: Vec3, roughness: f32) -> Vec3 { + f0 + (Vec3::splat(1.0 - roughness).max(f0) - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0) +} + +#[allow(clippy::too_many_arguments)] +pub fn outgoing_radiance( + light_color: Vec4, + albedo: Vec3, + attenuation: f32, + v: Vec3, + l: Vec3, + n: Vec3, + metalness: f32, + roughness: f32, +) -> Vec3 { + my_println!("outgoing_radiance"); + my_println!(" light_color: {light_color:?}"); + my_println!(" albedo: {albedo:?}"); + my_println!(" attenuation: {attenuation:?}"); + my_println!(" v: {v:?}"); + my_println!(" l: {l:?}"); + my_println!(" n: {n:?}"); + my_println!(" metalness: {metalness:?}"); + my_println!(" roughness: {roughness:?}"); + + let f0 = Vec3::splat(0.4).lerp(albedo, metalness); + my_println!(" f0: {f0:?}"); + let radiance = light_color.xyz() * attenuation; + my_println!(" radiance: {radiance:?}"); + let h = (v + l).alt_norm_or_zero(); + my_println!(" h: {h:?}"); + // cook-torrance brdf + let ndf: f32 = normal_distribution_ggx(n, h, roughness); + my_println!(" ndf: {ndf:?}"); + let g: f32 = geometry_smith(n, v, l, roughness); + my_println!(" g: {g:?}"); + let f: Vec3 = fresnel_schlick(h.dot(v).max(0.0), f0); + my_println!(" f: {f:?}"); + + let k_s = f; + let k_d = (Vec3::splat(1.0) - k_s) * (1.0 - metalness); + my_println!(" k_s: {k_s:?}"); + + let numerator: Vec3 = ndf * g * f; + my_println!(" numerator: {numerator:?}"); + let n_dot_l = n.dot(l).max(0.0); + my_println!(" n_dot_l: {n_dot_l:?}"); + let denominator: f32 = 4.0 * n.dot(v).max(0.0) * n_dot_l + 0.0001; + my_println!(" denominator: {denominator:?}"); + let specular: Vec3 = numerator / denominator; + my_println!(" specular: {specular:?}"); + + (k_d * albedo / core::f32::consts::PI + specular) * radiance * n_dot_l +} + +pub fn sample_irradiance, S: IsSampler>( + irradiance: &T, + irradiance_sampler: &S, + // Normal vector + n: Vec3, +) -> Vec3 { + irradiance.sample_by_lod(*irradiance_sampler, n, 0.0).xyz() +} + +pub fn sample_specular_reflection, S: IsSampler>( + prefiltered: &T, + prefiltered_sampler: &S, + // camera position in world space + camera_pos: Vec3, + // fragment position in world space + in_pos: Vec3, + // normal vector + n: Vec3, + roughness: f32, +) -> Vec3 { + let v = (camera_pos - in_pos).alt_norm_or_zero(); + let reflect_dir = math::reflect(-v, n); + prefiltered + .sample_by_lod(*prefiltered_sampler, reflect_dir, roughness * 4.0) + .xyz() +} + +/// Returns the `Material` from the stage's slab. +pub fn get_material( + material_id: Id, + has_lighting: bool, + material_slab: &[u32], +) -> MaterialDescriptor { + if material_id.is_none() { + // without an explicit material (or if the entire render has no lighting) + // the entity will not participate in any lighting calculations + MaterialDescriptor { + has_lighting: false, + ..Default::default() + } + } else { + let mut material = material_slab.read_unchecked(material_id); + material.has_lighting &= has_lighting; + material + } +} + +pub fn texture_color, S: IsSampler>( + texture_id: Id, + uv: Vec2, + atlas: &A, + sampler: &S, + atlas_size: glam::UVec2, + material_slab: &[u32], +) -> Vec4 { + let texture = material_slab.read(texture_id); + // uv is [0, 0] when texture_id is Id::NONE + let uv = texture.uv(uv, atlas_size); + crate::println!("uv: {uv}"); + let mut color: Vec4 = atlas.sample_by_lod(*sampler, uv, 0.0); + if texture_id.is_none() { + color = Vec4::splat(1.0); + } + color +} + +/// PBR fragment shader capable of being run on CPU or GPU. +#[allow(clippy::too_many_arguments)] +pub fn fragment_impl( + atlas: &A, + atlas_sampler: &S, + irradiance: &C, + irradiance_sampler: &S, + prefiltered: &C, + prefiltered_sampler: &S, + brdf: &T, + brdf_sampler: &S, + shadow_map: &DtA, + shadow_map_sampler: &S, + + geometry_slab: &[u32], + material_slab: &[u32], + lighting_slab: &[u32], + + renderlet_id: Id, + + frag_coord: Vec4, + in_color: Vec4, + in_uv0: Vec2, + in_uv1: Vec2, + in_norm: Vec3, + in_tangent: Vec3, + in_bitangent: Vec3, + in_pos: Vec3, + + output: &mut Vec4, +) where + A: Sample2dArray, + T: Sample2d, + DtA: Sample2dArray, + C: SampleCube, + S: IsSampler, +{ + let renderlet = geometry_slab.read_unchecked(renderlet_id); + let geom_desc = geometry_slab.read_unchecked(renderlet.geometry_descriptor_id); + crate::println!("pbr_desc_id: {:?}", renderlet.geometry_descriptor_id); + crate::println!("pbr_desc: {geom_desc:#?}"); + let GeometryDescriptor { + camera_id, + atlas_size, + resolution: _, + debug_channel, + has_lighting, + has_skinning: _, + perform_frustum_culling: _, + perform_occlusion_culling: _, + } = geom_desc; + + let material = get_material(renderlet.material_id, has_lighting, material_slab); + crate::println!("material: {:#?}", material); + + let albedo_tex_uv = if material.albedo_tex_coord == 0 { + in_uv0 + } else { + in_uv1 + }; + let albedo_tex_color = texture_color( + material.albedo_texture_id, + albedo_tex_uv, + atlas, + atlas_sampler, + atlas_size, + material_slab, + ); + my_println!("albedo_tex_color: {:?}", albedo_tex_color); + + let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 { + in_uv0 + } else { + in_uv1 + }; + let metallic_roughness_tex_color = texture_color( + material.metallic_roughness_texture_id, + metallic_roughness_uv, + atlas, + atlas_sampler, + atlas_size, + material_slab, + ); + my_println!( + "metallic_roughness_tex_color: {:?}", + metallic_roughness_tex_color + ); + + let normal_tex_uv = if material.normal_tex_coord == 0 { + in_uv0 + } else { + in_uv1 + }; + let normal_tex_color = texture_color( + material.normal_texture_id, + normal_tex_uv, + atlas, + atlas_sampler, + atlas_size, + material_slab, + ); + my_println!("normal_tex_color: {:?}", normal_tex_color); + + let ao_tex_uv = if material.ao_tex_coord == 0 { + in_uv0 + } else { + in_uv1 + }; + let ao_tex_color = texture_color( + material.ao_texture_id, + ao_tex_uv, + atlas, + atlas_sampler, + atlas_size, + material_slab, + ); + + let emissive_tex_uv = if material.emissive_tex_coord == 0 { + in_uv0 + } else { + in_uv1 + }; + let emissive_tex_color = texture_color( + material.emissive_texture_id, + emissive_tex_uv, + atlas, + atlas_sampler, + atlas_size, + material_slab, + ); + + let (norm, uv_norm) = if material.normal_texture_id.is_none() { + // there is no normal map, use the normal normal ;) + (in_norm, Vec3::ZERO) + } else { + // convert the normal from color coords to tangent space -1,1 + let sampled_norm = (normal_tex_color.xyz() * 2.0 - Vec3::splat(1.0)).alt_norm_or_zero(); + let tbn = glam::mat3( + in_tangent.alt_norm_or_zero(), + in_bitangent.alt_norm_or_zero(), + in_norm.alt_norm_or_zero(), + ); + // convert the normal from tangent space to world space + let norm = (tbn * sampled_norm).alt_norm_or_zero(); + (norm, sampled_norm) + }; + + let n = norm; + let albedo = albedo_tex_color * material.albedo_factor * in_color; + let roughness = metallic_roughness_tex_color.y * material.roughness_factor; + let metallic = metallic_roughness_tex_color.z * material.metallic_factor; + let ao = 1.0 + material.ao_strength * (ao_tex_color.x - 1.0); + let emissive = + emissive_tex_color.xyz() * material.emissive_factor * material.emissive_strength_multiplier; + let irradiance = sample_irradiance(irradiance, irradiance_sampler, n); + let camera = geometry_slab.read(camera_id); + let specular = sample_specular_reflection( + prefiltered, + prefiltered_sampler, + camera.position(), + in_pos, + n, + roughness, + ); + let brdf = + brdf::shader::sample_brdf(brdf, brdf_sampler, camera.position(), in_pos, n, roughness); + + fn colorize(u: Vec3) -> Vec4 { + ((u.alt_norm_or_zero() + Vec3::splat(1.0)) / 2.0).extend(1.0) + } + + crate::println!("debug_mode: {debug_channel:?}"); + use debug::DebugChannel::*; + match debug_channel { + None => {} + UvCoords0 => { + *output = colorize(Vec3::new(in_uv0.x, in_uv0.y, 0.0)); + return; + } + UvCoords1 => { + *output = colorize(Vec3::new(in_uv1.x, in_uv1.y, 0.0)); + return; + } + Normals => { + *output = colorize(norm); + return; + } + VertexColor => { + *output = in_color; + return; + } + VertexNormals => { + *output = colorize(in_norm); + return; + } + UvNormals => { + *output = colorize(uv_norm); + return; + } + Tangents => { + *output = colorize(in_tangent); + return; + } + Bitangents => { + *output = colorize(in_bitangent); + return; + } + DiffuseIrradiance => { + *output = irradiance.extend(1.0); + return; + } + SpecularReflection => { + *output = specular.extend(1.0); + return; + } + Brdf => { + *output = brdf.extend(1.0).extend(1.0); + return; + } + Roughness => { + *output = Vec3::splat(roughness).extend(1.0); + return; + } + Metallic => { + *output = Vec3::splat(metallic).extend(1.0); + return; + } + Albedo => { + *output = albedo; + return; + } + Occlusion => { + *output = Vec3::splat(ao).extend(1.0); + return; + } + Emissive => { + *output = emissive.extend(1.0); + return; + } + UvEmissive => { + *output = emissive_tex_color.xyz().extend(1.0); + return; + } + EmissiveFactor => { + *output = material.emissive_factor.extend(1.0); + return; + } + EmissiveStrength => { + *output = Vec3::splat(material.emissive_strength_multiplier).extend(1.0); + return; + } + } + + *output = if material.has_lighting { + shade_fragment( + shadow_map, + shadow_map_sampler, + camera.position(), + n, + in_pos, + albedo.xyz(), + metallic, + roughness, + ao, + emissive, + irradiance, + specular, + brdf, + lighting_slab, + frag_coord, + ) + } else { + crate::println!("no shading!"); + in_color * albedo_tex_color * material.albedo_factor + }; +} + +#[allow(clippy::too_many_arguments)] +pub fn shade_fragment( + shadow_map: &T, + shadow_map_sampler: &S, + // camera's position in world space + camera_pos: Vec3, + // normal of the fragment + in_norm: Vec3, + // position of the fragment in world space + in_pos: Vec3, + // base color of the fragment + albedo: Vec3, + metallic: f32, + roughness: f32, + ao: f32, + emissive: Vec3, + irradiance: Vec3, + prefiltered: Vec3, + brdf: Vec2, + + light_slab: &[u32], + frag_coord: Vec4, +) -> Vec4 +where + S: IsSampler, + T: Sample2dArray, +{ + let n = in_norm.alt_norm_or_zero(); + let v = (camera_pos - in_pos).alt_norm_or_zero(); + // There is always a `LightingDescriptor` stored at index `0` of the + // light slab. + let lighting_desc = light_slab.read_unchecked(Id::::new(0)); + // If light tiling is enabled, use the pre-computed tile's light list + let analytical_lights_array = if lighting_desc.light_tiling_descriptor_id.is_none() { + lighting_desc.analytical_lights_array + } else { + let tiling_descriptor = light_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id); + let tile_index = tiling_descriptor.tile_index_for_fragment(frag_coord.xy()); + let tile = light_slab.read_unchecked(tiling_descriptor.tiles_array.at(tile_index)); + tile.lights_array + }; + my_println!("lights: {analytical_lights_array:?}"); + my_println!("surface normal: {n:?}"); + my_println!("vector from surface to camera: {v:?}"); + + // accumulated outgoing radiance + let mut lo = Vec3::ZERO; + for light_id_id in analytical_lights_array.iter() { + // calculate per-light radiance + let light_id = light_slab.read(light_id_id); + if light_id.is_none() { + break; + } + let light = light_slab.read(light_id); + let transform = light_slab.read(light.transform_id); + crate::println!("transform: {transform:?}"); + let transform = Mat4::from(transform); + + // determine the light ray and the radiance + let (radiance, shadow) = match light.light_type { + LightStyle::Point => { + let PointLightDescriptor { + position, + color, + intensity, + } = light_slab.read(light.into_point_id()); + let position = transform.transform_point3(position); + // This definitely is the direction pointing from fragment to the light. + // It needs to stay this way. + // For more info, see + // + let frag_to_light = position - in_pos; + let distance = frag_to_light.length(); + if distance == 0.0 { + crate::println!("distance between point light and surface is zero"); + continue; + } + let l = frag_to_light.alt_norm_or_zero(); + let attenuation = intensity / (distance * distance); + let radiance = + outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness); + let shadow = if light.shadow_map_desc_id.is_some() { + // Shadow is 1.0 when the fragment is in the shadow of this light, + // and 0.0 in darkness + ShadowCalculation::new(light_slab, light, in_pos, n, l).run_point( + light_slab, + shadow_map, + shadow_map_sampler, + position, + ) + } else { + 0.0 + }; + (radiance, shadow) + } + + LightStyle::Spot => { + let spot_light_descriptor = light_slab.read(light.into_spot_id()); + let calculation = + SpotLightCalculation::new(spot_light_descriptor, transform, in_pos); + crate::println!("calculation: {calculation:#?}"); + if calculation.frag_to_light_distance == 0.0 { + continue; + } + let attenuation: f32 = spot_light_descriptor.intensity * calculation.contribution; + let radiance = outgoing_radiance( + spot_light_descriptor.color, + albedo, + attenuation, + v, + calculation.frag_to_light, + n, + metallic, + roughness, + ); + let shadow = if light.shadow_map_desc_id.is_some() { + // Shadow is 1.0 when the fragment is in the shadow of this light, + // and 0.0 in darkness + ShadowCalculation::new(light_slab, light, in_pos, n, calculation.frag_to_light) + .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler) + } else { + 0.0 + }; + (radiance, shadow) + } + + LightStyle::Directional => { + let DirectionalLightDescriptor { + direction, + color, + intensity, + } = light_slab.read(light.into_directional_id()); + let direction = transform.transform_vector3(direction); + let l = -direction.alt_norm_or_zero(); + let attenuation = intensity; + let radiance = + outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness); + let shadow = + if light.shadow_map_desc_id.is_some() { + // Shadow is 1.0 when the fragment is in the shadow of this light, + // and 0.0 in darkness + ShadowCalculation::new(light_slab, light, in_pos, n, l) + .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler) + } else { + 0.0 + }; + (radiance, shadow) + } + }; + crate::println!("radiance: {radiance}"); + crate::println!("shadow: {shadow}"); + lo += radiance * (1.0 - shadow); + } + + my_println!("lo: {lo:?}"); + // calculate reflectance at normal incidence; if dia-electric (like plastic) use + // F0 of 0.04 and if it's a metal, use the albedo color as F0 (metallic + // workflow) + let f0: Vec3 = Vec3::splat(0.04).lerp(albedo, metallic); + let cos_theta = n.dot(v).max(0.0); + let fresnel = fresnel_schlick_roughness(cos_theta, f0, roughness); + let ks = fresnel; + let kd = (1.0 - ks) * (1.0 - metallic); + let diffuse = irradiance * albedo; + let specular = prefiltered * (fresnel * brdf.x + brdf.y); + let color = (kd * diffuse + specular) * ao + lo + emissive; + color.extend(1.0) +} diff --git a/crates/renderling/src/primitive.rs b/crates/renderling/src/primitive.rs new file mode 100644 index 00000000..128b4044 --- /dev/null +++ b/crates/renderling/src/primitive.rs @@ -0,0 +1,8 @@ +//! Mesh primitives + +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; + +pub mod shader; diff --git a/crates/renderling/src/primitive/cpu.rs b/crates/renderling/src/primitive/cpu.rs new file mode 100644 index 00000000..b154f802 --- /dev/null +++ b/crates/renderling/src/primitive/cpu.rs @@ -0,0 +1,335 @@ +//! Mesh primitives. + +use core::ops::Deref; +use std::sync::{Arc, Mutex}; + +use craballoc::value::Hybrid; +use crabslab::{Array, Id}; + +use crate::{ + bvol::BoundingSphere, + geometry::{Indices, MorphTargetWeights, MorphTargets, Skin, Vertices}, + material::Material, + primitive::shader::PrimitiveDescriptor, + stage::Stage, + transform::Transform, + types::GpuOnlyArray, +}; + +/// A unit of rendering. +/// +/// A `Primitive` represents one draw call, or one mesh primitive. +pub struct Primitive { + pub(crate) descriptor: Hybrid, + + vertices: Arc>>>, + indices: Arc>>>, + + pub(crate) transform: Arc>>, + pub(crate) material: Arc>>, + skin: Arc>>, + morph_targets: Arc>>, +} + +impl Primitive { + /// Create a new [`Primitive`], automatically adding it to the + /// [`Stage`](crate::stage::Stage) to be drawn. + /// + /// The returned [`Primitive`] will have the stage's default [`Vertices`], + /// which is an all-white unit cube. + pub fn new(stage: &Stage) -> Self { + let descriptor = stage + .geometry + .slab_allocator() + .new_value(PrimitiveDescriptor::default()); + let primitive = Primitive { + descriptor, + vertices: Default::default(), + indices: Default::default(), + transform: Default::default(), + material: Default::default(), + skin: Default::default(), + morph_targets: Default::default(), + } + .with_vertices(stage.default_vertices()); + stage.add_primitive(&primitive); + primitive + } +} + +impl Clone for Primitive { + fn clone(&self) -> Self { + Self { + descriptor: self.descriptor.clone(), + vertices: self.vertices.clone(), + indices: self.indices.clone(), + transform: self.transform.clone(), + material: self.material.clone(), + skin: self.skin.clone(), + morph_targets: self.morph_targets.clone(), + } + } +} + +// Vertices impls +impl Primitive { + /// Set the vertex data of this primitive. + pub fn set_vertices(&self, vertices: impl Into>) -> &Self { + let vertices = vertices.into(); + let array = vertices.array(); + self.descriptor.modify(|d| d.vertices_array = array); + *self.vertices.lock().unwrap() = Some(vertices.clone()); + self + } + + /// Set the vertex data of this primitive and return the primitive. + pub fn with_vertices(self, vertices: impl Into>) -> Self { + self.set_vertices(vertices); + self + } +} + +// Indices impls +impl Primitive { + /// Set the index data of this primitive. + pub fn set_indices(&self, indices: impl Into>) -> &Self { + let indices = indices.into(); + let array = indices.array(); + self.descriptor.modify(|d| d.indices_array = array); + *self.indices.lock().unwrap() = Some(indices.clone()); + self + } + + /// Set the index data of this primitive and return the primitive. + pub fn with_indices(self, indices: impl Into>) -> Self { + self.set_indices(indices); + self + } + + /// Remove the indices from this primitive. + pub fn remove_indices(&self) -> &Self { + *self.indices.lock().unwrap() = None; + self.descriptor.modify(|d| d.indices_array = Array::NONE); + self + } +} + +// PrimitiveDescriptor impls +impl Primitive { + /// Return a pointer to the underlying descriptor on the GPU. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Return the underlying descriptor. + pub fn descriptor(&self) -> PrimitiveDescriptor { + self.descriptor.get() + } + + /// Set the bounds of this primitive. + pub fn set_bounds(&self, bounds: BoundingSphere) -> &Self { + self.descriptor.modify(|d| d.bounds = bounds); + self + } + + /// Set the bounds and return the primitive. + pub fn with_bounds(self, bounds: BoundingSphere) -> Self { + self.set_bounds(bounds); + self + } + + /// Get the bounds. + /// + /// Returns the current [`BoundingSphere`]. + pub fn bounds(&self) -> BoundingSphere { + self.descriptor.get().bounds + } + + /// Modify the bounds of the primitive. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a mutable reference to the + /// [`BoundingSphere`] and returns a value of type `T`. + pub fn modify_bounds(&self, f: impl FnOnce(&mut BoundingSphere) -> T) -> T { + self.descriptor.modify(|d| f(&mut d.bounds)) + } + + /// Set the visibility of this primitive. + pub fn set_visible(&self, visible: bool) -> &Self { + self.descriptor.modify(|d| d.visible = visible); + self + } + + /// Set the visibility and return the primitive. + pub fn with_visible(self, visible: bool) -> Self { + self.set_visible(visible); + self + } + + /// Return the primitive's visibility. + pub fn visible(&self) -> bool { + self.descriptor.get().visible + } + + /// Modify the visible of the primitive. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a mutable reference to the visibility and + /// returns a value of type `T`. + pub fn modify_visible(&self, f: impl FnOnce(&mut bool) -> T) -> T { + self.descriptor.modify(|d| f(&mut d.visible)) + } +} + +// Transform functions +impl Primitive { + /// Set the transform. + /// + /// # Note + /// This can be set with [`Transform`] or + /// [`NestedTransform`](crate::transform::NestedTransform). + pub fn set_transform(&self, transform: impl Into) -> &Self { + let transform = transform.into(); + self.descriptor.modify(|d| d.transform_id = transform.id()); + *self.transform.lock().unwrap() = Some(transform.clone()); + self + } + + /// Set the transform and return the `Primitive`. + /// + /// # Note + /// This can be set with [`Transform`] or + /// [`NestedTransform`](crate::transform::NestedTransform). + pub fn with_transform(self, transform: impl Into) -> Self { + self.set_transform(transform); + self + } + + /// Get the transform. + /// + /// Returns a reference to the current `Transform`, if any. + pub fn transform(&self) -> impl Deref> + '_ { + self.transform.lock().unwrap() + } + + /// Remove the transform from this primitive. + /// + /// This effectively makes the transform the identity. + pub fn remove_transform(&self) -> &Self { + self.descriptor.modify(|d| d.transform_id = Id::NONE); + *self.transform.lock().unwrap() = None; + self + } +} + +// Material impls +impl Primitive { + /// Set the material of this primitive. + pub fn set_material(&self, material: impl Into) -> &Self { + let material = material.into(); + self.descriptor.modify(|d| d.material_id = material.id()); + *self.material.lock().unwrap() = Some(material); + self + } + + /// Set the material and return the primitive. + pub fn with_material(self, material: impl Into) -> Self { + self.set_material(material); + self + } + + /// Get the material. + /// + /// Returns a reference to the current `Material`, if any. + pub fn material(&self) -> impl Deref> + '_ { + self.material.lock().unwrap() + } + + /// Remove the material from this primitive. + pub fn remove_material(&self) -> &Self { + self.descriptor.modify(|d| d.material_id = Id::NONE); + *self.material.lock().unwrap() = None; + self + } +} + +// Skin impls +impl Primitive { + /// Set the skin of this primitive. + pub fn set_skin(&self, skin: impl Into) -> &Self { + let skin = skin.into(); + self.descriptor.modify(|d| d.skin_id = skin.id()); + *self.skin.lock().unwrap() = Some(skin.clone()); + self + } + + /// Set the skin and return the primitive. + pub fn with_skin(self, skin: impl Into) -> Self { + self.set_skin(skin); + self + } + + /// Get the skin. + /// + /// Returns a reference to the current `Skin`, if any. + pub fn skin(&self) -> impl Deref> + '_ { + self.skin.lock().unwrap() + } + + /// Remove the skin from this primitive. + pub fn remove_skin(&self) -> &Self { + self.descriptor.modify(|d| d.skin_id = Id::NONE); + *self.skin.lock().unwrap() = None; + self + } +} + +// (MorphTargets, MorphTargetsWeights) impls +impl Primitive { + /// Set the morph targets and weights of this primitive. + pub fn set_morph_targets( + &self, + morph_targets: impl Into, + weights: impl Into, + ) -> &Self { + let morph_targets = morph_targets.into(); + let weights = weights.into(); + self.descriptor.modify(|d| { + d.morph_targets = morph_targets.array(); + d.morph_weights = weights.array(); + }); + *self.morph_targets.lock().unwrap() = Some((morph_targets.clone(), weights.clone())); + self + } + + /// Set the morph targets and weights and return the primitive. + pub fn with_morph_targets( + self, + morph_targets: impl Into, + weights: impl Into, + ) -> Self { + self.set_morph_targets(morph_targets, weights); + self + } + + /// Get the morph targets and weights. + /// + /// Returns a reference to the current `MorphTargets` and `MorphTargetsWeights`, if any. + pub fn morph_targets( + &self, + ) -> impl Deref> + '_ { + self.morph_targets.lock().unwrap() + } + + /// Remove the morph targets and weights from this primitive. + pub fn remove_morph_targets(&self) -> &Self { + self.descriptor.modify(|d| { + d.morph_targets = Array::NONE; + d.morph_weights = Array::NONE; + }); + *self.morph_targets.lock().unwrap() = None; + self + } +} diff --git a/crates/renderling/src/primitive/shader.rs b/crates/renderling/src/primitive/shader.rs new file mode 100644 index 00000000..ef8a5c1b --- /dev/null +++ b/crates/renderling/src/primitive/shader.rs @@ -0,0 +1,317 @@ +//! Shader support for rendering primitives. +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; +use spirv_std::{ + image::{Cubemap, Image2d, Image2dArray}, + spirv, Image, Sampler, +}; + +// use glam::Mat4; +// #[cfg(not(target_arch = "spirv"))] +// use glam::UVec2; + +// #[allow(unused_imports)] +// use spirv_std::num_traits::Float; + +use crate::{ + bvol::BoundingSphere, + geometry::{ + shader::{GeometryDescriptor, SkinDescriptor}, + MorphTarget, Vertex, + }, + material::shader::MaterialDescriptor, + math::IsVector, + transform::shader::TransformDescriptor, +}; + +#[allow(unused_imports)] +use spirv_std::num_traits::Float; + +/// Returned by [`PrimitiveDescriptor::get_vertex_info`]. +pub struct VertexInfo { + pub vertex: Vertex, + pub transform: TransformDescriptor, + pub model_matrix: Mat4, + pub world_pos: Vec3, +} + +/// A draw call used to render some geometry. +#[derive(Clone, Copy, PartialEq, SlabItem, Debug)] +#[offsets] +pub struct PrimitiveDescriptor { + pub visible: bool, + pub vertices_array: Array, + /// Bounding sphere of the entire primitive, in local space. + pub bounds: BoundingSphere, + pub indices_array: Array, + pub transform_id: Id, + pub material_id: Id, + pub skin_id: Id, + pub morph_targets: Array>, + pub morph_weights: Array, + pub geometry_descriptor_id: Id, +} + +impl Default for PrimitiveDescriptor { + fn default() -> Self { + PrimitiveDescriptor { + visible: true, + vertices_array: Array::default(), + bounds: BoundingSphere::default(), + indices_array: Array::default(), + transform_id: Id::NONE, + material_id: Id::NONE, + skin_id: Id::NONE, + morph_targets: Array::default(), + morph_weights: Array::default(), + geometry_descriptor_id: Id::new(0), + } + } +} + +impl PrimitiveDescriptor { + /// Returns the vertex at the given index and its related values. + /// + /// These values are often used in shaders, so they are grouped together. + pub fn get_vertex_info(&self, vertex_index: u32, geometry_slab: &[u32]) -> VertexInfo { + let vertex = self.get_vertex(vertex_index, geometry_slab); + let transform = self.get_transform(vertex, geometry_slab); + let model_matrix = Mat4::from(transform); + let world_pos = model_matrix.transform_point3(vertex.position); + VertexInfo { + vertex, + transform, + model_matrix, + world_pos, + } + } + /// Retrieve the transform of this `primitive`. + /// + /// This takes into consideration all skinning matrices. + pub fn get_transform(&self, vertex: Vertex, slab: &[u32]) -> TransformDescriptor { + let config = slab.read_unchecked(self.geometry_descriptor_id); + if config.has_skinning && self.skin_id.is_some() { + let skin = slab.read(self.skin_id); + TransformDescriptor::from(skin.get_skinning_matrix(vertex, slab)) + } else { + slab.read(self.transform_id) + } + } + + /// Retrieve the vertex from the slab, calculating any displacement due to + /// morph targets. + pub fn get_vertex(&self, vertex_index: u32, slab: &[u32]) -> Vertex { + let index = if self.indices_array.is_null() { + vertex_index as usize + } else { + slab.read(self.indices_array.at(vertex_index as usize)) as usize + }; + let vertex_id = self.vertices_array.at(index); + let mut vertex = slab.read_unchecked(vertex_id); + for i in 0..self.morph_targets.len() { + let morph_target_array = slab.read(self.morph_targets.at(i)); + let morph_target = slab.read(morph_target_array.at(index)); + let weight = slab.read(self.morph_weights.at(i)); + vertex.position += weight * morph_target.position; + vertex.normal += weight * morph_target.normal; + vertex.tangent += weight * morph_target.tangent.extend(0.0); + } + vertex + } + + pub fn get_vertex_count(&self) -> u32 { + if self.indices_array.is_null() { + self.vertices_array.len() as u32 + } else { + self.indices_array.len() as u32 + } + } +} + +#[cfg(test)] +/// A helper struct that contains all outputs of the primitive's PBR vertex shader. +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct PrimitivePbrVertexInfo { + pub primitive: PrimitiveDescriptor, + pub primitive_id: Id, + pub vertex_index: u32, + pub vertex: Vertex, + pub transform: TransformDescriptor, + pub model_matrix: Mat4, + pub view_projection: Mat4, + pub out_color: Vec4, + pub out_uv0: Vec2, + pub out_uv1: Vec2, + pub out_norm: Vec3, + pub out_tangent: Vec3, + pub out_bitangent: Vec3, + pub out_pos: Vec3, + pub out_clip_pos: Vec4, +} + +/// primitive vertex shader. +#[spirv(vertex)] +#[allow(clippy::too_many_arguments)] +pub fn primitive_vertex( + // Points at a `primitive` + #[spirv(instance_index)] primitive_id: Id, + // Which vertex within the primitive are we rendering + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + + #[spirv(flat)] out_primitive: &mut Id, + // TODO: Think about placing all these out values in a G-Buffer + // But do we have enough buffers + enough space on web? + // ...and can we write to buffers from vertex shaders on web? + out_color: &mut Vec4, + out_uv0: &mut Vec2, + out_uv1: &mut Vec2, + out_norm: &mut Vec3, + out_tangent: &mut Vec3, + out_bitangent: &mut Vec3, + out_world_pos: &mut Vec3, + #[spirv(position)] out_clip_pos: &mut Vec4, + // test-only info struct + #[cfg(test)] out_info: &mut PrimitivePbrVertexInfo, +) { + let primitive = geometry_slab.read_unchecked(primitive_id); + if !primitive.visible { + // put it outside the clipping frustum + *out_clip_pos = Vec4::new(10.0, 10.0, 10.0, 1.0); + return; + } + + *out_primitive = primitive_id; + + let VertexInfo { + vertex, + transform, + model_matrix, + world_pos, + } = primitive.get_vertex_info(vertex_index, geometry_slab); + *out_color = vertex.color; + *out_uv0 = vertex.uv0; + *out_uv1 = vertex.uv1; + *out_world_pos = world_pos; + + let scale2 = transform.scale * transform.scale; + let normal = vertex.normal.alt_norm_or_zero(); + let tangent = vertex.tangent.xyz().alt_norm_or_zero(); + let normal_w: Vec3 = (model_matrix * (normal / scale2).extend(0.0)) + .xyz() + .alt_norm_or_zero(); + *out_norm = normal_w; + + let tangent_w: Vec3 = (model_matrix * tangent.extend(0.0)) + .xyz() + .alt_norm_or_zero(); + *out_tangent = tangent_w; + + let bitangent_w = normal_w.cross(tangent_w) * if vertex.tangent.w >= 0.0 { 1.0 } else { -1.0 }; + *out_bitangent = bitangent_w; + + let camera_id = geometry_slab + .read_unchecked(primitive.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID); + let camera = geometry_slab.read(camera_id); + let clip_pos = camera.view_projection() * world_pos.extend(1.0); + *out_clip_pos = clip_pos; + #[cfg(test)] + { + *out_info = PrimitivePbrVertexInfo { + primitive_id, + vertex_index, + vertex, + transform, + model_matrix, + view_projection: camera.view_projection(), + out_clip_pos: clip_pos, + primitive, + out_color: *out_color, + out_uv0: *out_uv0, + out_uv1: *out_uv1, + out_norm: *out_norm, + out_tangent: *out_tangent, + out_bitangent: *out_bitangent, + out_pos: *out_world_pos, + }; + } +} + +/// primitive fragment shader +#[allow(clippy::too_many_arguments, dead_code)] +#[spirv(fragment)] +pub fn primitive_fragment( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] material_slab: &[u32], + #[spirv(descriptor_set = 0, binding = 2)] atlas: &Image2dArray, + #[spirv(descriptor_set = 0, binding = 3)] atlas_sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 4)] irradiance: &Cubemap, + #[spirv(descriptor_set = 0, binding = 5)] irradiance_sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 6)] prefiltered: &Cubemap, + #[spirv(descriptor_set = 0, binding = 7)] prefiltered_sampler: &Sampler, + #[spirv(descriptor_set = 0, binding = 8)] brdf: &Image2d, + #[spirv(descriptor_set = 0, binding = 9)] brdf_sampler: &Sampler, + #[spirv(storage_buffer, descriptor_set = 0, binding = 10)] light_slab: &[u32], + #[spirv(descriptor_set = 0, binding = 11)] shadow_map: &Image!(2D, type=f32, sampled, arrayed), + #[spirv(descriptor_set = 0, binding = 12)] shadow_map_sampler: &Sampler, + #[cfg(feature = "debug-slab")] + #[spirv(storage_buffer, descriptor_set = 0, binding = 13)] + debug_slab: &mut [u32], + + #[spirv(flat)] primitive_id: Id, + #[spirv(frag_coord)] frag_coord: Vec4, + in_color: Vec4, + in_uv0: Vec2, + in_uv1: Vec2, + in_norm: Vec3, + in_tangent: Vec3, + in_bitangent: Vec3, + world_pos: Vec3, + output: &mut Vec4, +) { + // proxy to a separate impl that allows us to test on CPU + crate::pbr::shader::fragment_impl( + atlas, + atlas_sampler, + irradiance, + irradiance_sampler, + prefiltered, + prefiltered_sampler, + brdf, + brdf_sampler, + shadow_map, + shadow_map_sampler, + geometry_slab, + material_slab, + light_slab, + primitive_id, + frag_coord, + in_color, + in_uv0, + in_uv1, + in_norm, + in_tangent, + in_bitangent, + world_pos, + output, + ); +} + +#[cfg(feature = "test_i8_16_extraction")] +#[spirv(compute(threads(32)))] +/// A shader to ensure that we can extract i8 and i16 values from a storage +/// buffer. +pub fn test_i8_i16_extraction( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &mut [u32], + #[spirv(global_invocation_id)] global_id: UVec3, +) { + let index = global_id.x as usize; + let (value, _, _) = crate::bits::extract_i8(index, 2, slab); + if value > 0 { + slab[index] = value as u32; + } + let (value, _, _) = crate::bits::extract_i16(index, 2, slab); + if value > 0 { + slab[index] = value as u32; + } +} diff --git a/crates/renderling/src/skybox.rs b/crates/renderling/src/skybox.rs index d3797f3c..a654fbf0 100644 --- a/crates/renderling/src/skybox.rs +++ b/crates/renderling/src/skybox.rs @@ -1,95 +1,7 @@ -//! Skybox shaders and CPU code. -use crabslab::{Id, Slab}; -use glam::{Mat3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; -use spirv_std::{ - image::{Cubemap, Image2d}, - spirv, Sampler, -}; - -#[allow(unused_imports)] -use spirv_std::num_traits::Float; - -use crate::{ - camera::Camera, - math::{self, IsVector}, -}; - +//! Rendering skylines at infinite distances. #[cfg(not(target_arch = "spirv"))] mod cpu; #[cfg(not(target_arch = "spirv"))] pub use cpu::*; -const INV_ATAN: Vec2 = Vec2::new(0.1591, core::f32::consts::FRAC_1_PI); - -/// Takes a unit direction and converts it to a uv lookup in an equirectangular -/// map. -pub fn direction_to_equirectangular_uv(dir: Vec3) -> Vec2 { - let mut uv = Vec2::new(f32::atan2(dir.z, dir.x), f32::asin(dir.y)); - uv *= INV_ATAN; - uv += 0.5; - uv -} - -/// Vertex shader for a skybox. -#[spirv(vertex)] -pub fn skybox_vertex( - #[spirv(instance_index)] camera_index: u32, - #[spirv(vertex_index)] vertex_index: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - local_pos: &mut Vec3, - #[spirv(position)] clip_pos: &mut Vec4, -) { - let camera_id = Id::::from(camera_index); - let camera = slab.read(camera_id); - let point = math::CUBE[vertex_index as usize]; - *local_pos = point; - let camera_view_without_translation = Mat3::from_mat4(camera.view()); - let rot_view = Mat4::from_mat3(camera_view_without_translation); - let position = camera.projection() * rot_view * point.extend(1.0); - *clip_pos = position.xyww(); -} - -/// Colors a skybox using a cubemap texture. -#[spirv(fragment)] -pub fn skybox_cubemap_fragment( - #[spirv(descriptor_set = 0, binding = 1)] texture: &Cubemap, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - local_pos: Vec3, - out_color: &mut Vec4, -) { - let env_color: Vec3 = texture.sample(*sampler, local_pos.alt_norm_or_zero()).xyz(); - *out_color = env_color.extend(1.0); -} - -/// Vertex shader that draws a cubemap. -/// -/// Uses the `instance_index` as the [`Id`] for a [`Camera`]. -/// -/// Used to create a cubemap from an equirectangular image as well as cubemap -/// convolutions. -#[spirv(vertex)] -pub fn skybox_cubemap_vertex( - #[spirv(instance_index)] camera_id: Id, - #[spirv(vertex_index)] vertex_index: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], - local_pos: &mut Vec3, - #[spirv(position)] gl_pos: &mut Vec4, -) { - let camera = slab.read(camera_id); - let pos = crate::math::CUBE[vertex_index as usize]; - *local_pos = pos; - *gl_pos = camera.view_projection() * pos.extend(1.0); -} - -/// Fragment shader that colors a skybox using an equirectangular texture. -#[spirv(fragment)] -pub fn skybox_equirectangular_fragment( - #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, - #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, - local_pos: Vec3, - out_color: &mut Vec4, -) { - let uv = direction_to_equirectangular_uv(local_pos.alt_norm_or_zero()); - let env_color: Vec3 = texture.sample(*sampler, uv).xyz(); - *out_color = env_color.extend(1.0); -} +pub mod shader; diff --git a/crates/renderling/src/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index f3d929e6..7f64cb55 100644 --- a/crates/renderling/src/skybox/cpu.rs +++ b/crates/renderling/src/skybox/cpu.rs @@ -1,14 +1,15 @@ //! CPU-side code for skybox rendering. -use craballoc::{ - prelude::{Hybrid, SlabAllocator}, - runtime::WgpuRuntime, -}; -use crabslab::Id; +use core::sync::atomic::AtomicBool; +use std::sync::Arc; + +use craballoc::{prelude::SlabAllocator, runtime::WgpuRuntime}; use glam::{Mat4, UVec2, Vec3}; use crate::{ - atlas::AtlasImage, camera::Camera, convolution::VertexPrefilterEnvironmentCubemapIds, - cubemap::EquirectangularImageToCubemapBlitter, texture::Texture, + atlas::AtlasImage, + camera::Camera, + cubemap::EquirectangularImageToCubemapBlitter, + texture::{self, Texture}, }; /// Render pipeline used to draw a skybox. @@ -146,26 +147,20 @@ pub(crate) fn create_skybox_render_pipeline( } } -/// An HDR skybox that also provides IBL cubemaps and lookups. +/// An HDR skybox. +/// +/// Skyboxes provide an environment cubemap around all your scenery +/// that acts as a background. /// -/// A clone of a skybox is a reference to the same skybox. +/// A [`Skybox`] can also be used to create [`Ibl`], which illuminates +/// your scene using the environment map as a light source. /// -/// Only available on the CPU. Not available in shaders. -// TODO: spilt Skybox into Skybox and IBL components. -// Skybox and IBL are different things. Sometimes you want to use a -// skybox without having it shade things. -// Also, the brdf_lut doesn't change, so should probably live in `Lighting` +/// All clones of a skybox point to the same underlying data. #[derive(Debug, Clone)] pub struct Skybox { + is_empty: Arc, // Cubemap texture of the environment cubemap - pub environment_cubemap: Texture, - // Cubemap texture of the pre-computed irradiance cubemap - pub irradiance_cubemap: Texture, - // Cubemap texture and mip maps of the specular highlights, - // where each mip level is a different roughness. - pub prefiltered_environment_cubemap: Texture, - // Texture of the pre-computed brdf integration - pub brdf_lut: Texture, + environment_cubemap: Texture, } impl Skybox { @@ -179,7 +174,9 @@ impl Skybox { format: crate::atlas::AtlasImageFormat::R32G32B32A32FLOAT, apply_linear_transfer: false, }; - Self::new(runtime, hdr_img) + let s = Self::new(runtime, hdr_img); + s.is_empty.store(true, std::sync::atomic::Ordering::Relaxed); + s } /// Create a new `Skybox`. @@ -188,15 +185,8 @@ impl Skybox { log::trace!("creating skybox"); let slab = SlabAllocator::new(runtime, "skybox-slab", wgpu::BufferUsages::VERTEX); - let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0); - let camera = slab.new_value(Camera::default().with_projection(proj)); - let roughness = slab.new_value(0.0f32); - let prefilter_ids = slab.new_value(VertexPrefilterEnvironmentCubemapIds { - camera: camera.id(), - roughness: roughness.id(), - }); - + let camera = Camera::new(&slab).with_projection(proj); let buffer = slab.commit(); let mut buffer_upkeep = || { let possibly_new_buffer = slab.commit(); @@ -247,38 +237,17 @@ impl Skybox { views, ); - // Convolve the environment map. - let irradiance_cubemap = Skybox::create_irradiance_map( - runtime, - &buffer, - &mut buffer_upkeep, - &environment_cubemap, - &camera, - views, - ); - - // Generate specular IBL pre-filtered environment map. - let prefiltered_environment_cubemap = Skybox::create_prefiltered_environment_map( - runtime, - &buffer, - &mut buffer_upkeep, - &camera, - &roughness, - prefilter_ids.id(), - &environment_cubemap, - views, - ); - - let brdf_lut = Skybox::create_precomputed_brdf_texture(runtime); - Skybox { + is_empty: Arc::new(false.into()), environment_cubemap, - irradiance_cubemap, - prefiltered_environment_cubemap, - brdf_lut, } } + /// Return a reference to the environment cubemap texture. + pub fn environment_cubemap_texture(&self) -> &texture::Texture { + &self.environment_cubemap + } + /// Convert an HDR [`AtlasImage`] into a texture. pub fn hdr_texture_from_atlas_image( runtime: impl AsRef, @@ -317,7 +286,7 @@ impl Skybox { buffer: &wgpu::Buffer, buffer_upkeep: impl FnMut(), hdr_texture: &Texture, - camera: &Hybrid, + camera: &Camera, views: [Mat4; 6], ) -> Texture { let runtime = runtime.as_ref(); @@ -340,7 +309,7 @@ impl Skybox { hdr_texture, ); - Self::render_cubemap( + texture::Texture::render_cubemap( runtime, &pipeline.0, buffer_upkeep, @@ -352,296 +321,9 @@ impl Skybox { ) } - #[allow(clippy::too_many_arguments)] - fn render_cubemap( - runtime: impl AsRef, - pipeline: &wgpu::RenderPipeline, - mut buffer_upkeep: impl FnMut(), - camera: &Hybrid, - bindgroup: &wgpu::BindGroup, - views: [Mat4; 6], - texture_size: u32, - mip_levels: Option, - ) -> Texture { - let runtime = runtime.as_ref(); - let device = &runtime.device; - let queue = &runtime.queue; - let mut cubemap_faces = Vec::new(); - let mip_levels = mip_levels.unwrap_or(1); - - // Render every cube face. - for (i, view) in views.iter().enumerate() { - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some(&format!("cubemap{i}")), - }); - - let mut cubemap_face = Texture::new_with( - runtime, - Some(&format!("cubemap{i}")), - Some( - wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - ), - None, - wgpu::TextureFormat::Rgba16Float, - 4, - 2, - texture_size, - texture_size, - 1, - &[], - ); - - // update the view to point at one of the cube faces - camera.modify(|c| c.set_view(*view)); - buffer_upkeep(); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&format!("cubemap{i}")), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &cubemap_face.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - render_pass.set_pipeline(pipeline); - render_pass.set_bind_group(0, Some(bindgroup), &[]); - render_pass.draw(0..36, 0..1); - } - - queue.submit([encoder.finish()]); - let mips = cubemap_face.generate_mips(runtime, Some("cubemap mips"), mip_levels); - cubemap_faces.push(cubemap_face); - cubemap_faces.extend(mips); - } - - Texture::new_cubemap_texture( - runtime, - Some("skybox cubemap"), - texture_size, - cubemap_faces.as_slice(), - wgpu::TextureFormat::Rgba16Float, - mip_levels, - ) - } - - fn create_irradiance_map( - runtime: impl AsRef, - buffer: &wgpu::Buffer, - buffer_upkeep: impl FnMut(), - environment_texture: &Texture, - camera: &Hybrid, - views: [Mat4; 6], - ) -> Texture { - let runtime = runtime.as_ref(); - let device = &runtime.device; - let pipeline = - crate::ibl::diffuse_irradiance::DiffuseIrradianceConvolutionRenderPipeline::new( - device, - wgpu::TextureFormat::Rgba16Float, - ); - - let bindgroup = crate::ibl::diffuse_irradiance::diffuse_irradiance_convolution_bindgroup( - device, - Some("irradiance"), - buffer, - environment_texture, - ); - - Self::render_cubemap( - runtime, - &pipeline.0, - buffer_upkeep, - camera, - &bindgroup, - views, - 32, - None, - ) - } - - #[allow(clippy::too_many_arguments)] - fn create_prefiltered_environment_map( - runtime: impl AsRef, - buffer: &wgpu::Buffer, - mut buffer_upkeep: impl FnMut(), - camera: &Hybrid, - roughness: &Hybrid, - prefilter_id: Id, - environment_texture: &Texture, - views: [Mat4; 6], - ) -> Texture { - let (pipeline, bindgroup) = - crate::ibl::prefiltered_environment::create_pipeline_and_bindgroup( - &runtime.as_ref().device, - buffer, - environment_texture, - ); - let mut cubemap_faces = Vec::new(); - - for (i, view) in views.iter().enumerate() { - for mip_level in 0..5 { - let mip_width: u32 = 128 >> mip_level; - let mip_height: u32 = 128 >> mip_level; - - let mut encoder = runtime.as_ref().device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("specular convolution"), - }, - ); - - let cubemap_face = Texture::new_with( - runtime.as_ref(), - Some(&format!("cubemap{i}{mip_level}prefiltered_environment")), - Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC), - None, - wgpu::TextureFormat::Rgba16Float, - 4, - 2, - mip_width, - mip_height, - 1, - &[], - ); - - // update the roughness for these mips - roughness.set(mip_level as f32 / 4.0); - // update the view to point at one of the cube faces - camera.modify(|c| c.set_view(*view)); - buffer_upkeep(); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&format!("cubemap{i}")), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &cubemap_face.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - render_pass.set_pipeline(&pipeline); - render_pass.set_bind_group(0, Some(&bindgroup), &[]); - render_pass.draw(0..36, prefilter_id.inner()..prefilter_id.inner() + 1); - } - - runtime.as_ref().queue.submit([encoder.finish()]); - cubemap_faces.push(cubemap_face); - } - } - - Texture::new_cubemap_texture( - runtime, - Some("prefiltered environment cubemap"), - 128, - cubemap_faces.as_slice(), - wgpu::TextureFormat::Rgba16Float, - 5, - ) - } - - fn create_precomputed_brdf_texture(runtime: impl AsRef) -> Texture { - let runtime = runtime.as_ref(); - let device = &runtime.device; - let queue = &runtime.queue; - let vertex_linkage = crate::linkage::brdf_lut_convolution_vertex::linkage(device); - let fragment_linkage = crate::linkage::brdf_lut_convolution_fragment::linkage(device); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("brdf_lut_convolution"), - layout: None, - vertex: wgpu::VertexState { - module: &vertex_linkage.module, - entry_point: Some(vertex_linkage.entry_point), - buffers: &[], - compilation_options: Default::default(), - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment_linkage.module, - entry_point: Some(fragment_linkage.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rg16Float, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - multiview: None, - cache: None, - }); - - let framebuffer = Texture::new_with( - runtime, - Some("brdf_lut"), - Some( - wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_SRC, - ), - None, - wgpu::TextureFormat::Rg16Float, - 2, - 2, - 512, - 512, - 1, - &[], - ); - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("brdf_lut_convolution"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &framebuffer.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::RED), - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - ..Default::default() - }); - - render_pass.set_pipeline(&pipeline); - render_pass.draw(0..6, 0..1); - } - queue.submit([encoder.finish()]); - framebuffer + /// Returns whether this skybox is empty. + pub fn is_empty(&self) -> bool { + self.is_empty.load(std::sync::atomic::Ordering::Relaxed) } } @@ -650,7 +332,9 @@ mod test { use glam::Vec3; use super::*; - use crate::{test::BlockOnFuture, texture::CopiedTextureBuffer, Context}; + use crate::{ + context::Context, pbr::brdf::BrdfLut, test::BlockOnFuture, texture::CopiedTextureBuffer, + }; #[test] fn hdr_skybox_scene() { @@ -661,25 +345,25 @@ mod test { let stage = ctx.new_stage(); - let _camera = stage.new_camera(Camera::new(proj, view)); + let _camera = stage.new_camera().with_projection_and_view(proj, view); let skybox = stage .new_skybox_from_path("../../img/hdr/resting_place.hdr") .unwrap(); - + let ibl = stage.new_ibl(&skybox); assert_eq!( wgpu::TextureFormat::Rgba16Float, - skybox.irradiance_cubemap.texture.format() + ibl.irradiance_cubemap.texture.format() ); assert_eq!( wgpu::TextureFormat::Rgba16Float, - skybox.prefiltered_environment_cubemap.texture.format() + ibl.prefiltered_environment_cubemap.texture.format() ); for i in 0..6 { // save out the irradiance face let copied_buffer = CopiedTextureBuffer::read_from( &ctx, - &skybox.irradiance_cubemap.texture, + &ibl.irradiance_cubemap.texture, 32, 32, 4, @@ -702,7 +386,7 @@ mod test { // save out the prefiltered environment faces' mips let copied_buffer = CopiedTextureBuffer::read_from( &ctx, - &skybox.prefiltered_environment_cubemap.texture, + &ibl.prefiltered_environment_cubemap.texture, mip_size as usize, mip_size as usize, 4, @@ -727,7 +411,7 @@ mod test { } } - stage.set_skybox(skybox); + stage.use_skybox(&skybox); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); @@ -739,9 +423,12 @@ mod test { fn precomputed_brdf() { assert_eq!(2, std::mem::size_of::()); let r = Context::headless(32, 32).block(); - let brdf_lut = Skybox::create_precomputed_brdf_texture(&r); - assert_eq!(wgpu::TextureFormat::Rg16Float, brdf_lut.texture.format()); - let copied_buffer = Texture::read(&r, &brdf_lut.texture, 512, 512, 2, 2); + let brdf_lut = BrdfLut::new(&r); + assert_eq!( + wgpu::TextureFormat::Rg16Float, + brdf_lut.texture().texture.format() + ); + let copied_buffer = Texture::read(&r, &brdf_lut.texture().texture, 512, 512, 2, 2); let pixels = copied_buffer.pixels(r.get_device()).block().unwrap(); let pixels: Vec = bytemuck::cast_slice::(pixels.as_slice()) .iter() diff --git a/crates/renderling/src/skybox/shader.rs b/crates/renderling/src/skybox/shader.rs new file mode 100644 index 00000000..bc9047a0 --- /dev/null +++ b/crates/renderling/src/skybox/shader.rs @@ -0,0 +1,91 @@ +//! Skybox shaders. + +use crabslab::{Id, Slab}; +use glam::{Mat3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; +use spirv_std::{ + image::{Cubemap, Image2d}, + spirv, Sampler, +}; + +#[allow(unused_imports)] +use spirv_std::num_traits::Float; + +use crate::{ + camera::shader::CameraDescriptor, + math::{self, IsVector}, +}; + +const INV_ATAN: Vec2 = Vec2::new(0.1591, core::f32::consts::FRAC_1_PI); + +/// Takes a unit direction and converts it to a uv lookup in an equirectangular +/// map. +pub fn direction_to_equirectangular_uv(dir: Vec3) -> Vec2 { + let mut uv = Vec2::new(f32::atan2(dir.z, dir.x), f32::asin(dir.y)); + uv *= INV_ATAN; + uv += 0.5; + uv +} + +/// Vertex shader for a skybox. +#[spirv(vertex)] +pub fn skybox_vertex( + #[spirv(instance_index)] camera_index: u32, + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + local_pos: &mut Vec3, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let camera_id = Id::::from(camera_index); + let camera = slab.read(camera_id); + let point = math::CUBE[vertex_index as usize]; + *local_pos = point; + let camera_view_without_translation = Mat3::from_mat4(camera.view()); + let rot_view = Mat4::from_mat3(camera_view_without_translation); + let position = camera.projection() * rot_view * point.extend(1.0); + *clip_pos = position.xyww(); +} + +/// Colors a skybox using a cubemap texture. +#[spirv(fragment)] +pub fn skybox_cubemap_fragment( + #[spirv(descriptor_set = 0, binding = 1)] texture: &Cubemap, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + local_pos: Vec3, + out_color: &mut Vec4, +) { + let env_color: Vec3 = texture.sample(*sampler, local_pos.alt_norm_or_zero()).xyz(); + *out_color = env_color.extend(1.0); +} + +/// Vertex shader that draws a cubemap. +/// +/// Uses the `instance_index` as the [`Id`] for a [`CameraDescriptor`]. +/// +/// Used to create a cubemap from an equirectangular image as well as cubemap +/// convolutions. +#[spirv(vertex)] +pub fn skybox_cubemap_vertex( + #[spirv(instance_index)] camera_id: Id, + #[spirv(vertex_index)] vertex_index: u32, + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + local_pos: &mut Vec3, + #[spirv(position)] gl_pos: &mut Vec4, +) { + let camera = slab.read(camera_id); + let pos = crate::math::CUBE[vertex_index as usize]; + *local_pos = pos; + *gl_pos = camera.view_projection() * pos.extend(1.0); +} + +/// Fragment shader that colors a skybox using an equirectangular texture. +#[spirv(fragment)] +pub fn skybox_equirectangular_fragment( + #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d, + #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler, + local_pos: Vec3, + out_color: &mut Vec4, +) { + let uv = direction_to_equirectangular_uv(local_pos.alt_norm_or_zero()); + let env_color: Vec3 = texture.sample(*sampler, uv).xyz(); + *out_color = env_color.extend(1.0); +} diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index a47e897e..69016fbc 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -1,512 +1,31 @@ -//! GPU staging area. +//! Scene staging. //! -//! The `Stage` object contains a slab buffer and a render pipeline. -//! It is used to stage objects for rendering. -use crabslab::{Array, Id, Slab, SlabItem}; -#[cfg(not(target_arch = "spirv"))] -use glam::UVec2; -use glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; -use spirv_std::{ - image::{Cubemap, Image2d, Image2dArray}, - spirv, Image, Sampler, -}; +//! The [`Stage`] is the entrypoint for staging data on the GPU and +//! interacting with lighting. -use crate::{ - bvol::BoundingSphere, geometry::GeometryDescriptor, math::IsVector, pbr::Material, - transform::Transform, -}; - -#[allow(unused_imports)] -use spirv_std::num_traits::Float; - -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] mod cpu; -#[cfg(not(target_arch = "spirv"))] +#[cfg(cpu)] pub use cpu::*; -#[cfg(all(feature = "gltf", not(target_arch = "spirv")))] -mod gltf_support; -#[cfg(all(feature = "gltf", not(target_arch = "spirv")))] -pub use gltf_support::*; - -/// A vertex skin. -/// -/// For more info on vertex skinning, see -/// -#[derive(Clone, Copy, Default, SlabItem)] -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -pub struct Skin { - // Ids of the skeleton nodes' global transforms used as joints in this skin. - pub joints: Array>, - // Contains the 4x4 inverse-bind matrices. - // - // When is none, each matrix is assumed to be the 4x4 identity matrix - // which implies that the inverse-bind matrices were pre-applied. - pub inverse_bind_matrices: Array, -} - -impl Skin { - pub fn get_joint_matrix(&self, i: usize, vertex: Vertex, slab: &[u32]) -> Mat4 { - let joint_index = vertex.joints[i] as usize; - let joint_id = slab.read(self.joints.at(joint_index)); - let joint_transform = slab.read(joint_id); - // First apply the inverse bind matrix to bring the vertex into the joint's - // local space, then apply the joint's current transformation to move it - // into world space. - let inverse_bind_matrix = slab.read(self.inverse_bind_matrices.at(joint_index)); - Mat4::from(joint_transform) * inverse_bind_matrix - } - - pub fn get_skinning_matrix(&self, vertex: Vertex, slab: &[u32]) -> Mat4 { - let mut skinning_matrix = Mat4::ZERO; - for i in 0..vertex.joints.len() { - let joint_matrix = self.get_joint_matrix(i, vertex, slab); - // Ensure weights are applied correctly to the joint matrix - let weight = vertex.weights[i]; - skinning_matrix += weight * joint_matrix; - } - - if skinning_matrix == Mat4::ZERO { - Mat4::IDENTITY - } else { - skinning_matrix - } - } -} - -/// A displacement target. -/// -/// Use to displace vertices using weights defined on the mesh. -/// -/// For more info on morph targets, see -/// -#[derive(Clone, Copy, Default, PartialEq, SlabItem)] -#[cfg_attr(cpu, derive(Debug))] -pub struct MorphTarget { - pub position: Vec3, - pub normal: Vec3, - pub tangent: Vec3, - // TODO: Extend MorphTargets to include UV and Color. - // I think this would take a contribution to the `gltf` crate. -} - -/// Returned by [`Renderlet::get_vertex_info`]. -pub struct VertexInfo { - pub vertex: Vertex, - pub transform: Transform, - pub model_matrix: Mat4, - pub world_pos: Vec3, -} - -/// A vertex in a mesh. -#[derive(Clone, Copy, core::fmt::Debug, PartialEq, SlabItem)] -pub struct Vertex { - pub position: Vec3, - pub color: Vec4, - pub uv0: Vec2, - pub uv1: Vec2, - pub normal: Vec3, - pub tangent: Vec4, - // Indices that point to this vertex's 'joint' transforms. - pub joints: [u32; 4], - // The weights of influence that each joint has over this vertex - pub weights: [f32; 4], -} - -impl Default for Vertex { - fn default() -> Self { - Self { - position: Default::default(), - color: Vec4::ONE, - uv0: Vec2::ZERO, - uv1: Vec2::ZERO, - normal: Vec3::Z, - tangent: Vec4::Y, - joints: [0; 4], - weights: [0.0; 4], - } - } -} - -impl Vertex { - pub fn with_position(mut self, p: impl Into) -> Self { - self.position = p.into(); - self - } - - pub fn with_color(mut self, c: impl Into) -> Self { - self.color = c.into(); - self - } - - pub fn with_uv0(mut self, uv: impl Into) -> Self { - self.uv0 = uv.into(); - self - } - - pub fn with_uv1(mut self, uv: impl Into) -> Self { - self.uv1 = uv.into(); - self - } - - pub fn with_normal(mut self, n: impl Into) -> Self { - self.normal = n.into(); - self - } - - pub fn with_tangent(mut self, t: impl Into) -> Self { - self.tangent = t.into(); - self - } - - pub fn generate_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 { - let ab = a - b; - let ac = a - c; - ab.cross(ac).normalize() - } - - pub fn generate_tangent(a: Vec3, a_uv: Vec2, b: Vec3, b_uv: Vec2, c: Vec3, c_uv: Vec2) -> Vec4 { - let ab = b - a; - let ac = c - a; - let n = ab.cross(ac); - let d_uv1 = b_uv - a_uv; - let d_uv2 = c_uv - a_uv; - let denom = d_uv1.x * d_uv2.y - d_uv2.x * d_uv1.y; - let denom_sign = if denom >= 0.0 { 1.0 } else { -1.0 }; - let denom = denom.abs().max(f32::EPSILON) * denom_sign; - let f = 1.0 / denom; - let s = f * Vec3::new( - d_uv2.y * ab.x - d_uv1.y * ac.x, - d_uv2.y * ab.y - d_uv1.y * ac.y, - d_uv2.y * ab.z - d_uv1.y * ac.z, - ); - let t = f * Vec3::new( - d_uv1.x * ac.x - d_uv2.x * ab.x, - d_uv1.x * ac.y - d_uv2.x * ab.y, - d_uv1.x * ac.z - d_uv2.x * ab.z, - ); - let n_cross_t_dot_s_sign = if n.cross(t).dot(s) >= 0.0 { 1.0 } else { -1.0 }; - (s - s.dot(n) * n) - .alt_norm_or_zero() - .extend(n_cross_t_dot_s_sign) - } - - #[cfg(cpu)] - /// A triangle list mesh of points. - pub fn cube_mesh() -> [Vertex; 36] { - let mut mesh = [Vertex::default(); 36]; - let unit_cube = crate::math::unit_cube(); - debug_assert_eq!(36, unit_cube.len()); - for (i, (position, normal)) in unit_cube.into_iter().enumerate() { - mesh[i].position = position; - mesh[i].normal = normal; - } - mesh - } -} - -/// A draw call used to render some geometry. -/// -/// ## Note -/// The default implentation returns a `Renderlet` with `pbr_config` set to -/// `Id::new(0)`. This corresponds to the `PbrConfig` that is maintained by -/// the [`Stage`]. -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, SlabItem)] -#[offsets] -pub struct Renderlet { - pub visible: bool, - pub vertices_array: Array, - /// Bounding sphere of the entire renderlet, in local space. - pub bounds: BoundingSphere, - pub indices_array: Array, - pub transform_id: Id, - pub material_id: Id, - pub skin_id: Id, - pub morph_targets: Array>, - pub morph_weights: Array, - pub geometry_descriptor_id: Id, -} - -impl Default for Renderlet { - fn default() -> Self { - Renderlet { - visible: true, - vertices_array: Array::default(), - bounds: BoundingSphere::default(), - indices_array: Array::default(), - transform_id: Id::NONE, - material_id: Id::NONE, - skin_id: Id::NONE, - morph_targets: Array::default(), - morph_weights: Array::default(), - geometry_descriptor_id: Id::new(0), - } - } -} - -impl Renderlet { - /// Returns the vertex at the given index and its related values. - /// - /// These values are often used in shaders, so they are grouped together. - pub fn get_vertex_info(&self, vertex_index: u32, geometry_slab: &[u32]) -> VertexInfo { - let vertex = self.get_vertex(vertex_index, geometry_slab); - let transform = self.get_transform(vertex, geometry_slab); - let model_matrix = Mat4::from(transform); - let world_pos = model_matrix.transform_point3(vertex.position); - VertexInfo { - vertex, - transform, - model_matrix, - world_pos, - } - } - /// Retrieve the transform of this `Renderlet`. - /// - /// This takes into consideration all skinning matrices. - pub fn get_transform(&self, vertex: Vertex, slab: &[u32]) -> Transform { - let config = slab.read_unchecked(self.geometry_descriptor_id); - if config.has_skinning && self.skin_id.is_some() { - let skin = slab.read(self.skin_id); - Transform::from(skin.get_skinning_matrix(vertex, slab)) - } else { - slab.read(self.transform_id) - } - } - - /// Retrieve the vertex from the slab, calculating any displacement due to - /// morph targets. - pub fn get_vertex(&self, vertex_index: u32, slab: &[u32]) -> Vertex { - let index = if self.indices_array.is_null() { - vertex_index as usize - } else { - slab.read(self.indices_array.at(vertex_index as usize)) as usize - }; - let vertex_id = self.vertices_array.at(index); - let mut vertex = slab.read_unchecked(vertex_id); - for i in 0..self.morph_targets.len() { - let morph_target_array = slab.read(self.morph_targets.at(i)); - let morph_target = slab.read(morph_target_array.at(index)); - let weight = slab.read(self.morph_weights.at(i)); - vertex.position += weight * morph_target.position; - vertex.normal += weight * morph_target.normal; - vertex.tangent += weight * morph_target.tangent.extend(0.0); - } - vertex - } - - pub fn get_vertex_count(&self) -> u32 { - if self.indices_array.is_null() { - self.vertices_array.len() as u32 - } else { - self.indices_array.len() as u32 - } - } -} - -#[cfg(test)] -/// A helper struct that contains all outputs of the Renderlet's PBR vertex shader. -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct RenderletPbrVertexInfo { - pub renderlet: Renderlet, - pub renderlet_id: Id, - pub vertex_index: u32, - pub vertex: Vertex, - pub transform: Transform, - pub model_matrix: Mat4, - pub view_projection: Mat4, - pub out_color: Vec4, - pub out_uv0: Vec2, - pub out_uv1: Vec2, - pub out_norm: Vec3, - pub out_tangent: Vec3, - pub out_bitangent: Vec3, - pub out_pos: Vec3, - pub out_clip_pos: Vec4, -} - -/// Renderlet vertex shader. -#[spirv(vertex)] -#[allow(clippy::too_many_arguments)] -pub fn renderlet_vertex( - // Points at a `Renderlet` - #[spirv(instance_index)] renderlet_id: Id, - // Which vertex within the renderlet are we rendering - #[spirv(vertex_index)] vertex_index: u32, - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], - - #[spirv(flat)] out_renderlet: &mut Id, - // TODO: Think about placing all these out values in a G-Buffer - // But do we have enough buffers + enough space on web? - // ...and can we write to buffers from vertex shaders on web? - out_color: &mut Vec4, - out_uv0: &mut Vec2, - out_uv1: &mut Vec2, - out_norm: &mut Vec3, - out_tangent: &mut Vec3, - out_bitangent: &mut Vec3, - out_world_pos: &mut Vec3, - #[spirv(position)] out_clip_pos: &mut Vec4, - // test-only info struct - #[cfg(test)] out_info: &mut RenderletPbrVertexInfo, -) { - let renderlet = geometry_slab.read_unchecked(renderlet_id); - if !renderlet.visible { - // put it outside the clipping frustum - *out_clip_pos = Vec4::new(10.0, 10.0, 10.0, 1.0); - return; - } - - *out_renderlet = renderlet_id; - - let VertexInfo { - vertex, - transform, - model_matrix, - world_pos, - } = renderlet.get_vertex_info(vertex_index, geometry_slab); - *out_color = vertex.color; - *out_uv0 = vertex.uv0; - *out_uv1 = vertex.uv1; - *out_world_pos = world_pos; - - let scale2 = transform.scale * transform.scale; - let normal = vertex.normal.alt_norm_or_zero(); - let tangent = vertex.tangent.xyz().alt_norm_or_zero(); - let normal_w: Vec3 = (model_matrix * (normal / scale2).extend(0.0)) - .xyz() - .alt_norm_or_zero(); - *out_norm = normal_w; - - let tangent_w: Vec3 = (model_matrix * tangent.extend(0.0)) - .xyz() - .alt_norm_or_zero(); - *out_tangent = tangent_w; - - let bitangent_w = normal_w.cross(tangent_w) * if vertex.tangent.w >= 0.0 { 1.0 } else { -1.0 }; - *out_bitangent = bitangent_w; - - let camera_id = geometry_slab - .read_unchecked(renderlet.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID); - let camera = geometry_slab.read(camera_id); - let clip_pos = camera.view_projection() * world_pos.extend(1.0); - *out_clip_pos = clip_pos; - #[cfg(test)] - { - *out_info = RenderletPbrVertexInfo { - renderlet_id, - vertex_index, - vertex, - transform, - model_matrix, - view_projection: camera.view_projection(), - out_clip_pos: clip_pos, - renderlet, - out_color: *out_color, - out_uv0: *out_uv0, - out_uv1: *out_uv1, - out_norm: *out_norm, - out_tangent: *out_tangent, - out_bitangent: *out_bitangent, - out_pos: *out_world_pos, - }; - } -} - -/// Renderlet fragment shader -#[allow(clippy::too_many_arguments, dead_code)] -#[spirv(fragment)] -pub fn renderlet_fragment( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32], - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] material_slab: &[u32], - #[spirv(descriptor_set = 0, binding = 2)] atlas: &Image2dArray, - #[spirv(descriptor_set = 0, binding = 3)] atlas_sampler: &Sampler, - #[spirv(descriptor_set = 0, binding = 4)] irradiance: &Cubemap, - #[spirv(descriptor_set = 0, binding = 5)] irradiance_sampler: &Sampler, - #[spirv(descriptor_set = 0, binding = 6)] prefiltered: &Cubemap, - #[spirv(descriptor_set = 0, binding = 7)] prefiltered_sampler: &Sampler, - #[spirv(descriptor_set = 0, binding = 8)] brdf: &Image2d, - #[spirv(descriptor_set = 0, binding = 9)] brdf_sampler: &Sampler, - #[spirv(storage_buffer, descriptor_set = 0, binding = 10)] light_slab: &[u32], - #[spirv(descriptor_set = 0, binding = 11)] shadow_map: &Image!(2D, type=f32, sampled, arrayed), - #[spirv(descriptor_set = 0, binding = 12)] shadow_map_sampler: &Sampler, - #[cfg(feature = "debug-slab")] - #[spirv(storage_buffer, descriptor_set = 0, binding = 13)] - debug_slab: &mut [u32], - - #[spirv(flat)] renderlet_id: Id, - #[spirv(frag_coord)] frag_coord: Vec4, - in_color: Vec4, - in_uv0: Vec2, - in_uv1: Vec2, - in_norm: Vec3, - in_tangent: Vec3, - in_bitangent: Vec3, - world_pos: Vec3, - output: &mut Vec4, -) { - // proxy to a separate impl that allows us to test on CPU - crate::pbr::fragment_impl( - atlas, - atlas_sampler, - irradiance, - irradiance_sampler, - prefiltered, - prefiltered_sampler, - brdf, - brdf_sampler, - shadow_map, - shadow_map_sampler, - geometry_slab, - material_slab, - light_slab, - renderlet_id, - frag_coord, - in_color, - in_uv0, - in_uv1, - in_norm, - in_tangent, - in_bitangent, - world_pos, - output, - ); -} - -#[cfg(feature = "test_i8_16_extraction")] -#[spirv(compute(threads(32)))] -/// A shader to ensure that we can extract i8 and i16 values from a storage -/// buffer. -pub fn test_i8_i16_extraction( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &mut [u32], - #[spirv(global_invocation_id)] global_id: UVec3, -) { - let index = global_id.x as usize; - let (value, _, _) = crate::bits::extract_i8(index, 2, slab); - if value > 0 { - slab[index] = value as u32; - } - let (value, _, _) = crate::bits::extract_i16(index, 2, slab); - if value > 0 { - slab[index] = value as u32; - } -} - #[cfg(test)] mod test { use craballoc::{prelude::SlabAllocator, runtime::CpuRuntime}; use glam::{Mat4, Quat, Vec3}; - use crate::{math::IsMatrix, stage::NestedTransform, transform::Transform}; + use crate::{ + math::IsMatrix, + transform::{shader::TransformDescriptor, NestedTransform}, + }; #[test] fn matrix_hierarchy_sanity() { - let a: Mat4 = Transform { + let a: Mat4 = TransformDescriptor { translation: Vec3::new(100.0, 100.0, 0.0), ..Default::default() } .into(); - let b: Mat4 = Transform { + let b: Mat4 = TransformDescriptor { scale: Vec3::splat(0.5), ..Default::default() } @@ -522,7 +41,7 @@ mod test { let mut mat = Mat4::IDENTITY; let mut local = Some(tfrm.clone()); while let Some(t) = local.take() { - let transform = t.get(); + let transform = t.local_descriptor(); mat = Mat4::from_scale_rotation_translation( transform.scale, transform.rotation, @@ -534,53 +53,43 @@ mod test { (t, r, s) } - #[expect(clippy::needless_borrows_for_generic_args, reason = "riffraff")] - let slab = SlabAllocator::::new(&CpuRuntime, "transform", ()); + let slab = SlabAllocator::new(CpuRuntime, "transform", ()); let a = NestedTransform::new(&slab); - a.set(Transform { - translation: Vec3::splat(100.0), - ..Default::default() - }); + a.set_local_translation(Vec3::splat(100.0)); let b = NestedTransform::new(&slab); - b.set(Transform { - rotation: Quat::from_scaled_axis(Vec3::Z), - ..Default::default() - }); + b.set_local_rotation(Quat::from_scaled_axis(Vec3::Z)); let c = NestedTransform::new(&slab); - c.set(Transform { - scale: Vec3::splat(2.0), - ..Default::default() - }); + c.set_local_scale(Vec3::splat(2.0)); a.add_child(&b); b.add_child(&c); - let Transform { + let TransformDescriptor { translation, rotation, scale, - } = c.get_global_transform(); + } = c.global_descriptor(); let global_transform = (translation, rotation, scale); let legacy_transform = legacy_get_world_transform(&c); assert_eq!(legacy_transform, global_transform); - c.modify(|t| t.translation = Vec3::ONE); + c.set_local_translation(Vec3::ONE); let all_updates = slab.get_updated_source_ids(); assert_eq!( std::collections::HashSet::from_iter([ - a.get_notifier_index(), - b.get_notifier_index(), - c.get_notifier_index() + a.global_transform.descriptor.notifier_index(), + b.global_transform.descriptor.notifier_index(), + c.global_transform.descriptor.notifier_index() ]), all_updates ); - let Transform { + let TransformDescriptor { translation, rotation, scale, - } = c.get_global_transform(); + } = c.global_descriptor(); let global_transform = (translation, rotation, scale); let legacy_transform = legacy_get_world_transform(&c); assert_eq!(legacy_transform, global_transform); diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index a0eadfa9..7db97dd8 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -1,38 +1,43 @@ //! GPU staging area. -//! -//! The `Stage` object contains a slab buffer and a render pipeline. -//! It is used to stage [`Renderlet`]s for rendering. use core::ops::Deref; use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use craballoc::prelude::*; use crabslab::Id; -use snafu::Snafu; +use glam::{Mat4, UVec2, Vec4}; +use snafu::{ResultExt, Snafu}; use std::sync::{atomic::AtomicBool, Arc, Mutex, RwLock}; +use crate::atlas::AtlasTexture; +use crate::camera::Camera; +use crate::geometry::{shader::GeometryDescriptor, MorphTarget, Vertex}; +#[cfg(gltf)] +use crate::gltf::GltfDocument; +use crate::light::{DirectionalLight, IsLight, Light, PointLight, SpotLight}; +use crate::material::Material; +use crate::pbr::brdf::BrdfLut; +use crate::pbr::ibl::Ibl; +use crate::primitive::Primitive; use crate::{ - atlas::{AtlasError, AtlasImage, AtlasImageError, AtlasTexture}, + atlas::{AtlasError, AtlasImage, AtlasImageError}, bindgroup::ManagedBindGroup, bloom::Bloom, - camera::Camera, + camera::shader::CameraDescriptor, debug::DebugOverlay, draw::DrawCalls, - geometry::Geometry, + geometry::{Geometry, Indices, MorphTargetWeights, MorphTargets, Skin, SkinJoint, Vertices}, light::{ - AnalyticalLight, Light, LightDetails, LightTiling, LightTilingConfig, Lighting, - LightingBindGroupLayoutEntries, LightingError, ShadowMap, + AnalyticalLight, LightTiling, LightTilingConfig, Lighting, LightingBindGroupLayoutEntries, + LightingError, ShadowMap, }, material::Materials, pbr::debug::DebugChannel, skybox::{Skybox, SkyboxRenderPipeline}, - stage::Renderlet, texture::{DepthTexture, Texture}, tonemapping::Tonemapping, - transform::Transform, - tuple::Bundle, + transform::{NestedTransform, Transform}, }; -use super::*; - +/// Enumeration of errors that may be the result of [`Stage`] functions. #[derive(Debug, Snafu)] pub enum StageError { #[snafu(display("{source}"))] @@ -40,6 +45,10 @@ pub enum StageError { #[snafu(display("{source}"))] Lighting { source: LightingError }, + + #[cfg(gltf)] + #[snafu(display("{source}"))] + Gltf { source: crate::gltf::StageGltfError }, } impl From for StageError { @@ -54,6 +63,13 @@ impl From for StageError { } } +#[cfg(gltf)] +impl From for StageError { + fn from(source: crate::gltf::StageGltfError) -> Self { + Self::Gltf { source } + } +} + fn create_msaa_textureview( device: &wgpu::Device, width: u32, @@ -79,14 +95,16 @@ fn create_msaa_textureview( .create_view(&wgpu::TextureViewDescriptor::default()) } +/// Result of calling [`Stage::commit`]. pub struct StageCommitResult { - pub geometry_buffer: SlabBuffer, - pub lighting_buffer: SlabBuffer, - pub materials_buffer: SlabBuffer, + pub(crate) geometry_buffer: SlabBuffer, + pub(crate) lighting_buffer: SlabBuffer, + pub(crate) materials_buffer: SlabBuffer, } impl StageCommitResult { - pub fn latest_creation_time(&self) -> usize { + /// Timestamp of the most recently created buffer used by the stage. + pub(crate) fn latest_creation_time(&self) -> usize { [ &self.geometry_buffer, &self.materials_buffer, @@ -98,7 +116,9 @@ impl StageCommitResult { .unwrap_or_default() } - pub fn should_invalidate(&self, previous_creation_time: usize) -> bool { + /// Whether or not the stage's bindgroups need to be invalidated as a result + /// of the call to [`Stage::commit`] that produced this `StageCommitResult`. + pub(crate) fn should_invalidate(&self, previous_creation_time: usize) -> bool { let mut should = false; if self.geometry_buffer.is_new_this_commit() { log::trace!("geometry buffer is new this frame"); @@ -205,7 +225,7 @@ impl RenderletBindGroup<'_> { } /// Performs a rendering of an entire scene, given the resources at hand. -pub struct StageRendering<'a> { +pub(crate) struct StageRendering<'a> { // TODO: include the rest of the needed paramaters from `stage`, and then remove `stage` pub stage: &'a Stage, pub pipeline: &'a wgpu::RenderPipeline, @@ -233,7 +253,7 @@ impl StageRendering<'_> { .get(should_invalidate_renderlet_bind_group, || { log::trace!("recreating renderlet bind group"); let atlas_texture = self.stage.materials.atlas().get_texture(); - let skybox = self.stage.skybox.read().unwrap(); + let ibl = self.stage.ibl.read().unwrap(); let shadow_map = self.stage.lighting.shadow_map_atlas.get_texture(); RenderletBindGroup { device: self.stage.device(), @@ -243,198 +263,112 @@ impl StageRendering<'_> { light_buffer: &commit_result.lighting_buffer, atlas_texture_view: &atlas_texture.view, atlas_texture_sampler: &atlas_texture.sampler, - irradiance_texture_view: &skybox.irradiance_cubemap.view, - irradiance_texture_sampler: &skybox.irradiance_cubemap.sampler, - prefiltered_texture_view: &skybox.prefiltered_environment_cubemap.view, - prefiltered_texture_sampler: &skybox - .prefiltered_environment_cubemap - .sampler, - brdf_texture_view: &skybox.brdf_lut.view, - brdf_texture_sampler: &skybox.brdf_lut.sampler, + irradiance_texture_view: &ibl.irradiance_cubemap.view, + irradiance_texture_sampler: &ibl.irradiance_cubemap.sampler, + prefiltered_texture_view: &ibl.prefiltered_environment_cubemap.view, + prefiltered_texture_sampler: &ibl.prefiltered_environment_cubemap.sampler, + brdf_texture_view: &self.stage.brdf_lut.inner.view, + brdf_texture_sampler: &self.stage.brdf_lut.inner.sampler, shadow_map_texture_view: &shadow_map.view, shadow_map_texture_sampler: &shadow_map.sampler, } .create() }); - self.stage.draw_calls.write().unwrap().upkeep(); let mut draw_calls = self.stage.draw_calls.write().unwrap(); let depth_texture = self.stage.depth_texture.read().unwrap(); // UNWRAP: safe because we know the depth texture format will always match let maybe_indirect_buffer = draw_calls.pre_draw(&depth_texture).unwrap(); - { - log::trace!("rendering"); - let label = Some("stage render"); - - let mut encoder = self - .stage - .device() - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label, - color_attachments: &[Some(self.color_attachment)], - depth_stencil_attachment: Some(self.depth_stencil_attachment), - ..Default::default() - }); - render_pass.set_pipeline(self.pipeline); - render_pass.set_bind_group(0, Some(renderlet_bind_group.as_ref()), &[]); - draw_calls.draw(&mut render_pass); - - let has_skybox = self.stage.has_skybox.load(Ordering::Relaxed); - if has_skybox { - let (pipeline, bindgroup) = self - .stage - .get_skybox_pipeline_and_bindgroup(&commit_result.geometry_buffer); - render_pass.set_pipeline(&pipeline.pipeline); - render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]); - let camera_id = self.stage.geometry.descriptor().get().camera_id.inner(); - render_pass.draw(0..36, camera_id..camera_id + 1); - } - } - let sindex = self.stage.queue().submit(std::iter::once(encoder.finish())); - (sindex, maybe_indirect_buffer) - } - } -} + log::trace!("rendering"); + let label = Some("stage render"); -/// A helper struct to build [`Renderlet`]s in the [`Geometry`] manager. -pub struct RenderletBuilder<'a, T> { - data: Renderlet, - resources: T, - stage: &'a Stage, -} + let mut encoder = self + .stage + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label, + color_attachments: &[Some(self.color_attachment)], + depth_stencil_attachment: Some(self.depth_stencil_attachment), + ..Default::default() + }); -impl<'a> RenderletBuilder<'a, ()> { - pub fn new(stage: &'a Stage) -> Self { - RenderletBuilder { - data: Renderlet::default(), - resources: (), - stage, + render_pass.set_pipeline(self.pipeline); + render_pass.set_bind_group(0, Some(renderlet_bind_group.as_ref()), &[]); + draw_calls.draw(&mut render_pass); + + let has_skybox = self.stage.has_skybox.load(Ordering::Relaxed); + if has_skybox { + let (pipeline, bindgroup) = self + .stage + .get_skybox_pipeline_and_bindgroup(&commit_result.geometry_buffer); + render_pass.set_pipeline(&pipeline.pipeline); + render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]); + let camera_id = self.stage.geometry.descriptor().get().camera_id.inner(); + render_pass.draw(0..36, camera_id..camera_id + 1); + } } + let sindex = self.stage.queue().submit(std::iter::once(encoder.finish())); + (sindex, maybe_indirect_buffer) } } -impl<'a, T: Bundle> RenderletBuilder<'a, T> { - pub fn suffix(self, element: S) -> RenderletBuilder<'a, T::Suffixed> { - RenderletBuilder { - data: self.data, - resources: self.resources.suffix(element), - stage: self.stage, - } - } - - pub fn with_vertices_array(mut self, array: Array) -> Self { - self.data.vertices_array = array; - self - } - - pub fn with_vertices( - mut self, - vertices: impl IntoIterator, - ) -> RenderletBuilder<'a, T::Suffixed>> { - let vertices = self.stage.geometry.new_vertices(vertices); - self.data.vertices_array = vertices.array(); - self.suffix(vertices) - } - - pub fn with_indices( - mut self, - indices: impl IntoIterator, - ) -> RenderletBuilder<'a, T::Suffixed>> { - let indices = self.stage.geometry.new_indices(indices); - self.data.indices_array = indices.array(); - self.suffix(indices) - } - - pub fn with_transform_id(mut self, transform_id: Id) -> Self { - self.data.transform_id = transform_id; - self - } - - pub fn with_transform( - mut self, - transform: Transform, - ) -> RenderletBuilder<'a, T::Suffixed>> { - let transform = self.stage.geometry.new_transform(transform); - self.data.transform_id = transform.id(); - self.suffix(transform) - } - - pub fn with_nested_transform(mut self, transform: &NestedTransform) -> Self { - self.data.transform_id = transform.global_transform_id(); - self - } - - pub fn with_material_id(mut self, material_id: Id) -> Self { - self.data.material_id = material_id; - self - } - - pub fn with_material( - mut self, - material: Material, - ) -> RenderletBuilder<'a, T::Suffixed>> { - let material = self.stage.materials.new_material(material); - self.data.material_id = material.id(); - self.suffix(material) - } - - pub fn with_skin_id(mut self, skin_id: Id) -> Self { - self.data.skin_id = skin_id; - self - } - - pub fn with_morph_targets( - mut self, - morph_targets: impl IntoIterator>, - ) -> (Self, HybridArray>) { - let morph_targets = self.stage.geometry.new_morph_targets_array(morph_targets); - self.data.morph_targets = morph_targets.array(); - (self, morph_targets) - } - - pub fn with_morph_weights( - mut self, - morph_weights: impl IntoIterator, - ) -> (Self, HybridArray) { - let morph_weights = self.stage.geometry.new_weights(morph_weights); - self.data.morph_weights = morph_weights.array(); - (self, morph_weights) - } - - pub fn with_geometry_descriptor_id(mut self, pbr_config_id: Id) -> Self { - self.data.geometry_descriptor_id = pbr_config_id; - self - } - - pub fn with_bounds(mut self, bounds: impl Into) -> Self { - self.data.bounds = bounds.into(); - self - } - - /// Build the [`Renderlet`], add it to the [`Stage`] with [`Stage::add_renderlet`] and - /// return the [`Hybrid`] along with any resources that were staged. - /// - /// The returned value will be a tuple with the [`Hybrid`] as the head, and - /// all other resources added as the tail. - pub fn build(self) -> > as Bundle>::Reduced - where - T::Suffixed>: Bundle, - { - let renderlet = self.stage.geometry.new_renderlet(self.data); - self.stage.add_renderlet(&renderlet); - self.resources.suffix(renderlet).reduce() - } -} - -/// Represents an entire scene worth of rendering data. +/// Entrypoint for staging data on the GPU and interacting with lighting. +/// +/// # Design +/// +/// The `Stage` struct serves as the central hub for managing and staging data on the GPU. +/// It provides a consistent API for creating resources, applying effects, and customizing parameters. +/// +/// The `Stage` uses a combination of `new_*`, `with_*`, `set_*`, and getter functions to facilitate +/// resource management and customization. +/// +/// Resources are managed internally, requiring no additional lifecycle work from the user. +/// This design simplifies the process of resource management, allowing developers to focus on creating and rendering +/// their scenes without worrying about the underlying GPU resource management. /// -/// A clone of a stage is a reference to the same stage. +/// # Resources /// -/// ## Note -/// Only available on the CPU. Not available in shaders. +/// The `Stage` is responsible for creating various resources and staging them on the GPU. +/// It handles the setup and management of the following resources: +/// +/// * [`Camera`]: Manages the view and projection matrices for rendering scenes. +/// - [`Stage::new_camera`] creates a new [`Camera`]. +/// - [`Stage::use_camera`] tells the `Stage` to use a camera. +/// * [`Transform`]: Represents the position, rotation, and scale of objects. +/// - [`Stage::new_transform`] creates a new [`Transform`]. +/// * [`NestedTransform`]: Allows for hierarchical transformations, useful for complex object hierarchies. +/// - [`Stage::new_nested_transform`] creates a new [`NestedTransform`] +/// * [`Vertices`]: Manages vertex data for rendering meshes. +/// - [`Stage::new_vertices`] +/// * [`Indices`]: Manages index data for rendering meshes with indexed drawing. +/// - [`Stage::new_indices`] +/// * [`Primitive`]: Represents a drawable object in the scene. +/// - [`Stage::new_primitive`] +/// * [`GltfDocument`]: Handles loading and managing GLTF assets. +/// - [`Stage::load_gltf_document_from_path`] loads a new GLTF document from the local filesystem. +/// - [`Stage::load_gltf_document_from_bytes`] parses a new GLTF document from pre-loaded bytes. +/// * [`Skin`]: Animation and rigging information. +/// - [`Stage::new_skin`] +/// +/// # Lighting effects +/// +/// The `Stage` also manages various lighting effects, which enhance the visual quality of the scene: +/// +/// * [`AnalyticalLight`]: Simulates a single light source, with three flavors: +/// - [`DirectionalLight`]: Represents sunlight or other distant light sources. +/// - [`PointLight`]: Represents a light source that emits light in all directions from a single point. +/// - [`SpotLight`]: Represents a light source that emits light in a cone shape. +/// * [`Skybox`]: Provides image-based lighting (IBL) for realistic environmental reflections and ambient lighting. +/// * [`Bloom`]: Adds a glow effect to bright areas of the scene, enhancing visual appeal. +/// * [`ShadowMap`]: Manages shadow mapping for realistic shadow rendering. +/// * [`LightTiling`]: Optimizes lighting calculations by dividing the scene into tiles for efficient processing. +/// +/// # Note +/// +/// Clones of [`Stage`] all point to the same underlying data. #[derive(Clone)] pub struct Stage { pub(crate) geometry: Geometry, @@ -460,8 +394,13 @@ pub struct Stage { pub(crate) debug_overlay: DebugOverlay, pub(crate) background_color: Arc>, + pub(crate) brdf_lut: BrdfLut, + + pub(crate) ibl: Arc>, + pub(crate) skybox: Arc>, pub(crate) skybox_bindgroup: Arc>>>, + // TODO: remove Stage.has_skybox, replace with Skybox::is_empty pub(crate) has_skybox: Arc, pub(crate) has_bloom: Arc, @@ -498,96 +437,118 @@ impl AsRef for Stage { } } -/// Geometry methods. +#[cfg(gltf)] +/// GLTF functions impl Stage { - /// Stage a [`Camera`] on the GPU. + pub fn load_gltf_document_from_path( + &self, + path: impl AsRef, + ) -> Result { + use snafu::ResultExt; + + let (document, buffers, images) = + gltf::import(&path).with_context(|_| crate::gltf::GltfSnafu { + path: Some(path.as_ref().to_path_buf()), + })?; + GltfDocument::from_gltf(self, &document, buffers, images) + } + + pub fn load_gltf_document_from_bytes( + &self, + bytes: impl AsRef<[u8]>, + ) -> Result { + let (document, buffers, images) = + gltf::import_slice(bytes).context(crate::gltf::GltfSnafu { path: None })?; + GltfDocument::from_gltf(self, &document, buffers, images) + } +} + +/// Geometry functions +impl Stage { + /// Returns the vertices of a white unit cube. + /// + /// This is the mesh of every [`Primitive`] that has not had its vertices set. + pub fn default_vertices(&self) -> &Vertices { + self.geometry.default_vertices() + } + + /// Stage a new [`Camera`] on the GPU. /// - /// If the camera has not been set, this camera will be used - /// automatically. - pub fn new_camera(&self, camera: Camera) -> Hybrid { - self.geometry.new_camera(camera) + /// If no camera is currently in use on the [`Stage`] through + /// [`Stage::use_camera`], this new camera will be used automatically. + pub fn new_camera(&self) -> Camera { + self.geometry.new_camera() } /// Use the given camera when rendering. - pub fn use_camera(&self, camera: impl AsRef>) { - self.geometry.use_camera(camera); + pub fn use_camera(&self, camera: impl AsRef) { + self.geometry.use_camera(camera.as_ref()); } /// Return the `Id` of the camera currently in use. - pub fn used_camera_id(&self) -> Id { + pub fn used_camera_id(&self) -> Id { self.geometry.descriptor().get().camera_id } /// Set the default camera `Id`. - pub fn use_camera_id(&self, camera_id: Id) { + pub fn use_camera_id(&self, camera_id: Id) { self.geometry .descriptor() .modify(|desc| desc.camera_id = camera_id); } /// Stage a [`Transform`] on the GPU. - pub fn new_transform(&self, transform: Transform) -> Hybrid { - self.geometry.new_transform(transform) + pub fn new_transform(&self) -> Transform { + self.geometry.new_transform() } /// Stage some vertex geometry data. - pub fn new_vertices(&self, data: impl IntoIterator) -> HybridArray { + pub fn new_vertices(&self, data: impl IntoIterator) -> Vertices { self.geometry.new_vertices(data) } /// Stage some vertex index data. - pub fn new_indices(&self, data: impl IntoIterator) -> HybridArray { + pub fn new_indices(&self, data: impl IntoIterator) -> Indices { self.geometry.new_indices(data) } - /// Create new morph targets. - // TODO: Move `MorphTarget` to geometry. + /// Stage new morph targets. pub fn new_morph_targets( &self, - data: impl IntoIterator, - ) -> HybridArray { + data: impl IntoIterator>, + ) -> MorphTargets { self.geometry.new_morph_targets(data) } - /// Create an array of morph target arrays. - pub fn new_morph_targets_array( + /// Stage new morph target weights. + pub fn new_morph_target_weights( &self, - data: impl IntoIterator>, - ) -> HybridArray> { - let morph_targets = data.into_iter(); - self.geometry.new_morph_targets_array(morph_targets) + data: impl IntoIterator, + ) -> MorphTargetWeights { + self.geometry.new_morph_target_weights(data) } - /// Create new morph target weights. - pub fn new_weights(&self, data: impl IntoIterator) -> HybridArray { - self.geometry.new_weights(data) - } - - /// Create a new array of joint transform ids that each point to a [`Transform`]. - pub fn new_joint_transform_ids( + /// Stage a new skin. + pub fn new_skin( &self, - data: impl IntoIterator>, - ) -> HybridArray> { - self.geometry.new_joint_transform_ids(data) - } - - /// Create a new array of matrices. - pub fn new_matrices(&self, data: impl IntoIterator) -> HybridArray { - self.geometry.new_matrices(data) - } - - /// Create a new skin. - // TODO: move `Skin` to geometry. - pub fn new_skin(&self, skin: Skin) -> Hybrid { - self.geometry.new_skin(skin) + joints: impl IntoIterator>, + inverse_bind_matrices: impl IntoIterator>, + ) -> Skin { + self.geometry.new_skin(joints, inverse_bind_matrices) } - /// Stage a new [`Renderlet`]. + /// Stage a new [`Primitive`] on the GPU. /// - /// The `Renderlet` should still be added to the draw list with - /// [`Stage::add_renderlet`]. - pub fn new_renderlet(&self, renderlet: Renderlet) -> Hybrid { - self.geometry.new_renderlet(renderlet) + /// The returned [`Primitive`] will automatically be added to this [`Stage`]. + /// + /// The returned [`Primitive`] will have the stage's default [`Vertices`], which is an all-white + /// unit cube. + /// + /// The returned [`Primitive`] uses the stage's default [`Material`], which is white and + /// **does not** participate in lighting. To change this, first create a [`Material`] with + /// [`Stage::new_material`] and then call [`Primitive::set_material`] with the new material. + pub fn new_primitive(&self) -> Primitive { + Primitive::new(self) } /// Returns a reference to the descriptor stored at the root of the @@ -599,14 +560,18 @@ impl Stage { /// Materials methods. impl Stage { - /// Stage a new [`Material`] on the GPU. - pub fn new_material(&self, material: Material) -> Hybrid { - self.materials.new_material(material) + /// Returns the default [`Material`]. + /// + /// The default is an all-white matte material. + pub fn default_material(&self) -> &Material { + self.materials.default_material() } - /// Create an array of materials, stored contiguously. - pub fn new_materials(&self, data: impl IntoIterator) -> HybridArray { - self.materials.new_materials(data) + /// Stage a new [`Material`] on the GPU. + /// + /// The returned [`Material`] can be customized using the builder pattern. + pub fn new_material(&self) -> Material { + self.materials.new_material() } /// Set the size of the atlas. @@ -631,7 +596,7 @@ impl Stage { pub fn add_images( &self, images: impl IntoIterator>, - ) -> Result>, StageError> { + ) -> Result, StageError> { let images = images.into_iter().map(|i| i.into()).collect::>(); let frames = self.materials.atlas().add_images(&images)?; @@ -661,7 +626,7 @@ impl Stage { pub fn set_images( &self, images: impl IntoIterator>, - ) -> Result>, StageError> { + ) -> Result, StageError> { let images = images.into_iter().map(|i| i.into()).collect::>(); let frames = self.materials.atlas().set_images(&images)?; @@ -674,57 +639,63 @@ impl Stage { /// Lighting methods. impl Stage { - /// Stage a new analytical light. - /// - /// `T` must be one of: - /// - [`DirectionalLightDescriptor`](crate::light::DirectionalLightDescriptor) - /// - [`SpotLightDescriptor`](crate::light::SpotLightDescriptor) - /// - [`PointLightDescriptor`](crate::light::PointLightDescriptor) - pub fn new_analytical_light(&self, light_descriptor: T) -> AnalyticalLight - where - T: Clone + Copy + SlabItem + Send + Sync, - Light: From>, - LightDetails: From>, - { - self.lighting.new_analytical_light(light_descriptor) + /// Stage a new directional light. + pub fn new_directional_light(&self) -> AnalyticalLight { + self.lighting.new_directional_light() + } + + /// Stage a new point light. + pub fn new_point_light(&self) -> AnalyticalLight { + self.lighting.new_point_light() + } + + /// Stage a new spot light. + pub fn new_spot_light(&self) -> AnalyticalLight { + self.lighting.new_spot_light() } - /// Add an [`AnalyticalLightBundle`] to the internal list of lights. + /// Add an [`AnalyticalLight`] to the internal list of lights. /// - /// This is called implicitly by [`Stage::new_analytical_light`]. + /// This is called implicitly by `Stage::new_*_light`. /// /// This can be used to add the light back to the scene after using /// [`Stage::remove_light`]. - pub fn add_light(&self, bundle: &AnalyticalLight) { + pub fn add_light(&self, bundle: &AnalyticalLight) + where + T: IsLight, + Light: From, + { self.lighting.add_light(bundle) } - /// Remove an [`AnalyticalLightBundle`] from the internal list of lights. + /// Remove an [`AnalyticalLight`] from the internal list of lights. /// /// Use this to exclude a light from rendering, without dropping the light. /// /// After calling this function you can include the light again using [`Stage::add_light`]. - pub fn remove_light(&self, bundle: &AnalyticalLight) { + pub fn remove_light(&self, bundle: &AnalyticalLight) { self.lighting.remove_light(bundle); } - /// Enable shadow mapping for the given [`AnalyticalLightBundle`], creating + /// Enable shadow mapping for the given [`AnalyticalLight`], creating /// a new [`ShadowMap`]. /// /// ## Tips for making a good shadow map /// /// 1. Make sure the map is big enough. /// Using a big map can fix some peter panning issues, even before - /// messing with bias in the [`ShadowMapDescriptor`]. + /// playing with bias in the returned [`ShadowMap`]. /// The bigger the map, the cleaner the shadows will be. This can /// also solve PCF problems. - /// 2. Don't set PCF samples too high in the [`ShadowMapDescriptor`], as + /// 2. Don't set PCF samples too high in the returned [`ShadowMap`], as /// this can _cause_ peter panning. /// 3. Ensure the **znear** and **zfar** parameters make sense, as the /// shadow map uses these to determine how much of the scene to cover. - pub fn new_shadow_map( + /// If you find that shadows are cut off in a straight line, it's likely + /// `znear` or `zfar` needs adjustment. + pub fn new_shadow_map( &self, - analytical_light_bundle: &AnalyticalLight, + analytical_light: &AnalyticalLight, // Size of the shadow map size: UVec2, // Distance to the near plane of the shadow map's frustum. @@ -735,10 +706,14 @@ impl Stage { // // Only objects within the shadow map's frustum will cast shadows. z_far: f32, - ) -> Result { + ) -> Result + where + T: IsLight, + Light: From, + { Ok(self .lighting - .new_shadow_map(analytical_light_bundle, size, z_near, z_far)?) + .new_shadow_map(analytical_light, size, z_near, z_far)?) } /// Enable light tiling, creating a new [`LightTiling`]. @@ -755,6 +730,132 @@ impl Stage { } } +/// Skybox methods +impl Stage { + /// Return the cached skybox render pipeline, creating it if necessary. + fn get_skybox_pipeline_and_bindgroup( + &self, + geometry_slab_buffer: &wgpu::Buffer, + ) -> (Arc, Arc) { + let msaa_sample_count = self.msaa_sample_count.load(Ordering::Relaxed); + // UNWRAP: safe because we're only ever called from the render thread. + let mut pipeline_guard = self.skybox_pipeline.write().unwrap(); + let pipeline = if let Some(pipeline) = pipeline_guard.as_mut() { + if pipeline.msaa_sample_count() != msaa_sample_count { + *pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline( + self.device(), + Texture::HDR_TEXTURE_FORMAT, + Some(msaa_sample_count), + )); + } + pipeline.clone() + } else { + let pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline( + self.device(), + Texture::HDR_TEXTURE_FORMAT, + Some(msaa_sample_count), + )); + *pipeline_guard = Some(pipeline.clone()); + pipeline + }; + // UNWRAP: safe because we're only ever called from the render thread. + let mut bindgroup = self.skybox_bindgroup.lock().unwrap(); + let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() { + bindgroup.clone() + } else { + let bg = Arc::new(crate::skybox::create_skybox_bindgroup( + self.device(), + geometry_slab_buffer, + self.skybox.read().unwrap().environment_cubemap_texture(), + )); + *bindgroup = Some(bg.clone()); + bg + }; + (pipeline, bindgroup) + } + + /// Used the given [`Skybox`]. + /// + /// To remove the currently used [`Skybox`], call [`Skybox::remove_skybox`]. + pub fn use_skybox(&self, skybox: &Skybox) -> &Self { + // UNWRAP: if we can't acquire the lock we want to panic. + let mut guard = self.skybox.write().unwrap(); + *guard = skybox.clone(); + self.has_skybox + .store(true, std::sync::atomic::Ordering::Relaxed); + *self.skybox_bindgroup.lock().unwrap() = None; + *self.textures_bindgroup.lock().unwrap() = None; + self + } + + /// Removes the currently used [`Skybox`]. + /// + /// Returns the currently used [`Skybox`], if any. + /// + /// After calling this the [`Stage`] will not render with any [`Skybox`], until + /// [`Skybox::use_skybox`] is called with another [`Skybox`]. + pub fn remove_skybox(&self) -> Option { + let mut guard = self.skybox.write().unwrap(); + if guard.is_empty() { + // Do nothing, the skybox is already empty + None + } else { + let skybox = guard.clone(); + *guard = Skybox::empty(self.runtime()); + Some(skybox) + } + } + + /// Returns a new [`Skybox`] using the HDR image at the given path, if possible. + /// + /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`]. + pub fn new_skybox_from_path( + &self, + path: impl AsRef, + ) -> Result { + let hdr = AtlasImage::from_hdr_path(path)?; + Ok(Skybox::new(self.runtime(), hdr)) + } + + /// Returns a new [`Skybox`] using the bytes of an HDR image, if possible. + /// + /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`]. + pub fn new_skybox_from_bytes(&self, bytes: &[u8]) -> Result { + let hdr = AtlasImage::from_hdr_bytes(bytes)?; + Ok(Skybox::new(self.runtime(), hdr)) + } +} + +/// Image based lighting methods +impl Stage { + /// Crate a new [`Ibl`] from the given [`Skybox`]. + pub fn new_ibl(&self, skybox: &Skybox) -> Ibl { + Ibl::new(self.runtime(), skybox) + } + + /// Use the given image based lighting. + /// + /// Use [`Stage::new_ibl`] to create a new [`Ibl`]. + pub fn use_ibl(&self, ibl: &Ibl) -> &Self { + let mut guard = self.ibl.write().unwrap(); + *guard = ibl.clone(); + self + } + + /// Remove the current image based lighting from the stage and return it, if any. + pub fn remove_ibl(&self) -> Option { + let mut guard = self.ibl.write().unwrap(); + if guard.is_empty() { + // Do nothing, we're already not using IBL + None + } else { + let ibl = guard.clone(); + *guard = Ibl::new(self.runtime(), &Skybox::empty(self.runtime())); + Some(ibl) + } + } +} + impl Stage { /// Returns the runtime. pub fn runtime(&self) -> &WgpuRuntime { @@ -769,20 +870,46 @@ impl Stage { &self.runtime().queue } - pub fn hdr_texture(&self) -> impl Deref + '_ { - self.hdr_texture.read().unwrap() + /// Returns a reference to the [`BrdfLut`]. + /// + /// This is used for creating skyboxes used in image based lighting. + pub fn brdf_lut(&self) -> &BrdfLut { + &self.brdf_lut } - pub fn builder(&self) -> RenderletBuilder<'_, ()> { - RenderletBuilder::new(self) + /// Sum the byte size of all used GPU memory. + /// + /// Adds together the byte size of all underlying slab buffers. + /// + /// ## Note + /// This does not take into consideration staged data that has not yet + /// been committed with either [`Stage::commit`] or [`Stage::render`]. + pub fn used_gpu_buffer_byte_size(&self) -> usize { + let num_u32s = self.geometry.slab_allocator().len() + + self.lighting.slab_allocator().len() + + self.materials.slab_allocator().len() + + self.bloom.slab_allocator().len() + + self.tonemapping.slab_allocator().len() + + self + .draw_calls + .read() + .unwrap() + .drawing_strategy() + .as_indirect() + .map(|draws| draws.slab_allocator().len()) + .unwrap_or_default(); + 4 * num_u32s + } + + pub fn hdr_texture(&self) -> impl Deref + '_ { + self.hdr_texture.read().unwrap() } /// Run all upkeep and commit all staged changes to the GPU. /// - /// This is done implicitly in [`Stage::render`] and [`StageRendering::run`]. + /// This is done implicitly in [`Stage::render`]. /// - /// This can be used after dropping [`Hybrid`] or [`Gpu`] resources to reclaim - /// those resources on the GPU. + /// This can be used after dropping resources to reclaim those resources on the GPU. #[must_use] pub fn commit(&self) -> StageCommitResult { let (materials_atlas_texture_was_recreated, materials_buffer) = self.materials.commit(); @@ -906,15 +1033,15 @@ impl Stage { }) } - pub fn create_renderlet_pipeline( + pub fn create_primitive_pipeline( device: &wgpu::Device, fragment_color_format: wgpu::TextureFormat, multisample_count: u32, ) -> wgpu::RenderPipeline { log::trace!("creating stage render pipeline"); let label = Some("renderlet"); - let vertex_linkage = crate::linkage::renderlet_vertex::linkage(device); - let fragment_linkage = crate::linkage::renderlet_fragment::linkage(device); + let vertex_linkage = crate::linkage::primitive_vertex::linkage(device); + let fragment_linkage = crate::linkage::primitive_fragment::linkage(device); let bind_group_layout = Self::renderlet_pipeline_bindgroup_layout(device); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { @@ -969,7 +1096,7 @@ impl Stage { } /// Create a new stage. - pub fn new(ctx: &crate::Context) -> Self { + pub fn new(ctx: &crate::context::Context) -> Self { let runtime = ctx.runtime(); let device = &runtime.device; let resolution @ UVec2 { x: w, y: h } = ctx.get_size(); @@ -1000,7 +1127,7 @@ impl Stage { ctx.get_render_target().format().add_srgb_suffix(), &bloom.get_mix_texture(), ); - let stage_pipeline = Self::create_renderlet_pipeline( + let stage_pipeline = Self::create_primitive_pipeline( device, wgpu::TextureFormat::Rgba16Float, multisample_count, @@ -1008,6 +1135,10 @@ impl Stage { let geometry_buffer = geometry.slab_allocator().commit(); let lighting = Lighting::new(stage_config.shadow_map_atlas_size, &geometry); + let brdf_lut = BrdfLut::new(runtime); + let skybox = Skybox::empty(runtime); + let ibl = Ibl::new(runtime, &skybox); + Self { materials, draw_calls: Arc::new(RwLock::new(DrawCalls::new( @@ -1025,10 +1156,12 @@ impl Stage { renderlet_bind_group: ManagedBindGroup::default(), renderlet_bind_group_created: Arc::new(0.into()), - skybox: Arc::new(RwLock::new(Skybox::empty(runtime))), + ibl: Arc::new(RwLock::new(ibl)), + skybox: Arc::new(RwLock::new(skybox)), skybox_bindgroup: Default::default(), skybox_pipeline: Default::default(), has_skybox: Arc::new(AtomicBool::new(false)), + brdf_lut, bloom, tonemapping, has_bloom: AtomicBool::from(true).into(), @@ -1081,7 +1214,7 @@ impl Stage { log::debug!("setting multisample count to {multisample_count}"); // UNWRAP: POP - *self.renderlet_pipeline.write().unwrap() = Self::create_renderlet_pipeline( + *self.renderlet_pipeline.write().unwrap() = Self::create_primitive_pipeline( self.device(), wgpu::TextureFormat::Rgba16Float, multisample_count, @@ -1281,17 +1414,6 @@ impl Stage { self } - /// Set the skybox. - pub fn set_skybox(&self, skybox: Skybox) { - // UNWRAP: if we can't acquire the lock we want to panic. - let mut guard = self.skybox.write().unwrap(); - *guard = skybox; - self.has_skybox - .store(true, std::sync::atomic::Ordering::Relaxed); - *self.skybox_bindgroup.lock().unwrap() = None; - *self.textures_bindgroup.lock().unwrap() = None; - } - /// Turn the bloom effect on or off. pub fn set_has_bloom(&self, has_bloom: bool) { self.has_bloom @@ -1331,92 +1453,36 @@ impl Stage { self } - /// Return the skybox render pipeline, creating it if necessary. - fn get_skybox_pipeline_and_bindgroup( - &self, - geometry_slab_buffer: &wgpu::Buffer, - ) -> (Arc, Arc) { - let msaa_sample_count = self.msaa_sample_count.load(Ordering::Relaxed); - // UNWRAP: safe because we're only ever called from the render thread. - let mut pipeline_guard = self.skybox_pipeline.write().unwrap(); - let pipeline = if let Some(pipeline) = pipeline_guard.as_mut() { - if pipeline.msaa_sample_count() != msaa_sample_count { - *pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline( - self.device(), - Texture::HDR_TEXTURE_FORMAT, - Some(msaa_sample_count), - )); - } - pipeline.clone() - } else { - let pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline( - self.device(), - Texture::HDR_TEXTURE_FORMAT, - Some(msaa_sample_count), - )); - *pipeline_guard = Some(pipeline.clone()); - pipeline - }; - // UNWRAP: safe because we're only ever called from the render thread. - let mut bindgroup = self.skybox_bindgroup.lock().unwrap(); - let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() { - bindgroup.clone() - } else { - let bg = Arc::new(crate::skybox::create_skybox_bindgroup( - self.device(), - geometry_slab_buffer, - &self.skybox.read().unwrap().environment_cubemap, - )); - *bindgroup = Some(bg.clone()); - bg - }; - (pipeline, bindgroup) - } - - /// Adds a renderlet to the internal list of renderlets to be drawn each + /// Adds a primitive to the internal list of renderlets to be drawn each /// frame. /// + /// Returns the number of primitives added. + /// /// If you drop the renderlet and no other references are kept, it will be /// removed automatically from the internal list and will cease to be /// drawn each frame. - pub fn add_renderlet(&self, renderlet: &Hybrid) { + pub fn add_primitive(&self, renderlet: &Primitive) -> usize { // UNWRAP: if we can't acquire the lock we want to panic. let mut draws = self.draw_calls.write().unwrap(); - draws.add_renderlet(renderlet); + draws.add_primitive(renderlet) } - /// Erase the given renderlet from the internal list of renderlets to be + /// Erase the given primitive from the internal list of primitives to be /// drawn each frame. - pub fn remove_renderlet(&self, renderlet: &Hybrid) { + /// + /// Returns the number of primitives added. + pub fn remove_primitive(&self, renderlet: &Primitive) -> usize { let mut draws = self.draw_calls.write().unwrap(); - draws.remove_renderlet(renderlet); + draws.remove_primitive(renderlet) } - /// Re-order the renderlets. + /// Sort the drawing order of renderlets. /// - /// This determines the order in which they are drawn each frame. - /// - /// If the `order` iterator is missing any renderlet ids, those missing - /// renderlets will be drawn _before_ the ordered ones, in no particular - /// order. - pub fn reorder_renderlets(&self, order: impl IntoIterator>) { - log::trace!("reordering renderlets"); + /// This determines the order in which [`Primitive`]s are drawn each frame. + pub fn sort_renderlets(&self, f: impl Fn(&Primitive, &Primitive) -> std::cmp::Ordering) { // UNWRAP: panic on purpose let mut guard = self.draw_calls.write().unwrap(); - guard.reorder_renderlets(order); - } - - /// Iterator over all staged [`Renderlet`]s. - /// - /// This iterator returns `Renderlets` wrapped in `WeakHybrid`, because they - /// are stored by weak references internally. - /// - /// You should have references of your own, but this is here as a convenience - /// method, and is used internally. - pub fn renderlets_iter(&self) -> impl Iterator> { - // UNWRAP: panic on purpose - let guard = self.draw_calls.read().unwrap(); - guard.renderlets_iter().collect::>().into_iter() + guard.sort_primitives(f); } /// Returns a clone of the current depth texture. @@ -1427,19 +1493,6 @@ impl Stage { } } - pub fn new_skybox_from_path( - &self, - path: impl AsRef, - ) -> Result { - let hdr = AtlasImage::from_hdr_path(path)?; - Ok(Skybox::new(self.runtime(), hdr)) - } - - pub fn new_skybox_from_bytes(&self, bytes: &[u8]) -> Result { - let hdr = AtlasImage::from_hdr_bytes(bytes)?; - Ok(Skybox::new(self.runtime(), hdr)) - } - /// Create a new [`NestedTransform`]. pub fn new_nested_transform(&self) -> NestedTransform { NestedTransform::new(self.geometry.slab_allocator()) @@ -1552,172 +1605,17 @@ impl Stage { } } -/// Manages scene heirarchy on the [`Stage`]. -/// -/// Clones all reference the same nested transform. -/// -/// Only available on CPU. -#[derive(Clone)] -pub struct NestedTransform { - pub(crate) global_transform: Ct::Container, - local_transform: Arc>, - children: Arc>>, - parent: Arc>>, -} - -impl core::fmt::Debug for NestedTransform { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let children = self - .children - .read() - .unwrap() - .iter() - .map(|nt| nt.global_transform.id()) - .collect::>(); - let parent = self - .parent - .read() - .unwrap() - .as_ref() - .map(|nt| nt.global_transform.id()); - f.debug_struct("NestedTransform") - .field("local_transform", &self.local_transform) - .field("children", &children) - .field("parent", &parent) - .finish() - } -} - -impl NestedTransform { - pub fn from_hybrid(hybrid: &NestedTransform) -> Self { - Self { - global_transform: WeakHybrid::from_hybrid(&hybrid.global_transform), - local_transform: hybrid.local_transform.clone(), - children: hybrid.children.clone(), - parent: hybrid.parent.clone(), - } - } - - pub(crate) fn upgrade(&self) -> Option { - Some(NestedTransform { - global_transform: self.global_transform.upgrade()?, - local_transform: self.local_transform.clone(), - children: self.children.clone(), - parent: self.parent.clone(), - }) - } -} - -impl NestedTransform { - pub fn new(slab: &SlabAllocator) -> Self { - let nested = NestedTransform { - local_transform: Arc::new(RwLock::new(Transform::default())), - global_transform: slab.new_value(Transform::default()), - children: Default::default(), - parent: Default::default(), - }; - nested.mark_dirty(); - nested - } - - pub fn get_notifier_index(&self) -> SourceId { - self.global_transform.notifier_index() - } - - fn mark_dirty(&self) { - self.global_transform.set(self.get_global_transform()); - for child in self.children.read().unwrap().iter() { - child.mark_dirty(); - } - } - - /// Modify the local transform. - /// - /// This automatically applies the local transform to the global transform. - pub fn modify(&self, f: impl Fn(&mut Transform)) { - { - // UNWRAP: panic on purpose - let mut local_guard = self.local_transform.write().unwrap(); - f(&mut local_guard); - } - self.mark_dirty() - } - - /// Set the local transform. - /// - /// This automatically applies the local transform to the global transform. - pub fn set(&self, transform: Transform) { - self.modify(move |t| { - *t = transform; - }); - } - - /// Returns the local transform. - pub fn get(&self) -> Transform { - *self.local_transform.read().unwrap() - } - - /// Returns the global transform. - pub fn get_global_transform(&self) -> Transform { - let maybe_parent_guard = self.parent.read().unwrap(); - let transform = self.get(); - let parent_transform = maybe_parent_guard - .as_ref() - .map(|parent| parent.get_global_transform()) - .unwrap_or_default(); - Transform::from(Mat4::from(parent_transform) * Mat4::from(transform)) - } - - /// Get a vector containing all the transforms up to the root. - pub fn get_all_transforms(&self) -> Vec { - let mut transforms = vec![]; - if let Some(parent) = self.parent() { - transforms.extend(parent.get_all_transforms()); - } - transforms.push(self.get()); - transforms - } - - pub fn global_transform_id(&self) -> Id { - self.global_transform.id() - } - - pub fn add_child(&self, node: &NestedTransform) { - *node.parent.write().unwrap() = Some(self.clone()); - node.mark_dirty(); - self.children.write().unwrap().push(node.clone()); - } - - pub fn remove_child(&self, node: &NestedTransform) { - self.children.write().unwrap().retain_mut(|child| { - if child.global_transform.id() == node.global_transform.id() { - node.mark_dirty(); - let _ = node.parent.write().unwrap().take(); - false - } else { - true - } - }); - } - - pub fn parent(&self) -> Option { - self.parent.read().unwrap().clone() - } -} - #[cfg(test)] mod test { - use craballoc::runtime::CpuRuntime; + use craballoc::{runtime::CpuRuntime, slab::SlabAllocator}; use crabslab::{Array, Id, Slab}; use glam::{Mat4, Vec2, Vec3, Vec4}; use crate::{ - camera::Camera, - geometry::{Geometry, GeometryDescriptor}, - stage::{cpu::SlabAllocator, NestedTransform, Renderlet, Vertex}, + context::Context, + geometry::{shader::GeometryDescriptor, Geometry, Vertex}, test::BlockOnFuture, - transform::Transform, - Context, + transform::NestedTransform, }; #[test] @@ -1757,21 +1655,10 @@ mod test { )] let slab = SlabAllocator::::new(&CpuRuntime, "transform", ()); // Setup a hierarchy of transforms - log::info!("new"); let root = NestedTransform::new(&slab); - let child = NestedTransform::new(&slab); - log::info!("set"); - child.set(Transform { - translation: Vec3::new(1.0, 0.0, 0.0), - ..Default::default() - }); - log::info!("grandchild"); - let grandchild = NestedTransform::new(&slab); - grandchild.set(Transform { - translation: Vec3::new(1.0, 0.0, 0.0), - ..Default::default() - }); - + let child = NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0)); + let grandchild = + NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0)); log::info!("hierarchy"); // Build the hierarchy root.add_child(&child); @@ -1779,7 +1666,7 @@ mod test { log::info!("get_global_transform"); // Calculate global transforms - let grandchild_global_transform = grandchild.get_global_transform(); + let grandchild_global_transform = grandchild.global_descriptor(); // Assert that the global transform is as expected assert_eq!( @@ -1795,10 +1682,12 @@ mod test { .new_stage() .with_background_color([1.0, 1.0, 1.0, 1.0]) .with_lighting(false); - let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); - let _triangle_rez = stage - .builder() - .with_vertices([ + let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + let _triangle_rez = stage.new_primitive().with_vertices( + stage.new_vertices([ Vertex::default() .with_position([10.0, 10.0, 0.0]) .with_color([0.0, 1.0, 1.0, 1.0]), @@ -1808,8 +1697,8 @@ mod test { Vertex::default() .with_position([90.0, 10.0, 0.0]) .with_color([1.0, 0.0, 1.0, 1.0]), - ]) - .build(); + ]), + ); log::debug!("rendering without msaa"); let frame = ctx.get_next_frame().unwrap(); @@ -1842,17 +1731,6 @@ mod test { frame.present(); } - #[test] - fn has_consistent_stage_renderlet_strong_count() { - let ctx = Context::headless(100, 100).block(); - let stage = ctx.new_stage(); - let r = stage.new_renderlet(Renderlet::default()); - assert_eq!(1, r.ref_count()); - - stage.add_renderlet(&r); - assert_eq!(1, r.ref_count()); - } - #[test] /// Tests that the PBR descriptor is written to slot 0 of the geometry buffer, /// and that it contains what we think it contains. diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index dbc3e655..c44a4806 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -6,7 +6,7 @@ use std::{ }; use craballoc::runtime::WgpuRuntime; -use glam::UVec2; +use glam::{Mat4, UVec2}; use image::{ load_from_memory, DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageError, Luma, PixelWithColorType, Rgba32FImage, @@ -14,7 +14,10 @@ use image::{ use mips::MipMapGenerator; use snafu::prelude::*; -use crate::atlas::{AtlasImage, AtlasImageFormat}; +use crate::{ + atlas::{AtlasImage, AtlasImageFormat}, + camera::Camera, +}; pub mod mips; @@ -84,6 +87,8 @@ pub(crate) fn get_next_texture_id() -> usize { #[derive(Debug, Clone)] pub struct Texture { pub texture: Arc, + // TODO: revisit whether we really need to create view and sampler for textures + // automatically pub view: Arc, pub sampler: Arc, pub(crate) id: usize, @@ -711,6 +716,89 @@ impl Texture { id: get_next_texture_id(), } } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn render_cubemap( + runtime: impl AsRef, + pipeline: &wgpu::RenderPipeline, + mut buffer_upkeep: impl FnMut(), + camera: &Camera, + bindgroup: &wgpu::BindGroup, + views: [Mat4; 6], + texture_size: u32, + mip_levels: Option, + ) -> Self { + let runtime = runtime.as_ref(); + let device = &runtime.device; + let queue = &runtime.queue; + let mut cubemap_faces = Vec::new(); + let mip_levels = mip_levels.unwrap_or(1); + + // Render every cube face. + for (i, view) in views.iter().enumerate() { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some(&format!("cubemap{i}")), + }); + + let mut cubemap_face = Texture::new_with( + runtime, + Some(&format!("cubemap{i}")), + Some( + wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::TEXTURE_BINDING, + ), + None, + wgpu::TextureFormat::Rgba16Float, + 4, + 2, + texture_size, + texture_size, + 1, + &[], + ); + + // update the view to point at one of the cube faces + camera.set_view(*view); + buffer_upkeep(); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&format!("cubemap{i}")), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &cubemap_face.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, Some(bindgroup), &[]); + render_pass.draw(0..36, 0..1); + } + + queue.submit([encoder.finish()]); + let mips = cubemap_face.generate_mips(runtime, Some("cubemap mips"), mip_levels); + cubemap_faces.push(cubemap_face); + cubemap_faces.extend(mips); + } + + Texture::new_cubemap_texture( + runtime, + Some("skybox cubemap"), + texture_size, + cubemap_faces.as_slice(), + wgpu::TextureFormat::Rgba16Float, + mip_levels, + ) + } } pub async fn read_depth_texture_to_image( @@ -1159,7 +1247,7 @@ impl CopiedTextureBuffer { #[cfg(test)] mod test { - use crate::{test::BlockOnFuture, texture::CopiedTextureBuffer, Context}; + use crate::{context::Context, test::BlockOnFuture, texture::CopiedTextureBuffer}; use super::Texture; diff --git a/crates/renderling/src/tonemapping/cpu.rs b/crates/renderling/src/tonemapping/cpu.rs index 4dd0edf8..be98f3fa 100644 --- a/crates/renderling/src/tonemapping/cpu.rs +++ b/crates/renderling/src/tonemapping/cpu.rs @@ -165,6 +165,10 @@ impl Tonemapping { } } + pub(crate) fn slab_allocator(&self) -> &SlabAllocator { + &self.slab + } + pub fn set_hdr_texture(&self, device: &wgpu::Device, hdr_texture: &Texture) { // UNWRAP: safe because the buffer is created in `Self::new` and guaranteed to // exist diff --git a/crates/renderling/src/transform.rs b/crates/renderling/src/transform.rs index 5d328e1d..1c4e12dc 100644 --- a/crates/renderling/src/transform.rs +++ b/crates/renderling/src/transform.rs @@ -1,68 +1,77 @@ -//! Decomposed 3d transform. -use crabslab::SlabItem; -use glam::{Mat4, Quat, Vec3}; +//! Decomposed 3d transforms and hierarchies. -use crate::math::IsMatrix; +#[cfg(cpu)] +mod cpu; +#[cfg(cpu)] +pub use cpu::*; -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, SlabItem)] -/// A decomposed transformation. -/// -/// `Transform` can be converted to/from [`Mat4`]. -pub struct Transform { - pub translation: Vec3, - pub rotation: Quat, - pub scale: Vec3, -} +pub mod shader { + use crabslab::SlabItem; + use glam::{Mat4, Quat, Vec3}; + + use crate::math::IsMatrix; -impl Default for Transform { - fn default() -> Self { - Self::IDENTITY + #[derive(Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)] + /// A GPU descriptor of a decomposed transformation. + /// + /// `TransformDescriptor` can be converted to/from [`Mat4`]. + pub struct TransformDescriptor { + pub translation: Vec3, + pub rotation: Quat, + pub scale: Vec3, } -} -impl From for Transform { - fn from(value: Mat4) -> Self { - let (scale, rotation, translation) = value.to_scale_rotation_translation_or_id(); - Transform { - translation, - rotation, - scale, + impl Default for TransformDescriptor { + fn default() -> Self { + Self::IDENTITY } } -} -impl From for Mat4 { - fn from( - Transform { - translation, - rotation, - scale, - }: Transform, - ) -> Self { - Mat4::from_scale_rotation_translation(scale, rotation, translation) + impl From for TransformDescriptor { + fn from(value: Mat4) -> Self { + let (scale, rotation, translation) = value.to_scale_rotation_translation_or_id(); + TransformDescriptor { + translation, + rotation, + scale, + } + } } -} -impl Transform { - pub const IDENTITY: Self = Transform { - translation: Vec3::ZERO, - rotation: Quat::IDENTITY, - scale: Vec3::ONE, - }; + impl From for Mat4 { + fn from( + TransformDescriptor { + translation, + rotation, + scale, + }: TransformDescriptor, + ) -> Self { + Mat4::from_scale_rotation_translation(scale, rotation, translation) + } + } + + impl TransformDescriptor { + pub const IDENTITY: Self = TransformDescriptor { + translation: Vec3::ZERO, + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + }; + } } #[cfg(test)] mod test { - use super::*; use crabslab::*; + use glam::{Quat, Vec3}; + + use crate::transform::shader::TransformDescriptor; #[test] fn transform_roundtrip() { assert_eq!(3, Vec3::SLAB_SIZE, "unexpected Vec3 slab size"); assert_eq!(4, Quat::SLAB_SIZE, "unexpected Quat slab size"); - assert_eq!(10, Transform::SLAB_SIZE); - let t = Transform::default(); + assert_eq!(10, TransformDescriptor::SLAB_SIZE); + let t = TransformDescriptor::default(); let mut slab = CpuSlab::new(vec![]); let t_id = slab.append(&t); pretty_assertions::assert_eq!( diff --git a/crates/renderling/src/transform/cpu.rs b/crates/renderling/src/transform/cpu.rs new file mode 100644 index 00000000..5e0c8343 --- /dev/null +++ b/crates/renderling/src/transform/cpu.rs @@ -0,0 +1,411 @@ +//! CPU side of transform. + +use std::sync::{Arc, RwLock}; + +use craballoc::{runtime::IsRuntime, slab::SlabAllocator, value::Hybrid}; +use crabslab::Id; +use glam::{Mat4, Quat, Vec3}; + +use super::shader::TransformDescriptor; + +/// A decomposed 3d transformation. +#[derive(Clone, Debug)] +pub struct Transform { + pub(crate) descriptor: Hybrid, +} + +impl From<&Transform> for Transform { + fn from(value: &Transform) -> Self { + value.clone() + } +} + +impl Transform { + /// Stage a new transform on the GPU. + pub(crate) fn new(slab: &SlabAllocator) -> Self { + let descriptor = slab.new_value(TransformDescriptor::default()); + Self { descriptor } + } + + /// Return a pointer to the underlying descriptor data on the GPU. + pub fn id(&self) -> Id { + self.descriptor.id() + } + + /// Return the a copy of the underlying descriptor. + pub fn descriptor(&self) -> TransformDescriptor { + self.descriptor.get() + } + + /// Set the descriptor. + pub fn set_descriptor(&self, descriptor: TransformDescriptor) -> &Self { + self.descriptor.set(descriptor); + self + } + + /// Set the descriptor and return the `Transform`. + pub fn with_descriptor(self, descriptor: TransformDescriptor) -> Self { + self.set_descriptor(descriptor); + self + } + + /// Return the transform in combined matrix format; + pub fn as_mat4(&self) -> Mat4 { + self.descriptor().into() + } + + /// Get the translation of the transform. + pub fn translation(&self) -> Vec3 { + self.descriptor.get().translation + } + + /// Modify the translation of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the translation vector and returns a value of type `T`. + pub fn modify_translation(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.descriptor.modify(|t| f(&mut t.translation)) + } + + /// Set the translation of the transform. + /// + /// # Arguments + /// + /// - `translation`: A 3d translation vector `Vec3`. + pub fn set_translation(&self, translation: impl Into) -> &Self { + self.descriptor + .modify(|t| t.translation = translation.into()); + self + } + + /// Set the translation of the transform. + /// + /// # Arguments + /// + /// - `translation`: A 3d translation vector `Vec3`. + pub fn with_translation(self, translation: impl Into) -> Self { + self.set_translation(translation); + self + } + + /// Get the rotation of the transform. + pub fn rotation(&self) -> Quat { + self.descriptor.get().rotation + } + + /// Modify the rotation of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the rotation quaternion and returns a value of type `T`. + pub fn modify_rotation(&self, f: impl FnOnce(&mut Quat) -> T) -> T { + self.descriptor.modify(|t| f(&mut t.rotation)) + } + + /// Set the rotation of the transform. + /// + /// # Arguments + /// + /// - `rotation`: A quaternion representing the rotation. + pub fn set_rotation(&self, rotation: impl Into) -> &Self { + self.descriptor.modify(|t| t.rotation = rotation.into()); + self + } + + /// Set the rotation of the transform. + /// + /// # Arguments + /// + /// - `rotation`: A quaternion representing the rotation. + pub fn with_rotation(self, rotation: impl Into) -> Self { + self.set_rotation(rotation); + self + } + + /// Get the scale of the transform. + pub fn scale(&self) -> Vec3 { + self.descriptor.get().scale + } + + /// Modify the scale of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the scale vector and returns a value of type `T`. + pub fn modify_scale(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + self.descriptor.modify(|t| f(&mut t.scale)) + } + + /// Set the scale of the transform. + /// + /// # Arguments + /// + /// - `scale`: A 3d scale vector `Vec3`. + pub fn set_scale(&self, scale: impl Into) -> &Self { + self.descriptor.modify(|t| t.scale = scale.into()); + self + } + + /// Set the scale of the transform. + /// + /// # Arguments + /// + /// - `scale`: A 3d scale vector `Vec3`. + pub fn with_scale(self, scale: impl Into) -> Self { + self.set_scale(scale); + self + } +} + +/// Manages scene heirarchy on the [`Stage`](crate::stage::Stage). +/// +/// Can be created with +/// [`Stage::new_nested_transform`](crate::stage::Stage::new_nested_transform). +/// +/// Clones all reference the same nested transform. +#[derive(Clone)] +pub struct NestedTransform { + pub(crate) global_transform: Transform, + local_transform: Arc>, + children: Arc>>, + parent: Arc>>, +} + +impl core::fmt::Debug for NestedTransform { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let children = self + .children + .read() + .unwrap() + .iter() + .map(|nt| nt.global_transform.id()) + .collect::>(); + let parent = self + .parent + .read() + .unwrap() + .as_ref() + .map(|nt| nt.global_transform.id()); + f.debug_struct("NestedTransform") + .field("local_transform", &self.local_transform) + .field("children", &children) + .field("parent", &parent) + .finish() + } +} + +impl From<&NestedTransform> for Transform { + fn from(value: &NestedTransform) -> Self { + value.global_transform.clone() + } +} + +impl From for Transform { + fn from(value: NestedTransform) -> Self { + value.global_transform + } +} + +impl NestedTransform { + /// Stage a new hierarchical transform on the GPU. + pub(crate) fn new(slab: &SlabAllocator) -> Self { + let nested = NestedTransform { + local_transform: Arc::new(RwLock::new(TransformDescriptor::default())), + global_transform: Transform::new(slab), + children: Default::default(), + parent: Default::default(), + }; + nested.mark_dirty(); + nested + } + + /// Get the _local_ translation of the transform. + pub fn local_translation(&self) -> Vec3 { + self.local_transform.read().unwrap().translation + } + + /// Modify the _local_ translation of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the translation vector and returns a value of type `T`. + pub fn modify_local_translation(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + let t = { + let mut local_transform = self.local_transform.write().unwrap(); + f(&mut local_transform.translation) + }; + self.mark_dirty(); + t + } + + /// Set the _local_ translation of the transform. + /// + /// # Arguments + /// + /// - `translation`: A 3d translation vector `Vec3`. + pub fn set_local_translation(&self, translation: impl Into) -> &Self { + self.local_transform.write().unwrap().translation = translation.into(); + self.mark_dirty(); + self + } + + /// Set the _local_ translation of the transform. + /// + /// # Arguments + /// + /// - `translation`: A 3d translation vector `Vec3`. + pub fn with_local_translation(self, translation: impl Into) -> Self { + self.set_local_translation(translation); + self + } + + /// Get the _local_ rotation of the transform. + pub fn local_rotation(&self) -> Quat { + self.local_transform.read().unwrap().rotation + } + + /// Modify the _local_ rotation of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the rotation quaternion and returns a value of type `T`. + pub fn modify_local_rotation(&self, f: impl FnOnce(&mut Quat) -> T) -> T { + let t = { + let mut local_transform = self.local_transform.write().unwrap(); + f(&mut local_transform.rotation) + }; + self.mark_dirty(); + t + } + + /// Set the _local_ rotation of the transform. + /// + /// # Arguments + /// + /// - `rotation`: A quaternion representing the rotation. + pub fn set_local_rotation(&self, rotation: impl Into) -> &Self { + self.local_transform.write().unwrap().rotation = rotation.into(); + self.mark_dirty(); + self + } + + /// Set the _local_ rotation of the transform. + /// + /// # Arguments + /// + /// - `rotation`: A quaternion representing the rotation. + pub fn with_local_rotation(self, rotation: impl Into) -> Self { + self.set_local_rotation(rotation); + self + } + + /// Get the _local_ scale of the transform. + pub fn local_scale(&self) -> Vec3 { + self.local_transform.read().unwrap().scale + } + + /// Modify the _local_ scale of the transform. + /// + /// # Arguments + /// + /// - `f`: A closure that takes a mutable reference to the scale vector and returns a value of type `T`. + pub fn modify_local_scale(&self, f: impl FnOnce(&mut Vec3) -> T) -> T { + let t = { + let mut local_transform = self.local_transform.write().unwrap(); + f(&mut local_transform.scale) + }; + self.mark_dirty(); + t + } + + /// Set the _local_ scale of the transform. + /// + /// # Arguments + /// + /// - `scale`: A 3d scale vector `Vec3`. + pub fn set_local_scale(&self, scale: impl Into) -> &Self { + self.local_transform.write().unwrap().scale = scale.into(); + self.mark_dirty(); + self + } + + /// Set the _local_ scale of the transform. + /// + /// # Arguments + /// + /// - `scale`: A 3d scale vector `Vec3`. + pub fn with_local_scale(self, scale: impl Into) -> Self { + self.set_local_scale(scale); + self + } + + /// Return a pointer to the underlying descriptor data on the GPU. + /// + /// The descriptor is the descriptor that describes the _global_ transform. + pub fn global_id(&self) -> Id { + self.global_transform.id() + } + + /// Return the descriptor of the _global_ transform. + /// + /// This traverses the heirarchy and computes the result. + pub fn global_descriptor(&self) -> TransformDescriptor { + let maybe_parent_guard = self.parent.read().unwrap(); + let transform = self.local_descriptor(); + let parent_transform = maybe_parent_guard + .as_ref() + .map(|parent| parent.global_descriptor()) + .unwrap_or_default(); + TransformDescriptor::from(Mat4::from(parent_transform) * Mat4::from(transform)) + } + + /// Return the descriptor of the _local_ tarnsform. + pub fn local_descriptor(&self) -> TransformDescriptor { + *self.local_transform.read().unwrap() + } + + fn mark_dirty(&self) { + self.global_transform + .descriptor + .set(self.global_descriptor()); + for child in self.children.read().unwrap().iter() { + child.mark_dirty(); + } + } + + /// Get a vector containing all the hierarchy's transforms. + /// + /// Starts with the root transform and ends with the local transform. + pub fn hierarchy(&self) -> Vec { + let mut transforms = vec![]; + if let Some(parent) = self.parent() { + transforms.extend(parent.hierarchy()); + } + transforms.push(self.local_descriptor()); + transforms + } + + pub fn add_child(&self, node: &NestedTransform) { + *node.parent.write().unwrap() = Some(self.clone()); + node.mark_dirty(); + self.children.write().unwrap().push(node.clone()); + } + + pub fn remove_child(&self, node: &NestedTransform) { + self.children.write().unwrap().retain_mut(|child| { + if child.global_transform.id() == node.global_transform.id() { + node.mark_dirty(); + let _ = node.parent.write().unwrap().take(); + false + } else { + true + } + }); + } + + /// Return a clone of the parent `NestedTransform`, if any. + pub fn parent(&self) -> Option { + self.parent.read().unwrap().clone() + } +} diff --git a/crates/renderling/src/tuple.rs b/crates/renderling/src/tuple.rs deleted file mode 100644 index c4316b35..00000000 --- a/crates/renderling/src/tuple.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Traits for preforming type-level operations on tuples. -//! -//! The traits are used for "smart" builders to organize return types. - -pub trait Bundle { - type Prefixed; - type Suffixed; - type Reduced; - - fn prefix(self, element: T) -> Self::Prefixed; - fn suffix(self, element: T) -> Self::Suffixed; - fn reduce(self) -> Self::Reduced; -} - -impl Bundle for () { - type Prefixed = (T,); - type Suffixed = (T,); - type Reduced = (); - - fn prefix(self, element: B) -> Self::Prefixed { - (element,) - } - - fn suffix(self, element: B) -> Self::Suffixed { - (element,) - } - - fn reduce(self) -> Self::Reduced { - self - } -} - -impl Bundle for (A,) { - type Prefixed = (B, A); - type Suffixed = (A, B); - type Reduced = A; - - fn prefix(self, element: B) -> Self::Prefixed { - (element, self.0) - } - - fn suffix(self, element: B) -> Self::Suffixed { - (self.0, element) - } - - fn reduce(self) -> Self::Reduced { - self.0 - } -} - -macro_rules! suffix { - ($($i:ident),*) => { - #[allow(non_snake_case)] - impl<$($i),*> Bundle for ($($i),*) { - type Prefixed = (T, $($i),*); - type Suffixed = ($($i),*, T); - type Reduced = Self; - - fn prefix(self, element: T) -> Self::Prefixed { - let ($($i),*) = self; - (element, $($i),*) - } - - fn suffix(self, element: T) -> Self::Suffixed { - let ($($i),*) = self; - ($($i),*, element) - } - - fn reduce(self) -> Self::Reduced { - self - } - } - }; -} - -suffix!(A, B); -suffix!(A, B, C); -suffix!(A, B, C, D); -suffix!(A, B, C, D, E); -suffix!(A, B, C, D, E, F); -suffix!(A, B, C, D, E, F, G); -suffix!(A, B, C, D, E, F, G, H); -suffix!(A, B, C, D, E, F, G, H, I); -suffix!(A, B, C, D, E, F, G, H, I, J); -suffix!(A, B, C, D, E, F, G, H, I, J, K); -suffix!(A, B, C, D, E, F, G, H, I, J, K, L); -suffix!(A, B, C, D, E, F, G, H, I, J, K, L, M); -suffix!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); -suffix!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); -suffix!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn sanity() { - let bundle = (); - let bundle: (f32,) = bundle.suffix(0.0); - let bundle: (f32, u32) = bundle.suffix(0u32); - let bundle: (f32, u32, char) = bundle.suffix('c'); - let _bundle: (&str, f32, u32, char) = bundle.prefix("blah"); - - let bundle: (&str,) = ("hello",); - let _bundle: &str = bundle.reduce(); - } -} diff --git a/crates/renderling/src/tutorial.rs b/crates/renderling/src/tutorial.rs index c8707b58..979930de 100644 --- a/crates/renderling/src/tutorial.rs +++ b/crates/renderling/src/tutorial.rs @@ -1,12 +1,12 @@ -//! Shaders used in the intro tutorial and in WASM tests. +//! Shaders used in the contributor intro tutorial and in WASM tests. use crabslab::{Array, Id, Slab, SlabItem}; use glam::{Vec3, Vec3Swizzles, Vec4}; use spirv_std::spirv; use crate::{ - geometry::GeometryDescriptor, - stage::{Renderlet, Vertex, VertexInfo}, + geometry::{shader::GeometryDescriptor, Vertex}, + primitive::shader::{PrimitiveDescriptor, VertexInfo}, }; /// Simple fragment shader that writes the input color to the output color. @@ -80,18 +80,19 @@ pub fn slabbed_vertices( *out_color = color; } -// TODO: fix all this documentation -/// This shader uses the `instance_index` as a slab [`Id`]. -/// The `instance_index` is the [`Id`] of a [`RenderUnit`]. -/// The [`RenderUnit`] contains an [`Array`] of [`Vertex`]s -/// as its mesh, the [`Id`]s of a [`Material`] and [`Camera`], +/// This shader uses the `instance_index` as a slab id. +/// The `instance_index` is the `id` of a [`PrimitiveDescriptor`]. +/// The [`PrimitiveDescriptor`] contains an [`Array`] of [`Vertex`]s +/// as its mesh, the [`Id`]s of a +/// [`MaterialDescriptor`](crate::material::shader::MaterialDescriptor) and +///[`CameraDescriptor`](crate::camera::shader::CameraDescriptor), /// and TRS transforms. /// The `vertex_index` is the index of a [`Vertex`] within the -/// [`RenderUnit`]'s `vertices` [`Array`]. +/// [`PrimitiveDescriptor`]'s `vertices` [`Array`]. #[spirv(vertex)] pub fn slabbed_renderlet( // Id of the array of vertices we are rendering - #[spirv(instance_index)] renderlet_id: Id, + #[spirv(instance_index)] primitive_id: Id, // Which vertex within the render unit are we rendering #[spirv(vertex_index)] vertex_index: u32, @@ -100,14 +101,14 @@ pub fn slabbed_renderlet( out_color: &mut Vec4, #[spirv(position)] clip_pos: &mut Vec4, ) { - let renderlet = slab.read(renderlet_id); + let prim = slab.read(primitive_id); let VertexInfo { vertex, model_matrix, .. - } = renderlet.get_vertex_info(vertex_index, slab); - let camera_id = slab - .read_unchecked(renderlet.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID); + } = prim.get_vertex_info(vertex_index, slab); + let camera_id = + slab.read_unchecked(prim.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID); let camera = slab.read(camera_id); *clip_pos = camera.view_projection() * model_matrix * vertex.position.xyz().extend(1.0); *out_color = vertex.color; diff --git a/crates/renderling/src/types.rs b/crates/renderling/src/types.rs new file mode 100644 index 00000000..41799c57 --- /dev/null +++ b/crates/renderling/src/types.rs @@ -0,0 +1,24 @@ +//! Type level machinery. + +use craballoc::value::{GpuArrayContainer, GpuContainer, HybridArrayContainer, HybridContainer}; + +/// Specifies that a staged value has been unloaded from the CPU +/// and now lives solely on the GPU. +pub type GpuOnly = GpuContainer; + +/// Specifies that a contiguous array of staged values has been +/// unloaded from the CPU and now lives solely on the GPU. +pub type GpuOnlyArray = GpuArrayContainer; + +/// Specifies that a staged value lives on both the CPU and GPU, +/// with the CPU value being a synchronized copy of the GPU value. +/// +/// Currently updates flow from the CPU to the GPU, but not back. +pub type GpuCpu = HybridContainer; + +/// Specifies that a contiguous array of staged values lives on both +/// the CPU and GPU, with the CPU values being synchronized copies +/// of the GPU values. +/// +/// Currently updates flow from the CPU to the GPU, but not back. +pub type GpuCpuArray = HybridArrayContainer; diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index bffe3ef7..a654450c 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -12,7 +12,7 @@ //! let mut ui = Ui::new(&ctx); //! //! let _path = ui -//! .new_path() +//! .path_builder() //! .with_stroke_color([1.0, 1.0, 0.0, 1.0]) //! .with_rectangle(Vec2::splat(10.0), Vec2::splat(60.0)) //! .stroke(); diff --git a/crates/renderling/src/ui/cpu.rs b/crates/renderling/src/ui/cpu.rs index 454ef0e3..d3455a44 100644 --- a/crates/renderling/src/ui/cpu.rs +++ b/crates/renderling/src/ui/cpu.rs @@ -1,16 +1,15 @@ //! CPU part of ui. +use core::sync::atomic::AtomicBool; use std::sync::{Arc, RwLock}; use crate::{ - atlas::AtlasTexture, + atlas::{shader::AtlasTextureDescriptor, AtlasTexture, TextureAddressMode, TextureModes}, camera::Camera, - geometry::Geometry, - stage::{NestedTransform, Renderlet, Stage}, - transform::Transform, - Context, + context::Context, + stage::Stage, + transform::NestedTransform, }; -use craballoc::prelude::{Hybrid, SourceId}; use crabslab::Id; use glam::{Quat, UVec2, Vec2, Vec3Swizzles, Vec4}; use glyph_brush::ab_glyph; @@ -66,80 +65,77 @@ pub enum UiError { /// `ImageId` can be created with [`Ui::load_image`]. #[repr(transparent)] #[derive(Clone, Copy, Debug)] -pub struct ImageId(usize); +pub struct ImageId(Id); /// A two dimensional transformation. /// /// Clones of `UiTransform` all point to the same data. #[derive(Clone, Debug)] pub struct UiTransform { + should_reorder: Arc, transform: NestedTransform, - renderlet_ids: Arc>>, } impl UiTransform { - pub(crate) fn id(&self) -> Id { - self.transform.global_transform_id() + fn mark_should_reorder(&self) { + self.should_reorder + .store(true, std::sync::atomic::Ordering::Relaxed); } pub fn set_translation(&self, t: Vec2) { - self.transform.modify(|a| { - a.translation.x = t.x; - a.translation.y = t.y; + self.mark_should_reorder(); + self.transform.modify_local_translation(|a| { + a.x = t.x; + a.y = t.y; }); } pub fn get_translation(&self) -> Vec2 { - let t = self.transform.get(); - t.translation.xy() + self.transform.local_translation().xy() } pub fn set_rotation(&self, radians: f32) { + self.mark_should_reorder(); let rotation = Quat::from_rotation_z(radians); - self.transform.modify(|t| { - t.rotation *= rotation; + // TODO: check to see if *= rotation makes sense here + self.transform.modify_local_rotation(|t| { + *t *= rotation; }); } pub fn get_rotation(&self) -> f32 { self.transform - .get() - .rotation + .local_rotation() .to_euler(glam::EulerRot::XYZ) .2 } pub fn set_z(&self, z: f32) { - self.transform.modify(|t| { - t.translation.z = z; + self.mark_should_reorder(); + self.transform.modify_local_translation(|t| { + t.z = z; }); } pub fn get_z(&self) -> f32 { - self.transform.get().translation.z + self.transform.local_translation().z } } #[derive(Clone)] #[repr(transparent)] -pub struct UiImage(Hybrid); +pub struct UiImage(AtlasTexture); /// A 2d user interface renderer. /// /// Clones of `Ui` all point to the same data. #[derive(Clone)] pub struct Ui { - camera: Hybrid, + camera: Camera, stage: Stage, - images: Arc>>, + should_reorder: Arc, + images: Arc, UiImage>>>, fonts: Arc>>, - // We keep a list of transforms that we use to "manually" order renderlets. - // - // This is required because interface elements have transparency. - // - // The `usize` key here is the update source notifier index, which is needed - // to re-order after any transform performs an update. - transforms: Arc>>, default_stroke_options: Arc>, default_fill_options: Arc>, } @@ -154,13 +150,14 @@ impl Ui { .with_bloom(false) .with_msaa_sample_count(4) .with_frustum_culling(false); - let camera = stage.new_camera(Camera::default_ortho2d(x as f32, y as f32)); + let (proj, view) = crate::camera::default_ortho2d(x as f32, y as f32); + let camera = stage.new_camera().with_projection_and_view(proj, view); Ui { camera, stage, + should_reorder: AtomicBool::new(true).into(), images: Default::default(), fonts: Default::default(), - transforms: Default::default(), default_stroke_options: Default::default(), default_fill_options: Default::default(), } @@ -225,27 +222,46 @@ impl Ui { self } - fn new_transform(&self, renderlet_ids: Vec>) -> UiTransform { + fn new_transform(&self) -> UiTransform { + self.mark_should_reorder(); let transform = self.stage.new_nested_transform(); - let transform = UiTransform { + UiTransform { transform, - renderlet_ids: Arc::new(renderlet_ids), - }; - self.transforms - .write() - .unwrap() - .insert(transform.transform.get_notifier_index(), transform.clone()); - transform + should_reorder: self.should_reorder.clone(), + } + } + + fn mark_should_reorder(&self) { + self.should_reorder + .store(true, std::sync::atomic::Ordering::Relaxed) } - pub fn new_path(&self) -> UiPathBuilder { + pub fn path_builder(&self) -> UiPathBuilder { + self.mark_should_reorder(); UiPathBuilder::new(self) } - pub fn new_text(&self) -> UiTextBuilder { + /// Remove the `path` from the [`Ui`]. + /// + /// The given `path` must have been created with this [`Ui`], otherwise this function is + /// a noop. + pub fn remove_path(&self, path: &UiPath) { + self.stage.remove_primitive(&path.primitive); + } + + pub fn text_builder(&self) -> UiTextBuilder { + self.mark_should_reorder(); UiTextBuilder::new(self) } + /// Remove the text from the [`Ui`]. + /// + /// The given `text` must have been created with this [`Ui`], otherwise this function is + /// a noop. + pub fn remove_text(&self, text: &UiText) { + self.stage.remove_primitive(&text.renderlet); + } + pub async fn load_font(&self, path: impl AsRef) -> Result { let path_s = path.as_ref(); let bytes = loading_bytes::load(path_s).await.context(LoadingSnafu)?; @@ -266,7 +282,7 @@ impl Ui { self.fonts.read().unwrap().clone() } - pub fn get_camera(&self) -> &Hybrid { + pub fn get_camera(&self) -> &Camera { &self.camera } @@ -284,57 +300,42 @@ impl Ui { .context(StageSnafu)? .pop() .unwrap(); - entry.modify(|t| { - t.modes.s = crate::atlas::TextureAddressMode::Repeat; - t.modes.t = crate::atlas::TextureAddressMode::Repeat; + entry.set_modes(TextureModes { + s: TextureAddressMode::Repeat, + t: TextureAddressMode::Repeat, }); let mut guard = self.images.write().unwrap(); - let id = guard.len(); - guard.push(UiImage(entry)); + let id = entry.id(); + guard.insert(id, UiImage(entry)); Ok(ImageId(id)) } - pub(crate) fn get_image(&self, index: usize) -> Option { - self.images.read().unwrap().get(index).cloned() + /// Remove an image previously loaded with [`Ui::load_image`]. + pub fn remove_image(&self, image_id: &ImageId) -> Option { + self.images.write().unwrap().remove(&image_id.0) } fn reorder_renderlets(&self) { - // UNWRAP: panic on purpose - let guard = self.transforms.read().unwrap(); - let mut transforms = guard.values().collect::>(); - transforms.sort_by(|a, b| { - let ta = a.transform.get_global_transform(); - let tb = b.transform.get_global_transform(); - ta.translation.z.total_cmp(&tb.translation.z) + self.stage.sort_renderlets(|a, b| { + let za = a + .transform() + .as_ref() + .map(|t| t.translation().z) + .unwrap_or_default(); + let zb = b + .transform() + .as_ref() + .map(|t| t.translation().z) + .unwrap_or_default(); + za.total_cmp(&zb) }); - self.stage.reorder_renderlets( - transforms - .iter() - .flat_map(|t| t.renderlet_ids.as_ref().clone()), - ); } pub fn render(&self, view: &wgpu::TextureView) { - let mut should_reorder = false; - // UNWRAP: panic on purpose - let mut transforms = self.transforms.write().unwrap(); - let geometry: &Geometry = self.stage.as_ref(); - for update_id in geometry - .slab_allocator() - .get_updated_source_ids() - .into_iter() + if self + .should_reorder + .swap(false, std::sync::atomic::Ordering::Relaxed) { - if let Some(ui_transform) = transforms.get(&update_id) { - if Arc::strong_count(&ui_transform.renderlet_ids) == 1 { - let _ = transforms.remove(&update_id); - } else { - should_reorder = true; - } - } - } - drop(transforms); - if should_reorder { - log::trace!("a ui transform changed, sorting the renderlets"); self.reorder_renderlets(); } self.stage.render(view); @@ -343,7 +344,7 @@ impl Ui { #[cfg(test)] pub(crate) mod test { - use crate::{color::rgb_hex_color, prelude::glam::Vec4}; + use crate::{color::rgb_hex_color, glam::Vec4}; pub struct Colors(std::iter::Cycle>); diff --git a/crates/renderling/src/ui/cpu/path.rs b/crates/renderling/src/ui/cpu/path.rs index 1d610b05..94e07aa7 100644 --- a/crates/renderling/src/ui/cpu/path.rs +++ b/crates/renderling/src/ui/cpu/path.rs @@ -1,12 +1,7 @@ //! Path and builder. //! //! Path colors are sRGB. -use crate::{ - pbr::Material, - stage::{Renderlet, Vertex}, -}; -use craballoc::prelude::{GpuArray, Hybrid}; -use crabslab::Id; +use crate::{geometry::Vertex, material::Material, primitive::Primitive}; use glam::{Vec2, Vec3, Vec3Swizzles, Vec4}; use lyon::{ path::traits::PathBuilder, @@ -19,11 +14,9 @@ use super::{ImageId, Ui, UiTransform}; pub use lyon::tessellation::{LineCap, LineJoin}; pub struct UiPath { - pub vertices: GpuArray, - pub indices: GpuArray, pub transform: UiTransform, - pub material: Hybrid, - pub renderlet: Hybrid, + pub material: Material, + pub primitive: Primitive, } #[derive(Clone, Copy)] @@ -349,23 +342,19 @@ impl UiPathBuilder { let l_path = self.inner.build(); let mut geometry = VertexBuffers::::new(); let mut tesselator = FillTessellator::new(); - + let material = self.ui.stage.new_material(); let mut size = Vec2::ONE; - let albedo_texture_id = if let Some(ImageId(index)) = options.image_id { - if let Some(image) = self.ui.get_image(index) { - let tex = image.0.get(); - log::debug!("size: {}", tex.size_px); - size.x = tex.size_px.x as f32; - size.y = tex.size_px.y as f32; - image.0.id() - } else { - Id::NONE + // If we have an image use it in the material + if let Some(ImageId(id)) = &options.image_id { + let guard = self.ui.images.read().unwrap(); + if let Some(image) = guard.get(id) { + let size_px = image.0.descriptor().size_px; + log::debug!("size: {}", size_px); + size.x = size_px.x as f32; + size.y = size_px.y as f32; + material.set_albedo_texture(&image.0); } - } else { - log::debug!("no image"); - Id::NONE - }; - + } tesselator .tessellate_path( l_path.as_slice(), @@ -386,31 +375,30 @@ impl UiPathBuilder { }), ) .unwrap(); - let (vertices, indices, material, renderlet) = self + let vertices = self .ui .stage - .builder() - .with_vertices(std::mem::take(&mut geometry.vertices)) - .with_indices( - std::mem::take(&mut geometry.indices) - .into_iter() - .map(|u| u as u32), - ) - .with_material(Material { - albedo_texture_id, - ..Default::default() - }) - .build(); + .new_vertices(std::mem::take(&mut geometry.vertices)); + let indices = self.ui.stage.new_indices( + std::mem::take(&mut geometry.indices) + .into_iter() + .map(|u| u as u32), + ); - let transform = self.ui.new_transform(vec![renderlet.id()]); - renderlet.modify(|r| r.transform_id = transform.id()); + let transform = self.ui.new_transform(); + let primitive = self + .ui + .stage + .new_primitive() + .with_vertices(&vertices) + .with_indices(&indices) + .with_material(&material) + .with_transform(&transform.transform); UiPath { - vertices: vertices.into_gpu_only(), - indices: indices.into_gpu_only(), transform, material, - renderlet, + primitive, } } @@ -433,23 +421,19 @@ impl UiPathBuilder { .with_line_cap(line_cap) .with_line_join(line_join) .with_line_width(line_width); - + let material = self.ui.stage.new_material(); let mut size = Vec2::ONE; - let albedo_texture_id = if let Some(ImageId(index)) = image_id { - if let Some(image) = self.ui.get_image(index) { - let tex = image.0.get(); - log::debug!("size: {}", tex.size_px); - size.x = tex.size_px.x as f32; - size.y = tex.size_px.y as f32; - image.0.id() - } else { - Id::NONE + // If we have an image, use it in the material + if let Some(ImageId(id)) = &image_id { + let guard = self.ui.images.read().unwrap(); + if let Some(image) = guard.get(id) { + let size_px = image.0.descriptor.get().size_px; + log::debug!("size: {}", size_px); + size.x = size_px.x as f32; + size.y = size_px.y as f32; + material.set_albedo_texture(&image.0); } - } else { - log::debug!("no image"); - Id::NONE - }; - + } tesselator .tessellate_path( l_path.as_slice(), @@ -470,31 +454,28 @@ impl UiPathBuilder { }), ) .unwrap(); - let (vertices, indices, material, renderlet) = self + let vertices = self .ui .stage - .builder() - .with_vertices(std::mem::take(&mut geometry.vertices)) - .with_indices( - std::mem::take(&mut geometry.indices) - .into_iter() - .map(|u| u as u32), - ) - .with_material(Material { - albedo_texture_id, - ..Default::default() - }) - .build(); - - let transform = self.ui.new_transform(vec![renderlet.id()]); - renderlet.modify(|r| r.transform_id = transform.id()); - + .new_vertices(std::mem::take(&mut geometry.vertices)); + let indices = self.ui.stage.new_indices( + std::mem::take(&mut geometry.indices) + .into_iter() + .map(|u| u as u32), + ); + let transform = self.ui.new_transform(); + let renderlet = self + .ui + .stage + .new_primitive() + .with_vertices(vertices) + .with_indices(indices) + .with_transform(&transform.transform) + .with_material(&material); UiPath { - vertices: vertices.into_gpu_only(), - indices: indices.into_gpu_only(), transform, material, - renderlet, + primitive: renderlet, } } @@ -524,13 +505,13 @@ impl UiPathBuilder { #[cfg(test)] mod test { use crate::{ + context::Context, math::hex_to_vec4, test::BlockOnFuture, ui::{ test::{cute_beach_palette, Colors}, Ui, }, - Context, }; use glam::Vec2; @@ -560,7 +541,7 @@ mod test { let ctx = Context::headless(100, 100).block(); let ui = Ui::new(&ctx).with_antialiasing(false); let builder = ui - .new_path() + .path_builder() .with_fill_color([1.0, 1.0, 0.0, 1.0]) .with_stroke_color([0.0, 1.0, 1.0, 1.0]) .with_rectangle(Vec2::splat(10.0), Vec2::splat(60.0)) @@ -607,7 +588,7 @@ mod test { // rectangle let fill = colors.next_color(); let _rect = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_rectangle(Vec2::splat(2.0), Vec2::splat(42.0)) @@ -616,7 +597,7 @@ mod test { // circle let fill = colors.next_color(); let _circ = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_circle([64.0, 22.0], 20.0) @@ -625,7 +606,7 @@ mod test { // ellipse let fill = colors.next_color(); let _elli = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_ellipse([104.0, 22.0], [20.0, 15.0], std::f32::consts::FRAC_PI_4) @@ -644,7 +625,7 @@ mod test { let fill = colors.next_color(); let center = Vec2::new(144.0, 22.0); let _penta = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_polygon(true, circle_points(5, 20.0).into_iter().map(|p| p + center)) @@ -653,7 +634,7 @@ mod test { let fill = colors.next_color(); let center = Vec2::new(184.0, 22.0); let _star = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_polygon( @@ -665,7 +646,7 @@ mod test { let fill = colors.next_color(); let tl = Vec2::new(210.0, 4.0); let _rrect = ui - .new_path() + .path_builder() .with_fill_color(fill) .with_stroke_color(hex_to_vec4(0x333333FF)) .with_rounded_rectangle(tl, tl + Vec2::new(40.0, 40.0), 5.0, 0.0, 0.0, 10.0) @@ -685,7 +666,7 @@ mod test { let image_id = futures_lite::future::block_on(ui.load_image("../../img/dirt.jpg")).unwrap(); let center = Vec2::splat(w / 2.0); let _path = ui - .new_path() + .path_builder() .with_polygon( true, star_points(7, w / 2.0, w / 3.0) diff --git a/crates/renderling/src/ui/cpu/text.rs b/crates/renderling/src/ui/cpu/text.rs index fa2325cd..b1820149 100644 --- a/crates/renderling/src/ui/cpu/text.rs +++ b/crates/renderling/src/ui/cpu/text.rs @@ -8,34 +8,17 @@ use std::{ }; use ab_glyph::Rect; -use craballoc::prelude::{GpuArray, Hybrid}; use glam::{Vec2, Vec4}; use glyph_brush::*; pub use ab_glyph::FontArc; pub use glyph_brush::{Section, Text}; -use crate::{ - atlas::AtlasTexture, - pbr::Material, - stage::{Renderlet, Vertex}, -}; +use crate::{atlas::AtlasTexture, geometry::Vertex, material::Material, primitive::Primitive}; use image::{DynamicImage, GenericImage, ImageBuffer, Luma, Rgba}; use super::{Ui, UiTransform}; -// TODO: make UiText able to be updated without fully destroying it -#[derive(Debug)] -pub struct UiText { - pub cache: GlyphCache, - pub vertices: GpuArray, - pub transform: UiTransform, - pub texture: Hybrid, - pub material: Hybrid, - pub renderlet: Hybrid, - pub bounds: (Vec2, Vec2), -} - pub struct UiTextBuilder { ui: Ui, material: Material, @@ -47,14 +30,14 @@ impl UiTextBuilder { pub fn new(ui: &Ui) -> Self { Self { ui: ui.clone(), - material: Material::default(), + material: ui.stage.new_material(), brush: GlyphBrushBuilder::using_fonts(ui.get_fonts()).build(), bounds: (Vec2::ZERO, Vec2::ZERO), } } pub fn set_color(&mut self, color: impl Into) -> &mut Self { - self.material.albedo_factor = color.into(); + self.material.set_albedo_factor(color.into()); self } @@ -86,7 +69,7 @@ impl UiTextBuilder { pub fn build(self) -> UiText { let UiTextBuilder { ui, - mut material, + material, bounds, brush, } = self; @@ -106,30 +89,48 @@ impl UiTextBuilder { // UNWRAP: panic on purpose let entry = ui.stage.add_images(Some(img)).unwrap().pop().unwrap(); - log::trace!("ui text texture: {entry:#?}"); - material.albedo_texture_id = entry.id(); - - let (vertices, material, renderlet) = ui + material.set_albedo_texture(&entry); + let vertices = ui.stage.new_vertices(mesh); + let transform = ui.new_transform(); + let renderlet = ui .stage - .builder() - .with_vertices(mesh) - .with_material(material) - .build(); - let transform = ui.new_transform(vec![renderlet.id()]); - renderlet.modify(|r| r.transform_id = transform.id()); - + .new_primitive() + .with_vertices(vertices) + .with_transform(&transform.transform) + .with_material(&material); UiText { - cache, + _cache: cache, bounds, - vertices: vertices.into_gpu_only(), transform, - texture: entry, - material, + _texture: entry, + _material: material, renderlet, } } } +pub struct UiText { + pub(crate) transform: UiTransform, + pub(crate) renderlet: Primitive, + pub(crate) bounds: (Vec2, Vec2), + + pub(crate) _cache: GlyphCache, + pub(crate) _texture: AtlasTexture, + pub(crate) _material: Material, +} + +impl UiText { + /// Returns the bounds of this text. + pub fn bounds(&self) -> (Vec2, Vec2) { + self.bounds + } + + /// Returns the transform of this text. + pub fn transform(&self) -> &UiTransform { + &self.transform + } +} + /// A text cache maintained mostly by ab_glyph. pub struct Cache { img: image::ImageBuffer, Vec>, @@ -330,7 +331,7 @@ impl GlyphCache { #[cfg(test)] mod test { - use crate::{test::BlockOnFuture, ui::Ui, Context}; + use crate::{context::Context, test::BlockOnFuture, ui::Ui}; use glyph_brush::Section; use super::*; @@ -346,7 +347,7 @@ mod test { let ui = Ui::new(&ctx); let _font_id = ui.add_font(font); let _text = ui - .new_text() + .text_builder() .with_section( Section::default() .add_text( @@ -390,13 +391,15 @@ mod test { ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"), ) .unwrap(); + log::info!("loaded font"); + let text1 = "Voluptas magnam sint et incidunt. Aliquam praesentium voluptas ut nemo \ laboriosam. Dicta qui et dicta."; let text2 = "Inventore impedit quo ratione ullam blanditiis soluta aliquid. Enim \ molestiae eaque ab commodi et.\nQuidem ex tempore ipsam. Incidunt suscipit \ aut commodi cum atque voluptate est."; let text = ui - .new_text() + .text_builder() .with_section( Section::default().add_text( Text::new(text1) @@ -415,33 +418,50 @@ mod test { .with_bounds((400.0, f32::INFINITY)), ) .build(); + log::info!("created text"); let (fill, stroke) = ui - .new_path() + .path_builder() .with_fill_color([1.0, 1.0, 0.0, 1.0]) .with_stroke_color([1.0, 0.0, 1.0, 1.0]) .with_rectangle(text.bounds.0, text.bounds.1) .fill_and_stroke(); + log::info!("filled and stroked"); - for path in [&fill, &stroke] { + for (i, path) in [&fill, &stroke].into_iter().enumerate() { + log::info!("for {i}"); // move the path to (50, 50) path.transform.set_translation(Vec2::new(51.0, 53.0)); + log::info!("translated"); // move it to the back - path.transform.set_z(-0.1); + path.transform.set_z(0.1); + log::info!("z'd"); } + log::info!("transformed"); let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); + log::info!("rendered"); let img = frame.read_image().block().unwrap(); - img_diff::assert_img_eq("ui/text/overlay.png", img); - let depth_img = ui - .stage - .get_depth_texture() - .read_image() - .block() - .unwrap() - .unwrap(); - img_diff::assert_img_eq("ui/text/overlay_depth.png", depth_img); + if let Err(e) = + img_diff::assert_img_eq_cfg_result("ui/text/overlay.png", img, Default::default()) + { + let depth_img = ui + .stage + .get_depth_texture() + .read_image() + .block() + .unwrap() + .unwrap(); + let e2 = img_diff::assert_img_eq_cfg_result( + "ui/text/overlay_depth.png", + depth_img, + Default::default(), + ) + .err() + .unwrap_or_default(); + panic!("{e}\n{e2}"); + } } #[test] @@ -452,8 +472,9 @@ mod test { ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"), ) .unwrap(); - let mut _text = ui - .new_text() + log::info!("loaded font"); + let text = ui + .text_builder() .with_section( Section::default() .add_text( @@ -472,8 +493,10 @@ mod test { img_diff::assert_img_eq("ui/text/can_recreate_0.png", img); log::info!("replacing text"); - _text = ui - .new_text() + ui.remove_text(&text); + + let _ = ui + .text_builder() .with_section( Section::default() .add_text( diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 838f7e93..18c7163f 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -1,9 +1,17 @@ //! WASM tests. #![allow(dead_code)] +use craballoc::{ + runtime::WgpuRuntime, + slab::{SlabAllocator, SlabBuffer}, +}; use glam::{Vec3, Vec4}; use image::DynamicImage; -use renderling::{prelude::*, texture::CopiedTextureBuffer}; +use renderling::{ + context::{Context, Frame}, + geometry::Vertex, + texture::CopiedTextureBuffer, +}; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; use web_sys::wasm_bindgen::prelude::*; use wire_types::{Error, PixelType}; @@ -40,14 +48,14 @@ async fn can_write_system_info_artifact() { #[wasm_bindgen_test] async fn can_create_headless_ctx() { - let _ctx = renderling::Context::try_new_headless(256, 256, None) + let _ctx = Context::try_new_headless(256, 256, None) .await .unwrap_throw(); } #[wasm_bindgen_test] async fn stage_creation() { - let ctx = renderling::Context::try_new_headless(256, 256, None) + let ctx = Context::try_new_headless(256, 256, None) .await .unwrap_throw(); let _stage = ctx.new_stage(); @@ -976,10 +984,13 @@ async fn can_clear_background() { let stage = ctx .new_stage() .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); + // ANCHOR: manual_context_frame let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); let seen = frame.read_image().await.unwrap(); assert_img_eq("clear.png", seen).await; + frame.present(); + // ANCHOR_END: manual_context_frame } // #[wasm_bindgen_test] @@ -1008,8 +1019,13 @@ async fn can_render_hello_triangle() { // This is a wasm version of cmy_triangle_sanity let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); - let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); - let _rez = stage.builder().with_vertices(right_tri_vertices()).build(); + let (projection, view) = renderling::camera::default_ortho2d(100.0, 100.0); + let _camera = stage + .new_camera() + .with_projection_and_view(projection, view); + let _rez = stage + .new_primitive() + .with_vertices(stage.new_vertices(right_tri_vertices())); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); diff --git a/crates/xtask/src/deps.rs b/crates/xtask/src/deps.rs new file mode 100644 index 00000000..43952b52 --- /dev/null +++ b/crates/xtask/src/deps.rs @@ -0,0 +1,26 @@ +//! Xtask dependency helpers. +//! +//! This module helps installing xtask's required dependencies. + +pub async fn has_binary(name: impl AsRef) -> bool { + let output = tokio::process::Command::new("hash") + .arg(name.as_ref()) + .output() + .await + .expect("Failed to execute process"); + + output.status.success() +} + +pub async fn cargo_install(name: impl AsRef) { + log::warn!("installing mdbook"); + + let mut process = tokio::process::Command::new("cargo") + .args(["install", name.as_ref()]) + .spawn() + .unwrap(); + let status = process.wait().await.unwrap(); + if !status.success() { + panic!("Failed installing {}", name.as_ref()); + } +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 514c82bd..30c48cdc 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1,6 +1,7 @@ //! A build helper for the `renderling` project. use clap::{Parser, Subcommand}; +mod deps; mod server; #[derive(Subcommand)] @@ -32,6 +33,62 @@ enum Command { #[clap(long)] chrome: bool, }, + /// Perform actions regarding the manual + Manual(Manual), +} + +#[derive(Parser)] +pub struct Manual { + /// Whether to skip building the docs + #[clap(long)] + no_build_docs: bool, + + /// Whether to skip testing the manual + #[clap(long)] + no_test: bool, + + /// The URL to the renderling docs + #[clap(long, default_value = "http://localhost:4000")] + docs_url: String, + + /// Serve the manual instead of simply building it + #[clap(long)] + serve: bool, +} + +impl Manual { + async fn install_deps() { + const DEPS: &[&str] = &["mdbook", "mdbook-environment"]; + for dep in DEPS { + if !deps::has_binary(dep).await { + deps::cargo_install(dep).await; + } + } + } + + async fn build_docs() { + log::info!("building docs"); + let mut process = tokio::process::Command::new("cargo") + .args(["doc", "-p", "renderling", "--all-features"]) + .spawn() + .unwrap(); + let status = process.wait().await.unwrap(); + if !status.success() { + panic!("Failed building docs"); + } + } + + async fn test() { + log::info!("testing the manual snippets"); + let mut process = tokio::process::Command::new("cargo") + .args(["test", "-p", "test-manual"]) + .spawn() + .unwrap(); + let status = process.wait().await.unwrap(); + if !status.success() { + panic!("Failed testing manual"); + } + } } #[derive(Parser)] @@ -99,5 +156,55 @@ async fn main() { Command::WasmServer => { server::serve().await; } + Command::Manual(Manual { + no_build_docs, + no_test, + docs_url, + serve, + }) => { + log::info!("checking dependencies for the manual"); + Manual::install_deps().await; + if !no_test { + Manual::test().await; + } + if !no_build_docs { + Manual::build_docs().await; + } + + if serve { + log::info!("serving manual"); + + // serve the docs in the meantime + let _docs_handle = tokio::spawn(server::serve_docs()); + + let mut build = tokio::process::Command::new("mdbook") + .arg("serve") + .current_dir( + std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR")).join("manual"), + ) + .env("DOCS_URL", docs_url) + .spawn() + .unwrap(); + let build_status = build.wait().await.unwrap(); + if !build_status.success() { + log::error!("could not build the manual"); + } + } else { + log::info!("building manual"); + + let mut build = tokio::process::Command::new("mdbook") + .arg("build") + .env("DOCS_URL", docs_url) + .current_dir( + std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR")).join("manual"), + ) + .spawn() + .unwrap(); + let build_status = build.wait().await.unwrap(); + if !build_status.success() { + log::error!("could not build the manual"); + } + } + } } } diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs index 1dfd8f61..f1b54c27 100644 --- a/crates/xtask/src/server.rs +++ b/crates/xtask/src/server.rs @@ -17,7 +17,7 @@ use tokio::io::AsyncWriteExt; use wire_types::Error; pub async fn serve() { - log::info!("starting the webdriver proxy"); + log::info!("starting the xtask server"); let app = Router::new() .route("/test_img/{*path}", get(static_file)) .route("/assert_img_eq/{*filename}", options(accept)) @@ -35,7 +35,7 @@ pub async fn serve() { /// Responds with access control headers to allow anything from anywhere. async fn accept(request: Request) -> Response { - log::info!("accept: {request:#?}"); + log::debug!("accept: {request:#?}"); Response::builder() .status(StatusCode::OK) .header("accept", "*/*") @@ -46,16 +46,25 @@ async fn accept(request: Request) -> Response { .unwrap() } -async fn static_file(Path(path): Path) -> Result { - log::info!("requested static '{path}'"); - let test_img = std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("test_img"); - let path = test_img.join(path); - if path.exists() { - let bytes = tokio::fs::read(&path).await.map_err(|e| { - log::error!("could not read path '{path:?}': {e}"); +async fn static_file_inner( + path: impl AsRef, + prefix: impl AsRef, +) -> Result { + log::info!( + "requested '{}' '{}'", + prefix.as_ref().display(), + path.as_ref().display() + ); + let mut full_path = prefix.as_ref().join(path); + if full_path.is_dir() { + full_path = full_path.join("index.html"); + } + if full_path.exists() { + let bytes = tokio::fs::read(&full_path).await.map_err(|e| { + log::error!("could not read path '{full_path:?}': {e}"); StatusCode::BAD_REQUEST })?; - let mime = new_mime_guess::from_path(path); + let mime = new_mime_guess::from_path(full_path); let mimetype = if let Some(mt) = mime.first() { mt.to_string() } else { @@ -72,11 +81,16 @@ async fn static_file(Path(path): Path) -> Result { })?; Ok(resp) } else { - log::error!("{path:?} not found"); + log::error!("{full_path:?} not found"); Err(StatusCode::NOT_FOUND) } } +async fn static_file(Path(path): Path) -> Result { + let test_img_dir = std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("test_img"); + static_file_inner(path, test_img_dir).await +} + fn image_from_wire(img: wire_types::Image) -> Result { match img.pixel { wire_types::PixelType::Rgb8 => { @@ -195,3 +209,18 @@ async fn artifact(Path(parts): Path>, body: Body) -> Response { .body(Json(result).into_response().into_body()) .unwrap() } + +pub async fn serve_docs() { + log::info!("starting the xtask docs server"); + let app = Router::new().route("/{*rest}", get(docs)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") + .await + .unwrap(); + log::info!("serving docs"); + axum::serve(listener, app).await.unwrap(); +} + +async fn docs(Path(path): Path) -> impl IntoResponse { + log::info!("path: {path:#?}"); + static_file_inner(path, "target/doc").await +} diff --git a/manual/.gitignore b/manual/.gitignore new file mode 100644 index 00000000..7585238e --- /dev/null +++ b/manual/.gitignore @@ -0,0 +1 @@ +book diff --git a/manual/book.toml b/manual/book.toml new file mode 100644 index 00000000..2379f046 --- /dev/null +++ b/manual/book.toml @@ -0,0 +1,21 @@ +[book] +authors = ["Schell Carl Scivally"] +language = "en" +src = "src" +title = "The Renderling Manual" +description = "Operations manual for the Renderling real-time renderer" + +# The "environment" preprocessor, provided by `mdbook-environment`, +# references variables from here and the environment that can then be +# interpolated in the book with `{{VARIABLE}}`. +# +# Setting variables here overrides any set in the environment, so for +# variables that must change based on environment we add them here +# commented out. +[preprocessor.environment] +# when deploying the manual +# DOCS_URL = "https://docs.rs/renderling/latest" + +# when running locally +# DOCS_URL = "http://localhost:4000" + diff --git a/manual/src/SUMMARY.md b/manual/src/SUMMARY.md new file mode 100644 index 00000000..b1f1b29d --- /dev/null +++ b/manual/src/SUMMARY.md @@ -0,0 +1,11 @@ +# Summary + +- [Welcome](./welcome.md) +- [Project setup](./setup.md) +- [Context creation](./context.md) +- [Staging resources](./stage.md) +- [Loading GLTF files](./gltf.md) +- [Rendering with a skybox](./skybox.md) +- [Lighting](./lighting.md) + - [Analytical lights](./lighting/analytical.md) + - [Image based lighting](./lighting/ibl.md) diff --git a/manual/src/assets/gltf-example-shadow.png b/manual/src/assets/gltf-example-shadow.png new file mode 100644 index 00000000..e22d44d0 Binary files /dev/null and b/manual/src/assets/gltf-example-shadow.png differ diff --git a/manual/src/assets/gltf-example-unlit.png b/manual/src/assets/gltf-example-unlit.png new file mode 100644 index 00000000..04c26d2a Binary files /dev/null and b/manual/src/assets/gltf-example-unlit.png differ diff --git a/manual/src/assets/helipad.jpg b/manual/src/assets/helipad.jpg new file mode 100644 index 00000000..d36c5417 Binary files /dev/null and b/manual/src/assets/helipad.jpg differ diff --git a/manual/src/assets/skybox.png b/manual/src/assets/skybox.png new file mode 100644 index 00000000..4081a39a Binary files /dev/null and b/manual/src/assets/skybox.png differ diff --git a/manual/src/assets/stage-example-gone.png b/manual/src/assets/stage-example-gone.png new file mode 100644 index 00000000..c06b0e02 Binary files /dev/null and b/manual/src/assets/stage-example-gone.png differ diff --git a/manual/src/assets/stage-example.png b/manual/src/assets/stage-example.png new file mode 100644 index 00000000..98ef0adf Binary files /dev/null and b/manual/src/assets/stage-example.png differ diff --git a/manual/src/context.md b/manual/src/context.md new file mode 100644 index 00000000..ac933837 --- /dev/null +++ b/manual/src/context.md @@ -0,0 +1,57 @@ +# Context + +The first step of any `renderling` program starts with [`renderling::context::Context`][Context]. + +The `Context` is responsible for managing the underlying [`wgpu`][wgpu] runtime, including the +instance, adapter and queue. +It also sets up the [`RenderTarget`][RenderTarget], according to how the `Context` was created. + +On that note, it's important to know that there are two main ways to create a `Context`: + +1. A headless context, which renders to a texture, can be created with + [`Context::headless`][Context#headless] or + [`Context::try_new_headless`][Context#try_headless], depending on your error + handling scenario. +2. A surface context, with a window (possibly from [`winit`][winit]) or a canvas + from [`web-sys`][web-sys]. + +```rust, ignore +{{#include ../../crates/examples/src/context.rs:create}} +``` + +## Getting a frame + +Another important concept is the [`Frame`][Frame]. Each time you'd like to present a new image +you must acquire a frame from the `Context` with [`Context::get_next_frame`][Context#get_next_frame] +and present it with [`Frame::present`][Frame#present]. + +### Presenting on WASM + +When on WASM (aka running in a browser), [`Frame::present`][Frame#present] is a noop. +It's still a good idea to use it though, so you don't forget when programming in native. + +### Saving the frame + +You can also read out the frame to an image provided by the [`image`][image] +crate. See the [`Frame`][Frame] docs for help with the `read_*` functions. + +### Frame example + +```rust, ignore +{{#include ../../crates/examples/src/context.rs:frame}} +``` + +[Context]: {{DOCS_URL}}/renderling/context/struct.Context.html +[Context#headless]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.headless +[Context#try_headless]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.try_headless +[Context#get_next_frame]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.get_next_frame + +[Frame]: {{DOCS_URL}}/renderling/context/struct.Frame.html +[Frame#present]: {{DOCS_URL}}/renderling/context/struct.Frame.html#method.present + +[RenderTarget]: {{DOCS_URL}}/renderling/context/struct.RenderTarget.html + +[image]: https://crates.io/crates/image +[wgpu]: https://crates.io/crates/wgpu +[winit]: https://crates.io/crates/winit +[web-sys]: https://crates.io/crates/web-sys diff --git a/manual/src/gltf.md b/manual/src/gltf.md new file mode 100644 index 00000000..1c2eea3f --- /dev/null +++ b/manual/src/gltf.md @@ -0,0 +1,79 @@ +# Loading GLTF files 📂 + +`renderling`'s built-in model format is [GLTF](https://www.khronos.org/gltf/), a +versatile and efficient format for transmitting 3D models. GLTF, which stands +for GL Transmission Format, is designed to be a compact, interoperable format +that can be used across various platforms and applications. It supports a wide +range of features including geometry, materials, animations, and more, making it +a popular choice for 3D graphics. + +## Using GLTF files + +The previous section on [staging resources](./stage) covered the creation of +various GPU resources such as [`Camera`], [`Vertices`], [`Material`], +[`Primitive`], and [`Transform`]. When you load a GLTF file into `renderling`, +it automatically stages a collection of these resources. This means that the +GLTF file is parsed, and the corresponding GPU resources are created and +returned to you, allowing you to integrate them into your application +seamlessly. + +## Example + +We'll start by creating our [`Context`], [`Stage`] and [`Camera`]: + +```rust,ignore +{{#include ../../crates/examples/src/gltf.rs:setup}} +``` + +Then we load our GLTF file through the [`Stage`] with +[`Stage::load_gltf_document_from_path`], and as long as there are no errors it returns a +[`GltfDocument`]: + +```rust,ignore +{{#include ../../crates/examples/src/gltf.rs:load}} +``` + +On WASM we would use [`Stage::load_gltf_document_from_bytes`] as the filesystem +is unavailable. + +Notice how in the above example we call [`GltfDocument::into_gpu_only`] to +unload the mesh geometry from the CPU. + +## Render + +```rust,ignore +{{#include ../../crates/examples/src/gltf.rs:render_1}} +``` + +## Result + +![a loaded GLTF file, a marble bust, in shadow](assets/gltf-example-shadow.png) + +But wait! It's all in shadow. + +This is because we haven't added any lighting. + +We have two options here: +1. Turn of lighting and show the scene "unlit", using [`Stage::set_has_lighting`] +2. Add some lights + +For now we'll go with option `1`, as lighting happens in a later section: + +```rust,ignore +{{#include ../../crates/examples/src/gltf.rs:no_lights}} +``` + +![a loaded GLTF file, a marble bust, unlit](assets/gltf-example-unlit.png) + +[`Context`]: {{DOCS_URL}}/renderling/context/struct.Context.html +[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html +[`Stage::load_gltf_document_from_bytes`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.load_gltf_document_from_bytes +[`Stage::load_gltf_document_from_path`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.load_gltf_document_from_path +[`Stage::set_has_lighting`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_has_lighting +[`GltfDocument`]: {{DOCS_URL}}/renderling/gltf/struct.GltfDocument.html +[`GltfDocument::into_gpu_only`]: {{DOCS_URL}}/renderling/gltf/struct.GltfDocument.html#method.into_gpu_only +[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html +[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html +[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html +[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html +[`Transform`]: {{DOCS_URL}}/renderling/transform/struct.Transform.html diff --git a/manual/src/lighting.md b/manual/src/lighting.md new file mode 100644 index 00000000..1e0b6286 --- /dev/null +++ b/manual/src/lighting.md @@ -0,0 +1,13 @@ +# Lighting 💡 + +Lighting in `renderling` comes in a few flavors: + +1. Unlit +2. Analytical lights + * directional + * point + * spot +3. Image based lighting + +We've already used the "unlit" method of turning off all lighting on the stage. +Now let's learn about analytical lights, and then image based lighting. diff --git a/manual/src/lighting/analytical.md b/manual/src/lighting/analytical.md new file mode 100644 index 00000000..818aa0b0 --- /dev/null +++ b/manual/src/lighting/analytical.md @@ -0,0 +1,3 @@ +# Analytical lights + +TODO diff --git a/manual/src/lighting/ibl.md b/manual/src/lighting/ibl.md new file mode 100644 index 00000000..06422137 --- /dev/null +++ b/manual/src/lighting/ibl.md @@ -0,0 +1,3 @@ +# Image based lighting + +TODO diff --git a/manual/src/reflinks.md b/manual/src/reflinks.md new file mode 100644 index 00000000..3c0252d8 --- /dev/null +++ b/manual/src/reflinks.md @@ -0,0 +1,43 @@ +# docs + +[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html +[`Camera::with_default_perspective`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html#method.with_default_perspective + +[`Context`]: {{DOCS_URL}}/renderling/context/struct.Context.html +[`Context::new_stage`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.new_stage +[`Context::headless`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.headless +[`Context::try_headless`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.try_headless +[`Context::get_next_frame`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.get_next_frame + +[`Frame`]: {{DOCS_URL}}/renderling/context/struct.Frame.html +[`Frame::present`]: {{DOCS_URL}}/renderling/context/struct.Frame.html#method.present + +[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html + +[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html + +[`Mat4`]: https://docs.rs/glam/latest/glam/f32/struct.Mat4.html + +[`RenderTarget`]: {{DOCS_URL}}/renderling/context/struct.RenderTarget.html + +[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html +[`Stage::new_camera`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_camera + +[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html +[`Vertices::get_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.get_vertex +[`Vertices::modify_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.modify_vertex +[`Vertices::set_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.set_vertex + +[`Vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertex.html + +# friends + +[glam]: https://crates.io/crates/glam +[image]: https://crates.io/crates/image +[wgpu]: https://crates.io/crates/wgpu +[winit]: https://crates.io/crates/winit +[web-sys]: https://crates.io/crates/web-sys + +# other + +[builder-pattern]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html diff --git a/manual/src/setup.md b/manual/src/setup.md new file mode 100644 index 00000000..d628da42 --- /dev/null +++ b/manual/src/setup.md @@ -0,0 +1,40 @@ +# Setup + +`renderling` is a Rust library, so first you'll need to get familiar with the +language. Visit if you're not +already familiar. + +Once you're ready, start a new project with `cargo new`. +Then `cd` into your project directory and add `renderling` as a dependency: + +``` +cargo add --git https://github.com/schell/renderling.git --branch main +``` + +## patch crates.io + +`renderling` is special in that all the shaders are written in Rust using +[Rust-GPU](https://rust-gpu.github.io/), which is currently between +releases. For this reason we need to add an entry to the `[patch.crates-io]` +section of our `Cargo.toml`: + +```toml +[patch.crates-io] +spirv-std = { git = "https://github.com/rust-gpu/rust-gpu.git", rev = "de03e8d" } +``` + +This is a temporary workaround that will be resolved after the next Rust-GPU +release. + +The rest is Rust business as usual. + +## WASM + +TODO: write about setting up a WASM project. + +## Re-exports + +`renderling` **re-exports** [`glam`][glam] from its top level module, +because it provides the underlying mathematical types used throughout the API. + +[glam]: https://crates.io/crates/glam diff --git a/manual/src/skybox.md b/manual/src/skybox.md new file mode 100644 index 00000000..8b05374b --- /dev/null +++ b/manual/src/skybox.md @@ -0,0 +1,49 @@ +# Rendering a skybox 🌌 + +One of the most striking effects we can provide is a +[skybox](https://en.wikipedia.org/wiki/Skybox_(video_games)). + +Using a skybox is an easy way to improve immersion, and with +`renderling` your skyboxes can also illuminate the scene, but +we'll save that for a later example. For now let's set up +simple skybox for our marble bust scene. + +## Building on the stage example + +We'll start out this example by extending the example from the +[loading GLTF files](./gltf) section. In that example we loaded +a model of an old marble bust: + +```rust,ignore +{{#include ../../crates/examples/src/skybox.rs:setup}} +``` + +![image of a marble bust](assets/gltf-example-unlit.png) + +## Adding the skybox + +In `renderling`, skyboxes get their background from an "HDR" image. +These are typically large three dimensional images. You can find +free HDR images [at PolyHaven](https://polyhaven.com/hdris) and other +places around the web. + +For this example we'll be using this HDR: + +![Rooftop helipad](assets/helipad.jpg) + +```rust,ignore +{{#include ../../crates/examples/src/skybox.rs:skybox}} +``` + +Then we render: + +```rust,ignore +{{#include ../../crates/examples/src/skybox.rs:render_skybox}} +``` + + +## Results + +And there we go! + +![renderling skybox](assets/skybox.png) diff --git a/manual/src/stage.md b/manual/src/stage.md new file mode 100644 index 00000000..9d06a2c7 --- /dev/null +++ b/manual/src/stage.md @@ -0,0 +1,209 @@ +# Staging resources 🎭 + +The [`Stage`] is the most important type in `renderling`. +It's responsible for staging all your scene's data on the GPU, as well as +linking all the various effects together and rendering it all. + + +- [Stage creation](#stage-creation) +- [Resource creation](#resource-creation) + - [Camera](#camera) + - [glam and re-exports](#glam-and-re-exports) + - [Creation](#creation) + - [Geometry](#geometry) + - [Material](#material) + - [Primitive](#primitive) +- [Rendering](#rendering) +- [Results](#results) +- [Removing resources](#removing-resources) +- [Visibility](#visibility) + + + +## Stage creation + +The `Stage` is created with [`Context::new_stage`]. + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:creation}} +``` + +Notice that context creation is _asynchronous_. Most of the `renderling` API is +synchronous, but context creation is one of two exceptions - the other being +reading data back from the GPU. + +Also note that we can set the background color of the stage using a `Vec4`. +Above we've set the background to a light gray. + +## Resource creation + +Now we'll begin using the `Stage` to create our scene's resources. At the end of +all our staging we should end up with a [`Camera`] and one simple +[`Primitive`] representing a colored unit cube, sitting right in +front of the camera. + +### Camera + +In order to see our scene we need a [`Camera`]. + +The camera controls the way our scene looks when rendered. It uses separate projection and view +matrices to that end. Discussing these matrices is out of scope for this manual, but there are +plenty of resources online about what they are and how to use them. + +#### glam and re-exports + +One important detail about these matrices, though, is that they come from the [`glam`][glam] +library. Specifically they are [`Mat4`][Mat4], which are a 4x4 transformation matrix. + +#### Creation + +On with our camera. Creation is dead simple using [`Stage::new_camera`]. + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:camera}} +``` + +Each resource returned by the many `Stage::new_*` functions return resources that adhere +to the [builder pattern][builder]. That means the value a `Stage::new_*` function returns +can be chained with other calls that configure it. This pattern is nice because it +allows your editor to display the customizations available, which makes API discovery +easier for everyone. + +Above we use [`Camera::with_default_perspective`] to +set the camera to use a default perspective projection. + +Note that usually when we create a `Camera`, we have to tell +the `Stage` that we want to **use** the camera, but the first `Camera` created will +automatically be used. We could potentially have _many_ cameras and switch them around at will +by calling `Stage::use_camera` before rendering. + +### Geometry + +The first step to creating a [`Primitive`] is staging some vertices in a triangle +mesh. For this example we'll use the triangle mesh of the unit cube. The +[`renderling::math`][math] module provides a convenience function for generating this mesh. + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:unit_cube_vertices}} +``` + +Here we create [`Vertices`], which stages the unit cube points on the GPU. + +Next we'll unload those points from the CPU, to free up the memory: + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:unload_vertices}} +``` + +Unloading the CPU memory like this isn't strictly necessary, but it's beneficial to +know about. If we were planning on inspecting or modifying the underlying +[`Vertex`] values with [`Vertices::get_vertex`] and +[`Vertices::modify_vertex`], we could skip this step. +After unloading, however, we can still set a [`Vertex`] at a specific +index using [`Vertices::set_vertex`]. + +### Material + +Next we stage a [`Material`]. + +Materials denote how a mesh looks by specifying various colors and shading values, +as well as whether or not the material is lit by our lighting, which we'll talk about +in later chapters. For now we'll provide a material that doesn't really do anything +except let the vertex colors show through. + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:material}} +``` + +### Primitive + +Now that we have some [`Vertices`] and a [`Material`] we can create our primitive +using the familiar builder pattern. + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:prim}} +``` + +We don't actually do anything with the primitive at this point though. + +## Rendering + +Now the scene is set and we're ready to render. + +Rendering is a three-step process: + +1. Get the next frame +2. Render the staged scene into the view of the frame +3. Present the frame + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:render}} +``` + +Above we added an extra step where we read an image of the frame from the GPU, +so we can see it here. + +## Results + +![image of a unit cube with colored vertices](assets/stage-example.png) + +And there you have it! We've rendered a nice cube. + +## Removing resources + +To remove resources from the stage we can usually just `Drop` them from all +scopes. There are a few types that require extra work to remove, though. + +[`Primitive`]s must be manually removed with [`Stage::remove_primitive`], +which removes the primitive from all internal lists (like the list of draw calls). + +Lights must also be removed from the stage for similar reasons. + +Now we'll run through removing the cube primitive, but first let's see how many +bytes we've committed to the GPU through the stage: + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:committed_size_bytes}} +``` + +As of this writing, these lines print out `8296`, or roughly 8k bytes. +That may seem like a lot for one cube, but keep in mind that is a count of +all bytes in all buffers, including any internal machinery. + +Now let's remove the cube primitive, drop the other resources, and render again: + +```rust,ignore +{{#include ../../crates/examples/src/stage.rs:removal}} +``` + +![the cube is gone](assets/stage-example-gone.png) + +## Visibility + +If instead we wanted to keep the resources around but make the [`Primitive`] invisible, +we could have used [`Primitive::set_visible`]. + +See the [`Stage`] and [`Primitive`] docs for more info. + +[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html +[`Stage::new_camera`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_camera +[`Stage::remove_primitive`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.remove_primitive +[`Context::new_stage`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.new_stage +[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html +[`Primitive::set_visible`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html#method.set_visible +[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html +[`Camera::with_default_perspective`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html#method.with_default_perspective +[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html +[`Vertices::get_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.get_vertex +[`Vertices::modify_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.modify_vertex +[`Vertices::set_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.set_vertex +[`Vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertex.html +[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html + +[math]: {{DOCS_URL}}/renderling/math/index.html + +[Mat4]: https://docs.rs/glam/latest/glam/f32/struct.Mat4.html + +[glam]: https://crates.io/crates/glam + +[builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html diff --git a/manual/src/welcome.md b/manual/src/welcome.md new file mode 100644 index 00000000..b2f53510 --- /dev/null +++ b/manual/src/welcome.md @@ -0,0 +1,41 @@ +
+ renderling mascot +
+ +# Welcome + +Welcome to the `renderling` operator's manual! + +`renderling` is a cutting-edge, GPU-driven renderer designed to efficiently +handle complex scenes by leveraging GPU capabilities for most rendering +operations. It is particularly suited for indie game developers and researchers +interested in high-performance graphics rendering while working with GLTF files +and large-scale scenes. + +The library is written in Rust and supports modern rendering techniques such as +forward+ rendering and physically based shading, making it ideal for +applications requiring advanced lighting and material effects. + + +This project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund +established by [NLnet](https://nlnet.nl) with financial support from the +European Commission's [Next Generation Internet](https://ngi.eu) program. +Learn more at the [NLnet project page](https://nlnet.nl/project/Renderling). + +[NLnet foundation logo](https://nlnet.nl) [NGI Zero Logo](https://nlnet.nl/core) + +## Helpful Links + +* The official website is . + Here you can read the latest news and implementation details. + You're also likely reading this on this site! + +* The documentation is <{{DOCS_URL}}/renderling/index.html>. + +* The GitHub repo and issue track is . + +* The project site on NLnet is