diff --git a/Cargo.toml b/Cargo.toml index 35aa37f373ae9..391cd213472b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,7 @@ semicolon_if_nothing_returned = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" -#TODO(rust 1.77): enable `ref_as_ptr` -# ref_as_ptr = "warn" +ref_as_ptr = "warn" [workspace.lints.rust] unsafe_op_in_unsafe_fn = "warn" diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index c34d9ba234800..9f1f38890a2bd 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -6,7 +6,7 @@ use std::f32::consts::TAU; use bevy_color::Color; use bevy_math::primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, - Polyline3d, Primitive3d, Segment3d, Sphere, Torus, + Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, }; use bevy_math::{Dir3, Quat, Vec3}; @@ -943,3 +943,32 @@ impl Drop for Torus3dBuilder<'_, '_, '_, T> { }); } } + +// tetrahedron + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Tetrahedron, + position: Vec3, + rotation: Quat, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + let [a, b, c, d] = primitive + .vertices + .map(rotate_then_translate_3d(rotation, position)); + + let lines = [(a, b), (a, c), (a, d), (b, c), (b, d), (c, d)]; + + let color = color.into(); + for (a, b) in lines.into_iter() { + self.line(a, b, color); + } + } +} diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index 91c697b3299d9..592e1679e5168 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -45,7 +45,7 @@ unsafe_code )] -use std::ptr::null_mut; +use std::ptr::{self, null_mut}; use glam::Vec3; @@ -830,7 +830,7 @@ unsafe fn Build4RuleGroups( let mut neigh_indexR: i32 = 0; let vert_index: i32 = *piTriListIn.offset((f * 3i32 + i) as isize); let ref mut fresh2 = (*pTriInfos.offset(f as isize)).AssignedGroup[i as usize]; - *fresh2 = &mut *pGroups.offset(iNrActiveGroups as isize) as *mut SGroup; + *fresh2 = ptr::from_mut(&mut *pGroups.offset(iNrActiveGroups as isize)); (*(*pTriInfos.offset(f as isize)).AssignedGroup[i as usize]) .iVertexRepresentative = vert_index; (*(*pTriInfos.offset(f as isize)).AssignedGroup[i as usize]).bOrientPreservering = @@ -838,7 +838,7 @@ unsafe fn Build4RuleGroups( (*(*pTriInfos.offset(f as isize)).AssignedGroup[i as usize]).iNrFaces = 0i32; let ref mut fresh3 = (*(*pTriInfos.offset(f as isize)).AssignedGroup[i as usize]).pFaceIndices; - *fresh3 = &mut *piGroupTrianglesBuffer.offset(iOffset as isize) as *mut i32; + *fresh3 = ptr::from_mut(&mut *piGroupTrianglesBuffer.offset(iOffset as isize)); iNrActiveGroups += 1; AddTriToGroup((*pTriInfos.offset(f as isize)).AssignedGroup[i as usize], f); bOrPre = if (*pTriInfos.offset(f as isize)).iFlag & 8i32 != 0i32 { diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 6fbe30e8f02a8..8bf4c70baa093 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -308,10 +308,10 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); - } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13 { - shader_defs.push("SHADOW_FILTER_METHOD_CASTANO_13".into()); - } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14 { - shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); } #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] @@ -489,11 +489,11 @@ pub fn prepare_deferred_lighting_pipelines( ShadowFilteringMethod::Hardware2x2 => { view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; } - ShadowFilteringMethod::Castano13 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; } - ShadowFilteringMethod::Jimenez14 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; } } diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index c73dcaf525b2d..642c6d0808a35 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -460,8 +460,6 @@ pub struct TransmittedShadowReceiver; /// /// The different modes use different approaches to /// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing). -/// -/// Currently does not affect point lights. #[derive(Component, ExtractComponent, Reflect, Clone, Copy, PartialEq, Eq, Default)] #[reflect(Component, Default)] pub enum ShadowFilteringMethod { @@ -469,20 +467,29 @@ pub enum ShadowFilteringMethod { /// /// Fast but poor quality. Hardware2x2, - /// Method by Ignacio Castaño for The Witness using 9 samples and smart - /// filtering to achieve the same as a regular 5x5 filter kernel. + /// Approximates a fixed Gaussian blur, good when TAA isn't in use. /// /// Good quality, good performance. + /// + /// For directional and spot lights, this uses a [method by Ignacio Castaño + /// for *The Witness*] using 9 samples and smart filtering to achieve the same + /// as a regular 5x5 filter kernel. + /// + /// [method by Ignacio Castaño for *The Witness*]: https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/ #[default] - Castano13, - /// Method by Jorge Jimenez for Call of Duty: Advanced Warfare using 8 - /// samples in spiral pattern, randomly-rotated by interleaved gradient - /// noise with spatial variation. + Gaussian, + /// A randomized filter that varies over time, good when TAA is in use. /// /// Good quality when used with /// [`TemporalAntiAliasSettings`](bevy_core_pipeline::experimental::taa::TemporalAntiAliasSettings) /// and good performance. - Jimenez14, + /// + /// For directional and spot lights, this uses a [method by Jorge Jimenez for + /// *Call of Duty: Advanced Warfare*] using 8 samples in spiral pattern, + /// randomly-rotated by interleaved gradient noise with spatial variation. + /// + /// [method by Jorge Jimenez for *Call of Duty: Advanced Warfare*]: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/ + Temporal, } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index 6ff5d39e4e8f1..f90556f2ffbbb 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -51,6 +51,6 @@ impl Default for PointLight { } impl PointLight { - pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; + pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 082a07dbb6219..a616db5b6973c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -618,11 +618,11 @@ pub fn queue_material_meshes( ShadowFilteringMethod::Hardware2x2 => { view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; } - ShadowFilteringMethod::Castano13 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; } - ShadowFilteringMethod::Jimenez14 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; } } diff --git a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs index 906fbe259f544..d3b6eac8e6d7b 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_draw_prepare.rs @@ -113,11 +113,11 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( ShadowFilteringMethod::Hardware2x2 => { view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; } - ShadowFilteringMethod::Castano13 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; } - ShadowFilteringMethod::Jimenez14 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; } } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 0196a8956f5b2..d422028785c9e 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,6 +1,6 @@ use bevy_asset::Asset; use bevy_color::Alpha; -use bevy_math::{Affine2, Mat3, Vec4}; +use bevy_math::{Affine2, Affine3, Mat2, Mat3, Vec2, Vec3, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_asset::RenderAssets, render_resource::*, @@ -487,6 +487,66 @@ pub struct StandardMaterial { pub uv_transform: Affine2, } +impl StandardMaterial { + /// Horizontal flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with horizontally flipped texture coords + pub const FLIP_HORIZONTAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::new(-1.0, 0.0), Vec2::Y), + translation: Vec2::X, + }; + + /// Vertical flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with vertically flipped texture coords + pub const FLIP_VERTICAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::X, Vec2::new(0.0, -1.0)), + translation: Vec2::Y, + }; + + /// Flipping X 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped X coords + pub const FLIP_X: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::new(-1.0, 0.0, 0.0), Vec3::Y, Vec3::Z), + translation: Vec3::X, + }; + + /// Flipping Y 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Y coords + pub const FLIP_Y: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::new(0.0, -1.0, 0.0), Vec3::Z), + translation: Vec3::Y, + }; + + /// Flipping Z 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Z coords + pub const FLIP_Z: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::Y, Vec3::new(0.0, 0.0, -1.0)), + translation: Vec3::Z, + }; + + /// Flip the texture coordinates of the material. + pub fn flip(&mut self, horizontal: bool, vertical: bool) { + if horizontal { + // Multiplication of `Affine2` is order dependent, which is why + // we do not use the `*=` operator. + self.uv_transform = Self::FLIP_HORIZONTAL * self.uv_transform; + } + if vertical { + self.uv_transform = Self::FLIP_VERTICAL * self.uv_transform; + } + } + + /// Consumes the material and returns a material with flipped texture coordinates + pub fn flipped(mut self, horizontal: bool, vertical: bool) -> Self { + self.flip(horizontal, vertical); + self + } +} + impl Default for StandardMaterial { fn default() -> Self { StandardMaterial { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index ddc81604f4d13..ebe7274f7b8db 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1068,8 +1068,8 @@ bitflags::bitflags! { const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; const SHADOW_FILTER_METHOD_RESERVED_BITS = Self::SHADOW_FILTER_METHOD_MASK_BITS << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; const SHADOW_FILTER_METHOD_HARDWARE_2X2 = 0 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; - const SHADOW_FILTER_METHOD_CASTANO_13 = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; - const SHADOW_FILTER_METHOD_JIMENEZ_14 = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_GAUSSIAN = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_TEMPORAL = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; const VIEW_PROJECTION_RESERVED_BITS = Self::VIEW_PROJECTION_MASK_BITS << Self::VIEW_PROJECTION_SHIFT_BITS; const VIEW_PROJECTION_NONSTANDARD = 0 << Self::VIEW_PROJECTION_SHIFT_BITS; const VIEW_PROJECTION_PERSPECTIVE = 1 << Self::VIEW_PROJECTION_SHIFT_BITS; @@ -1397,10 +1397,10 @@ impl SpecializedMeshPipeline for MeshPipeline { key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); - } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13 { - shader_defs.push("SHADOW_FILTER_METHOD_CASTANO_13".into()); - } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14 { - shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); } let blur_quality = diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index c1f0ec449e6ae..0ced974ebfeda 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -5,6 +5,7 @@ utils::{PI, interleaved_gradient_noise}, utils, } +#import bevy_render::maths::orthonormalize // Do the lookup, using HW 2x2 PCF and comparison fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i32) -> f32 { @@ -26,6 +27,40 @@ fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i #endif } +// Numbers determined by trial and error that gave nice results. +const SPOT_SHADOW_TEXEL_SIZE: f32 = 0.0134277345; +const POINT_SHADOW_SCALE: f32 = 0.003; +const POINT_SHADOW_TEMPORAL_OFFSET_SCALE: f32 = 0.5; + +// These are the standard MSAA sample point positions from D3D. They were chosen +// to get a reasonable distribution that's not too regular. +// +// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels?redirectedfrom=MSDN +const D3D_SAMPLE_POINT_POSITIONS: array, 8> = array( + vec2( 0.125, -0.375), + vec2(-0.125, 0.375), + vec2( 0.625, 0.125), + vec2(-0.375, -0.625), + vec2(-0.625, 0.625), + vec2(-0.875, -0.125), + vec2( 0.375, 0.875), + vec2( 0.875, -0.875), +); + +// And these are the coefficients corresponding to the probability distribution +// function of a 2D Gaussian lobe with zero mean and the identity covariance +// matrix at those points. +const D3D_SAMPLE_POINT_COEFFS: array = array( + 0.157112, + 0.157112, + 0.138651, + 0.130251, + 0.114946, + 0.114946, + 0.107982, + 0.079001, +); + // https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1 fn sample_shadow_map_castano_thirteen(light_local: vec2, depth: f32, array_index: i32) -> f32 { let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); @@ -75,31 +110,39 @@ fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { return min2 + (value - min1) * (max2 - min2) / (max1 - min1); } -fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { - let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); - - let random_angle = 2.0 * PI * interleaved_gradient_noise(light_local * shadow_map_size, view_bindings::globals.frame_count); +// Creates a random rotation matrix using interleaved gradient noise. +// +// See: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/ +fn random_rotation_matrix(scale: vec2) -> mat2x2 { + let random_angle = 2.0 * PI * interleaved_gradient_noise( + scale, view_bindings::globals.frame_count); let m = vec2(sin(random_angle), cos(random_angle)); - let rotation_matrix = mat2x2( + return mat2x2( m.y, -m.x, m.x, m.y ); +} + +fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size); // Empirically chosen fudge factor to make PCF look better across different CSM cascades let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size); let uv_offset_scale = f / (texel_size * shadow_map_size); // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) - let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; - let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale; - let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale; - let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale; - let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale; - let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale; - let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale; - let sample_offset8 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale; + let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; + let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale; + let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale; + let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale; + let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale; + let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale; + let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale; + let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale; var sum = 0.0; + sum += sample_shadow_map_hardware(light_local + sample_offset0, depth, array_index); sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index); sum += sample_shadow_map_hardware(light_local + sample_offset2, depth, array_index); sum += sample_shadow_map_hardware(light_local + sample_offset3, depth, array_index); @@ -107,14 +150,13 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_ sum += sample_shadow_map_hardware(light_local + sample_offset5, depth, array_index); sum += sample_shadow_map_hardware(light_local + sample_offset6, depth, array_index); sum += sample_shadow_map_hardware(light_local + sample_offset7, depth, array_index); - sum += sample_shadow_map_hardware(light_local + sample_offset8, depth, array_index); return sum / 8.0; } fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { -#ifdef SHADOW_FILTER_METHOD_CASTANO_13 +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN return sample_shadow_map_castano_thirteen(light_local, depth, array_index); -#else ifdef SHADOW_FILTER_METHOD_JIMENEZ_14 +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 return sample_shadow_map_hardware(light_local, depth, array_index); @@ -126,3 +168,160 @@ fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel return 0.0; #endif } + +// NOTE: Due to the non-uniform control flow in `shadows::fetch_point_shadow`, +// we must use the Level variant of textureSampleCompare to avoid undefined +// behavior due to some of the fragments in a quad (2x2 fragments) being +// processed not being sampled, and this messing with mip-mapping functionality. +// The shadow maps have no mipmaps so Level just samples from LOD 0. +fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: u32) -> f32 { +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, depth); +#else + return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth); +#endif +} + +fn sample_shadow_cubemap_at_offset( + position: vec2, + coeff: f32, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> f32 { + return sample_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ) * coeff; +} + +// This more or less does what Castano13 does, but in 3D space. Castano13 is +// essentially an optimized 2D Gaussian filter that takes advantage of the +// bilinear filtering hardware to reduce the number of samples needed. This +// trick doesn't apply to cubemaps, so we manually apply a Gaussian filter over +// the standard 8xMSAA pattern instead. +fn sample_shadow_cubemap_gaussian( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + var up = vec3(0.0, 1.0, 0.0); + if (dot(up, normalize(light_local)) > 0.99) { + up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. + } + let basis = orthonormalize(light_local, up) * scale * distance_to_light; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7], + basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +// This is a port of the Jimenez14 filter above to the 3D space. It jitters the +// points in the spiral pattern after first creating a 2D orthonormal basis +// along the principal light direction. +fn sample_shadow_cubemap_temporal( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + var up = vec3(0.0, 1.0, 0.0); + if (dot(up, normalize(light_local)) > 0.99) { + up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. + } + let basis = orthonormalize(light_local, up) * scale * distance_to_light; + + let rotation_matrix = random_rotation_matrix(vec2(1.0)); + + let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset1 = rotation_matrix * utils::SPIRAL_OFFSET_1_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset2 = rotation_matrix * utils::SPIRAL_OFFSET_2_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset3 = rotation_matrix * utils::SPIRAL_OFFSET_3_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset4 = rotation_matrix * utils::SPIRAL_OFFSET_4_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset5 = rotation_matrix * utils::SPIRAL_OFFSET_5_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset6 = rotation_matrix * utils::SPIRAL_OFFSET_6_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset7 = rotation_matrix * utils::SPIRAL_OFFSET_7_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +fn sample_shadow_cubemap( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, +) -> f32 { +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN + return sample_shadow_cubemap_gaussian( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id); +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_temporal( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id); +#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 + return sample_shadow_cubemap_hardware(light_local, depth, light_id); +#else + // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. + // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) + // This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined. + // Set to 0 to make it obvious that something is wrong. + return 0.0; +#endif +} diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 1b2d526c14792..edc5b5d9e9c1d 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -4,7 +4,7 @@ mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, utils::hsv2rgb, - shadow_sampling::sample_shadow_map + shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map} } const flip_z: vec3 = vec3(1.0, 1.0, -1.0); @@ -39,16 +39,7 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: v // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, // so we have to flip the z-axis when sampling. - // NOTE: Due to the non-uniform control flow above, we must use the Level variant of - // textureSampleCompare to avoid undefined behavior due to some of the fragments in - // a quad (2x2 fragments) being processed not being sampled, and this messing with - // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples - // from LOD 0. -#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT - return textureSampleCompare(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, frag_ls * flip_z, depth); -#else - return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, frag_ls * flip_z, i32(light_id), depth); -#endif + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id); } fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { @@ -99,9 +90,12 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: ve // 0.1 must match POINT_LIGHT_NEAR_Z let depth = 0.1 / -projected_position.z; - // Number determined by trial and error that gave nice results. - let texel_size = 0.0134277345; - return sample_shadow_map(shadow_uv, depth, i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset, texel_size); + return sample_shadow_map( + shadow_uv, + depth, + i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset, + SPOT_SHADOW_TEXEL_SIZE + ); } fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index 53c254f3746f8..4070a8679a5b8 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -51,3 +51,16 @@ fn inverse_affine3(affine: mat4x3) -> mat4x3 { let inv_matrix3 = inverse_mat3x3(matrix3); return mat4x3(inv_matrix3[0], inv_matrix3[1], inv_matrix3[2], -(inv_matrix3 * affine[3])); } + +// Creates an orthonormal basis given a Z vector and an up vector (which becomes +// Y after orthonormalization). +// +// The results are equivalent to the Gram-Schmidt process [1]. +// +// [1]: https://math.stackexchange.com/a/1849294 +fn orthonormalize(z_unnormalized: vec3, up: vec3) -> mat3x3 { + let z_basis = normalize(z_unnormalized); + let x_basis = normalize(cross(z_basis, up)); + let y_basis = cross(z_basis, x_basis); + return mat3x3(x_basis, y_basis, z_basis); +} diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index cbac3705ddcd5..df33640716496 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -80,6 +80,8 @@ impl Eq for MeshVertexBufferLayoutRef {} impl Hash for MeshVertexBufferLayoutRef { fn hash(&self, state: &mut H) { - (&*self.0 as *const MeshVertexBufferLayout as usize).hash(state); + // Hash the address of the underlying data, so two layouts that share the same + // `MeshVertexBufferLayout` will have the same hash. + (Arc::as_ptr(&self.0) as usize).hash(state); } } diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index f0aaa9265bb5c..41336571ae5e6 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -70,6 +70,7 @@ fn setup( shadows_enabled: true, intensity: 10_000_000., range: 100.0, + shadow_depth_bias: 0.2, ..default() }, transform: Transform::from_xyz(8.0, 16.0, 8.0), diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index 7c1c9ad61e3b3..a047098fe6a17 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -252,14 +252,14 @@ fn cycle_filter_methods( let filter_method_string; *filter_method = match *filter_method { ShadowFilteringMethod::Hardware2x2 => { - filter_method_string = "Castano13".to_string(); - ShadowFilteringMethod::Castano13 + filter_method_string = "Gaussian".to_string(); + ShadowFilteringMethod::Gaussian } - ShadowFilteringMethod::Castano13 => { - filter_method_string = "Jimenez14".to_string(); - ShadowFilteringMethod::Jimenez14 + ShadowFilteringMethod::Gaussian => { + filter_method_string = "Temporal".to_string(); + ShadowFilteringMethod::Temporal } - ShadowFilteringMethod::Jimenez14 => { + ShadowFilteringMethod::Temporal => { filter_method_string = "Hardware2x2".to_string(); ShadowFilteringMethod::Hardware2x2 }