diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index 593d8cfa..8aa35a61 100644 --- a/crates/renderling/src/bvol.rs +++ b/crates/renderling/src/bvol.rs @@ -44,7 +44,7 @@ pub fn intersect_planes(p0: &Vec4, p1: &Vec4, p2: &Vec4) -> Vec3 { /// Calculates distance between plane and point pub fn dist_bpp(plane: &Vec4, point: Vec3) -> f32 { - plane.x * point.x + plane.y * point.y + plane.z * point.z + plane.w + (plane.x * point.x + plane.y * point.y + plane.z * point.z + plane.w).abs() } /// Calculates the most inside vertex of an AABB. @@ -123,8 +123,8 @@ impl Aabb { self.min == self.max } - /// Determines whether this `Aabb` can be seen by `camera` after being transformed by - /// `transform`. + /// 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 { let transform = Mat4::from(transform); let min = transform.transform_point3(self.min); @@ -364,14 +364,14 @@ pub trait BVol { /// In order for a bounding volume to be inside the frustum, it must not be /// culled by any plane. /// - /// Coherence is provided by the `lpindex` argument, which should be the index of - /// the first plane found that culls this volume, given as part of the return - /// value of this function. + /// Coherence is provided by the `lpindex` argument, which should be the + /// index of the first plane found that culls this volume, given as part + /// of the return value of this function. /// /// Returns `true` if the volume is outside the frustum, `false` otherwise. /// - /// Returns the index of first plane found that culls this volume, to cache and use later - /// as a short circuit. + /// Returns the index of first plane found that culls this volume, to cache + /// and use later as a short circuit. fn coherent_test_is_volume_outside_frustum( &self, frustum: &Frustum, diff --git a/crates/renderling/src/camera.rs b/crates/renderling/src/camera.rs index 0436b6b4..08fb231f 100644 --- a/crates/renderling/src/camera.rs +++ b/crates/renderling/src/camera.rs @@ -1,8 +1,8 @@ //! Camera projection, view and utilities. use crabslab::SlabItem; -use glam::{Mat4, Vec3}; +use glam::{Mat4, Vec3, Vec4Swizzles}; -use crate::bvol::Frustum; +use crate::bvol::{dist_bpp, Frustum}; /// A camera used for transforming the stage during rendering. /// @@ -80,6 +80,23 @@ impl Camera { pub fn view_projection(&self) -> Mat4 { self.projection * self.view } + + pub fn z_near(&self) -> f32 { + dist_bpp(&self.frustum.planes[0], self.position) + } + + pub fn z_far(&self) -> f32 { + dist_bpp(&self.frustum.planes[5], self.position) + } + + /// Linearize and normalize a depth value. + pub fn linearize_depth_value(&self, depth: f32) -> f32 { + let z_near = self.z_near(); + let z_far = self.z_far(); + let z_linear = (2.0 * z_near) / (z_far + z_near - depth * (z_far - z_near)); + // Normalize the linearized depth to [0, 1] + (z_linear - z_near) / (z_far - z_near) + } } /// Returns the projection and view matrices for a camera with default @@ -88,8 +105,8 @@ impl Camera { /// The default projection and view matrices are defined as: /// /// ```rust -/// use renderling::prelude::*; /// use glam::*; +/// use renderling::prelude::*; /// /// let width = 800.0; /// let height = 600.0; @@ -148,3 +165,25 @@ pub fn default_ortho2d(width: f32, height: f32) -> (Mat4, Mat4) { let view = Mat4::IDENTITY; (projection, view) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn camera_znear_zfar() { + let znear = 0.01; + let zfar = 100.0; + let projection = Mat4::perspective_rh(core::f32::consts::PI / 4.0, 1.0, znear, zfar); + let view = Mat4::look_at_rh(Vec3::new(5.0, 5.0, 10.0), Vec3::ZERO, Vec3::Y); + let camera = Camera::new(projection, view); + + log::info!("near_plane: {}", camera.frustum.planes[0]); + log::info!("znear: {}", camera.z_near()); + log::info!("far_plane: {}", camera.frustum.planes[5]); + log::info!("zfar: {}", camera.z_far()); + + assert_eq!(znear, camera.z_near(), "znear"); + assert_eq!(zfar, camera.z_far(), "zfar"); + } +} diff --git a/crates/renderling/src/cull.rs b/crates/renderling/src/cull.rs index 5475ca85..daabc9fc 100644 --- a/crates/renderling/src/cull.rs +++ b/crates/renderling/src/cull.rs @@ -65,7 +65,7 @@ pub struct DepthPyramidDescriptor { 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 + !(global_invocation.x < current_size.x && global_invocation.y < current_size.y) } /// Return the [`Id`] of the depth at the given `mip_level` and coordinate. @@ -85,7 +85,7 @@ pub type DepthPyramidImageMut = Image!(2D, format = r32f, depth = false); /// /// It is assumed that a [`PyramidDescriptor`] is stored at index `0` in the /// given slab. -#[spirv(compute(threads(32)))] +#[spirv(compute(threads(32, 32, 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, diff --git a/crates/renderling/src/cull/cpu.rs b/crates/renderling/src/cull/cpu.rs index 7efbaed7..2d8520e0 100644 --- a/crates/renderling/src/cull/cpu.rs +++ b/crates/renderling/src/cull/cpu.rs @@ -1,12 +1,13 @@ //! CPU side of compute culling. -use crabslab::Array; +use crabslab::{Array, Slab}; use glam::UVec2; -use snafu::Snafu; +use snafu::{OptionExt, Snafu}; use std::sync::Arc; use crate::{ - slab::{GpuArray, Hybrid, SlabAllocator}, + camera::Camera, + slab::{GpuArray, Hybrid, SlabAllocator, SlabAllocatorError}, texture::Texture, }; @@ -22,6 +23,18 @@ pub enum CullingError { #[snafu(display("Missing depth pyramid mip {index}"))] MissingMip { index: usize }, + + #[snafu(display("{source}"))] + SlabError { source: SlabAllocatorError }, + + #[snafu(display("Could not read mip {index}"))] + ReadMip { index: usize }, +} + +impl From for CullingError { + fn from(source: SlabAllocatorError) -> Self { + CullingError::SlabError { source } + } } const FRUSTUM_LABEL: Option<&str> = Some("compute-frustum-culling"); @@ -207,6 +220,7 @@ impl DepthPyramid { queue: &wgpu::Queue, size: UVec2, ) -> (Arc, bool) { + log::info!("resizing depth pyramid to {size}"); let mip = self.slab.new_array(vec![]); self.mip_data = vec![]; self.desc.modify(|desc| desc.mip = mip.array()); @@ -238,6 +252,44 @@ impl DepthPyramid { pub fn size(&self) -> UVec2 { self.desc.get().size } + + pub async fn read_images( + &self, + ctx: &crate::Context, + camera: &Camera, + ) -> Result, CullingError> { + let size = self.size(); + let slab_data = self + .slab + .read(ctx.get_device(), ctx.get_queue(), Self::LABEL, 0..) + .await?; + let mut images = vec![]; + let mut min = f32::MAX; + let mut max = f32::MIN; + for (i, mip) in self.mip_data.iter().enumerate() { + let depth_data: Vec = slab_data + .read_vec(mip.array()) + .into_iter() + .map(|depth| { + if i == 0 { + min = min.min(depth); + max = max.max(depth); + } + camera.linearize_depth_value(depth) + // depth + }) + .collect(); + log::info!("min: {min}"); + log::info!("max: {max}"); + let width = size.x >> i; + let height = size.y >> i; + let image: image::ImageBuffer, Vec> = + image::ImageBuffer::from_raw(width, height, depth_data) + .context(ReadMipSnafu { index: i })?; + images.push(image::DynamicImage::from(image)); + } + Ok(images) + } } /// Copies the depth texture to the top of the depth pyramid. @@ -378,7 +430,7 @@ impl ComputeCopyDepth { /// Computes occlusion culling on the GPU. pub struct OcclusionCulling { sample_count: u32, - depth_pyramid: DepthPyramid, + pub(crate) depth_pyramid: DepthPyramid, compute_copy_depth: ComputeCopyDepth, } @@ -413,6 +465,7 @@ impl OcclusionCulling { ) -> Result<(), CullingError> { let sample_count = depth_texture.texture.sample_count(); if sample_count != self.sample_count { + log::warn!("sample_count changed, invalidating"); self.invalidate(); // let (bindgroup_layout, pipeline) = // Self::create_bindgroup_layout_and_pipeline(device, @@ -423,6 +476,7 @@ impl OcclusionCulling { let extent = depth_texture.texture.size(); let size = UVec2::new(extent.width, extent.height); let (depth_pyramid_buffer, should_invalidate) = if size != self.depth_pyramid.size() { + log::warn!("depth texture size changed, invalidating"); self.invalidate(); self.depth_pyramid.resize(device, queue, size) } else { @@ -448,3 +502,65 @@ impl OcclusionCulling { Ok(()) } } + +#[cfg(test)] +mod test { + use crate::prelude::*; + use glam::{Mat4, Quat, Vec3, Vec4}; + + + #[test] + fn occlusion_culling_sanity() { + let ctx = Context::headless(100, 100); + 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_value(Camera::new( + 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 geometry = stage.new_array(crate::test::gpu_cube_vertices()); + let transform = stage.new_value(Transform { + scale: Vec3::new(6.0, 6.0, 6.0), + rotation: Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4), + ..Default::default() + }); + let cube = stage.new_value(Renderlet { + camera_id: camera.id(), + vertices_array: geometry.array(), + transform_id: transform.id(), + ..Default::default() + }); + stage.add_renderlet(&cube); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + frame.present(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame.read_image().unwrap(); + img_diff::save("cull/pyramid/frame.png", img); + frame.present(); + + let depth_texture = stage.get_depth_texture(); + let depth_img = depth_texture.read_image().unwrap(); + img_diff::save("cull/pyramid/depth.png", depth_img); + + let pyramid_images = futures_lite::future::block_on( + stage + .draw_calls + .read() + .unwrap() + .drawing_strategy + .as_indirect() + .unwrap() + .occlusion_culling + .depth_pyramid + .read_images(&ctx, &camera.get()), + ) + .unwrap(); + for (i, img) in pyramid_images.into_iter().enumerate() { + img_diff::save(&format!("cull/pyramid/mip_{i}.png"), img); + } + } +} diff --git a/crates/renderling/src/draw/cpu.rs b/crates/renderling/src/draw/cpu.rs index f4ca1ccb..27c0787c 100644 --- a/crates/renderling/src/draw/cpu.rs +++ b/crates/renderling/src/draw/cpu.rs @@ -34,11 +34,11 @@ impl InternalRenderlet { } } -struct IndirectDraws { +pub(crate) struct IndirectDraws { slab: SlabAllocator, draws: Vec>, frustum_culling: FrustumCulling, - occlusion_culling: OcclusionCulling, + pub(crate) occlusion_culling: OcclusionCulling, } impl IndirectDraws { @@ -113,7 +113,7 @@ impl From> for DrawIndirectArgs { } } -enum DrawingStrategy { +pub(crate) enum DrawingStrategy { /// The standard drawing method that includes compute culling. Indirect(IndirectDraws), /// Fallback drawing method for web targets. @@ -123,27 +123,38 @@ enum DrawingStrategy { Direct, } +impl DrawingStrategy { + #[cfg(test)] + pub fn as_indirect(&self) -> Option<&IndirectDraws> { + if let DrawingStrategy::Indirect(i) = self { + Some(i) + } else { + None + } + } +} + /// Used to determine which objects are drawn and maintains the /// list of all [`Renderlet`]s. pub struct DrawCalls { /// Internal representation of all staged renderlets. internal_renderlets: Vec, - drawing_strategy: DrawingStrategy, + pub(crate) drawing_strategy: DrawingStrategy, } impl DrawCalls { /// Create a new [`DrawCalls`]. /// - /// `use_compute_culling` can be used to set whether frustum culling is used as a GPU compute - /// step before drawing. This is a native-only option. + /// `use_compute_culling` can be used to set whether frustum culling is used + /// as a GPU compute step before drawing. This is a native-only option. pub fn new(ctx: &Context, use_compute_culling: bool, size: UVec2, sample_count: u32) -> Self { let can_use_multi_draw_indirect = ctx.get_adapter().features().contains( wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::MULTI_DRAW_INDIRECT, ); if use_compute_culling && !can_use_multi_draw_indirect { log::warn!( - "`use_compute_culling` is `true`, but the MULTI_DRAW_INDIRECT feature \ - is not available. No compute culling will occur." + "`use_compute_culling` is `true`, but the MULTI_DRAW_INDIRECT feature is not \ + available. No compute culling will occur." ) } let can_use_compute_culling = use_compute_culling && can_use_multi_draw_indirect; @@ -259,8 +270,8 @@ impl DrawCalls { /// Perform pre-draw steps like compute culling, if available. /// - /// This does not do upkeep, please call [`DrawCalls::upkeep`] before calling this - /// function. + /// This does not do upkeep, please call [`DrawCalls::upkeep`] before + /// calling this function. pub fn pre_draw( &mut self, device: &wgpu::Device, @@ -292,8 +303,8 @@ impl DrawCalls { .run(device, queue, depth_texture)?; } else { log::warn!( - "DrawCalls::pre_render called without first calling `upkeep` \ - - no culling was performed" + "DrawCalls::pre_render called without first calling `upkeep` - no culling \ + was performed" ); } } diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 852dc7f3..3aab3302 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -397,7 +397,7 @@ mod test { .with_color([r, g, b, 1.0]) } - fn gpu_cube_vertices() -> Vec { + pub fn gpu_cube_vertices() -> Vec { math::UNIT_INDICES .iter() .map(|i| cmy_gpu_vertex(math::UNIT_POINTS[*i])) @@ -430,9 +430,6 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let depth_texture = stage.get_depth_texture(); - let depth_img = depth_texture.read_image().unwrap(); - img_diff::save("cmy_cube/sanity_depth.png", depth_img); let img = frame.read_image().unwrap(); img_diff::assert_img_eq("cmy_cube/sanity.png", img); } diff --git a/crates/renderling/src/linkage/cull-compute_copy_depth_to_pyramid.spv b/crates/renderling/src/linkage/cull-compute_copy_depth_to_pyramid.spv index bf1f2ede..4b3c7915 100644 Binary files a/crates/renderling/src/linkage/cull-compute_copy_depth_to_pyramid.spv and b/crates/renderling/src/linkage/cull-compute_copy_depth_to_pyramid.spv differ diff --git a/crates/renderling/src/linkage/cull-compute_frustum_culling.spv b/crates/renderling/src/linkage/cull-compute_frustum_culling.spv index 69d00f7b..3a339623 100644 Binary files a/crates/renderling/src/linkage/cull-compute_frustum_culling.spv and b/crates/renderling/src/linkage/cull-compute_frustum_culling.spv differ diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index ca91fa04..7efc9b9c 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -53,8 +53,9 @@ impl Skin { 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. + // 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 } @@ -228,7 +229,8 @@ impl Default for Renderlet { } impl Renderlet { - /// Retrieve the vertex from the slab, calculating any displacement due to morph targets. + /// 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