diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 228de223caa98..bac3c25cf6af8 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -55,10 +55,10 @@ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ - camera::CameraUpdateSystem, extract_resource::ExtractResourcePlugin, prelude::Color, - render_asset::prepare_assets, render_graph::RenderGraph, render_phase::sort_phase_system, - render_resource::Shader, texture::Image, view::VisibilitySystems, ExtractSchedule, Render, - RenderApp, RenderSet, + camera::CameraUpdateSystem, extract_component::ExtractComponentPlugin, + extract_resource::ExtractResourcePlugin, prelude::Color, render_asset::prepare_assets, + render_graph::RenderGraph, render_phase::sort_phase_system, render_resource::Shader, + texture::Image, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::TransformSystem; use environment_map::EnvironmentMapPlugin; @@ -69,6 +69,7 @@ pub const UTILS_HANDLE: Handle = Handle::weak_from_u128(1900548483293416 pub const CLUSTERED_FORWARD_HANDLE: Handle = Handle::weak_from_u128(166852093121196815); pub const PBR_LIGHTING_HANDLE: Handle = Handle::weak_from_u128(14170772752254856967); pub const SHADOWS_HANDLE: Handle = Handle::weak_from_u128(11350275143789590502); +pub const SHADOW_SAMPLING_HANDLE: Handle = Handle::weak_from_u128(3145627513789590502); pub const PBR_SHADER_HANDLE: Handle = Handle::weak_from_u128(4805239651767701046); pub const PBR_PREPASS_SHADER_HANDLE: Handle = Handle::weak_from_u128(9407115064344201137); pub const PBR_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(16550102964439850292); @@ -124,6 +125,12 @@ impl Plugin for PbrPlugin { "render/shadows.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + SHADOW_SAMPLING_HANDLE, + "render/shadow_sampling.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, PBR_FUNCTIONS_HANDLE, @@ -168,6 +175,7 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .init_resource::() .init_resource::() .init_resource::() @@ -182,6 +190,7 @@ impl Plugin for PbrPlugin { EnvironmentMapPlugin, ExtractResourcePlugin::::default(), FogPlugin, + ExtractComponentPlugin::::default(), )) .configure_sets( PostUpdate, diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index d68036a830848..502b22047de4f 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -6,6 +6,7 @@ use bevy_reflect::prelude::*; use bevy_render::{ camera::Camera, color::Color, + extract_component::ExtractComponent, extract_resource::ExtractResource, prelude::Projection, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere}, @@ -606,6 +607,36 @@ pub struct NotShadowCaster; #[reflect(Component, Default)] pub struct NotShadowReceiver; +/// Add this component to a [`Camera3d`](bevy_core_pipeline::core_3d::Camera3d) +/// to control how to anti-alias shadow edges. +/// +/// 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 { + /// Hardware 2x2. + /// + /// 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. + /// + /// Good quality, good performance. + #[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. + /// + /// Good quality when used with + /// [`TemporalAntiAliasSettings`](bevy_core_pipeline::experimental::taa::TemporalAntiAliasSettings) + /// and good performance. + Jimenez14, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SimulationLightSystems { AddClusters, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index c3287af0d9cbf..104d423afdab8 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,7 +1,7 @@ use crate::{ render, AlphaMode, DrawMesh, DrawPrepass, EnvironmentMapLight, MeshPipeline, MeshPipelineKey, PrepassPipelinePlugin, PrepassPlugin, RenderMeshInstances, ScreenSpaceAmbientOcclusionSettings, - SetMeshBindGroup, SetMeshViewBindGroup, Shadow, + SetMeshBindGroup, SetMeshViewBindGroup, Shadow, ShadowFilteringMethod, }; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; @@ -440,6 +440,7 @@ pub fn queue_material_meshes( Option<&Tonemapping>, Option<&DebandDither>, Option<&EnvironmentMapLight>, + Option<&ShadowFilteringMethod>, Option<&ScreenSpaceAmbientOcclusionSettings>, Option<&NormalPrepass>, Option<&TemporalAntiAliasSettings>, @@ -456,6 +457,7 @@ pub fn queue_material_meshes( tonemapping, dither, environment_map, + shadow_filter_method, ssao, normal_prepass, taa_settings, @@ -482,6 +484,19 @@ pub fn queue_material_meshes( if environment_map_loaded { view_key |= MeshPipelineKey::ENVIRONMENT_MAP; } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Castano13 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13; + } + ShadowFilteringMethod::Jimenez14 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14; + } + } + if !view.hdr { if let Some(tonemapping) = tonemapping { view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index dc7a115df5a3c..790470c3ae495 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -659,24 +659,35 @@ bitflags::bitflags! { const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + 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; } } impl MeshPipelineKey { const MSAA_MASK_BITS: u32 = 0b111; const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); + const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - Self::PRIMITIVE_TOPOLOGY_MASK_BITS.count_ones(); + const BLEND_MASK_BITS: u32 = 0b11; const BLEND_SHIFT_BITS: u32 = Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones(); + const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; const TONEMAP_METHOD_SHIFT_BITS: u32 = Self::BLEND_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); + const SHADOW_FILTER_METHOD_MASK_BITS: u32 = 0b11; + const SHADOW_FILTER_METHOD_SHIFT_BITS: u32 = + Self::TONEMAP_METHOD_SHIFT_BITS - Self::SHADOW_FILTER_METHOD_MASK_BITS.count_ones(); + pub fn from_msaa_samples(msaa_samples: u32) -> Self { let msaa_bits = (msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; @@ -904,6 +915,16 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("TAA".into()); } + let shadow_filter_method = + 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()); + } + let format = if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { @@ -1069,10 +1090,12 @@ pub fn prepare_mesh_view_bind_groups( Option<&EnvironmentMapLight>, &Tonemapping, )>, - images: Res>, - mut fallback_images: FallbackImagesMsaa, - mut fallback_depths: FallbackImagesDepth, - fallback_cubemap: Res, + (images, mut fallback_images, mut fallback_depths, fallback_cubemap): ( + Res>, + FallbackImagesMsaa, + FallbackImagesDepth, + Res, + ), msaa: Res, globals_buffer: Res, tonemapping_luts: Res, diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl new file mode 100644 index 0000000000000..ab1d69c3ac39c --- /dev/null +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -0,0 +1,132 @@ +#define_import_path bevy_pbr::shadow_sampling + +#import bevy_pbr::mesh_view_bindings as view_bindings +#import bevy_pbr::utils PI + +// Do the lookup, using HW 2x2 PCF and comparison +fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i32) -> f32 { +#ifdef NO_ARRAY_TEXTURES_SUPPORT + return textureSampleCompareLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_sampler, + light_local, + depth, + ); +#else + return textureSampleCompareLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_sampler, + light_local, + array_index, + depth, + ); +#endif +} + +// 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)); + let inv_shadow_map_size = 1.0 / shadow_map_size; + + let uv = light_local * shadow_map_size; + var base_uv = floor(uv + 0.5); + let s = (uv.x + 0.5 - base_uv.x); + let t = (uv.y + 0.5 - base_uv.y); + base_uv -= 0.5; + base_uv *= inv_shadow_map_size; + + let uw0 = (4.0 - 3.0 * s); + let uw1 = 7.0; + let uw2 = (1.0 + 3.0 * s); + + let u0 = (3.0 - 2.0 * s) / uw0 - 2.0; + let u1 = (3.0 + s) / uw1; + let u2 = s / uw2 + 2.0; + + let vw0 = (4.0 - 3.0 * t); + let vw1 = 7.0; + let vw2 = (1.0 + 3.0 * t); + + let v0 = (3.0 - 2.0 * t) / vw0 - 2.0; + let v1 = (3.0 + t) / vw1; + let v2 = t / vw2 + 2.0; + + var sum = 0.0; + + sum += uw0 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u0, v0) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u1, v0) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u2, v0) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u0, v1) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u1, v1) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u2, v1) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u0, v2) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u1, v2) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u2, v2) * inv_shadow_map_size), depth, array_index); + + return sum * (1.0 / 144.0); +} + +// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence +fn interleaved_gradient_noise(pixel_coordinates: vec2) -> f32 { + let frame = f32(view_bindings::globals.frame_count % 64u); + let xy = pixel_coordinates + 5.588238 * frame; + return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y)); +} + +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); + let m = vec2(sin(random_angle), cos(random_angle)); + let rotation_matrix = mat2x2( + m.y, -m.x, + m.x, m.y + ); + + // 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 * vec2(-0.7071, 0.7071)) * uv_offset_scale; + let sample_offset2 = (rotation_matrix * vec2(-0.0000, -0.8750)) * uv_offset_scale; + let sample_offset3 = (rotation_matrix * vec2( 0.5303, 0.5303)) * uv_offset_scale; + let sample_offset4 = (rotation_matrix * vec2(-0.6250, -0.0000)) * uv_offset_scale; + let sample_offset5 = (rotation_matrix * vec2( 0.3536, -0.3536)) * uv_offset_scale; + let sample_offset6 = (rotation_matrix * vec2(-0.0000, 0.3750)) * uv_offset_scale; + let sample_offset7 = (rotation_matrix * vec2(-0.1768, -0.1768)) * uv_offset_scale; + let sample_offset8 = (rotation_matrix * vec2( 0.1250, 0.0000)) * uv_offset_scale; + + var sum = 0.0; + 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); + sum += sample_shadow_map_hardware(light_local + sample_offset4, depth, array_index); + 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 + return sample_shadow_map_castano_thirteen(light_local, depth, array_index); +#else ifdef SHADOW_FILTER_METHOD_JIMENEZ_14 + 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); +#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 bcd87ae978e43..9ace738252ea6 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -3,6 +3,8 @@ #import bevy_pbr::mesh_view_types POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE #import bevy_pbr::mesh_view_bindings as view_bindings #import bevy_pbr::utils hsv2rgb +#import bevy_pbr::shadow_sampling sample_shadow_map + const flip_z: vec3 = vec3(1.0, 1.0, -1.0); fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { @@ -95,13 +97,9 @@ 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; - #ifdef NO_ARRAY_TEXTURES_SUPPORT - return textureSampleCompare(view_bindings::directional_shadow_textures, view_bindings::directional_shadow_textures_sampler, - shadow_uv, depth); - #else - return textureSampleCompareLevel(view_bindings::directional_shadow_textures, view_bindings::directional_shadow_textures_sampler, - shadow_uv, i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset, depth); - #endif + // 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); } fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { @@ -115,7 +113,7 @@ fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { return (*light).num_cascades; } -fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { +fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { let light = &view_bindings::lights.directional_lights[light_id]; let cascade = &(*light).cascades[cascade_index]; @@ -141,25 +139,9 @@ fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, s let light_local = offset_position_ndc.xy * flip_correction + vec2(0.5, 0.5); let depth = offset_position_ndc.z; - // do the lookup, using HW PCF and comparison - // NOTE: Due to non-uniform control flow above, we must use the level variant of the texture - // sampler to avoid use of implicit derivatives causing possible undefined behavior. -#ifdef NO_ARRAY_TEXTURES_SUPPORT - return textureSampleCompareLevel( - view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, - light_local, - depth - ); -#else - return textureSampleCompareLevel( - view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, - light_local, - i32((*light).depth_texture_base_index + cascade_index), - depth - ); -#endif + + let array_index = i32((*light).depth_texture_base_index + cascade_index); + return sample_shadow_map(light_local, depth, array_index, (*cascade).texel_size); } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { @@ -170,7 +152,7 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_nor return 1.0; } - var shadow = sample_cascade(light_id, cascade_index, frag_position, surface_normal); + var shadow = sample_directional_cascade(light_id, cascade_index, frag_position, surface_normal); // Blend with the next cascade, if there is one. let next_cascade_index = cascade_index + 1u; @@ -178,7 +160,7 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_nor let this_far_bound = (*light).cascades[cascade_index].far_bound; let next_near_bound = (1.0 - (*light).cascades_overlap_proportion) * this_far_bound; if (-view_z >= next_near_bound) { - let next_shadow = sample_cascade(light_id, next_cascade_index, frag_position, surface_normal); + let next_shadow = sample_directional_cascade(light_id, next_cascade_index, frag_position, surface_normal); shadow = mix(shadow, next_shadow, (-view_z - next_near_bound) / (this_far_bound - next_near_bound)); } }