bevy/crates/bevy_pbr/src/render/pbr_lighting.wgsl
Patrick Walton bf3692a011
Introduce support for mixed lighting by allowing lights to opt out of contributing diffuse light to lightmapped objects. (#16761)
This PR adds support for *mixed lighting* to Bevy, whereby some parts of
the scene are lightmapped, while others take part in real-time lighting.
(Here *real-time lighting* means lighting at runtime via the PBR shader,
as opposed to precomputed light using lightmaps.) It does so by adding a
new field, `affects_lightmapped_meshes` to `IrradianceVolume` and
`AmbientLight`, and a corresponding field
`affects_lightmapped_mesh_diffuse` to `DirectionalLight`, `PointLight`,
`SpotLight`, and `EnvironmentMapLight`. By default, this value is set to
true; when set to false, the light contributes nothing to the diffuse
irradiance component to meshes with lightmaps.

Note that specular light is unaffected. This is because the correct way
to bake specular lighting is *directional lightmaps*, which we have no
support for yet.

There are two general ways I expect this field to be used:

1. When diffuse indirect light is baked into lightmaps, irradiance
volumes and reflection probes shouldn't contribute any diffuse light to
the static geometry that has a lightmap. That's because the baking tool
should have already accounted for it, and in a higher-quality fashion,
as lightmaps typically offer a higher effective texture resolution than
the light probe does.

2. When direct diffuse light is baked into a lightmap, punctual lights
shouldn't contribute any diffuse light to static geometry with a
lightmap, to avoid double-counting. It may seem odd to bake *direct*
light into a lightmap, as opposed to indirect light. But there is a use
case: in a scene with many lights, avoiding light leaks requires shadow
mapping, which quickly becomes prohibitive when many lights are
involved. Baking lightmaps allows light leaks to be eliminated on static
geometry.

A new example, `mixed_lighting`, has been added. It demonstrates a sofa
(model from the [glTF Sample Assets]) that has been lightmapped offline
using [Bakery]. It has four modes:

1. In *baked* mode, all objects are locked in place, and all the diffuse
direct and indirect light has been calculated ahead of time. Note that
the bottom of the sphere has a red tint from the sofa, illustrating that
the baking tool captured indirect light for it.

2. In *mixed direct* mode, lightmaps capturing diffuse direct and
indirect light have been pre-calculated for the static objects, but the
dynamic sphere has real-time lighting. Note that, because the diffuse
lighting has been entirely pre-calculated for the scenery, the dynamic
sphere casts no shadow. In a real app, you would typically use real-time
lighting for the most important light so that dynamic objects can shadow
the scenery and relegate baked lighting to the less important lights for
which shadows aren't as important. Also note that there is no red tint
on the sphere, because there is no global illumination applied to it. In
an actual game, you could fix this problem by supplementing the
lightmapped objects with an irradiance volume.

3. In *mixed indirect* mode, all direct light is calculated in
real-time, and the static objects have pre-calculated indirect lighting.
This corresponds to the mode that most applications are expected to use.
Because direct light on the scenery is computed dynamically, shadows are
fully supported. As in mixed direct mode, there is no global
illumination on the sphere; in a real application, irradiance volumes
could be used to supplement the lightmaps.

4. In *real-time* mode, no lightmaps are used at all, and all punctual
lights are rendered in real-time. No global illumination exists.

In the example, you can click around to move the sphere, unless you're
in baked mode, in which case the sphere must be locked in place to be
lit correctly.

## Showcase

Baked mode:
![Screenshot 2024-12-13
112926](https://github.com/user-attachments/assets/cc00d84e-abd7-4117-97e9-17267d815c6a)

Mixed direct mode:
![Screenshot 2024-12-13
112933](https://github.com/user-attachments/assets/49997305-349a-4f6a-b451-8cccbb469889)

Mixed indirect mode (default):
![Screenshot 2024-12-13
112939](https://github.com/user-attachments/assets/0f4f6d8a-998f-474b-9fa5-fe4c212c921c)

Real-time mode:
![Screenshot 2024-12-13
112944](https://github.com/user-attachments/assets/fdbc4535-d902-4ba0-bfbc-f5c7b723fac8)

## Migration guide

* The `AmbientLight` resource, the `IrradianceVolume` component, and the
`EnvironmentMapLight` component now have `affects_lightmapped_meshes`
fields. If you don't need to use that field (for example, if you aren't
using lightmaps), you can safely set the field to true.
* `DirectionalLight`, `PointLight`, and `SpotLight` now have
`affects_lightmapped_mesh_diffuse` fields. If you don't need to use that
field (for example, if you aren't using lightmaps), you can safely set
the field to true.

[glTF Sample Assets]:
https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main

[Bakery]:
https://geom.io/bakery/wiki/index.php?title=Bakery_-_GPU_Lightmapper
2024-12-16 23:48:33 +00:00

630 lines
23 KiB
WebGPU Shading Language
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define_import_path bevy_pbr::lighting
#import bevy_pbr::{
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
mesh_view_bindings as view_bindings,
}
#import bevy_render::maths::PI
const LAYER_BASE: u32 = 0;
const LAYER_CLEARCOAT: u32 = 1;
// From the Filament design doc
// https://google.github.io/filament/Filament.html#table_symbols
// Symbol Definition
// v View unit vector
// l Incident light unit vector
// n Surface normal unit vector
// h Half unit vector between l and v
// f BRDF
// f_d Diffuse component of a BRDF
// f_r Specular component of a BRDF
// α Roughness, remapped from using input perceptualRoughness
// σ Diffuse reflectance
// Ω Spherical domain
// f0 Reflectance at normal incidence
// f90 Reflectance at grazing angle
// χ+(a) Heaviside function (1 if a>0 and 0 otherwise)
// nior Index of refraction (IOR) of an interface
// ⟨n⋅l⟩ Dot product clamped to [0..1]
// ⟨a⟩ Saturated value (clamped to [0..1])
// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material
// and consists of two components, the diffuse component (f_d) and the specular component (f_r):
// f(v,l) = f_d(v,l) + f_r(v,l)
//
// The form of the microfacet model is the same for diffuse and specular
// f_r(v,l) = f_d(v,l) = 1 / { |n⋅v||n⋅l| } ∫_Ω D(m,α) G(v,l,m) f_m(v,l,m) (v⋅m) (l⋅m) dm
//
// In which:
// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets
// G models the visibility (or occlusion or shadow-masking) of the microfacets
// f_m is the microfacet BRDF and differs between specular and diffuse components
//
// The above integration needs to be approximated.
// Input to a lighting function for a single layer (either the base layer or the
// clearcoat layer).
struct LayerLightingInput {
// The normal vector.
N: vec3<f32>,
// The reflected vector.
R: vec3<f32>,
// The normal vector ⋅ the view vector.
NdotV: f32,
// The perceptual roughness of the layer.
perceptual_roughness: f32,
// The roughness of the layer.
roughness: f32,
}
// Input to a lighting function (`point_light`, `spot_light`,
// `directional_light`).
struct LightingInput {
#ifdef STANDARD_MATERIAL_CLEARCOAT
layers: array<LayerLightingInput, 2>,
#else // STANDARD_MATERIAL_CLEARCOAT
layers: array<LayerLightingInput, 1>,
#endif // STANDARD_MATERIAL_CLEARCOAT
// The world-space position.
P: vec3<f32>,
// The vector to the view.
V: vec3<f32>,
// The diffuse color of the material.
diffuse_color: vec3<f32>,
// Specular reflectance at the normal incidence angle.
//
// This should be read F₀, but due to Naga limitations we can't name it that.
F0_: vec3<f32>,
// Constants for the BRDF approximation.
//
// See `EnvBRDFApprox` in
// <https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile>.
// What we call `F_ab` they call `AB`.
F_ab: vec2<f32>,
#ifdef STANDARD_MATERIAL_CLEARCOAT
// The strength of the clearcoat layer.
clearcoat_strength: f32,
#endif // STANDARD_MATERIAL_CLEARCOAT
#ifdef STANDARD_MATERIAL_ANISOTROPY
// The anisotropy strength, reflecting the amount of increased roughness in
// the tangent direction.
anisotropy: f32,
// The tangent direction for anisotropy: i.e. the direction in which
// roughness increases.
Ta: vec3<f32>,
// The bitangent direction, which is the cross product of the normal with
// the tangent direction.
Ba: vec3<f32>,
#endif // STANDARD_MATERIAL_ANISOTROPY
}
// Values derived from the `LightingInput` for both diffuse and specular lights.
struct DerivedLightingInput {
// The half-vector between L, the incident light vector, and V, the view
// vector.
H: vec3<f32>,
// The normal vector ⋅ the incident light vector.
NdotL: f32,
// The normal vector ⋅ the half-vector.
NdotH: f32,
// The incident light vector ⋅ the half-vector.
LdotH: f32,
}
// distanceAttenuation is simply the square falloff of light intensity
// combined with a smooth attenuation at the edge of the light radius
//
// light radius is a non-physical construct for efficiency purposes,
// because otherwise every light affects every fragment in the scene
fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 {
let factor = distanceSquare * inverseRangeSquared;
let smoothFactor = saturate(1.0 - factor * factor);
let attenuation = smoothFactor * smoothFactor;
return attenuation * 1.0 / max(distanceSquare, 0.0001);
}
// Normal distribution function (specular D)
// Based on https://google.github.io/filament/Filament.html#citation-walter07
// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α21) + 1)^2 }
// Simple implementation, has precision problems when using fp16 instead of fp32
// see https://google.github.io/filament/Filament.html#listing_speculardfp16
fn D_GGX(roughness: f32, NdotH: f32, h: vec3<f32>) -> f32 {
let oneMinusNdotHSquared = 1.0 - NdotH * NdotH;
let a = NdotH * roughness;
let k = roughness / (oneMinusNdotHSquared + a * a);
let d = k * k * (1.0 / PI);
return d;
}
// An approximation of the anisotropic GGX distribution function.
//
// 1
// D(𝐡) = ───────────────────────────────────────────────────
// παα_b((𝐡𝐭)² / αₜ²) + (𝐡𝐛)² / α_b² + (𝐡𝐧)²)²
//
// * `T` = 𝐭 = the tangent direction = the direction of increased roughness.
//
// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness.
//
// * `at` = αₜ = the alpha-roughness in the tangent direction.
//
// * `ab` = α_b = the alpha-roughness in the bitangent direction.
//
// This is from the `KHR_materials_anisotropy` spec:
// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#individual-lights>
fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 {
let a2 = at * ab;
let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH);
let w2 = a2 / dot(f, f);
let d = a2 * w2 * w2 * (1.0 / PI);
return d;
}
// Visibility function (Specular G)
// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) }
// such that f_r becomes
// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0)
// where
// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1α2) + α2) + n⋅v sqrt((n⋅l)^2 (1α2) + α2) }
// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv
fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 {
let a2 = roughness * roughness;
let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2);
let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2);
let v = 0.5 / (lambdaV + lambdaL);
return v;
}
// The visibility function, anisotropic variant.
fn V_GGX_anisotropic(
at: f32,
ab: f32,
NdotL: f32,
NdotV: f32,
BdotV: f32,
TdotV: f32,
TdotL: f32,
BdotL: f32,
) -> f32 {
let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV));
let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL));
let v = 0.5 / (GGX_V + GGX_L);
return saturate(v);
}
// A simpler, but nonphysical, alternative to Smith-GGX. We use this for
// clearcoat, per the Filament spec.
//
// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1
fn V_Kelemen(LdotH: f32) -> f32 {
return 0.25 / (LdotH * LdotH);
}
// Fresnel function
// see https://google.github.io/filament/Filament.html#citation-schlick94
// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 f_0) (1 v⋅h)^5
fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VdotH: f32) -> vec3<f32> {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
}
fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 {
// not using mix to keep the vec3 and float versions identical
return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
}
fn fresnel(f0: vec3<f32>, LdotH: f32) -> vec3<f32> {
// f_90 suitable for ambient occlusion
// see https://google.github.io/filament/Filament.html#lighting/occlusion
let f90 = saturate(dot(f0, vec3<f32>(50.0 * 0.33)));
return F_Schlick_vec(f0, f90, LdotH);
}
// Given distribution, visibility, and Fresnel term, calculates the final
// specular light.
//
// Multiscattering approximation:
// <https://google.github.io/filament/Filament.html#listing_energycompensationimpl>
fn specular_multiscatter(
input: ptr<function, LightingInput>,
D: f32,
V: f32,
F: vec3<f32>,
specular_intensity: f32,
) -> vec3<f32> {
// Unpack.
let F0 = (*input).F0_;
let F_ab = (*input).F_ab;
var Fr = (specular_intensity * D * V) * F;
Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0);
return Fr;
}
// Specular BRDF
// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf
// N, V, and L must all be normalized.
fn derive_lighting_input(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>) -> DerivedLightingInput {
var input: DerivedLightingInput;
var H: vec3<f32> = normalize(L + V);
input.H = H;
input.NdotL = saturate(dot(N, L));
input.NdotH = saturate(dot(N, H));
input.LdotH = saturate(dot(L, H));
return input;
}
// Returns L in the `xyz` components and the specular intensity in the `w` component.
fn compute_specular_layer_values_for_point_light(
input: ptr<function, LightingInput>,
layer: u32,
V: vec3<f32>,
light_to_frag: vec3<f32>,
light_position_radius: f32,
) -> vec4<f32> {
// Unpack.
let R = (*input).layers[layer].R;
let a = (*input).layers[layer].roughness;
// Representative Point Area Lights.
// see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
let centerToRay = dot(light_to_frag, R) * R - light_to_frag;
let closestPoint = light_to_frag + centerToRay * saturate(
light_position_radius * inverseSqrt(dot(centerToRay, centerToRay)));
let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
let normalizationFactor = a / saturate(a + (light_position_radius * 0.5 * LspecLengthInverse));
let intensity = normalizationFactor * normalizationFactor;
let L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
return vec4(L, intensity);
}
// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) }
fn specular(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
specular_intensity: f32,
) -> vec3<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let F0 = (*input).F0_;
let H = (*derived_input).H;
let NdotL = (*derived_input).NdotL;
let NdotH = (*derived_input).NdotH;
let LdotH = (*derived_input).LdotH;
// Calculate distribution.
let D = D_GGX(roughness, NdotH, H);
// Calculate visibility.
let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL);
// Calculate the Fresnel term.
let F = fresnel(F0, LdotH);
// Calculate the specular light.
let Fr = specular_multiscatter(input, D, V, F, specular_intensity);
return Fr;
}
// Calculates the specular light for the clearcoat layer. Returns Fc, the
// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in
// the second channel.
//
// <https://google.github.io/filament/Filament.html#listing_clearcoatbrdf>
fn specular_clearcoat(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
clearcoat_strength: f32,
specular_intensity: f32,
) -> vec2<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_CLEARCOAT].roughness;
let H = (*derived_input).H;
let NdotH = (*derived_input).NdotH;
let LdotH = (*derived_input).LdotH;
// Calculate distribution.
let Dc = D_GGX(roughness, NdotH, H);
// Calculate visibility.
let Vc = V_Kelemen(LdotH);
// Calculate the Fresnel term.
let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength;
// Calculate the specular light.
let Frc = (specular_intensity * Dc * Vc) * Fc;
return vec2(Fc, Frc);
}
#ifdef STANDARD_MATERIAL_ANISOTROPY
fn specular_anisotropy(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
L: vec3<f32>,
specular_intensity: f32,
) -> vec3<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let V = (*input).V;
let F0 = (*input).F0_;
let anisotropy = (*input).anisotropy;
let Ta = (*input).Ta;
let Ba = (*input).Ba;
let H = (*derived_input).H;
let NdotL = (*derived_input).NdotL;
let NdotH = (*derived_input).NdotH;
let LdotH = (*derived_input).LdotH;
let TdotL = dot(Ta, L);
let BdotL = dot(Ba, L);
let TdotH = dot(Ta, H);
let BdotH = dot(Ba, H);
let TdotV = dot(Ta, V);
let BdotV = dot(Ba, V);
let ab = roughness * roughness;
let at = mix(ab, 1.0, anisotropy * anisotropy);
let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH);
let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL);
let Fa = fresnel(F0, LdotH);
// Calculate the specular light.
let Fr = specular_multiscatter(input, Da, Va, Fa, specular_intensity);
return Fr;
}
#endif // STANDARD_MATERIAL_ANISOTROPY
// Diffuse BRDF
// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf
// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm
//
// simplest approximation
// float Fd_Lambert() {
// return 1.0 / PI;
// }
//
// vec3 Fd = diffuseColor * Fd_Lambert();
//
// Disney approximation
// See https://google.github.io/filament/Filament.html#citation-burley12
// minimal quality difference
fn Fd_Burley(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
) -> f32 {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let NdotL = (*derived_input).NdotL;
let LdotH = (*derived_input).LdotH;
let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH;
let lightScatter = F_Schlick(1.0, f90, NdotL);
let viewScatter = F_Schlick(1.0, f90, NdotV);
return lightScatter * viewScatter * (1.0 / PI);
}
// Scale/bias approximation
// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
// TODO: Use a LUT (more accurate)
fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
let r = perceptual_roughness * c0 + c1;
let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
return vec2<f32>(-1.04, 1.04) * a004 + r.zw;
}
fn EnvBRDFApprox(F0: vec3<f32>, F_ab: vec2<f32>) -> vec3<f32> {
return F0 * F_ab.x + F_ab.y;
}
fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 {
// clamp perceptual roughness to prevent precision problems
// According to Filament design 0.089 is recommended for mobile
// Filament uses 0.045 for non-mobile
let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0);
return clampedPerceptualRoughness * clampedPerceptualRoughness;
}
fn point_light(
light_id: u32,
input: ptr<function, LightingInput>,
enable_diffuse: bool
) -> vec3<f32> {
// Unpack.
let diffuse_color = (*input).diffuse_color;
let P = (*input).P;
let N = (*input).layers[LAYER_BASE].N;
let V = (*input).V;
let light = &view_bindings::clusterable_objects.data[light_id];
let light_to_frag = (*light).position_radius.xyz - P;
let L = normalize(light_to_frag);
let distance_square = dot(light_to_frag, light_to_frag);
let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w);
// Base layer
let specular_L_intensity = compute_specular_layer_values_for_point_light(
input,
LAYER_BASE,
V,
light_to_frag,
(*light).position_radius.w,
);
var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz);
let specular_intensity = specular_L_intensity.w;
#ifdef STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular_anisotropy(input, &specular_derived_input, L, specular_intensity);
#else // STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular(input, &specular_derived_input, specular_intensity);
#endif // STANDARD_MATERIAL_ANISOTROPY
// Clearcoat
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Unpack.
let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
let clearcoat_strength = (*input).clearcoat_strength;
// Perform specular input calculations again for the clearcoat layer. We
// can't reuse the above because the clearcoat normal might be different
// from the main layer normal.
let clearcoat_specular_L_intensity = compute_specular_layer_values_for_point_light(
input,
LAYER_CLEARCOAT,
V,
light_to_frag,
(*light).position_radius.w,
);
var clearcoat_specular_derived_input =
derive_lighting_input(clearcoat_N, V, clearcoat_specular_L_intensity.xyz);
// Calculate the specular light.
let clearcoat_specular_intensity = clearcoat_specular_L_intensity.w;
let Fc_Frc = specular_clearcoat(
input,
&clearcoat_specular_derived_input,
clearcoat_strength,
clearcoat_specular_intensity
);
let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term.
let Frc = Fc_Frc.g; // Clearcoat light.
#endif // STANDARD_MATERIAL_CLEARCOAT
// Diffuse.
// Comes after specular since its N⋅L is used in the lighting equation.
var derived_input = derive_lighting_input(N, V, L);
var diffuse = vec3(0.0);
if (enable_diffuse) {
diffuse = diffuse_color * Fd_Burley(input, &derived_input);
}
// See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
// Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩
// where
// f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
// Φ is luminous power in lumens
// our rangeAttenuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
// For a point light, luminous intensity, I, in lumens per steradian is given by:
// I = Φ / 4 π
// The derivation of this can be seen here: https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU
var color: vec3<f32>;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Account for the Fresnel term from the clearcoat darkening the main layer.
//
// <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
color = (diffuse + specular_light * inv_Fc) * inv_Fc + Frc;
#else // STANDARD_MATERIAL_CLEARCOAT
color = diffuse + specular_light;
#endif // STANDARD_MATERIAL_CLEARCOAT
return color * (*light).color_inverse_square_range.rgb *
(rangeAttenuation * derived_input.NdotL);
}
fn spot_light(
light_id: u32,
input: ptr<function, LightingInput>,
enable_diffuse: bool
) -> vec3<f32> {
// reuse the point light calculations
let point_light = point_light(light_id, input, enable_diffuse);
let light = &view_bindings::clusterable_objects.data[light_id];
// reconstruct spot dir from x/z and y-direction flag
var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u {
spot_dir.y = -spot_dir.y;
}
let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz;
// calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight
// spot_scale and spot_offset have been precomputed
// note we normalize here to get "l" from the filament listing. spot_dir is already normalized
let cd = dot(-spot_dir, normalize(light_to_frag));
let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w);
let spot_attenuation = attenuation * attenuation;
return point_light * spot_attenuation;
}
fn directional_light(
light_id: u32,
input: ptr<function, LightingInput>,
enable_diffuse: bool
) -> vec3<f32> {
// Unpack.
let diffuse_color = (*input).diffuse_color;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let N = (*input).layers[LAYER_BASE].N;
let V = (*input).V;
let roughness = (*input).layers[LAYER_BASE].roughness;
let light = &view_bindings::lights.directional_lights[light_id];
let L = (*light).direction_to_light.xyz;
var derived_input = derive_lighting_input(N, V, L);
var diffuse = vec3(0.0);
if (enable_diffuse) {
diffuse = diffuse_color * Fd_Burley(input, &derived_input);
}
#ifdef STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular_anisotropy(input, &derived_input, L, 1.0);
#else // STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular(input, &derived_input, 1.0);
#endif // STANDARD_MATERIAL_ANISOTROPY
#ifdef STANDARD_MATERIAL_CLEARCOAT
let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
let clearcoat_strength = (*input).clearcoat_strength;
// Perform specular input calculations again for the clearcoat layer. We
// can't reuse the above because the clearcoat normal might be different
// from the main layer normal.
var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L);
let Fc_Frc =
specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0);
let inv_Fc = 1.0 - Fc_Frc.r;
let Frc = Fc_Frc.g;
#endif // STANDARD_MATERIAL_CLEARCOAT
var color: vec3<f32>;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Account for the Fresnel term from the clearcoat darkening the main layer.
//
// <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL +
Frc * derived_clearcoat_input.NdotL;
#else // STANDARD_MATERIAL_CLEARCOAT
color = (diffuse + specular_light) * derived_input.NdotL;
#endif // STANDARD_MATERIAL_CLEARCOAT
return color * (*light).color.rgb;
}