bevy/crates/bevy_pbr/src/light_probe/environment_map.wgsl
Sou1gh0st 9da18cce2a
Add support for environment map transformation (#14290)
# Objective

- Fixes: https://github.com/bevyengine/bevy/issues/14036

## Solution

- Add a world space transformation for the environment sample direction.

## Testing

- I have tested the newly added `transform` field using the newly added
`rotate_environment_map` example.


https://github.com/user-attachments/assets/2de77c65-14bc-48ee-b76a-fb4e9782dbdb


## Migration Guide

- Since we have added a new filed to the `EnvironmentMapLight` struct,
users will need to include `..default()` or some rotation value in their
initialization code.
2024-07-19 15:00:50 +00:00

233 lines
8.9 KiB
WebGPU Shading Language

#define_import_path bevy_pbr::environment_map
#import bevy_pbr::light_probe::query_light_probe
#import bevy_pbr::mesh_view_bindings as bindings
#import bevy_pbr::mesh_view_bindings::light_probes
#import bevy_pbr::mesh_view_bindings::environment_map_uniform
#import bevy_pbr::lighting::{
F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT
}
struct EnvironmentMapLight {
diffuse: vec3<f32>,
specular: vec3<f32>,
};
struct EnvironmentMapRadiances {
irradiance: vec3<f32>,
radiance: vec3<f32>,
}
// Define two versions of this function, one for the case in which there are
// multiple light probes and one for the case in which only the view light probe
// is present.
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
input: ptr<function, LightingInput>,
layer: u32,
world_position: vec3<f32>,
found_diffuse_indirect: bool,
) -> EnvironmentMapRadiances {
// Unpack.
let perceptual_roughness = (*input).layers[layer].perceptual_roughness;
let N = (*input).layers[layer].N;
let R = (*input).layers[layer].R;
var radiances: EnvironmentMapRadiances;
// Search for a reflection probe that contains the fragment.
var query_result = query_light_probe(world_position, /*is_irradiance_volume=*/ false);
// If we didn't find a reflection probe, use the view environment map if applicable.
if (query_result.texture_index < 0) {
query_result.texture_index = light_probes.view_cubemap_index;
query_result.intensity = light_probes.intensity_for_view;
}
// If there's no cubemap, bail out.
if (query_result.texture_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
let radiance_level = perceptual_roughness * f32(textureNumLevels(
bindings::specular_environment_maps[query_result.texture_index]) - 1u);
if (!found_diffuse_indirect) {
var irradiance_sample_dir = N;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the diffuse environment cubemap itself.
irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
irradiance_sample_dir.z = -irradiance_sample_dir.z;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
irradiance_sample_dir,
0.0).rgb * query_result.intensity;
}
var radiance_sample_dir = R;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the specular environment cubemap itself.
radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
radiance_sample_dir.z = -radiance_sample_dir.z;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
radiance_sample_dir,
radiance_level).rgb * query_result.intensity;
return radiances;
}
#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
input: ptr<function, LightingInput>,
layer: u32,
world_position: vec3<f32>,
found_diffuse_indirect: bool,
) -> EnvironmentMapRadiances {
// Unpack.
let perceptual_roughness = (*input).layers[layer].perceptual_roughness;
let N = (*input).layers[layer].N;
let R = (*input).layers[layer].R;
var radiances: EnvironmentMapRadiances;
if (light_probes.view_cubemap_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
// Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform
// because textureNumLevels() does not work on WebGL2
let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view);
let intensity = light_probes.intensity_for_view;
if (!found_diffuse_indirect) {
var irradiance_sample_dir = N;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the diffuse environment cubemap itself.
irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
irradiance_sample_dir.z = -irradiance_sample_dir.z;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_map,
bindings::environment_map_sampler,
irradiance_sample_dir,
0.0).rgb * intensity;
}
var radiance_sample_dir = R;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the specular environment cubemap itself.
radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
radiance_sample_dir.z = -radiance_sample_dir.z;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_map,
bindings::environment_map_sampler,
radiance_sample_dir,
radiance_level).rgb * intensity;
return radiances;
}
#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Adds the environment map light from the clearcoat layer to that of the base
// layer.
fn environment_map_light_clearcoat(
out: ptr<function, EnvironmentMapLight>,
input: ptr<function, LightingInput>,
found_diffuse_indirect: bool,
) {
// Unpack.
let world_position = (*input).P;
let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV;
let clearcoat_strength = (*input).clearcoat_strength;
// Calculate the Fresnel term `Fc` for the clearcoat layer.
// 0.04 is a hardcoded value for F0 from the Filament spec.
let clearcoat_F0 = vec3<f32>(0.04);
let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength;
let inv_Fc = 1.0 - Fc;
let clearcoat_radiances = compute_radiances(
input, LAYER_CLEARCOAT, world_position, found_diffuse_indirect);
// Composite the clearcoat layer on top of the existing one.
// These formulas are from Filament:
// <https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/clearcoat>
(*out).diffuse *= inv_Fc;
(*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc;
}
#endif // STANDARD_MATERIAL_CLEARCOAT
fn environment_map_light(
input: ptr<function, LightingInput>,
found_diffuse_indirect: bool,
) -> EnvironmentMapLight {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let diffuse_color = (*input).diffuse_color;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let F_ab = (*input).F_ab;
let F0 = (*input).F0_;
let world_position = (*input).P;
var out: EnvironmentMapLight;
let radiances = compute_radiances(input, LAYER_BASE, world_position, found_diffuse_indirect);
if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) {
out.diffuse = vec3(0.0);
out.specular = vec3(0.0);
return out;
}
// No real world material has specular values under 0.02, so we use this range as a
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
// See: https://google.github.io/filament/Filament.html#specularocclusion
let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33)));
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
// Useful reference: https://bruop.github.io/ibl
let Fr = max(vec3(1.0 - roughness), F0) - F0;
let kS = F0 + Fr * pow(1.0 - NdotV, 5.0);
let Ess = F_ab.x + F_ab.y;
let FssEss = kS * Ess * specular_occlusion;
let Ems = 1.0 - Ess;
let Favg = F0 + (1.0 - F0) / 21.0;
let Fms = FssEss * Favg / (1.0 - Ems * Favg);
let FmsEms = Fms * Ems;
let Edss = 1.0 - (FssEss + FmsEms);
let kD = diffuse_color * Edss;
if (!found_diffuse_indirect) {
out.diffuse = (FmsEms + kD) * radiances.irradiance;
} else {
out.diffuse = vec3(0.0);
}
out.specular = FssEss * radiances.radiance;
#ifdef STANDARD_MATERIAL_CLEARCOAT
environment_map_light_clearcoat(&out, input, found_diffuse_indirect);
#endif // STANDARD_MATERIAL_CLEARCOAT
return out;
}