mirror of
https://github.com/bevyengine/bevy
synced 2025-01-06 18:28:59 +00:00
44928e0df4
# 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`
128 lines
6.1 KiB
WebGPU Shading Language
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
|
|
}
|