diff --git a/Cargo.toml b/Cargo.toml index ddd4b9444812d..0d6a58c11803c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3452,6 +3452,17 @@ description = "Demonstrates animation masks" category = "Animation" 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 = false + [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 0000000000000..d386497ac1efe 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 0000000000000..614c4d29680bb 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 0000000000000..c4987e1ea67a1 --- /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 0000000000000..a4da3043caca7 Binary files /dev/null and b/assets/models/PalmTree/StylizedWater.png differ diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 7771e95ba313a..add1b38850b54 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -152,6 +152,10 @@ pub struct GpuClusterableObject { pub(crate) shadow_depth_bias: f32, pub(crate) shadow_normal_bias: f32, pub(crate) spot_light_tan_angle: f32, + pub(crate) soft_shadow_size: f32, + pub(crate) shadow_map_near_z: f32, + pub(crate) pad_a: f32, + pub(crate) pad_b: f32, } pub enum GpuClusterableObjects { diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 378f91b629d48..10d8a57962f2b 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -259,11 +259,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 21, + 22, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 22, + 23, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 9ae1f6992710f..89bc3d28468c7 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, Debug)] 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 6ce0ee8758c41..28b463041192e 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -580,8 +580,6 @@ pub fn update_point_light_frusta( Or<(Changed, Changed)>, >, ) { - let clip_from_view = - 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)) @@ -597,6 +595,12 @@ pub fn update_point_light_frusta( continue; } + let clip_from_view = Mat4::perspective_infinite_reverse_rh( + std::f32::consts::FRAC_PI_2, + 1.0, + point_light.shadow_map_near_z, + ); + // 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 @@ -639,7 +643,8 @@ pub fn update_spot_light_frusta( let view_backward = transform.back(); let spot_world_from_view = spot_light_world_from_view(transform); - let spot_clip_from_view = spot_light_clip_from_view(spot_light.outer_angle); + let spot_clip_from_view = + spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); *frustum = Frustum::from_clip_from_world_custom_far( diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index 5baa7f66f6ff1..b4db7ce4f45f7 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -22,29 +22,63 @@ 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. + + /// Simulates a light source coming from a spherical volume with the given + /// radius. + /// + /// This affects the size of specular highlights created by this light, as + /// well as the soft shadow penumbra size. 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. + /// + /// 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 [`PointLight::radius`] 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_shadows_enabled: bool, + /// 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 +92,10 @@ impl Default for PointLight { range: 20.0, radius: 0.0, shadows_enabled: false, + soft_shadows_enabled: false, 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 +103,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 621ee70f143b0..f2e595fbf8c49 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -7,23 +7,85 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default, Debug)] 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 of 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, + + /// Simulates a light source coming from a spherical volume with the given + /// radius. + /// + /// This affects the size of specular highlights created by this light, as + /// well as the soft shadow penumbra size. 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. + /// + /// 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. + /// + /// 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 [`SpotLight::radius`] 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_shadows_enabled: bool, + + /// 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 the 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 +96,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 +111,10 @@ impl Default for SpotLight { range: 20.0, radius: 0.0, shadows_enabled: false, + soft_shadows_enabled: false, 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 216fd93e78039..86fd2151ce00c 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -19,6 +19,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}; @@ -35,8 +36,10 @@ pub struct ExtractedPointLight { pub radius: f32, pub transform: GlobalTransform, pub shadows_enabled: bool, + pub soft_shadows_enabled: bool, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, + pub shadow_map_near_z: f32, pub spot_light_angles: Option<(f32, f32)>, } @@ -47,6 +50,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, @@ -79,6 +83,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, @@ -134,8 +139,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 @@ -143,27 +150,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), } } } @@ -252,11 +262,13 @@ pub fn extract_lights( radius: point_light.radius, transform: *transform, shadows_enabled: point_light.shadows_enabled, + soft_shadows_enabled: point_light.soft_shadows_enabled, 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(( @@ -301,11 +313,13 @@ pub fn extract_lights( radius: spot_light.radius, transform: *transform, shadows_enabled: spot_light.shadows_enabled, + soft_shadows_enabled: spot_light.soft_shadows_enabled, 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, @@ -342,6 +356,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 @@ -356,8 +371,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, @@ -502,9 +515,9 @@ pub(crate) fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { ) } -pub(crate) fn spot_light_clip_from_view(angle: f32) -> Mat4 { +pub(crate) fn spot_light_clip_from_view(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)] @@ -549,8 +562,6 @@ pub fn prepare_lights( }; // 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)) @@ -685,6 +696,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(); @@ -727,9 +744,17 @@ 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: if light.soft_shadows_enabled { + light.radius + } else { + 0.0 + }, 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, + pad_a: 0.0, + pad_b: 0.0, }); global_light_meta.entity_to_index.insert(entity, index); } @@ -771,6 +796,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, @@ -878,6 +904,12 @@ 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, + ); + for (face_index, (view_rotation, frustum)) in cube_face_rotations .iter() .zip(&point_light_frusta.unwrap().frusta) @@ -946,7 +978,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_clip_from_view(angle); + let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); let depth_texture_view = directional_light_depth_texture diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index a7bbb7515aa1a..007ed983e5fad 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1837,11 +1837,11 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 21, + 23, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 22, + 24, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index d0a506e9c3e42..9446b3c9a9180 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -213,11 +213,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"), @@ -227,11 +229,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)), - // ClusterableObjects + // 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, @@ -242,7 +246,7 @@ fn layout_entries( ), // ClusteredLightIndexLists ( - 7, + 9, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -255,7 +259,7 @@ fn layout_entries( ), // ClusterOffsetsAndCounts ( - 8, + 10, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -266,16 +270,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, @@ -284,10 +288,10 @@ fn layout_entries( .visibility(ShaderStages::VERTEX), ), // Screen space reflection settings - (13, uniform_buffer::(true)), + (15, uniform_buffer::(true)), // Screen space ambient occlusion texture ( - 14, + 16, texture_2d(TextureSampleType::Float { filterable: false }), ), ), @@ -296,10 +300,10 @@ fn layout_entries( // EnvironmentMapLight let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (15, environment_map_entries[0]), - (16, environment_map_entries[1]), - (17, environment_map_entries[2]), - (18, environment_map_entries[3]), + (17, environment_map_entries[0]), + (18, environment_map_entries[1]), + (19, environment_map_entries[2]), + (20, environment_map_entries[3]), )); // Irradiance volumes @@ -307,16 +311,16 @@ fn layout_entries( let irradiance_volume_entries = irradiance_volume::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (19, irradiance_volume_entries[0]), - (20, irradiance_volume_entries[1]), + (21, irradiance_volume_entries[0]), + (22, irradiance_volume_entries[1]), )); } // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (21, tonemapping_lut_entries[0]), - (22, tonemapping_lut_entries[1]), + (23, tonemapping_lut_entries[0]), + (24, tonemapping_lut_entries[1]), )); // Prepass @@ -326,7 +330,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([23, 24, 25, 26]) + .zip([25, 26, 27, 28]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -337,10 +341,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 27, + 29, texture_2d(TextureSampleType::Float { filterable: true }), ), - (28, sampler(SamplerBindingType::Filtering)), + (30, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -527,23 +531,25 @@ 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, clusterable_objects_binding.clone()), + (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, clusterable_objects_binding.clone()), ( - 7, + 9, cluster_bindings .clusterable_object_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, ssr_binding.clone()), - (14, ssao_view), + (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, ssr_binding.clone()), + (16, ssao_view), )); let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( @@ -560,10 +566,10 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (15, diffuse_texture_view), - (16, specular_texture_view), - (17, sampler), - (18, environment_map_binding.clone()), + (17, diffuse_texture_view), + (18, specular_texture_view), + (19, sampler), + (20, environment_map_binding.clone()), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -572,10 +578,10 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (15, diffuse_texture_views.as_slice()), - (16, specular_texture_views.as_slice()), - (17, sampler), - (18, environment_map_binding.clone()), + (17, diffuse_texture_views.as_slice()), + (18, specular_texture_views.as_slice()), + (19, sampler), + (20, environment_map_binding.clone()), )); } } @@ -596,21 +602,21 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((19, texture_view), (20, sampler))); + entries = entries.extend_with_indices(((21, texture_view), (22, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { entries = entries - .extend_with_indices(((19, texture_views.as_slice()), (20, sampler))); + .extend_with_indices(((21, texture_views.as_slice()), (22, sampler))); } None => {} } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((21, lut_bindings.0), (22, lut_bindings.1))); + entries = entries.extend_with_indices(((23, lut_bindings.0), (24, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -620,7 +626,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([23, 24, 25, 26]) + .zip([25, 26, 27, 28]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -636,7 +642,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((27, transmission_view), (28, transmission_sampler))); + entries.extend_with_indices(((29, transmission_view), (30, 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 5036e81673e98..dfda3a576b317 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -13,88 +13,91 @@ #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 clusterable_objects: types::ClusterableObjects; -@group(0) @binding(7) var clusterable_object_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; #else -@group(0) @binding(6) var clusterable_objects: types::ClusterableObjects; -@group(0) @binding(7) var clusterable_object_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_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 ssr_settings: types::ScreenSpaceReflectionsSettings; -@group(0) @binding(14) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(15) var ssr_settings: types::ScreenSpaceReflectionsSettings; +@group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d; #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(15) var diffuse_environment_maps: binding_array, 8u>; -@group(0) @binding(16) var specular_environment_maps: binding_array, 8u>; +@group(0) @binding(17) var diffuse_environment_maps: binding_array, 8u>; +@group(0) @binding(18) var specular_environment_maps: binding_array, 8u>; #else -@group(0) @binding(15) var diffuse_environment_map: texture_cube; -@group(0) @binding(16) var specular_environment_map: texture_cube; +@group(0) @binding(17) var diffuse_environment_map: texture_cube; +@group(0) @binding(18) var specular_environment_map: texture_cube; #endif -@group(0) @binding(17) var environment_map_sampler: sampler; -@group(0) @binding(18) var environment_map_uniform: types::EnvironmentMapUniform; +@group(0) @binding(19) var environment_map_sampler: sampler; +@group(0) @binding(20) var environment_map_uniform: types::EnvironmentMapUniform; #ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(19) var irradiance_volumes: binding_array, 8u>; +@group(0) @binding(21) var irradiance_volumes: binding_array, 8u>; #else -@group(0) @binding(19) var irradiance_volume: texture_3d; +@group(0) @binding(21) var irradiance_volume: texture_3d; #endif -@group(0) @binding(20) var irradiance_volume_sampler: sampler; +@group(0) @binding(22) var irradiance_volume_sampler: sampler; #endif -@group(0) @binding(21) var dt_lut_texture: texture_3d; -@group(0) @binding(22) var dt_lut_sampler: sampler; +// NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. +@group(0) @binding(23) var dt_lut_texture: texture_3d; +@group(0) @binding(24) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(23) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(25) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(24) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(26) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(25) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(27) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(23) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(25) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(24) var normal_prepass_texture: texture_2d; +@group(0) @binding(26) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(25) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(27) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(26) var deferred_prepass_texture: texture_2d; +@group(0) @binding(28) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(27) var view_transmission_texture: texture_2d; -@group(0) @binding(28) var view_transmission_sampler: sampler; +@group(0) @binding(29) var view_transmission_texture: texture_2d; +@group(0) @binding(30) 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 35e981cc8598d..a01b135093d9b 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -11,6 +11,10 @@ struct ClusterableObject { shadow_depth_bias: f32, shadow_normal_bias: f32, spot_light_tan_angle: f32, + soft_shadow_size: f32, + shadow_map_near_z: f32, + pad_a: f32, + pad_b: f32, }; const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; @@ -28,6 +32,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 268a50163f688..511d2236246ec 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -447,8 +447,14 @@ fn apply_pbr_lighting( var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u - && (view_bindings::clusterable_objects.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); + && (view_bindings::clusterable_objects.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, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); } let light_contrib = lighting::spot_light(light_id, &lighting_input); @@ -467,7 +473,12 @@ fn apply_pbr_lighting( var transmitted_shadow: f32 = 1.0; if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - transmitted_shadow = shadows::fetch_spot_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); } let transmitted_light_contrib = diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index ec155cf3fcb77..0c546a37c6d3e 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,40 @@ 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 WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#ifdef NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + 0.0, + ); +#else // NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + array_index, + 0.0, + ); +#endif // NO_ARRAY_TEXTURES_SUPPORT + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); + +#endif // WEBGL2 +} + // 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 +147,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 +157,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 +202,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 +264,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,10 +310,54 @@ 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_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 WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#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 - return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth); + 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); + +#endif // WEBGL2 } fn sample_shadow_cubemap_at_offset( @@ -198,6 +376,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 +447,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 +463,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 +512,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 +524,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 110d8c7fff828..0e539f00091c5 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::clusterable_objects.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 0000000000000..f99cb465163dc --- /dev/null +++ b/examples/3d/pcss.rs @@ -0,0 +1,417 @@ +//! Demonstrates percentage-closer soft shadows (PCSS). + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::{ + experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, + prepass::{DepthPrepass, MotionVectorPrepass}, + Skybox, + }, + math::vec3, + pbr::{CubemapVisibleEntities, ShadowFilteringMethod, VisibleMeshEntities}, + prelude::*, + render::{ + camera::TemporalJitter, + primitives::{CubemapFrusta, Frustum}, + }, +}; + +use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender}; + +#[path = "../helpers/widgets.rs"] +mod widgets; + +/// The size of the light, which affects the size of the penumbras. +const LIGHT_RADIUS: 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)] +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: bool, +} + +impl Default for AppStatus { + fn default() -> Self { + Self { + light_type: default(), + shadow_filter: default(), + soft_shadows: true, + } + } +} + +/// The type of light presently in the scene: directional, point, or spot. +#[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, +} + +/// Each example setting that can be toggled in the UI. +#[derive(Clone, Copy, PartialEq)] +enum AppSetting { + /// The type of light presently in the scene: directional, point, or spot. + LightType(LightType), + /// The type of shadow filter. + ShadowFilter(ShadowFilter), + /// Whether PCSS is enabled or disabled. + SoftShadows(bool), +} + +/// 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_systems(Startup, setup) + .add_systems(Update, widgets::handle_ui_interactions::) + .add_systems( + Update, + update_radio_buttons.after(widgets::handle_ui_interactions::), + ) + .add_systems( + Update, + ( + handle_light_type_change, + handle_shadow_filter_change, + handle_pcss_toggle, + ) + .after(widgets::handle_ui_interactions::), + ) + .run(); +} + +/// Creates all the objects in the scene. +fn setup(mut commands: Commands, asset_server: Res, app_status: Res) { + spawn_camera(&mut commands, &asset_server); + spawn_light(&mut commands, &app_status); + spawn_gltf_scene(&mut commands, &asset_server); + spawn_buttons(&mut commands); +} + +/// 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()) + // We want MSAA off for TAA to work properly. + .insert(Msaa::Off) + // 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, + rotation: Quat::IDENTITY, + }); +} + +/// 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(VisibleMeshEntities::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) { + commands + .spawn(NodeBundle { + style: widgets::main_ui_style(), + ..default() + }) + .with_children(|parent| { + widgets::spawn_option_buttons( + parent, + "Light Type", + &[ + (AppSetting::LightType(LightType::Directional), "Directional"), + (AppSetting::LightType(LightType::Point), "Point"), + (AppSetting::LightType(LightType::Spot), "Spot"), + ], + ); + widgets::spawn_option_buttons( + parent, + "Shadow Filter", + &[ + (AppSetting::ShadowFilter(ShadowFilter::Temporal), "Temporal"), + ( + AppSetting::ShadowFilter(ShadowFilter::NonTemporal), + "Non-Temporal", + ), + ], + ); + widgets::spawn_option_buttons( + parent, + "Soft Shadows", + &[ + (AppSetting::SoftShadows(true), "On"), + (AppSetting::SoftShadows(false), "Off"), + ], + ); + }); +} + +/// Updates the style of the radio buttons that enable and disable soft shadows +/// to reflect whether PCSS is enabled. +fn update_radio_buttons( + mut widgets: Query< + ( + Option<&mut BackgroundColor>, + Option<&mut Text>, + &WidgetClickSender, + ), + Or<(With, With)>, + >, + app_status: Res, +) { + for (image, text, sender) in widgets.iter_mut() { + let selected = match **sender { + AppSetting::LightType(light_type) => light_type == app_status.light_type, + AppSetting::ShadowFilter(shadow_filter) => shadow_filter == app_status.shadow_filter, + AppSetting::SoftShadows(soft_shadows) => soft_shadows == app_status.soft_shadows, + }; + + if let Some(mut bg_color) = image { + widgets::update_ui_radio_button(&mut bg_color, selected); + } + if let Some(mut text) = text { + widgets::update_ui_radio_button_text(&mut text, selected); + } + } +} + +/// Handles requests from the user to change the type of light. +fn handle_light_type_change( + mut commands: Commands, + mut lights: Query, With, With)>>, + mut events: EventReader>, + mut app_status: ResMut, +) { + for event in events.read() { + let AppSetting::LightType(light_type) = **event else { + continue; + }; + app_status.light_type = light_type; + + for light in lights.iter_mut() { + let light_commands = commands + .entity(light) + .remove::() + .remove::() + .remove::(); + match light_type { + LightType::Point => { + light_commands.insert(create_point_light(&app_status)); + } + LightType::Spot => { + light_commands.insert(create_spot_light(&app_status)); + } + LightType::Directional => { + light_commands.insert(create_directional_light(&app_status)); + } + } + } + } +} + +/// Handles requests from the user to change the shadow filter method. +/// +/// This system is also responsible for enabling and disabling TAA as +/// appropriate. +fn handle_shadow_filter_change( + mut commands: Commands, + mut cameras: Query<(Entity, &mut ShadowFilteringMethod)>, + mut events: EventReader>, + mut app_status: ResMut, +) { + for event in events.read() { + let AppSetting::ShadowFilter(shadow_filter) = **event else { + continue; + }; + app_status.shadow_filter = shadow_filter; + + for (camera, mut shadow_filtering_method) in cameras.iter_mut() { + match shadow_filter { + ShadowFilter::NonTemporal => { + *shadow_filtering_method = ShadowFilteringMethod::Gaussian; + commands.entity(camera).remove::(); + } + ShadowFilter::Temporal => { + *shadow_filtering_method = ShadowFilteringMethod::Temporal; + commands + .entity(camera) + .insert(TemporalAntiAliasing::default()); + } + } + } + } +} + +/// Handles requests from the user to toggle soft shadows on and off. +fn handle_pcss_toggle( + mut lights: Query>, + mut events: EventReader>, + mut app_status: ResMut, +) { + for event in events.read() { + let AppSetting::SoftShadows(value) = **event else { + continue; + }; + app_status.soft_shadows = value; + + // Recreating the lights is the simplest way to toggle soft shadows. + for (directional_light, point_light, spot_light) in lights.iter_mut() { + if let Some(mut directional_light) = directional_light { + *directional_light = create_directional_light(&app_status); + } + if let Some(mut point_light) = point_light { + *point_light = create_point_light(&app_status); + } + if let Some(mut spot_light) = spot_light { + *spot_light = create_spot_light(&app_status); + } + } + } +} + +/// Creates the [`DirectionalLight`] component with the appropriate settings. +fn create_directional_light(app_status: &AppStatus) -> DirectionalLight { + DirectionalLight { + shadows_enabled: true, + soft_shadow_size: if app_status.soft_shadows { + Some(LIGHT_RADIUS) + } else { + None + }, + shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS, + ..default() + } +} + +/// Creates the [`PointLight`] component with the appropriate settings. +fn create_point_light(app_status: &AppStatus) -> PointLight { + PointLight { + intensity: POINT_LIGHT_INTENSITY, + range: POINT_LIGHT_RANGE, + shadows_enabled: true, + radius: LIGHT_RADIUS, + soft_shadows_enabled: app_status.soft_shadows, + shadow_depth_bias: POINT_SHADOW_DEPTH_BIAS, + shadow_map_near_z: SHADOW_MAP_NEAR_Z, + ..default() + } +} + +/// Creates the [`SpotLight`] component with the appropriate settings. +fn create_spot_light(app_status: &AppStatus) -> SpotLight { + SpotLight { + intensity: POINT_LIGHT_INTENSITY, + range: POINT_LIGHT_RANGE, + radius: LIGHT_RADIUS, + shadows_enabled: true, + soft_shadows_enabled: app_status.soft_shadows, + shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS, + shadow_map_near_z: SHADOW_MAP_NEAR_Z, + ..default() + } +} diff --git a/examples/README.md b/examples/README.md index 92e03582aa205..cd8c7edcccd22 100644 --- a/examples/README.md +++ b/examples/README.md @@ -156,6 +156,7 @@ Example | Description [Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications) [Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping [Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations +[Percentage-closer soft shadows](../examples/3d/pcss.rs) | Demonstrates percentage-closer soft shadows (PCSS) [Physically Based Rendering](../examples/3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties [Reflection Probes](../examples/3d/reflection_probes.rs) | Demonstrates reflection probes [Render to Texture](../examples/3d/render_to_texture.rs) | Shows how to render to a texture, useful for mirrors, UI, or exporting images diff --git a/examples/helpers/widgets.rs b/examples/helpers/widgets.rs new file mode 100644 index 0000000000000..1c1a2aae9e3ec --- /dev/null +++ b/examples/helpers/widgets.rs @@ -0,0 +1,177 @@ +//! Simple widgets for example UI. + +use bevy::{ecs::system::EntityCommands, prelude::*}; + +/// An event that's sent whenever the user changes one of the settings by +/// clicking a radio button. +#[derive(Clone, Event, Deref, DerefMut)] +pub struct WidgetClickEvent(T); + +/// A marker component that we place on all widgets that send +/// [`WidgetClickEvent`]s of the given type. +#[derive(Clone, Component, Deref, DerefMut)] +pub struct WidgetClickSender(T) +where + T: Clone + Send + Sync + 'static; + +/// A marker component that we place on all radio `Button`s. +#[derive(Clone, Copy, Component)] +pub struct RadioButton; + +/// A marker component that we place on all `Text` inside radio buttons. +#[derive(Clone, Copy, Component)] +pub struct RadioButtonText; + +/// Returns a [`Style`] appropriate for the outer main UI node. +/// +/// This UI is in the bottom left corner and has flex column support +pub fn main_ui_style() -> 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() + } +} + +/// Spawns a single radio button that allows configuration of a setting. +/// +/// The type parameter specifies the value that will be packaged up and sent in +/// a [`WidgetClickEvent`] when the radio button is clicked. +pub fn spawn_option_button( + parent: &mut ChildBuilder, + option_value: T, + option_name: &str, + is_selected: bool, + is_first: bool, + is_last: bool, +) 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) }), + background_color: BackgroundColor(bg_color), + ..default() + }) + .insert(RadioButton) + .insert(WidgetClickSender(option_value.clone())) + .with_children(|parent| { + spawn_ui_text(parent, option_name, fg_color) + .insert(RadioButtonText) + .insert(WidgetClickSender(option_value)); + }); +} + +/// Spawns the buttons that allow configuration of a setting. +/// +/// The user may change the setting to any one of the labeled `options`. The +/// value of the given type parameter will be packaged up and sent as a +/// [`WidgetClickEvent`] when one of the radio buttons is clicked. +pub fn spawn_option_buttons(parent: &mut ChildBuilder, title: &str, options: &[(T, &str)]) +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, Color::BLACK).insert(Style { + width: Val::Px(125.0), + ..default() + }); + + for (option_index, (option_value, option_name)) in options.iter().cloned().enumerate() { + spawn_option_button( + parent, + option_value, + option_name, + option_index == 0, + option_index == 0, + option_index == options.len() - 1, + ); + } + }); +} + +/// Spawns text for the UI. +/// +/// Returns the `EntityCommands`, which allow further customization of the text +/// style. +pub fn spawn_ui_text<'a>( + parent: &'a mut ChildBuilder, + label: &str, + color: Color, +) -> EntityCommands<'a> { + parent.spawn(TextBundle::from_section( + label, + TextStyle { + font_size: 18.0, + color, + ..default() + }, + )) +} + +/// Checks for clicks on the radio buttons and sends `RadioButtonChangeEvent`s +/// as necessary. +pub fn handle_ui_interactions( + mut interactions: Query< + (&Interaction, &WidgetClickSender), + (With