bevy/crates/bevy_pbr/src/render/shadow_sampling.wgsl
Marco Buono 44928e0df4
StandardMaterial Light Transmission (#8015)
# Objective

<img width="1920" alt="Screenshot 2023-04-26 at 01 07 34"
src="https://user-images.githubusercontent.com/418473/234467578-0f34187b-5863-4ea1-88e9-7a6bb8ce8da3.png">

This PR adds both diffuse and specular light transmission capabilities
to the `StandardMaterial`, with support for screen space refractions.
This enables realistically representing a wide range of real-world
materials, such as:

  - Glass; (Including frosted glass)
  - Transparent and translucent plastics;
  - Various liquids and gels;
  - Gemstones;
  - Marble;
  - Wax;
  - Paper;
  - Leaves;
  - Porcelain.

Unlike existing support for transparency, light transmission does not
rely on fixed function alpha blending, and therefore works with both
`AlphaMode::Opaque` and `AlphaMode::Mask` materials.

## Solution

- Introduces a number of transmission related fields in the
`StandardMaterial`;
- For specular transmission:
- Adds logic to take a view main texture snapshot after the opaque
phase; (in order to perform screen space refractions)
- Introduces a new `Transmissive3d` phase to the renderer, to which all
meshes with `transmission > 0.0` materials are sent.
- Calculates a light exit point (of the approximate mesh volume) using
`ior` and `thickness` properties
- Samples the snapshot texture with an adaptive number of taps across a
`roughness`-controlled radius enabling “blurry” refractions
- For diffuse transmission:
- Approximates transmitted diffuse light by using a second, flipped +
displaced, diffuse-only Lambertian lobe for each light source.

## To Do

- [x] Figure out where `fresnel_mix()` is taking place, if at all, and
where `dielectric_specular` is being calculated, if at all, and update
them to use the `ior` value (Not a blocker, just a nice-to-have for more
correct BSDF)
- To the _best of my knowledge, this is now taking place, after
964340cdd. The fresnel mix is actually "split" into two parts in our
implementation, one `(1 - fresnel(...))` in the transmission, and
`fresnel()` in the light implementations. A surface with more
reflectance now will produce slightly dimmer transmission towards the
grazing angle, as more of the light gets reflected.
- [x] Add `transmission_texture`
- [x] Add `diffuse_transmission_texture`
- [x] Add `thickness_texture`
- [x] Add `attenuation_distance` and `attenuation_color`
- [x] Connect values to glTF loader
  - [x] `transmission` and `transmission_texture`
  - [x] `thickness` and `thickness_texture`
  - [x] `ior`
- [ ] `diffuse_transmission` and `diffuse_transmission_texture` (needs
upstream support in `gltf` crate, not a blocker)
- [x] Add support for multiple screen space refraction “steps”
- [x] Conditionally create no transmission snapshot texture at all if
`steps == 0`
- [x] Conditionally enable/disable screen space refraction transmission
snapshots
- [x] Read from depth pre-pass to prevent refracting pixels in front of
the light exit point
- [x] Use `interleaved_gradient_noise()` function for sampling blur in a
way that benefits from TAA
- [x] Drill down a TAA `#define`, tweak some aspects of the effect
conditionally based on it
- [x] Remove const array that's crashing under HLSL (unless a new `naga`
release with https://github.com/gfx-rs/naga/pull/2496 comes out before
we merge this)
- [ ] Look into alternatives to the `switch` hack for dynamically
indexing the const array (might not be needed, compilers seem to be
decent at expanding it)
- [ ] Add pipeline keys for gating transmission (do we really want/need
this?)
- [x] Tweak some material field/function names?

## A Note on Texture Packing

_This was originally added as a comment to the
`specular_transmission_texture`, `thickness_texture` and
`diffuse_transmission_texture` documentation, I removed it since it was
more confusing than helpful, and will likely be made redundant/will need
to be updated once we have a better infrastructure for preprocessing
assets_

Due to how channels are mapped, you can more efficiently use a single
shared texture image
for configuring the following:

- R - `specular_transmission_texture`
- G - `thickness_texture`
- B - _unused_
- A - `diffuse_transmission_texture`

The `KHR_materials_diffuse_transmission` glTF extension also defines a
`diffuseTransmissionColorTexture`,
that _we don't currently support_. One might choose to pack the
intensity and color textures together,
using RGB for the color and A for the intensity, in which case this
packing advice doesn't really apply.

---

## Changelog

- Added a new `Transmissive3d` render phase for rendering specular
transmissive materials with screen space refractions
- Added rendering support for transmitted environment map light on the
`StandardMaterial` as a fallback for screen space refractions
- Added `diffuse_transmission`, `specular_transmission`, `thickness`,
`ior`, `attenuation_distance` and `attenuation_color` to the
`StandardMaterial`
- Added `diffuse_transmission_texture`, `specular_transmission_texture`,
`thickness_texture` to the `StandardMaterial`, gated behind a new
`pbr_transmission_textures` cargo feature (off by default, for maximum
hardware compatibility)
- Added `Camera3d::screen_space_specular_transmission_steps` for
controlling the number of “layers of transparency” rendered for
transmissive objects
- Added a `TransmittedShadowReceiver` component for enabling shadows in
(diffusely) transmitted light. (disabled by default, as it requires
carefully setting up the `thickness` to avoid self-shadow artifacts)
- Added support for the `KHR_materials_transmission`,
`KHR_materials_ior` and `KHR_materials_volume` glTF extensions
- Renamed items related to temporal jitter for greater consistency

## Migration Guide

- `SsaoPipelineKey::temporal_noise` has been renamed to
`SsaoPipelineKey::temporal_jitter`
- The `TAA` shader def (controlled by the presence of the
`TemporalAntiAliasSettings` component in the camera) has been replaced
with the `TEMPORAL_JITTER` shader def (controlled by the presence of the
`TemporalJitter` component in the camera)
- `MeshPipelineKey::TAA` has been replaced by
`MeshPipelineKey::TEMPORAL_JITTER`
- The `TEMPORAL_NOISE` shader def has been consolidated with
`TEMPORAL_JITTER`
2023-10-31 20:59:02 +00:00

128 lines
6.1 KiB
WebGPU Shading Language

#define_import_path bevy_pbr::shadow_sampling
#import bevy_pbr::{
mesh_view_bindings as view_bindings,
utils::{PI, interleaved_gradient_noise},
utils,
}
// Do the lookup, using HW 2x2 PCF and comparison
fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i32) -> f32 {
#ifdef NO_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
light_local,
depth,
);
#else
return textureSampleCompareLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
light_local,
array_index,
depth,
);
#endif
}
// https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1
fn sample_shadow_map_castano_thirteen(light_local: vec2<f32>, depth: f32, array_index: i32) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let inv_shadow_map_size = 1.0 / shadow_map_size;
let uv = light_local * shadow_map_size;
var base_uv = floor(uv + 0.5);
let s = (uv.x + 0.5 - base_uv.x);
let t = (uv.y + 0.5 - base_uv.y);
base_uv -= 0.5;
base_uv *= inv_shadow_map_size;
let uw0 = (4.0 - 3.0 * s);
let uw1 = 7.0;
let uw2 = (1.0 + 3.0 * s);
let u0 = (3.0 - 2.0 * s) / uw0 - 2.0;
let u1 = (3.0 + s) / uw1;
let u2 = s / uw2 + 2.0;
let vw0 = (4.0 - 3.0 * t);
let vw1 = 7.0;
let vw2 = (1.0 + 3.0 * t);
let v0 = (3.0 - 2.0 * t) / vw0 - 2.0;
let v1 = (3.0 + t) / vw1;
let v2 = t / vw2 + 2.0;
var sum = 0.0;
sum += uw0 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u0, v0) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u1, v0) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u2, v0) * inv_shadow_map_size), depth, array_index);
sum += uw0 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u0, v1) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u1, v1) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u2, v1) * inv_shadow_map_size), depth, array_index);
sum += uw0 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u0, v2) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u1, v2) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u2, v2) * inv_shadow_map_size), depth, array_index);
return sum * (1.0 / 144.0);
}
fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
fn sample_shadow_map_jimenez_fourteen(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let random_angle = 2.0 * PI * interleaved_gradient_noise(light_local * shadow_map_size, view_bindings::globals.frame_count);
let m = vec2(sin(random_angle), cos(random_angle));
let rotation_matrix = mat2x2(
m.y, -m.x,
m.x, m.y
);
// Empirically chosen fudge factor to make PCF look better across different CSM cascades
let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size);
let uv_offset_scale = f / (texel_size * shadow_map_size);
// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135)
let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale;
let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale;
let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale;
let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale;
let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale;
let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale;
let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale;
let sample_offset8 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale;
var sum = 0.0;
sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset2, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset3, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset4, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset5, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset6, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset7, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset8, depth, array_index);
return sum / 8.0;
}
fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
#ifdef SHADOW_FILTER_METHOD_CASTANO_13
return sample_shadow_map_castano_thirteen(light_local, depth, array_index);
#else ifdef SHADOW_FILTER_METHOD_JIMENEZ_14
return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size);
#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2
return sample_shadow_map_hardware(light_local, depth, array_index);
#else
// This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined.
// (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows)
// This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined.
// Set to 0 to make it obvious that something is wrong.
return 0.0;
#endif
}