diff --git a/Cargo.toml b/Cargo.toml index 947f4875278e92..3666980e80c42c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3083,6 +3083,17 @@ description = "Demonstrates volumetric fog and lighting" category = "3D Rendering" wasm = true +[[example]] +name = "pcss" +path = "examples/3d/pcss.rs" +doc-scrape-examples = true + +[package.metadata.example.pcss] +name = "Percentage-closer soft shadows" +description = "Demonstrates percentage-closer soft shadows (PCSS)" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/environment_maps/sky_skybox.ktx2 b/assets/environment_maps/sky_skybox.ktx2 new file mode 100644 index 00000000000000..d386497ac1efe9 Binary files /dev/null and b/assets/environment_maps/sky_skybox.ktx2 differ diff --git a/assets/models/PalmTree/PalmTree.bin b/assets/models/PalmTree/PalmTree.bin new file mode 100644 index 00000000000000..614c4d29680bb5 Binary files /dev/null and b/assets/models/PalmTree/PalmTree.bin differ diff --git a/assets/models/PalmTree/PalmTree.gltf b/assets/models/PalmTree/PalmTree.gltf new file mode 100644 index 00000000000000..c4987e1ea67a11 --- /dev/null +++ b/assets/models/PalmTree/PalmTree.gltf @@ -0,0 +1,1066 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.1.63", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 3, + 6, + 9, + 12, + 15, + 18, + 21, + 22, + 23 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"B\u00e9zierCurve", + "rotation":[ + -0.6492608785629272, + 0.6492608189582825, + -0.28010788559913635, + 0.28010791540145874 + ], + "scale":[ + -1.5591872930526733, + -1.5591872930526733, + -1.5591872930526733 + ], + "translation":[ + 0.7588800191879272, + 1.8171958923339844, + -0.701636791229248 + ] + }, + { + "name":"B\u00e9zierCurve.001" + }, + { + "mesh":1, + "name":"Grid", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551956415176392, + 0.06293392181396484, + -0.1808854639530182 + ] + }, + { + "children":[ + 1, + 2 + ], + "name":"Empty", + "rotation":[ + 0, + -0.16959351301193237, + 0, + 0.9855141043663025 + ], + "translation":[ + 0.27010849118232727, + 3.3713648319244385, + -0.5507277250289917 + ] + }, + { + "name":"B\u00e9zierCurve.002" + }, + { + "mesh":2, + "name":"Grid.001", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551955819129944, + 0.06293395161628723, + -0.18088552355766296 + ] + }, + { + "children":[ + 4, + 5 + ], + "name":"Empty.001", + "rotation":[ + -0.1744275838136673, + -0.9500948786735535, + -0.04670312628149986, + 0.2543886601924896 + ], + "translation":[ + -0.32273390889167786, + 3.377293348312378, + -0.6218688488006592 + ] + }, + { + "name":"B\u00e9zierCurve.003" + }, + { + "mesh":3, + "name":"Grid.002", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551957011222839, + 0.06293392181396484, + -0.18088555335998535 + ] + }, + { + "children":[ + 7, + 8 + ], + "name":"Empty.002", + "rotation":[ + 0, + 0.9468244314193726, + 0, + 0.3217506408691406 + ], + "translation":[ + -0.10338221490383148, + 3.377293348312378, + -0.7404372692108154 + ] + }, + { + "name":"B\u00e9zierCurve.004" + }, + { + "mesh":4, + "name":"Grid.003", + "scale":[ + 0.7174922823905945, + 1, + 1 + ], + "translation":[ + 0.8551957011222839, + 0.0629342570900917, + -0.18088553845882416 + ] + }, + { + "children":[ + 10, + 11 + ], + "name":"Empty.003", + "rotation":[ + 0.039769601076841354, + -0.5909609794616699, + -0.054099712520837784, + 0.803900957107544 + ], + "translation":[ + -0.020384281873703003, + 3.377293348312378, + -0.3432328999042511 + ] + }, + { + "name":"B\u00e9zierCurve.005" + }, + { + "mesh":5, + "name":"Grid.004", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551955819129944, + 0.0629342794418335, + -0.18088550865650177 + ] + }, + { + "children":[ + 13, + 14 + ], + "name":"Empty.004", + "rotation":[ + 0.06433407217264175, + 0.6805833578109741, + 0.06868407875299454, + 0.7266016602516174 + ], + "translation":[ + 0.14561158418655396, + 3.377293348312378, + -0.633725643157959 + ] + }, + { + "name":"B\u00e9zierCurve.006" + }, + { + "mesh":6, + "name":"Grid.005", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551957607269287, + 0.06293407082557678, + -0.18088555335998535 + ] + }, + { + "children":[ + 16, + 17 + ], + "name":"Empty.005", + "rotation":[ + -0.027264947071671486, + 0.32132646441459656, + -0.08003053814172745, + 0.9431866407394409 + ], + "translation":[ + 0.14561158418655396, + 3.377293348312378, + -0.633725643157959 + ] + }, + { + "name":"B\u00e9zierCurve.007" + }, + { + "mesh":7, + "name":"Grid.006", + "scale":[ + 0.7174922227859497, + 1, + 1 + ], + "translation":[ + 0.8551956415176392, + 0.06293423473834991, + -0.18088550865650177 + ] + }, + { + "children":[ + 19, + 20 + ], + "name":"Empty.006", + "rotation":[ + 0.025538405403494835, + -0.8604785799980164, + -0.015095553360879421, + 0.5086221694946289 + ], + "translation":[ + -0.13840311765670776, + 3.38228178024292, + -0.48537585139274597 + ] + }, + { + "mesh":8, + "name":"Landscape", + "rotation":[ + 0, + -0.07845905423164368, + 0, + 0.9969173669815063 + ], + "scale":[ + 1.0773953199386597, + 1.0773954391479492, + 1.0773953199386597 + ], + "translation":[ + -1.4325428009033203, + 0.049118101596832275, + -17.66829490661621 + ] + }, + { + "mesh":9, + "name":"Landscape_plane", + "scale":[ + 6.12558650970459, + 6.12558650970459, + 6.12558650970459 + ] + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Trunk", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.61811763048172, + 0.26356762647628784, + 0.11393062770366669, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.5 + } + }, + { + "doubleSided":true, + "name":"Leaves", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.2105390429496765, + 0.8000074625015259, + 0.14856106042861938, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.5 + } + }, + { + "doubleSided":true, + "name":"Material.001", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 1, + 0.9668273329734802, + 0.5682248473167419, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.5 + } + }, + { + "doubleSided":true, + "emissiveFactor":[ + 1, + 1, + 1 + ], + "emissiveTexture":{ + "index":0 + }, + "name":"Water", + "pbrMetallicRoughness":{ + "baseColorTexture":{ + "index":1 + }, + "metallicFactor":0, + "roughnessFactor":0.5 + } + } + ], + "meshes":[ + { + "name":"B\u00e9zierCurve", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + }, + { + "name":"Grid", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.001", + "primitives":[ + { + "attributes":{ + "POSITION":8, + "NORMAL":9, + "TEXCOORD_0":10 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.002", + "primitives":[ + { + "attributes":{ + "POSITION":11, + "NORMAL":12, + "TEXCOORD_0":13 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.003", + "primitives":[ + { + "attributes":{ + "POSITION":14, + "NORMAL":15, + "TEXCOORD_0":16 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.004", + "primitives":[ + { + "attributes":{ + "POSITION":17, + "NORMAL":18, + "TEXCOORD_0":19 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.005", + "primitives":[ + { + "attributes":{ + "POSITION":20, + "NORMAL":21, + "TEXCOORD_0":22 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Grid.006", + "primitives":[ + { + "attributes":{ + "POSITION":23, + "NORMAL":24, + "TEXCOORD_0":25 + }, + "indices":7, + "material":1 + } + ] + }, + { + "name":"Landscape.001", + "primitives":[ + { + "attributes":{ + "POSITION":26, + "NORMAL":27, + "TEXCOORD_0":28 + }, + "indices":29, + "material":2 + } + ] + }, + { + "name":"Landscape_plane", + "primitives":[ + { + "attributes":{ + "POSITION":30, + "NORMAL":31, + "TEXCOORD_0":32 + }, + "indices":33, + "material":3 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + }, + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"StylizedWater", + "uri":"StylizedWater.png" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":720, + "max":[ + 1.0449047088623047, + 0.10000000149011612, + 0.4650161862373352 + ], + "min":[ + -1.0722216367721558, + -0.10000000149011612, + -0.10050036013126373 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":720, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":720, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":1260, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":150, + "max":[ + 0.6420668959617615, + 0.27458858489990234, + 0.5718027353286743 + ], + "min":[ + -1.78363037109375, + -0.2425653040409088, + -0.18408852815628052 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5123, + "count":756, + "type":"SCALAR" + }, + { + "bufferView":8, + "componentType":5126, + "count":150, + "max":[ + 0.6420671343803406, + 0.2745886743068695, + 0.5718027949333191 + ], + "min":[ + -1.7836307287216187, + -0.24256540834903717, + -0.18408851325511932 + ], + "type":"VEC3" + }, + { + "bufferView":9, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":10, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":11, + "componentType":5126, + "count":150, + "max":[ + 0.6420671343803406, + 0.27458858489990234, + 0.5718027949333191 + ], + "min":[ + -1.783630609512329, + -0.2425653636455536, + -0.18408846855163574 + ], + "type":"VEC3" + }, + { + "bufferView":12, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":13, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":14, + "componentType":5126, + "count":150, + "max":[ + 0.6420667767524719, + 0.2745886743068695, + 0.5718027949333191 + ], + "min":[ + -1.783630609512329, + -0.24256554245948792, + -0.18408846855163574 + ], + "type":"VEC3" + }, + { + "bufferView":15, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":16, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":17, + "componentType":5126, + "count":150, + "max":[ + 0.6420665383338928, + 0.2745887041091919, + 0.5718027353286743 + ], + "min":[ + -1.78363037109375, + -0.24256561696529388, + -0.1840885579586029 + ], + "type":"VEC3" + }, + { + "bufferView":18, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":19, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":20, + "componentType":5126, + "count":150, + "max":[ + 0.6420668959617615, + 0.27458861470222473, + 0.5718027949333191 + ], + "min":[ + -1.783630609512329, + -0.24256546795368195, + -0.18408843874931335 + ], + "type":"VEC3" + }, + { + "bufferView":21, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":22, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":23, + "componentType":5126, + "count":150, + "max":[ + 0.642067015171051, + 0.2745887339115143, + 0.5718027353286743 + ], + "min":[ + -1.78363037109375, + -0.24256548285484314, + -0.1840885430574417 + ], + "type":"VEC3" + }, + { + "bufferView":24, + "componentType":5126, + "count":150, + "type":"VEC3" + }, + { + "bufferView":25, + "componentType":5126, + "count":150, + "type":"VEC2" + }, + { + "bufferView":26, + "componentType":5126, + "count":4096, + "max":[ + 32, + 1, + 32 + ], + "min":[ + -32, + -1, + -32 + ], + "type":"VEC3" + }, + { + "bufferView":27, + "componentType":5126, + "count":4096, + "type":"VEC3" + }, + { + "bufferView":28, + "componentType":5126, + "count":4096, + "type":"VEC2" + }, + { + "bufferView":29, + "componentType":5123, + "count":23814, + "type":"SCALAR" + }, + { + "bufferView":30, + "componentType":5126, + "count":4, + "max":[ + 32, + 0.009999999776482582, + 32 + ], + "min":[ + -32, + 0.009999999776482582, + -32 + ], + "type":"VEC3" + }, + { + "bufferView":31, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":32, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":33, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":8640, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":8640, + "byteOffset":8640, + "target":34962 + }, + { + "buffer":0, + "byteLength":5760, + "byteOffset":17280, + "target":34962 + }, + { + "buffer":0, + "byteLength":2520, + "byteOffset":23040, + "target":34963 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":25560, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":27360, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":29160, + "target":34962 + }, + { + "buffer":0, + "byteLength":1512, + "byteOffset":30360, + "target":34963 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":31872, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":33672, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":35472, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":36672, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":38472, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":40272, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":41472, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":43272, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":45072, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":46272, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":48072, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":49872, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":51072, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":52872, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":54672, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":55872, + "target":34962 + }, + { + "buffer":0, + "byteLength":1800, + "byteOffset":57672, + "target":34962 + }, + { + "buffer":0, + "byteLength":1200, + "byteOffset":59472, + "target":34962 + }, + { + "buffer":0, + "byteLength":49152, + "byteOffset":60672, + "target":34962 + }, + { + "buffer":0, + "byteLength":49152, + "byteOffset":109824, + "target":34962 + }, + { + "buffer":0, + "byteLength":32768, + "byteOffset":158976, + "target":34962 + }, + { + "buffer":0, + "byteLength":47628, + "byteOffset":191744, + "target":34963 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":239372, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":239420, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":239468, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":239500, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":239512, + "uri":"PalmTree.bin" + } + ] +} diff --git a/assets/models/PalmTree/StylizedWater.png b/assets/models/PalmTree/StylizedWater.png new file mode 100644 index 00000000000000..a4da3043caca76 Binary files /dev/null and b/assets/models/PalmTree/StylizedWater.png differ diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index 62d0c928613db3..b0a94a48976336 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -11,8 +11,8 @@ @group(0) @binding(3) var dt_lut_texture: texture_3d; @group(0) @binding(4) var dt_lut_sampler: sampler; #else - @group(0) @binding(19) var dt_lut_texture: texture_3d; - @group(0) @binding(20) var dt_lut_sampler: sampler; + @group(0) @binding(21) var dt_lut_texture: texture_3d; + @group(0) @binding(22) var dt_lut_sampler: sampler; #endif // Half the size of the crossfade region between shadows and midtones and diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 9c74971ca15a56..ea2c0bd5755b37 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -50,7 +50,11 @@ use super::*; #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct DirectionalLight { + /// The color of the light. + /// + /// By default, this is white. pub color: Color, + /// Illuminance in lux (lumens per square meter), representing the amount of /// light projected onto surfaces by this light source. Lux is used here /// instead of lumens because a directional light illuminates all surfaces @@ -58,10 +62,45 @@ pub struct DirectionalLight { /// can only be specified for light sources which emit light from a specific /// area. pub illuminance: f32, + + /// Whether this light casts shadows. + /// + /// Note that shadows are rather expensive and become more so with every + /// light that casts them. In general, it's best to aggressively limit the + /// number of lights with shadows enabled to one or two at most. pub shadows_enabled: bool, + + /// Whether soft shadows are enabled, and if so, the size of the light. + /// + /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS, + /// cause shadows to become blurrier (i.e. their penumbra increases in + /// radius) as they extend away from objects. The blurriness of the shadow + /// depends on the size of the light; larger lights result in larger + /// penumbras and therefore blurrier shadows. + /// + /// Currently, soft shadows are rather noisy if not using the temporal mode. + /// If you enable soft shadows, consider choosing + /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing + /// (TAA) to smooth the noise out over time. + /// + /// Note that soft shadows are significantly more expensive to render than + /// hard shadows. + pub soft_shadow_size: Option, + + /// A value that adjusts the tradeoff between self-shadowing artifacts and + /// proximity of shadows to their casters. + /// + /// This value frequently must be tuned to the specific scene; this is + /// normal and a well-known part of the shadow mapping workflow. If set too + /// low, unsightly shadow patterns appear on objects not in shadow as + /// objects incorrectly cast shadows on themselves, known as *shadow acne*. + /// If set too high, shadows detach from the objects casting them and seem + /// to "fly" off the objects, known as *Peter Panning*. pub shadow_depth_bias: f32, - /// A bias applied along the direction of the fragment's surface normal. It is scaled to the - /// shadow map's texel size so that it is automatically adjusted to the orthographic projection. + + /// A bias applied along the direction of the fragment's surface normal. It + /// is scaled to the shadow map's texel size so that it is automatically + /// adjusted to the orthographic projection. pub shadow_normal_bias: f32, } @@ -71,6 +110,7 @@ impl Default for DirectionalLight { color: Color::WHITE, illuminance: light_consts::lux::AMBIENT_DAYLIGHT, shadows_enabled: false, + soft_shadow_size: None, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, } diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index c0b82602454ff3..c13be990dd82d7 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1767,13 +1767,6 @@ pub fn update_point_light_frusta( Or<(Changed, Changed)>, >, ) { - let projection = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); - let view_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); - for (entity, transform, point_light, mut cubemap_frusta) in &mut views { // The frusta are used for culling meshes to the light for shadow mapping // so if shadow mapping is disabled for this light, then the frusta are @@ -1784,6 +1777,16 @@ pub fn update_point_light_frusta( continue; } + let projection = Mat4::perspective_infinite_reverse_rh( + std::f32::consts::FRAC_PI_2, + 1.0, + point_light.shadow_map_near_z, + ); + let view_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + // ignore scale because we don't want to effectively scale light radius and range // by applying those as a view transform to shadow map rendering of objects // and ignore rotation because we want the shadow map projections to align with the axes @@ -1826,7 +1829,8 @@ pub fn update_spot_light_frusta( let view_backward = transform.back(); let spot_view = spot_light_view_matrix(transform); - let spot_projection = spot_light_projection_matrix(spot_light.outer_angle); + let spot_projection = + spot_light_projection_matrix(spot_light.outer_angle, spot_light.shadow_map_near_z); let view_projection = spot_projection * spot_view.inverse(); *frustum = Frustum::from_view_projection_custom_far( diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index 9ca85cd4db1679..a2cd7ec21eeedc 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -22,29 +22,60 @@ use super::*; pub struct PointLight { /// The color of this light source. pub color: Color, + /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. pub intensity: f32, + /// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by /// this light at all, so it's important to tune this together with `intensity` to prevent hard /// lighting cut-offs. pub range: f32, + /// Simulates a light source coming from a spherical volume with the given radius. Only affects /// the size of specular highlights created by this light. Because of this, large values may not /// produce the intended result -- for example, light radius does not affect shadow softness or /// diffuse lighting. pub radius: f32, + /// Whether this light casts shadows. pub shadows_enabled: bool, + + /// Whether soft shadows are enabled, and if so, the size of the light. + /// + /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS, + /// cause shadows to become blurrier (i.e. their penumbra increases in + /// radius) as they extend away from objects. The blurriness of the shadow + /// depends on the size of the light; larger lights result in larger + /// penumbras and therefore blurrier shadows. + /// + /// Currently, soft shadows are rather noisy if not using the temporal mode. + /// If you enable soft shadows, consider choosing + /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing + /// (TAA) to smooth the noise out over time. + /// + /// Note that soft shadows are significantly more expensive to render than + /// hard shadows. + pub soft_shadow_size: Option, + /// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions /// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments. /// Too high of a depth bias can lead to shadows detaching from their casters, or /// "peter-panning". This bias can be tuned together with `shadow_normal_bias` to correct shadow /// artifacts for a given scene. pub shadow_depth_bias: f32, + /// A bias applied along the direction of the fragment's surface normal. It is scaled to the /// shadow map's texel size so that it can be small close to the camera and gets larger further /// away. pub shadow_normal_bias: f32, + + /// The distance from the light to near Z plane in the shadow map. + /// + /// Objects closer than this distance to the light won't cast shadows. + /// Setting this higher increases the shadow map's precision. + /// + /// This only has an effect if shadows are enabled. + pub shadow_map_near_z: f32, } impl Default for PointLight { @@ -58,8 +89,10 @@ impl Default for PointLight { range: 20.0, radius: 0.0, shadows_enabled: false, + soft_shadow_size: None, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, } } } @@ -67,4 +100,5 @@ impl Default for PointLight { impl PointLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; + pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1; } diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index ab34196ff03fb1..afd4933b3201c1 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -7,23 +7,78 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default)] pub struct SpotLight { + /// The color of the light. + /// + /// By default, this is white. pub color: Color, + /// Luminous power in lumens, representing the amount of light emitted by this source in all directions. pub intensity: f32, + + /// Range in meters that this light illuminates. + /// + /// Note that this value affects resolution the shadow maps; generally, the + /// higher you set it, the lower-resolution your shadow maps will be. + /// Consequently, you should set this value to be only the size that you need. pub range: f32, + pub radius: f32, + + /// Whether this light casts shadows. + /// + /// Note that shadows are rather expensive and become more so with every + /// light that casts them. In general, it's best to aggressively limit the + /// number of lights with shadows enabled to one or two at most. pub shadows_enabled: bool, + + /// Whether soft shadows are enabled, and if so, the size of the light. + /// + /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS, + /// cause shadows to become blurrier (i.e. their penumbra increases in + /// radius) as they extend away from objects. The blurriness of the shadow + /// depends on the size of the light; larger lights result in larger + /// penumbras and therefore blurrier shadows. + /// + /// Currently, soft shadows are rather noisy if not using the temporal mode. + /// If you enable soft shadows, consider choosing + /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing + /// (TAA) to smooth the noise out over time. + /// + /// Note that soft shadows are significantly more expensive to render than + /// hard shadows. + pub soft_shadow_size: Option, + + /// A value that adjusts the tradeoff between self-shadowing artifacts and + /// proximity of shadows to their casters. + /// + /// This value frequently must be tuned to the specific scene; this is + /// normal and a well-known part of the shadow mapping workflow. If set too + /// low, unsightly shadow patterns appear on objects not in shadow as + /// objects incorrectly cast shadows on themselves, known as *shadow acne*. + /// If set too high, shadows detach from the objects casting them and seem + /// to "fly" off the objects, known as *Peter Panning*. pub shadow_depth_bias: f32, + /// A bias applied along the direction of the fragment's surface normal. It is scaled to the /// shadow map's texel size so that it can be small close to the camera and gets larger further /// away. pub shadow_normal_bias: f32, + + /// The distance from the light to near Z plane in the shadow map. + /// + /// Objects closer than this distance to the light won't cast shadows. + /// Setting this higher increases the shadow map's precision. + /// + /// This only has an effect if shadows are enabled. + pub shadow_map_near_z: f32, + /// Angle defining the distance from the spot light direction to the outer limit /// of the light's cone of effect. /// `outer_angle` should be < `PI / 2.0`. /// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle /// approaches this limit. pub outer_angle: f32, + /// Angle defining the distance from the spot light direction to the inner limit /// of the light's cone of effect. /// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff. @@ -34,6 +89,7 @@ pub struct SpotLight { impl SpotLight { pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02; pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8; + pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1; } impl Default for SpotLight { @@ -48,8 +104,10 @@ impl Default for SpotLight { range: 20.0, radius: 0.0, shadows_enabled: false, + soft_shadow_size: None, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, + shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, inner_angle: 0.0, outer_angle: std::f32::consts::FRAC_PI_4, } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b425a251aad491..cf4fc8e920fc68 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -20,6 +20,7 @@ use bevy_render::{ Extract, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_utils::prelude::default; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::tracing::{error, warn}; @@ -36,8 +37,10 @@ pub struct ExtractedPointLight { pub radius: f32, pub transform: GlobalTransform, pub shadows_enabled: bool, + pub soft_shadow_size: Option, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, + pub shadow_map_near_z: f32, pub spot_light_angles: Option<(f32, f32)>, } @@ -48,6 +51,7 @@ pub struct ExtractedDirectionalLight { pub transform: GlobalTransform, pub shadows_enabled: bool, pub volumetric: bool, + pub soft_shadow_size: Option, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub cascade_shadow_config: CascadeShadowConfig, @@ -64,8 +68,10 @@ pub struct GpuPointLight { color_inverse_square_range: Vec4, position_radius: Vec4, flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, + shadow_map_near_z: f32, spot_light_tan_angle: f32, } @@ -170,6 +176,7 @@ pub struct GpuDirectionalLight { color: Vec4, dir_to_light: Vec3, flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, num_cascades: u32, @@ -228,8 +235,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1; #[derive(Resource, Clone)] pub struct ShadowSamplers { - pub point_light_sampler: Sampler, - pub directional_light_sampler: Sampler, + pub point_light_comparison_sampler: Sampler, + pub point_light_linear_sampler: Sampler, + pub directional_light_comparison_sampler: Sampler, + pub directional_light_linear_sampler: Sampler, } // TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system @@ -237,27 +246,30 @@ impl FromWorld for ShadowSamplers { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); + let base_sampler_descriptor = SamplerDescriptor { + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..default() + }; + ShadowSamplers { - point_light_sampler: render_device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - address_mode_w: AddressMode::ClampToEdge, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - mipmap_filter: FilterMode::Nearest, - compare: Some(CompareFunction::GreaterEqual), - ..Default::default() - }), - directional_light_sampler: render_device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - address_mode_w: AddressMode::ClampToEdge, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - mipmap_filter: FilterMode::Nearest, + point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { compare: Some(CompareFunction::GreaterEqual), - ..Default::default() + ..base_sampler_descriptor }), + point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + directional_light_comparison_sampler: render_device.create_sampler( + &SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }, + ), + directional_light_linear_sampler: render_device + .create_sampler(&base_sampler_descriptor), } } } @@ -397,11 +409,13 @@ pub fn extract_lights( radius: point_light.radius, transform: *transform, shadows_enabled: point_light.shadows_enabled, + soft_shadow_size: point_light.soft_shadow_size, shadow_depth_bias: point_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset shadow_normal_bias: point_light.shadow_normal_bias * point_light_texel_size * std::f32::consts::SQRT_2, + shadow_map_near_z: point_light.shadow_map_near_z, spot_light_angles: None, }; point_lights_values.push(( @@ -446,11 +460,13 @@ pub fn extract_lights( radius: spot_light.radius, transform: *transform, shadows_enabled: spot_light.shadows_enabled, + soft_shadow_size: spot_light.soft_shadow_size, shadow_depth_bias: spot_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset shadow_normal_bias: spot_light.shadow_normal_bias * texel_size * std::f32::consts::SQRT_2, + shadow_map_near_z: spot_light.shadow_map_near_z, spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), }, render_visible_entities, @@ -487,6 +503,7 @@ pub fn extract_lights( illuminance: directional_light.illuminance, transform: *transform, volumetric: volumetric_light.is_some(), + soft_shadow_size: directional_light.soft_shadow_size, shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset @@ -501,8 +518,6 @@ pub fn extract_lights( } } -pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32; - pub(crate) struct CubeMapFace { pub(crate) target: Vec3, pub(crate) up: Vec3, @@ -676,9 +691,9 @@ pub(crate) fn spot_light_view_matrix(transform: &GlobalTransform) -> Mat4 { ) } -pub(crate) fn spot_light_projection_matrix(angle: f32) -> Mat4 { +pub(crate) fn spot_light_projection_matrix(angle: f32, near_z: f32) -> Mat4 { // spot light projection FOV is 2x the angle from spot light center to outer edge - Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, POINT_LIGHT_NEAR_Z) + Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) } #[allow(clippy::too_many_arguments)] @@ -719,14 +734,6 @@ pub fn prepare_lights( return; }; - // Pre-calculate for PointLights - let cube_face_projection = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); - let cube_face_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); - global_light_meta.entity_to_index.clear(); let mut point_lights: Vec<_> = point_lights.iter().collect::>(); @@ -856,6 +863,12 @@ pub fn prepare_lights( flags |= PointLightFlags::SHADOWS_ENABLED; } + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + std::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles { Some((inner, outer)) => { let light_direction = light.transform.forward(); @@ -898,8 +911,10 @@ pub fn prepare_lights( .extend(1.0 / (light.range * light.range)), position_radius: light.transform.translation().extend(light.radius), flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, + shadow_map_near_z: light.shadow_map_near_z, spot_light_tan_angle, }); global_light_meta.entity_to_index.insert(entity, index); @@ -942,6 +957,7 @@ pub fn prepare_lights( // direction is negated to be ready for N.L dir_to_light: light.transform.back().into(), flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, num_cascades: num_cascades as u32, @@ -1047,6 +1063,16 @@ pub fn prepare_lights( // and ignore rotation because we want the shadow map projections to align with the axes let view_translation = GlobalTransform::from_translation(light.transform.translation()); + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + std::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + let cube_face_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + for (face_index, (view_rotation, frustum)) in cube_face_rotations .iter() .zip(&point_light_frusta.unwrap().frusta) @@ -1115,7 +1141,7 @@ pub fn prepare_lights( let angle = light.spot_light_angles.expect("lights should be sorted so that \ [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; - let spot_projection = spot_light_projection_matrix(angle); + let spot_projection = spot_light_projection_matrix(angle, light.shadow_map_near_z); let depth_texture_view = directional_light_depth_texture diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 89c5557fbe088d..dad06eb242bbe7 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -212,11 +212,13 @@ fn layout_entries( ))] texture_cube(TextureSampleType::Depth), ), - // Point Shadow Texture Array Sampler + // Point Shadow Texture Array Comparison Sampler (3, sampler(SamplerBindingType::Comparison)), + // Point Shadow Texture Array Linear Sampler + (4, sampler(SamplerBindingType::Filtering)), // Directional Shadow Texture Array ( - 4, + 5, #[cfg(any( not(feature = "webgl"), not(target_arch = "wasm32"), @@ -226,11 +228,13 @@ fn layout_entries( #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] texture_2d(TextureSampleType::Depth), ), - // Directional Shadow Texture Array Sampler - (5, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Comparison Sampler + (6, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Linear Sampler + (7, sampler(SamplerBindingType::Filtering)), // PointLights ( - 6, + 8, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -241,7 +245,7 @@ fn layout_entries( ), // ClusteredLightIndexLists ( - 7, + 9, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -252,7 +256,7 @@ fn layout_entries( ), // ClusterOffsetsAndCounts ( - 8, + 10, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -263,16 +267,16 @@ fn layout_entries( ), // Globals ( - 9, + 11, uniform_buffer::(false).visibility(ShaderStages::VERTEX_FRAGMENT), ), // Fog - (10, uniform_buffer::(true)), + (12, uniform_buffer::(true)), // Light probes - (11, uniform_buffer::(true)), + (13, uniform_buffer::(true)), // Visibility ranges ( - 12, + 14, buffer_layout( visibility_ranges_buffer_binding_type, false, @@ -282,7 +286,7 @@ fn layout_entries( ), // Screen space ambient occlusion texture ( - 13, + 15, texture_2d(TextureSampleType::Float { filterable: false }), ), ), @@ -291,9 +295,9 @@ fn layout_entries( // EnvironmentMapLight let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (14, environment_map_entries[0]), - (15, environment_map_entries[1]), - (16, environment_map_entries[2]), + (16, environment_map_entries[0]), + (17, environment_map_entries[1]), + (18, environment_map_entries[2]), )); // Irradiance volumes @@ -301,16 +305,16 @@ fn layout_entries( let irradiance_volume_entries = irradiance_volume::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (17, irradiance_volume_entries[0]), - (18, irradiance_volume_entries[1]), + (19, irradiance_volume_entries[0]), + (20, irradiance_volume_entries[1]), )); } // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (19, tonemapping_lut_entries[0]), - (20, tonemapping_lut_entries[1]), + (21, tonemapping_lut_entries[0]), + (22, tonemapping_lut_entries[1]), )); // Prepass @@ -320,7 +324,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([21, 22, 23, 24]) + .zip([23, 24, 25, 26]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -331,10 +335,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 25, + 27, texture_2d(TextureSampleType::Float { filterable: true }), ), - (26, sampler(SamplerBindingType::Filtering)), + (28, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -515,17 +519,19 @@ pub fn prepare_mesh_view_bind_groups( (0, view_binding.clone()), (1, light_binding.clone()), (2, &shadow_bindings.point_light_depth_texture_view), - (3, &shadow_samplers.point_light_sampler), - (4, &shadow_bindings.directional_light_depth_texture_view), - (5, &shadow_samplers.directional_light_sampler), - (6, point_light_binding.clone()), - (7, cluster_bindings.light_index_lists_binding().unwrap()), - (8, cluster_bindings.offsets_and_counts_binding().unwrap()), - (9, globals.clone()), - (10, fog_binding.clone()), - (11, light_probes_binding.clone()), - (12, visibility_ranges_buffer.as_entire_binding()), - (13, ssao_view), + (3, &shadow_samplers.point_light_comparison_sampler), + (4, &shadow_samplers.point_light_linear_sampler), + (5, &shadow_bindings.directional_light_depth_texture_view), + (6, &shadow_samplers.directional_light_comparison_sampler), + (7, &shadow_samplers.directional_light_linear_sampler), + (8, point_light_binding.clone()), + (9, cluster_bindings.light_index_lists_binding().unwrap()), + (10, cluster_bindings.offsets_and_counts_binding().unwrap()), + (11, globals.clone()), + (12, fog_binding.clone()), + (13, light_probes_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + (15, ssao_view), )); let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( @@ -542,9 +548,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (14, diffuse_texture_view), - (15, specular_texture_view), - (16, sampler), + (16, diffuse_texture_view), + (17, specular_texture_view), + (18, sampler), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -553,9 +559,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (14, diffuse_texture_views.as_slice()), - (15, specular_texture_views.as_slice()), - (16, sampler), + (16, diffuse_texture_views.as_slice()), + (17, specular_texture_views.as_slice()), + (18, sampler), )); } } @@ -576,21 +582,21 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((17, texture_view), (18, sampler))); + entries = entries.extend_with_indices(((19, texture_view), (20, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { entries = entries - .extend_with_indices(((17, texture_views.as_slice()), (18, sampler))); + .extend_with_indices(((19, texture_views.as_slice()), (20, sampler))); } None => {} } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((19, lut_bindings.0), (20, lut_bindings.1))); + entries = entries.extend_with_indices(((21, lut_bindings.0), (22, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -600,7 +606,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([21, 22, 23, 24]) + .zip([23, 24, 25, 26]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -616,7 +622,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((25, transmission_view), (26, transmission_sampler))); + entries.extend_with_indices(((27, transmission_view), (28, transmission_sampler))); commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index b8e74c60b8b437..b6fabee889714d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -13,87 +13,89 @@ #else @group(0) @binding(2) var point_shadow_textures: texture_depth_cube_array; #endif -@group(0) @binding(3) var point_shadow_textures_sampler: sampler_comparison; +@group(0) @binding(3) var point_shadow_textures_comparison_sampler: sampler_comparison; +@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler; #ifdef NO_ARRAY_TEXTURES_SUPPORT -@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d; +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d; #else -@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d_array; +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array; #endif -@group(0) @binding(5) var directional_shadow_textures_sampler: sampler_comparison; +@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison; +@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 -@group(0) @binding(6) var point_lights: types::PointLights; -@group(0) @binding(7) var cluster_light_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var point_lights: types::PointLights; +@group(0) @binding(9) var cluster_light_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; #else -@group(0) @binding(6) var point_lights: types::PointLights; -@group(0) @binding(7) var cluster_light_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var point_lights: types::PointLights; +@group(0) @binding(9) var cluster_light_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; #endif -@group(0) @binding(9) var globals: Globals; -@group(0) @binding(10) var fog: types::Fog; -@group(0) @binding(11) var light_probes: types::LightProbes; +@group(0) @binding(11) var globals: Globals; +@group(0) @binding(12) var fog: types::Fog; +@group(0) @binding(13) var light_probes: types::LightProbes; const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 -@group(0) @binding(12) var visibility_ranges: array>; +@group(0) @binding(14) var visibility_ranges: array>; #else -@group(0) @binding(12) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; +@group(0) @binding(14) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; #endif -@group(0) @binding(13) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(15) var screen_space_ambient_occlusion_texture: texture_2d; #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(14) var diffuse_environment_maps: binding_array, 8u>; -@group(0) @binding(15) var specular_environment_maps: binding_array, 8u>; +@group(0) @binding(16) var diffuse_environment_maps: binding_array, 8u>; +@group(0) @binding(17) var specular_environment_maps: binding_array, 8u>; #else -@group(0) @binding(14) var diffuse_environment_map: texture_cube; -@group(0) @binding(15) var specular_environment_map: texture_cube; +@group(0) @binding(16) var diffuse_environment_map: texture_cube; +@group(0) @binding(17) var specular_environment_map: texture_cube; #endif -@group(0) @binding(16) var environment_map_sampler: sampler; +@group(0) @binding(18) var environment_map_sampler: sampler; #ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(17) var irradiance_volumes: binding_array, 8u>; +@group(0) @binding(19) var irradiance_volumes: binding_array, 8u>; #else -@group(0) @binding(17) var irradiance_volume: texture_3d; +@group(0) @binding(19) var irradiance_volume: texture_3d; #endif -@group(0) @binding(18) var irradiance_volume_sampler: sampler; +@group(0) @binding(20) var irradiance_volume_sampler: sampler; #endif // NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. -@group(0) @binding(19) var dt_lut_texture: texture_3d; -@group(0) @binding(20) var dt_lut_sampler: sampler; +@group(0) @binding(21) var dt_lut_texture: texture_3d; +@group(0) @binding(22) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(21) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(22) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(23) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(21) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(22) var normal_prepass_texture: texture_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(23) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(24) var deferred_prepass_texture: texture_2d; +@group(0) @binding(26) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(25) var view_transmission_texture: texture_2d; -@group(0) @binding(26) var view_transmission_sampler: sampler; +@group(0) @binding(27) var view_transmission_texture: texture_2d; +@group(0) @binding(28) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index f517daec4d6b47..e6f58287a73839 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -8,8 +8,10 @@ struct PointLight { position_radius: vec4, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, + shadow_map_near_z: f32, spot_light_tan_angle: f32, }; @@ -28,6 +30,7 @@ struct DirectionalLight { direction_to_light: vec3, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, num_cascades: u32, diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index f789b3f76a4aa6..c956f67e08a7ac 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -366,7 +366,12 @@ fn apply_pbr_lighting( var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal); + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::point_lights.data[light_id].shadow_map_near_z, + ); } let light_contrib = lighting::spot_light(light_id, &lighting_input); diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index ec155cf3fcb77a..53873e6e653583 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -12,14 +12,14 @@ fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i #ifdef NO_ARRAY_TEXTURES_SUPPORT return textureSampleCompare( view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, + view_bindings::directional_shadow_textures_comparison_sampler, light_local, depth, ); #else return textureSampleCompareLevel( view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, + view_bindings::directional_shadow_textures_comparison_sampler, light_local, array_index, depth, @@ -27,6 +27,31 @@ fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i #endif } +// Does a single sample of the blocker search, a part of the PCSS algorithm. +// This is the variant used for directional lights. +fn search_for_blockers_in_shadow_map_hardware( + light_local: vec2, + depth: f32, + array_index: i32, +) -> vec2 { +#ifdef NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSample( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + ); +#else + let sampled_depth = textureSample( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + array_index, + ); +#endif + + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); +} + // 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; @@ -113,9 +138,9 @@ fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { // 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 { +fn random_rotation_matrix(scale: vec2, temporal: bool) -> mat2x2 { let random_angle = 2.0 * PI * interleaved_gradient_noise( - scale, view_bindings::globals.frame_count); + scale, select(1u, view_bindings::globals.frame_count, temporal)); let m = vec2(sin(random_angle), cos(random_angle)); return mat2x2( m.y, -m.x, @@ -123,13 +148,28 @@ fn random_rotation_matrix(scale: vec2) -> mat2x2 { ); } -fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { +// Calculates the distance between spiral samples for the given texel size and +// penumbra size. This is used for the Jimenez '14 (i.e. temporal) variant of +// shadow sampling. +fn calculate_uv_offset_scale_jimenez_fourteen(texel_size: f32, blur_size: f32) -> vec2 { 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); + return f * blur_size / (texel_size * shadow_map_size); +} + +fn sample_shadow_map_jimenez_fourteen( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + blur_size: f32, + temporal: bool, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, temporal); + let uv_offset_scale = calculate_uv_offset_scale_jimenez_fourteen(texel_size, blur_size); // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; @@ -153,11 +193,57 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_ return sum / 8.0; } +// Performs the blocker search portion of percentage-closer soft shadows (PCSS). +// This is the variation used for directional lights. +// +// We can't use Castano '13 here because that has a hard-wired fixed size, while +// the PCSS algorithm requires a search size that varies based on the size of +// the light. So we instead use the D3D sample point positions, spaced according +// to the search size, to provide a sample pattern in a similar manner to the +// cubemap sampling approach we use for PCF. +// +// `search_size` is the size of the search region in texels. +fn search_for_blockers_in_shadow_map( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + search_size: f32, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let uv_offset_scale = search_size / (texel_size * shadow_map_size); + + let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; + let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; + let offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; + let offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; + let offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; + let offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; + let offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; + let offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; + + var sum = vec2(0.0); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset0, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset1, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset2, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset3, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset4, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset5, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset6, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset7, depth, array_index); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { #ifdef SHADOW_FILTER_METHOD_GAUSSIAN return sample_shadow_map_castano_thirteen(light_local, depth, array_index); #else ifdef SHADOW_FILTER_METHOD_TEMPORAL - return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size); + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, 1.0, true); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 return sample_shadow_map_hardware(light_local, depth, array_index); #else @@ -169,6 +255,45 @@ fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel #endif } +// Samples the shadow map for a directional light when percentage-closer soft +// shadows are being used. +// +// We first search for a *blocker*, which is the average depth value of any +// shadow map samples that are adjacent to the sample we're considering. That +// allows us to determine the penumbra size; a larger gap between the blocker +// and the depth of this sample results in a wider penumbra. Finally, we sample +// the shadow map the same way we do in PCF, using that penumbra width. +// +// A good overview of the technique: +// +fn sample_shadow_map_pcss( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + light_size: f32, +) -> f32 { + // Determine the average Z value of the closest blocker. + let z_blocker = search_for_blockers_in_shadow_map( + light_local, depth, array_index, texel_size, light_size); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + + // FIXME: We can't use Castano '13 here because that has a hard-wired fixed + // size. So we instead use Jimenez '14 unconditionally. In the non-temporal + // variant this is unfortunately rather noisy. This may be improvable in the + // future by generating a mip chain of the shadow map and using that to + // provide better blurs. +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, true); +#else // SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, false); +#endif // SHADOW_FILTER_METHOD_TEMPORAL +} + // 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 @@ -176,12 +301,48 @@ fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel // 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); + return textureSampleCompare( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + depth + ); #else - return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth); + return textureSampleCompareLevel( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + i32(light_id), + depth + ); #endif } +// Performs one sample of the blocker search. This variation of the blocker +// search function is for point and spot lights. +fn search_for_blockers_in_shadow_cubemap_hardware( + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + ); +#else + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + i32(light_id), + ); +#endif + + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); +} + fn sample_shadow_cubemap_at_offset( position: vec2, coeff: f32, @@ -198,6 +359,26 @@ fn sample_shadow_cubemap_at_offset( ) * coeff; } +// Computes the search position and performs one sample of the blocker search. +// This variation of the blocker search function is for point and spot lights. +// +// `x_basis`, `y_basis`, and `light_local` form an orthonormal basis over which +// the blocker search happens. +fn search_for_blockers_in_shadow_cubemap_at_offset( + position: vec2, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { + return search_for_blockers_in_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ); +} + // 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 @@ -249,12 +430,13 @@ fn sample_shadow_cubemap_gaussian( // 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( +fn sample_shadow_cubemap_jittered( light_local: vec3, depth: f32, scale: f32, distance_to_light: f32, light_id: u32, + temporal: bool, ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. @@ -264,7 +446,7 @@ fn sample_shadow_cubemap_temporal( } let basis = orthonormalize(light_local, up) * scale * distance_to_light; - let rotation_matrix = random_rotation_matrix(vec2(1.0)); + let rotation_matrix = random_rotation_matrix(vec2(1.0), temporal); let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ * POINT_SHADOW_TEMPORAL_OFFSET_SCALE; @@ -313,8 +495,8 @@ fn sample_shadow_cubemap( 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); + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id, true); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 return sample_shadow_cubemap_hardware(light_local, depth, light_id); #else @@ -325,3 +507,76 @@ fn sample_shadow_cubemap( return 0.0; #endif } + +// Searches for PCSS blockers in a cubemap. This is the variant of the blocker +// search used for point and spot lights. +// +// This follows the logic in `sample_shadow_cubemap_gaussian`, but uses linear +// sampling instead of percentage-closer filtering. +// +// The `scale` parameter represents the size of the light. +fn search_for_blockers_in_shadow_cubemap( + 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: vec2 = vec2(0.0); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_id); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + +// Samples the shadow map for a point or spot light when percentage-closer soft +// shadows are being used. +// +// A good overview of the technique: +// +fn sample_shadow_cubemap_pcss( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, + light_size: f32, +) -> f32 { + let z_blocker = search_for_blockers_in_shadow_cubemap( + light_local, depth, light_size, distance_to_light, light_id); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, true); +#else + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, false); +#endif +} diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 21b25f7f3aebf5..d76f59677a7715 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -3,7 +3,10 @@ #import bevy_pbr::{ mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, - shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map} + shadow_sampling::{ + SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_cubemap_pcss, + sample_shadow_map, sample_shadow_map_pcss, + } } #import bevy_render::{ @@ -41,12 +44,30 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: v let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; let depth = zw.x / zw.y; - // 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. + // If soft shadows are enabled, use the PCSS path. Cubemaps assume a + // left-handed coordinate space, so we have to flip the z-axis when + // sampling. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_cubemap_pcss( + frag_ls * flip_z, + distance_to_light, + depth, + light_id, + (*light).soft_shadow_size, + ); + } + + // 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. 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 { +fn fetch_spot_shadow( + light_id: u32, + frag_position: vec4, + surface_normal: vec3, + near_z: f32, +) -> f32 { let light = &view_bindings::point_lights.data[light_id]; let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; @@ -91,15 +112,16 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: ve // convert to uv coordinates let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); - // 0.1 must match POINT_LIGHT_NEAR_Z - let depth = 0.1 / -projected_position.z; + let depth = near_z / -projected_position.z; - return sample_shadow_map( - shadow_uv, - depth, - i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset, - SPOT_SHADOW_TEXEL_SIZE - ); + // If soft shadows are enabled, use the PCSS path. + let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset; + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE, (*light).soft_shadow_size); + } + + return sample_shadow_map(shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE); } fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { @@ -146,7 +168,12 @@ fn world_to_directional_light_local( return vec4(light_local, depth, 1.0); } -fn sample_directional_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]; @@ -161,7 +188,15 @@ fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: } let array_index = i32((*light).depth_texture_base_index + cascade_index); - return sample_shadow_map(light_local.xy, light_local.z, array_index, (*cascade).texel_size); + let texel_size = (*cascade).texel_size; + + // If soft shadows are enabled, use the PCSS path. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + light_local.xy, light_local.z, array_index, texel_size, (*light).soft_shadow_size); + } + + return sample_shadow_map(light_local.xy, light_local.z, array_index, texel_size); } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { diff --git a/examples/3d/pcss.rs b/examples/3d/pcss.rs new file mode 100644 index 00000000000000..34448dd8e881bc --- /dev/null +++ b/examples/3d/pcss.rs @@ -0,0 +1,641 @@ +//! Demonstrates percentage-closer soft shadows (PCSS). + +use std::{f32::consts::PI, marker::PhantomData}; + +use bevy::{ + core_pipeline::{ + experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasSettings}, + prepass::{DepthPrepass, MotionVectorPrepass}, + Skybox, + }, + ecs::system::EntityCommands, + math::vec3, + pbr::{CubemapVisibleEntities, ShadowFilteringMethod}, + prelude::*, + render::{ + camera::TemporalJitter, + primitives::{CubemapFrusta, Frustum}, + view::VisibleEntities, + }, +}; + +/// The path to the UI font. +static FONT_PATH: &str = "fonts/FiraMono-Medium.ttf"; + +/// The size of the soft shadow penumbras when PCSS is enabled. +const SOFT_SHADOW_SIZE: f32 = 10.0; + +/// The intensity of the point and spot lights. +const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0; + +/// The range in meters of the point and spot lights. +const POINT_LIGHT_RANGE: f32 = 110.0; + +/// The depth bias for directional and spot lights. This value is set higher +/// than the default to avoid shadow acne. +const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20; + +/// The depth bias for point lights. This value is set higher than the default to +/// avoid shadow acne. +/// +/// Unfortunately, there is a bit of Peter Panning with this value, because of +/// the distance and angle of the light. This can't be helped in this scene +/// without increasing the shadow map size beyond reasonable limits. +const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35; + +/// The near Z value for the shadow map, in meters. This is set higher than the +/// default in order to achieve greater resolution in the shadow map for point +/// and spot lights. +const SHADOW_MAP_NEAR_Z: f32 = 50.0; + +/// The current application settings (light type, shadow filter, and the status +/// of PCSS). +#[derive(Resource, Default)] +struct AppStatus { + /// The type of light presently in the scene: either directional or point. + light_type: LightType, + /// The type of shadow filter: Gaussian or temporal. + shadow_filter: ShadowFilter, + /// Whether soft shadows are enabled. + soft_shadows: SoftShadows, +} + +/// The type of light presently in the scene: either directional or point. +#[derive(Clone, Copy, Default, PartialEq)] +enum LightType { + /// A directional light, with a cascaded shadow map. + #[default] + Directional, + /// A point light, with a cube shadow map. + Point, + /// A spot light, with a cube shadow map. + Spot, +} + +/// The type of shadow filter. +/// +/// Generally, `Gaussian` is preferred when temporal antialiasing isn't in use, +/// while `Temporal` is preferred when TAA is in use. In this example, this +/// setting also turns TAA on and off. +#[derive(Clone, Copy, Default, PartialEq)] +enum ShadowFilter { + /// The non-temporal Gaussian filter (Castano '13 for directional lights, an + /// analogous alternative for point and spot lights). + #[default] + NonTemporal, + /// The temporal Gaussian filter (Jimenez '14 for directional lights, an + /// analogous alternative for point and spot lights). + Temporal, +} + +/// Whether PCSS is enabled or disabled. +#[derive(Clone, Copy, Default, PartialEq)] +enum SoftShadows { + /// Soft shadows (PCSS) are enabled. + #[default] + Enabled, + /// Soft shadows (PCSS) are disabled. + Disabled, +} + +/// A marker component that we place on all radio `Button`s. +/// +/// The type parameter specifies the setting that this button controls: one of +/// `LightType`, `ShadowFilter`, or `SoftShadows`. +#[derive(Component, Deref, DerefMut)] +struct RadioButton(T); + +/// A marker component that we place on all `Text` inside radio buttons. +/// +/// The type parameter specifies the setting that this button controls: one of +/// `LightType`, `ShadowFilter`, or `SoftShadows`. +#[derive(Component, Deref, DerefMut)] +struct RadioButtonText(T); + +/// An event that's sent whenever the user changes one of the settings by +/// clicking a radio button. +/// +/// The type parameter specifies the setting that was changed: one of +/// `LightType`, `ShadowFilter`, or `SoftShadows`. +#[derive(Event)] +struct RadioButtonChangeEvent(PhantomData); + +/// The example application entry point. +fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Percentage Closer Soft Shadows Example".into(), + ..default() + }), + ..default() + })) + .add_plugins(TemporalAntiAliasPlugin) + .add_event::>() + .add_event::>() + .add_event::>() + .add_systems(Startup, setup) + .add_systems(Update, handle_ui_interactions) + .add_systems( + Update, + ( + update_light_type_radio_buttons, + update_shadow_filter_radio_buttons, + update_soft_shadow_radio_buttons, + ) + .after(handle_ui_interactions), + ) + .add_systems( + Update, + ( + handle_light_type_change, + handle_shadow_filter_change, + handle_pcss_toggle, + ) + .after(handle_ui_interactions), + ) + .run(); +} + +/// Creates all the objects in the scene. +fn setup(mut commands: Commands, asset_server: Res, app_status: Res) { + let font = asset_server.load(FONT_PATH); + + spawn_camera(&mut commands, &asset_server); + spawn_light(&mut commands, &app_status); + spawn_gltf_scene(&mut commands, &asset_server); + spawn_buttons(&mut commands, &font); +} + +/// Spawns the camera, with the initial shadow filtering method. +fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { + commands + .spawn(Camera3dBundle { + transform: Transform::from_xyz(-12.912 * 0.7, 4.466 * 0.7, -10.624 * 0.7) + .with_rotation(Quat::from_euler( + EulerRot::YXZ, + -134.76 / 180.0 * PI, + -0.175, + 0.0, + )), + ..default() + }) + .insert(ShadowFilteringMethod::Gaussian) + // `TemporalJitter` is needed for TAA. Note that it does nothing without + // `TemporalAntiAliasSettings`. + .insert(TemporalJitter::default()) + // The depth prepass is needed for TAA. + .insert(DepthPrepass) + // The motion vector prepass is needed for TAA. + .insert(MotionVectorPrepass) + // Add a nice skybox. + .insert(Skybox { + image: asset_server.load("environment_maps/sky_skybox.ktx2"), + brightness: 500.0, + }); +} + +/// Spawns the initial light. +fn spawn_light(commands: &mut Commands, app_status: &AppStatus) { + // Because this light can become a directional light, point light, or spot + // light depending on the settings, we add the union of the components + // necessary for this light to behave as all three of those. + commands + .spawn(DirectionalLightBundle { + directional_light: create_directional_light(app_status), + transform: Transform::from_rotation(Quat::from_array([ + 0.6539259, + -0.34646285, + 0.36505926, + -0.5648683, + ])) + .with_translation(vec3(57.693, 34.334, -6.422)), + ..default() + }) + // These two are needed for point lights. + .insert(CubemapVisibleEntities::default()) + .insert(CubemapFrusta::default()) + // These two are needed for spot lights. + .insert(VisibleEntities::default()) + .insert(Frustum::default()); +} + +/// Loads and spawns the glTF palm tree scene. +fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) { + commands.spawn(SceneBundle { + scene: asset_server.load("models/PalmTree/PalmTree.gltf#Scene0"), + ..default() + }); +} + +/// Spawns all the buttons at the bottom of the screen. +fn spawn_buttons(commands: &mut Commands, font: &Handle) { + commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + position_type: PositionType::Absolute, + row_gap: Val::Px(6.0), + left: Val::Px(10.0), + bottom: Val::Px(10.0), + ..default() + }, + ..default() + }) + .with_children(|parent| { + spawn_option_buttons( + parent, + "Light Type", + &[ + (LightType::Directional, "Directional"), + (LightType::Point, "Point"), + (LightType::Spot, "Spot"), + ], + font, + ); + spawn_option_buttons( + parent, + "Shadow Filter", + &[ + (ShadowFilter::Temporal, "Temporal"), + (ShadowFilter::NonTemporal, "Non-Temporal"), + ], + font, + ); + spawn_option_buttons( + parent, + "Soft Shadows", + &[(SoftShadows::Enabled, "On"), (SoftShadows::Disabled, "Off")], + font, + ); + }); +} + +/// Spawns the buttons that allow configuration of a setting. +/// +/// The user may change the setting to any one of the labeled `options`. +/// +/// The type parameter specifies the particular setting: one of `LightType`, +/// `ShadowFilter`, or `SoftShadows`. +fn spawn_option_buttons( + parent: &mut ChildBuilder, + title: &str, + options: &[(T, &str)], + font: &Handle, +) where + T: Clone + Send + Sync + 'static, +{ + // Add the parent node for the row. + parent + .spawn(NodeBundle { + style: Style { + align_items: AlignItems::Center, + ..default() + }, + ..default() + }) + .with_children(|parent| { + spawn_ui_text(parent, title, font, Color::BLACK).insert(Style { + width: Val::Px(125.0), + ..default() + }); + + for (option_index, (option_value, option_name)) in options.iter().enumerate() { + spawn_option_button( + parent, + option_value, + option_name, + option_index == 0, + option_index == 0, + option_index == options.len() - 1, + font, + ); + } + }); +} + +/// Spawns a single radio button that allows configuration of a setting. +/// +/// The type parameter specifies the particular setting: one of `LightType`, +/// `ShadowFilter`, or `SoftShadows`. +fn spawn_option_button( + parent: &mut ChildBuilder, + option_value: &T, + option_name: &str, + is_selected: bool, + is_first: bool, + is_last: bool, + font: &Handle, +) where + T: Clone + Send + Sync + 'static, +{ + let (bg_color, fg_color) = if is_selected { + (Color::WHITE, Color::BLACK) + } else { + (Color::BLACK, Color::WHITE) + }; + + // Add the button node. + parent + .spawn(ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)).with_left(if is_first { + Val::Px(1.0) + } else { + Val::Px(0.0) + }), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::axes(Val::Px(12.0), Val::Px(6.0)), + ..default() + }, + border_color: BorderColor(Color::WHITE), + border_radius: BorderRadius::ZERO + .with_left(if is_first { Val::Px(6.0) } else { Val::Px(0.0) }) + .with_right(if is_last { Val::Px(6.0) } else { Val::Px(0.0) }), + image: UiImage::default().with_color(bg_color), + ..default() + }) + .insert(RadioButton(option_value.clone())) + .with_children(|parent| { + spawn_ui_text(parent, option_name, font, fg_color) + .insert(RadioButtonText(option_value.clone())); + }); +} + +/// Spawns text for the UI. +/// +/// Returns the `EntityCommands`, which allow further customization of the text +/// style. +fn spawn_ui_text<'a>( + parent: &'a mut ChildBuilder, + label: &str, + font: &Handle, + color: Color, +) -> EntityCommands<'a> { + parent.spawn(TextBundle::from_section( + label, + TextStyle { + font: font.clone(), + font_size: 18.0, + color, + }, + )) +} + +/// Checks for clicks on the radio buttons and sends `RadioButtonChangeEvent`s +/// as necessary. +fn handle_ui_interactions( + mut interactions: Query< + ( + &Interaction, + AnyOf<( + &RadioButton, + &RadioButton, + &RadioButton, + )>, + ), + With