Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1//! The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material
2//! and consists of two components, the diffuse component (f_d) and the specular component (f_r):
3//! f(v,l) = f_d(v,l) + f_r(v,l)
4//!
5//! The form of the microfacet model is the same for diffuse and specular
6//! 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
7//!
8//! In which:
9//! D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets
10//! G models the visibility (or occlusion or shadow-masking) of the microfacets
11//! f_m is the microfacet BRDF and differs between specular and diffuse components
12//!
13//! The above integration needs to be approximated.
14//!
15//! From the Filament design doc
16//! https://google.github.io/filament/Filament.md.html#table_symbols
17//! Symbol Definition
18//! v View unit vector
19//! l Incident light unit vector
20//! n Surface normal unit vector
21//! h Half unit vector between l and v
22//! f BRDF
23//! f_d Diffuse component of a BRDF
24//! f_r Specular component of a BRDF
25//! α Roughness, remapped from using input perceptualRoughness
26//! σ Diffuse reflectance
27//! Ω Spherical domain
28//! f0 Reflectance at normal incidence
29//! f90 Reflectance at grazing angle
30//! χ+(a) Heaviside function (1 if a>0 and 0 otherwise)
31//! nior Index of refraction (IOR) of an interface
32//! ⟨n⋅l⟩ Dot product clamped to [0..1]
33//! ⟨a⟩ Saturated value (clamped to [0..1])
35import package::{
36 render::mesh_view_types::{POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, RectLight},
37 render::mesh_view_bindings as view_bindings,
38 atmosphere::functions::{calculate_visible_sun_ratio, clamp_to_surface},
39 atmosphere::bruneton_functions::transmittance_lut_r_mu_to_uv,
40};
41import bevy_render::maths::{PI, orthonormalize, quat_rotate};
43const LAYER_BASE: u32 = 0;
44const LAYER_CLEARCOAT: u32 = 1;
46/// Input to a lighting function for a single layer (either the base layer or the
47/// clearcoat layer).
48struct LayerLightingInput {
49 /// The normal vector.
50 N: vec3<f32>,
51 /// The reflected vector.
52 R: vec3<f32>,
53 /// The normal vector ⋅ the view vector.
54 NdotV: f32,
56 /// The perceptual roughness of the layer.
57 perceptual_roughness: f32,
58 /// The roughness of the layer.
59 roughness: f32,
62/// Input to a lighting function (`point_light`, `spot_light`,
63/// `directional_light`).
64struct LightingInput {
65 @if(STANDARD_MATERIAL_CLEARCOAT)
66 layers: array<LayerLightingInput, 2>,
67 @else // STANDARD_MATERIAL_CLEARCOAT
68 layers: array<LayerLightingInput, 1>,
70 /// The world-space position.
71 P: vec3<f32>,
72 /// The vector to the view.
73 V: vec3<f32>,
75 /// The diffuse color of the material.
76 diffuse_color: vec3<f32>,
78 /// The 0-1 metallic factor of the material.
79 metallic: f32,
81 /// Specular reflectance at the normal incidence angle.
82 F0_dielectric: vec3<f32>,
83 F0_metallic: vec3<f32>,
85 /// Constants for the BRDF approximation.
86 ///
87 /// See `EnvBRDFApprox` in
88 /// <https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile>.
89 /// What we call `F_ab` they call `AB`.
90 F_ab: vec2<f32>,
92 /// The strength of the clearcoat layer.
93 @if(STANDARD_MATERIAL_CLEARCOAT)
94 clearcoat_strength: f32,
96 /// The anisotropy strength, reflecting the amount of increased roughness in
97 /// the tangent direction.
98 @if(STANDARD_MATERIAL_ANISOTROPY)
99 anisotropy: f32,
100 /// The tangent direction for anisotropy: i.e. the direction in which
101 /// roughness increases.
102 @if(STANDARD_MATERIAL_ANISOTROPY)
103 Ta: vec3<f32>,
104 /// The bitangent direction, which is the cross product of the normal with
105 /// the tangent direction.
106 @if(STANDARD_MATERIAL_ANISOTROPY)
107 Ba: vec3<f32>,
110/// Values derived from the `LightingInput` for both diffuse and specular lights.
111struct DerivedLightingInput {
112 /// The half-vector between L, the incident light vector, and V, the view
113 /// vector.
114 H: vec3<f32>,
115 /// The normal vector ⋅ the incident light vector.
116 NdotL: f32,
117 /// The normal vector ⋅ the half-vector.
118 NdotH: f32,
119 /// The incident light vector ⋅ the half-vector.
120 LdotH: f32,
123/// distanceAttenuation is simply the square falloff of light intensity
124/// combined with a smooth attenuation at the edge of the light radius
125///
126/// light radius is a non-physical construct for efficiency purposes,
127/// because otherwise every light affects every fragment in the scene
128fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 {
129 return getRangeFalloff(distanceSquare, inverseRangeSquared) * 1.0 / max(distanceSquare, 0.0001);
132// Falloff without the distance attenuation, for lights that have it baked-in (eg LTC lights)
133fn getRangeFalloff(distanceSquare: f32, inverseRangeSquared: f32) -> f32 {
134 let factor = distanceSquare * inverseRangeSquared;
135 let smoothFactor = saturate(1.0 - factor * factor);
136 return smoothFactor * smoothFactor;
139/// Normal distribution function (specular D)
140/// Based on https://google.github.io/filament/Filament.md.html#citation-walter07
141///
142/// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α2−1) + 1)^2 }
143///
144/// Simple implementation, has precision problems when using fp16 instead of fp32
145/// see https://google.github.io/filament/Filament.md.html#listing_speculardfp16
146fn D_GGX(roughness: f32, NdotH: f32) -> f32 {
147 let oneMinusNdotHSquared = 1.0 - NdotH * NdotH;
148 let a = NdotH * roughness;
149 let k = roughness / (oneMinusNdotHSquared + a * a);
150 let d = k * k * (1.0 / PI);
151 return d;
154/// An approximation of the anisotropic GGX distribution function.
155///
156/// 1
157/// D(𝐡) = ───────────────────────────────────────────────────
158/// παₜα_b((𝐡 ⋅ 𝐭)² / αₜ²) + (𝐡 ⋅ 𝐛)² / α_b² + (𝐡 ⋅ 𝐧)²)²
159///
160/// * `T` = 𝐭 = the tangent direction = the direction of increased roughness.
161///
162/// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness.
163///
164/// * `at` = αₜ = the alpha-roughness in the tangent direction.
165///
166/// * `ab` = α_b = the alpha-roughness in the bitangent direction.
167///
168/// This is from the `KHR_materials_anisotropy` spec:
169/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#individual-lights>
170fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 {
171 let a2 = at * ab;
172 let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH);
173 let w2 = a2 / dot(f, f);
174 let d = a2 * w2 * w2 * (1.0 / PI);
175 return d;
178/// Visibility function (Specular G)
179/// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) }
180/// such that f_r becomes
181/// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0)
182/// where
183/// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) }
184/// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.md.html#listing_approximatedspecularv
185fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 {
186 let a2 = roughness * roughness;
187 let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2);
188 let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2);
189 let v = 0.5 / (lambdaV + lambdaL);
190 return v;
193/// The visibility function, anisotropic variant.
194fn V_GGX_anisotropic(
195 at: f32,
196 ab: f32,
197 NdotL: f32,
198 NdotV: f32,
199 BdotV: f32,
200 TdotV: f32,
201 TdotL: f32,
202 BdotL: f32,
203) -> f32 {
204 let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV));
205 let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL));
206 let v = 0.5 / (GGX_V + GGX_L);
207 return saturate(v);
210/// Probability-density function that matches the bounded VNDF sampler
211/// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 2)
212fn ggx_vndf_pdf(i: vec3<f32>, NdotH: f32, roughness: f32) -> f32 {
213 let ndf = D_GGX(roughness, NdotH);
215 // Common terms
216 let ai = roughness * i.xy;
217 let len2 = dot(ai, ai);
218 let t = sqrt(len2 + i.z * i.z);
219 if i.z >= 0.0 {
220 let a = roughness;
221 let s = 1.0 + length(i.xy);
222 let a2 = a * a;
223 let s2 = s * s;
224 let k = (1.0 - a2) * s2 / (s2 + a2 * i.z * i.z);
225 return ndf / (2.0 * (k * i.z + t));
226 }
228 // Backfacing case
229 return ndf * (t - i.z) / (2.0 * len2);
232/// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1)
233fn sample_visible_ggx(
234 xi: vec2<f32>,
235 roughness: f32,
236 normal: vec3<f32>,
237 view: vec3<f32>,
238) -> vec3<f32> {
239 let n = normal;
240 let alpha = roughness;
242 // Decompose view into components parallel/perpendicular to the normal
243 let wi_n = dot(view, n);
244 let wi_z = -n * wi_n;
245 let wi_xy = view + wi_z;
247 // Warp view vector to the unit-roughness configuration
248 let wi_std = -normalize(alpha * wi_xy + wi_z);
250 // Compute wi_std.z once for reuse
251 let wi_std_z = dot(wi_std, n);
253 // Bounded VNDF sampling
254 // Compute the bound parameter k (Eq. 5) and the scaled z–limit b (Eq. 6)
255 let s = 1.0 + length(wi_xy);
256 let a = clamp(alpha, 0.0, 1.0);
257 let a2 = a * a;
258 let s2 = s * s;
259 let k = (1.0 - a2) * s2 / (s2 + a2 * wi_n * wi_n);
260 let b = select(wi_std_z, k * wi_std_z, wi_n > 0.0);
262 // Sample a spherical cap in (-b, 1]
263 let z = 1.0 - xi.y * (1.0 + b);
264 let sin_theta = sqrt(max(0.0, 1.0 - z * z));
265 let phi = 2.0 * PI * xi.x - PI;
266 let x = sin_theta * cos(phi);
267 let y = sin_theta * sin(phi);
268 let c_std = vec3f(x, y, z);
270 // Rotate the sample so that the normal aligns with +Z
271 let TBN = orthonormalize(n);
272 let c = TBN * c_std;
274 // Half-vector in the standard frame
275 let wm_std = c + wi_std;
276 let wm_std_z = n * dot(n, wm_std);
277 let wm_std_xy = wm_std_z - wm_std;
279 // Unwarp back to original roughness and compute microfacet normal
280 let H = normalize(alpha * wm_std_xy + wm_std_z);
282 // Reflect view to obtain the outgoing (light) direction
283 return reflect(-view, H);
286/// Smith geometric shadowing function
287fn G_Smith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 {
288 let k = roughness / 2.0;
289 let GGXL = NdotL / (NdotL * (1.0 - k) + k);
290 let GGXV = NdotV / (NdotV * (1.0 - k) + k);
291 return GGXL * GGXV;
294/// A simpler, but nonphysical, alternative to Smith-GGX. We use this for
295/// clearcoat, per the Filament spec.
296///
297/// https://google.github.io/filament/Filament.md.html#materialsystem/clearcoatmodel
298fn V_Kelemen(LdotH: f32) -> f32 {
299 let l_dot_h = max(LdotH, 0.0001);
300 return 0.25 / (l_dot_h * l_dot_h);
303/// Fresnel function
304/// see https://google.github.io/filament/Filament.md.html#citation-schlick94
305/// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5
306fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VdotH: f32) -> vec3<f32> {
307 // not using mix to keep the vec3 and float versions identical
308 return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
311/// Fresnel function
312/// see https://google.github.io/filament/Filament.md.html#citation-schlick94
313/// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5
314fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 {
315 // not using mix to keep the vec3 and float versions identical
316 return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
319fn fresnel(f0: vec3<f32>, LdotH: f32) -> vec3<f32> {
320 // f_90 suitable for ambient occlusion
321 // see https://google.github.io/filament/Filament.md.html#lighting/occlusion
322 let f90 = saturate(dot(f0, vec3<f32>(50.0 * 0.33)));
323 return F_Schlick_vec(f0, f90, LdotH);
326/// Given distribution, visibility, and Fresnel term, calculates the final
327/// specular light.
328///
329/// Multiscattering approximation:
330/// <https://google.github.io/filament/Filament.md.html#listing_energycompensationimpl>
331fn specular_multiscatter(
332 D: f32,
333 V: f32,
334 F: vec3<f32>,
335 F0: vec3<f32>,
336 F_ab: vec2<f32>,
337 specular_intensity: f32,
338) -> vec3<f32> {
339 var Fr = (specular_intensity * D * V) * F;
340 // F_ab.x + F_ab.y is dfg.y in Filament
341 // See section 9.5 and listing 29 in the Filament spec
342 Fr *= 1.0 + F0 * (1.0 / (F_ab.x + F_ab.y) - 1.0);
343 return Fr;
346// Specular BRDF
347// https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf
349/// N, V, and L must all be normalized.
350fn derive_lighting_input(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>) -> DerivedLightingInput {
351 var input: DerivedLightingInput;
352 var H: vec3<f32> = normalize(L + V);
353 input.H = H;
354 input.NdotL = saturate(dot(N, L));
355 input.NdotH = saturate(dot(N, H));
356 input.LdotH = saturate(dot(L, H));
357 return input;
360/// Returns L in the `xyz` components and the modified roughness in the `w` component.
361fn compute_specular_layer_values_for_point_light(
362 input: ptr<function, LightingInput>,
363 layer: u32,
364 V: vec3<f32>,
365 light_to_frag: vec3<f32>,
366 light_radius: f32,
367 distance: f32,
368) -> vec4<f32> {
369 // Unpack.
370 let R = (*input).layers[layer].R;
371 let a = (*input).layers[layer].roughness;
373 // Representative Point Area Lights.
374 // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
375 var LtFdotR = dot(light_to_frag, R);
377 // HACK: the following line is an amendment to fix a discontinuity when a surface
378 // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318
379 //
380 // This sentence in the reference is crux of the problem: "We approximate finding the point with the
381 // smallest angle to the reflection ray by finding the point with the smallest distance to the ray."
382 // This approximation turns out to be completely wrong for points inside or near the sphere.
383 // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it.
384 // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero.
385 // However, this is still far from physically accurate. Deriving an exact solution would help,
386 // but really we should adopt a superior solution to area lighting, such as:
387 // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al.
388 LtFdotR = max(0.0001, LtFdotR);
390 let centerToRay = LtFdotR * R - light_to_frag;
391 let closestPoint = light_to_frag + centerToRay * saturate(
392 light_radius * inverseSqrt(dot(centerToRay, centerToRay)));
393 let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
395 // Karis 2013, page 14. The constant 2 (or 3) is hand tuned to fit reference.
396 // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
397 let a_prime = saturate(a + light_radius / (2.0 * distance));
399 let L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
400 return vec4(L, a_prime);
403/// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
404/// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) }
405fn specular(
406 input: ptr<function, LightingInput>,
407 derived_input: ptr<function, DerivedLightingInput>,
408 roughness: f32,
409 specular_intensity: f32,
410) -> vec3<f32> {
411 // Unpack.
412 let NdotV = (*input).layers[LAYER_BASE].NdotV;
413 let F0 = mix((*input).F0_dielectric, (*input).F0_metallic, (*input).metallic);
414 let NdotL = (*derived_input).NdotL;
415 let NdotH = (*derived_input).NdotH;
416 let LdotH = (*derived_input).LdotH;
418 // Calculate distribution.
419 let D = D_GGX(roughness, NdotH);
420 // Calculate visibility.
421 let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL);
422 // Calculate the Fresnel term.
423 let F = fresnel(F0, LdotH);
425 // Calculate the specular light.
426 let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity);
427 return Fr;
430/// Calculates the specular light for the clearcoat layer. Returns Fc, the
431/// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in
432/// the second channel.
433///
434/// <https://google.github.io/filament/Filament.md.html#listing_clearcoatbrdf>
435fn specular_clearcoat(
436 input: ptr<function, LightingInput>,
437 derived_input: ptr<function, DerivedLightingInput>,
438 clearcoat_strength: f32,
439 roughness: f32,
440 specular_intensity: f32,
441) -> vec2<f32> {
442 // Unpack.
443 let NdotH = (*derived_input).NdotH;
444 let LdotH = (*derived_input).LdotH;
446 // Calculate distribution.
447 let Dc = D_GGX(roughness, NdotH);
448 // Calculate visibility.
449 let Vc = V_Kelemen(LdotH);
450 // Calculate the Fresnel term.
451 let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength;
452 // Calculate the specular light.
453 let Frc = (specular_intensity * Dc * Vc) * Fc;
454 return vec2(Fc, Frc);
457@if(STANDARD_MATERIAL_ANISOTROPY)
458fn specular_anisotropy(
459 input: ptr<function, LightingInput>,
460 derived_input: ptr<function, DerivedLightingInput>,
461 L: vec3<f32>,
462 roughness: f32,
463 specular_intensity: f32,
464) -> vec3<f32> {
465 // Unpack.
466 let NdotV = (*input).layers[LAYER_BASE].NdotV;
467 let V = (*input).V;
468 let F0 = mix((*input).F0_dielectric, (*input).F0_metallic, (*input).metallic);
469 let anisotropy = (*input).anisotropy;
470 let Ta = (*input).Ta;
471 let Ba = (*input).Ba;
472 let H = (*derived_input).H;
473 let NdotL = (*derived_input).NdotL;
474 let NdotH = (*derived_input).NdotH;
475 let LdotH = (*derived_input).LdotH;
477 let TdotL = dot(Ta, L);
478 let BdotL = dot(Ba, L);
479 let TdotH = dot(Ta, H);
480 let BdotH = dot(Ba, H);
481 let TdotV = dot(Ta, V);
482 let BdotV = dot(Ba, V);
484 let ab = roughness * roughness;
485 let at = mix(ab, 1.0, anisotropy * anisotropy);
487 let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH);
488 let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL);
489 let Fa = fresnel(F0, LdotH);
491 // Calculate the specular light.
492 let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity);
493 return Fr;
496/// Diffuse BRDF
497/// https://google.github.io/filament/Filament.md.html#materialsystem/diffusebrdf
498/// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm
499///
500/// simplest approximation
501/// float Fd_Lambert() {
502/// return 1.0 / PI;
503/// }
504///
505/// vec3 Fd = diffuseColor * Fd_Lambert();
506///
507/// Disney approximation
508/// See https://google.github.io/filament/Filament.md.html#citation-burley12
509/// minimal quality difference
510fn Fd_Burley(
511 input: ptr<function, LightingInput>,
512 derived_input: ptr<function, DerivedLightingInput>,
513) -> f32 {
514 // Unpack.
515 let roughness = (*input).layers[LAYER_BASE].roughness;
516 let NdotV = (*input).layers[LAYER_BASE].NdotV;
517 let NdotL = (*derived_input).NdotL;
518 let LdotH = (*derived_input).LdotH;
520 let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH;
521 let lightScatter = F_Schlick(1.0, f90, NdotL);
522 let viewScatter = F_Schlick(1.0, f90, NdotV);
523 return lightScatter * viewScatter * (1.0 / PI);
526// Scale/bias approximation
527fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
528@if(DFG_LUT)
529 return textureSampleLevel(view_bindings::dfg_lut, view_bindings::dfg_lut_sampler, vec2<f32>(NdotV, perceptual_roughness), 0.0).rg;
530@else {
531 // Polynomial approximation, see https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
532 let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
533 let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
534 let r = perceptual_roughness * c0 + c1;
535 let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
536 // Keep F_ab positive to avoid divide-by-zero in downstream BRDF terms.
537 let f_ab_epsilon = 0.00005;
538 return max(vec2<f32>(-1.04, 1.04) * a004 + r.zw, vec2<f32>(f_ab_epsilon));
542fn EnvBRDFApprox(F0: vec3<f32>, F_ab: vec2<f32>) -> vec3<f32> {
543 return F0 * F_ab.x + F_ab.y;
546fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 {
547 // clamp perceptual roughness to prevent precision problems
548 // According to Filament design 0.089 is recommended for mobile
549 // Filament uses 0.045 for non-mobile
550 let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0);
551 return clampedPerceptualRoughness * clampedPerceptualRoughness;
554// this must align with CubemapLayout in decal/clustered.rs
555const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0;
556const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1;
557const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2;
558const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3;
560const X_PLUS: u32 = 0;
561const X_MINUS: u32 = 1;
562const Y_PLUS: u32 = 2;
563const Y_MINUS: u32 = 3;
564const Z_MINUS: u32 = 4;
565const Z_PLUS: u32 = 5;
567fn cubemap_uv(direction: vec3<f32>, cubemap_type: u32) -> vec2<f32> {
568 let abs_direction = abs(direction);
569 let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z));
571 let face_index = select(
572 select(X_PLUS, X_MINUS, direction.x < 0.0),
573 select(
574 select(Y_PLUS, Y_MINUS, direction.y < 0.0),
575 select(Z_PLUS, Z_MINUS, direction.z < 0.0),
576 max_axis != abs_direction.y
577 ),
578 max_axis != abs_direction.x
579 );
581 var face_uv: vec2<f32>;
582 var divisor: f32;
583 var corner_uv: vec2<u32> = vec2(0, 0);
584 var face_size: vec2<f32>;
586 switch face_index {
587 case X_PLUS: { face_uv = vec2<f32>(direction.z, -direction.y); divisor = direction.x; }
588 case X_MINUS: { face_uv = vec2<f32>(-direction.z, -direction.y); divisor = -direction.x; }
589 case Y_PLUS: { face_uv = vec2<f32>(direction.x, -direction.z); divisor = direction.y; }
590 case Y_MINUS: { face_uv = vec2<f32>(direction.x, direction.z); divisor = -direction.y; }
591 case Z_PLUS: { face_uv = vec2<f32>(direction.x, direction.y); divisor = direction.z; }
592 case Z_MINUS: { face_uv = vec2<f32>(direction.x, -direction.y); divisor = -direction.z; }
593 default: {}
594 }
595 face_uv = (face_uv / divisor) * 0.5 + 0.5;
597 switch cubemap_type {
598 case CUBEMAP_TYPE_CROSS_VERTICAL: {
599 face_size = vec2(1.0/3.0, 1.0/4.0);
600 corner_uv = vec2<u32>((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu);
601 }
602 case CUBEMAP_TYPE_CROSS_HORIZONTAL: {
603 face_size = vec2(1.0/4.0, 1.0/3.0);
604 corner_uv = vec2<u32>((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu);
605 }
606 case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: {
607 face_size = vec2(1.0/6.0, 1.0);
608 corner_uv.x = face_index;
609 }
610 case CUBEMAP_TYPE_SEQUENCE_VERTICAL: {
611 face_size = vec2(1.0, 1.0/6.0);
612 corner_uv.y = face_index;
613 }
614 default: {}
615 }
617 return (vec2<f32>(corner_uv) + face_uv) * face_size;
620/// This is a modification to Karis 2013 for area lights to fix an issue where the specular
621/// reflection on smooth materials looks too rough and dim. We lerp between the base roughness
622/// and Karis2013 roughness with a lerp factor tuned by looking at reference renders. The goal
623/// is to preserve sharp specular highlights on smooth materials, without blowing out specular
624/// highlights on rough materials.
625///
626/// The ideal solution is to switch to Linearly Transformed Cosines, which is more accurate in
627/// all cases, and a popular choice for realtime.
628fn specular_fix_remap(a: f32) -> f32 {
629 let inv_a_sq = (1.0 - a) * (1.0 - a);
630 return 1.0 - inv_a_sq * inv_a_sq;
633fn point_light(
634 light_id: u32,
635 input: ptr<function, LightingInput>,
636 enable_diffuse: bool,
637 enable_texture: bool,
638) -> vec3<f32> {
639 // Unpack.
640 let diffuse_color = (*input).diffuse_color;
641 let P = (*input).P;
642 let N = (*input).layers[LAYER_BASE].N;
643 let V = (*input).V;
645 let light = &view_bindings::clustered_lights.data[light_id];
646 let light_to_frag = (*light).position_radius.xyz - P;
647 let L = normalize(light_to_frag);
648 let distance_square = dot(light_to_frag, light_to_frag);
649 let distance = sqrt(distance_square);
650 let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w);
652 // Base layer
654 let a = (*input).layers[LAYER_BASE].roughness;
655 let specular_L_a_prime = compute_specular_layer_values_for_point_light(
656 input,
657 LAYER_BASE,
658 V,
659 light_to_frag,
660 (*light).position_radius.w,
661 distance,
662 );
663 let L_spec = specular_L_a_prime.xyz;
664 let a_prime = specular_L_a_prime.w;
665 var specular_derived_input = derive_lighting_input(N, V, L_spec);
667 let normalizationFactor = a / a_prime;
668 let specular_intensity = normalizationFactor * normalizationFactor;
670 let brdf_roughness = mix(a, a_prime, specular_fix_remap(a));
672@if(STANDARD_MATERIAL_ANISOTROPY)
673 var specular_light = specular_anisotropy(input, &specular_derived_input, L, brdf_roughness, specular_intensity);
674@else // STANDARD_MATERIAL_ANISOTROPY
675 var specular_light = specular(input, &specular_derived_input, brdf_roughness, specular_intensity);
677 // Sphere area light visibility (solid-angle attenuation)
678 let light_radius = (*light).position_radius.w;
679 if light_radius > 0.0 {
680 let solid_angle = light_radius * light_radius / (distance * distance);
681 specular_light *= saturate(specular_derived_input.NdotL / max(specular_derived_input.NdotL + solid_angle, 1e-4));
682 }
684 // Clearcoat
686 // Unpack.
687 @if(STANDARD_MATERIAL_CLEARCOAT)
688 let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
689 @if(STANDARD_MATERIAL_CLEARCOAT)
690 let clearcoat_strength = (*input).clearcoat_strength;
691 @if(STANDARD_MATERIAL_CLEARCOAT)
692 let clearcoat_a = (*input).layers[LAYER_CLEARCOAT].roughness;
694 // Perform specular input calculations again for the clearcoat layer. We
695 // can't reuse the above because the clearcoat normal might be different
696 // from the main layer normal.
697 @if(STANDARD_MATERIAL_CLEARCOAT)
698 let clearcoat_specular_L_a_prime = compute_specular_layer_values_for_point_light(
699 input,
700 LAYER_CLEARCOAT,
701 V,
702 light_to_frag,
703 (*light).position_radius.w,
704 distance,
705 );
706 @if(STANDARD_MATERIAL_CLEARCOAT)
707 let L_clearcoat_spec = clearcoat_specular_L_a_prime.xyz;
708 @if(STANDARD_MATERIAL_CLEARCOAT)
709 let clearcoat_a_prime = clearcoat_specular_L_a_prime.w;
710 @if(STANDARD_MATERIAL_CLEARCOAT)
711 var clearcoat_specular_derived_input =
712 derive_lighting_input(clearcoat_N, V, L_clearcoat_spec);
714 // Calculate the specular light.
715 @if(STANDARD_MATERIAL_CLEARCOAT)
716 let clearcoat_normalizationFactor = clearcoat_a / clearcoat_a_prime;
718 @if(STANDARD_MATERIAL_CLEARCOAT)
719 let clearcoat_specular_intensity = clearcoat_normalizationFactor * clearcoat_normalizationFactor;
721 @if(STANDARD_MATERIAL_CLEARCOAT)
722 let clearcoat_brdf_roughness = mix(clearcoat_a, clearcoat_a_prime, specular_fix_remap(clearcoat_a));
724 @if(STANDARD_MATERIAL_CLEARCOAT)
725 let Fc_Frc = specular_clearcoat(
726 input,
727 &clearcoat_specular_derived_input,
728 clearcoat_strength,
729 clearcoat_brdf_roughness,
730 clearcoat_specular_intensity
731 );
732 @if(STANDARD_MATERIAL_CLEARCOAT)
733 let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term.
734 @if(STANDARD_MATERIAL_CLEARCOAT)
735 var Frc = Fc_Frc.g; // Clearcoat light.
737 // Sphere area light visibility (solid-angle attenuation) for clearcoat
738 @if(STANDARD_MATERIAL_CLEARCOAT)
739 if light_radius > 0.0 {
740 let solid_angle = light_radius * light_radius / (distance * distance);
741 Frc *= saturate(clearcoat_specular_derived_input.NdotL / max(clearcoat_specular_derived_input.NdotL + solid_angle, 1e-4));
742 }
744 // Diffuse.
745 // Comes after specular since its N⋅L is used in the lighting equation.
746 var derived_input = derive_lighting_input(N, V, L);
747 var diffuse = vec3(0.0);
748 if (enable_diffuse) {
749 diffuse = diffuse_color * Fd_Burley(input, &derived_input);
750 }
752 // See https://google.github.io/filament/Filament.md.html#mjx-eqn-pointLightLuminanceEquation
753 // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩
754 // where
755 // f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
756 // Φ is luminous power in lumens
757 // our rangeAttenuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
759 // For a point light, luminous intensity, I, in lumens per steradian is given by:
760 // I = Φ / 4 π
761 // The derivation of this can be seen here: https://google.github.io/filament/Filament.md.html#mjx-eqn-pointLightLuminousPower
763 // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU
765 var color_times_NdotL: vec3<f32>;
766@if(STANDARD_MATERIAL_CLEARCOAT)
767 // Account for the Fresnel term from the clearcoat darkening the main layer.
768 //
769 // <https://google.github.io/filament/Filament.md.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
770 color_times_NdotL = (diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL * inv_Fc) * inv_Fc + Frc * clearcoat_specular_derived_input.NdotL;
771@else // STANDARD_MATERIAL_CLEARCOAT
772 color_times_NdotL = diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL;
774 var texture_sample = 1f;
776@if(LIGHT_TEXTURES)
777 if enable_texture && (*light).decal_index != 0xFFFFFFFFu {
778 let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz;
779 let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag;
780 let decal_uv = cubemap_uv(relative_position, cubemap_type);
781 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
783 texture_sample = textureSampleLevel(
784 view_bindings::clustered_decal_textures[image_index],
785 view_bindings::clustered_decal_sampler,
786 decal_uv,
787 0.0
788 ).r;
789 }
791 return color_times_NdotL * (*light).color_inverse_square_range.rgb *
792 rangeAttenuation * texture_sample;
795fn spot_light(
796 light_id: u32,
797 input: ptr<function, LightingInput>,
798 enable_diffuse: bool
799) -> vec3<f32> {
800 // reuse the point light calculations
801 let point_light = point_light(light_id, input, enable_diffuse, false);
803 let light = &view_bindings::clustered_lights.data[light_id];
805 // reconstruct spot dir from x/z and y-direction flag
806 var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
807 spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
808 if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u {
809 spot_dir.y = -spot_dir.y;
810 }
811 let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz;
813 // calculate attenuation based on filament formula https://google.github.io/filament/Filament.md.html#listing_glslpunctuallight
814 // spot_scale and spot_offset have been precomputed
815 // note we normalize here to get "l" from the filament listing. spot_dir is already normalized
816 let cd = dot(-spot_dir, normalize(light_to_frag));
817 let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w);
818 let spot_attenuation = attenuation * attenuation;
820 var texture_sample = 1f;
822@if(LIGHT_TEXTURES)
823 if (*light).decal_index != 0xFFFFFFFFu {
824 let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world *
825 vec4((*input).P, 1.0)).xyz;
826 if local_position.z < 0.0 {
827 let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5;
828 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
830 texture_sample = textureSampleLevel(
831 view_bindings::clustered_decal_textures[image_index],
832 view_bindings::clustered_decal_sampler,
833 decal_uv,
834 0.0
835 ).r;
836 }
837 }
839 return point_light * spot_attenuation * texture_sample;
842fn directional_light(
843 light_id: u32,
844 input: ptr<function, LightingInput>,
845 enable_diffuse: bool
846) -> vec3<f32> {
847 // Unpack.
848 let diffuse_color = (*input).diffuse_color;
849 let NdotV = (*input).layers[LAYER_BASE].NdotV;
850 let N = (*input).layers[LAYER_BASE].N;
851 let V = (*input).V;
852 let roughness = (*input).layers[LAYER_BASE].roughness;
854 let light = &view_bindings::lights.directional_lights[light_id];
856 let L = (*light).direction_to_light.xyz;
857 var derived_input = derive_lighting_input(N, V, L);
859 var diffuse = vec3(0.0);
860 if (enable_diffuse) {
861 diffuse = diffuse_color * Fd_Burley(input, &derived_input);
862 }
864@if(STANDARD_MATERIAL_ANISOTROPY)
865 let specular_light = specular_anisotropy(input, &derived_input, L, roughness, 1.0);
866@else // STANDARD_MATERIAL_ANISOTROPY
867 let specular_light = specular(input, &derived_input, roughness, 1.0);
869 @if(STANDARD_MATERIAL_CLEARCOAT)
870 let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
871 @if(STANDARD_MATERIAL_CLEARCOAT)
872 let clearcoat_strength = (*input).clearcoat_strength;
873 @if(STANDARD_MATERIAL_CLEARCOAT)
874 let clearcoat_roughness = (*input).layers[LAYER_CLEARCOAT].roughness;
876 // Perform specular input calculations again for the clearcoat layer. We
877 // can't reuse the above because the clearcoat normal might be different
878 // from the main layer normal.
879 @if(STANDARD_MATERIAL_CLEARCOAT)
880 var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L);
882 @if(STANDARD_MATERIAL_CLEARCOAT)
883 let Fc_Frc =
884 specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, clearcoat_roughness, 1.0);
885 @if(STANDARD_MATERIAL_CLEARCOAT)
886 let inv_Fc = 1.0 - Fc_Frc.r;
887 @if(STANDARD_MATERIAL_CLEARCOAT)
888 let Frc = Fc_Frc.g;
890 var color: vec3<f32>;
891@if(STANDARD_MATERIAL_CLEARCOAT)
892 // Account for the Fresnel term from the clearcoat darkening the main layer.
893 //
894 // <https://google.github.io/filament/Filament.md.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
895 color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL +
896 Frc * derived_clearcoat_input.NdotL;
897@else // STANDARD_MATERIAL_CLEARCOAT
898 color = (diffuse + specular_light) * derived_input.NdotL;
900 var texture_sample = 1f;
902@if(LIGHT_TEXTURES)
903 if (*light).decal_index != 0xFFFFFFFFu {
904 let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world *
905 vec4((*input).P, 1.0)).xyz;
906 let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5;
908 // if tiled or within tile
909 if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u)
910 || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv)
911 {
912 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
914 texture_sample = textureSampleLevel(
915 view_bindings::clustered_decal_textures[image_index],
916 view_bindings::clustered_decal_sampler,
917 decal_uv - floor(decal_uv),
918 0.0
919 ).r;
920 } else {
921 texture_sample = 0f;
922 }
923 }
925color *= (*light).color.rgb * texture_sample;
927@if(ATMOSPHERE) {
928 let P = (*input).P;
929 let atmosphere = view_bindings::atmosphere;
930 let P_as = (
931 view_bindings::atmosphere.world_to_atmosphere * vec4(P, 1.0)
932 ).xyz;
933 let P_clamped = clamp_to_surface(atmosphere, P_as);
934 let r = length(P_clamped);
935 let local_up = normalize(P_clamped);
936 let mu_light = dot(L, local_up);
938 // Sample atmosphere
939 let transmittance = sample_transmittance_lut(r, mu_light);
940 let sun_visibility = calculate_visible_sun_ratio(atmosphere, r, mu_light, (*light).sun_disk_angular_size);
942 // Apply atmospheric effects
943 color *= transmittance * sun_visibility;
946 return color;
949/// Linearly Transformed Cosines (LTC) area light evaluation.
950/// Based on: Heitz et al. 2016, "Real-Time Polygonal-Light Shading with Linearly Transformed Cosines"
951///
952/// Integrate one edge of the spherical polygon formed by the LTC-transformed quad.
953/// Implements Eq. 11 using a polynomial approximation based on https://advances.realtimerendering.com/s2016/s2016_ltc_rnd.pdf
954/// Using the equation as-is produces visible artifacts.
955fn ltc_integrate_edge(v1: vec3<f32>, v2: vec3<f32>) -> f32 {
956 let x = dot(v1, v2);
957 let y = abs(x);
958 let a = 0.8543985 + (0.4965155 + 0.0145206 * y) * y;
959 let b = 3.4175940 + (4.1616724 + y) * y;
960 let v = a / b;
961 let theta_sintheta = select(0.5 * inverseSqrt(max(1.0 - x * x, 1e-7)) - v, v, x > 0.0);
962 return cross(v1, v2).z * theta_sintheta;
965fn ltc_integrate_quad(
966 N: vec3<f32>,
967 V: vec3<f32>,
968 P: vec3<f32>,
969 Minv: mat3x3<f32>,
970 points: array<vec3<f32>, 4>
971) -> f32 {
972 let T1 = normalize(V - N * dot(V, N));
973 let T2 = -cross(N, T1);
975 let Minv_local = Minv * transpose(mat3x3<f32>(T1, T2, N));
977 // Transform quad vertices into LTC-distorted space
978 var L: array<vec3<f32>, 4>;
979 L[0] = Minv_local * (points[0] - P);
980 L[1] = Minv_local * (points[1] - P);
981 L[2] = Minv_local * (points[2] - P);
982 L[3] = Minv_local * (points[3] - P);
984 // Clip against z >= 0 hemisphere (at most 5 verts output for a quad)
986 // TODO: clipping could be made cheaper with a spherical proxy, see https://advances.realtimerendering.com/s2016/s2016_ltc_rnd.pdf slides 87-102
987 var clipped: array<vec3<f32>, 5>;
988 var n_clipped: i32 = 0;
990 for (var i = 0i; i < 4i; i++) {
991 let a = L[i];
992 let b = L[(i + 1) % 4];
993 if (a.z >= 0.0) {
994 clipped[n_clipped] = a;
995 n_clipped++;
996 }
997 if ((a.z >= 0.0) != (b.z >= 0.0)) {
998 let t = a.z / (a.z - b.z);
999 clipped[n_clipped] = mix(a, b, t);
1000 n_clipped++;
1004 if (n_clipped == 0) {
1005 return 0.0;
1008 for (var i = 0i; i < n_clipped; i++) {
1009 clipped[i] = normalize(clipped[i]);
1012 // Sum edge integrals over clipped polygon
1013 var sum = 0.0;
1014 for (var i = 0i; i < n_clipped; i++) {
1015 sum += ltc_integrate_edge(clipped[i], clipped[(i + 1) % n_clipped]);
1018 return sum;
1021@if(AREA_LIGHT_LUTS)
1022/// Decodes a rect light packed into a `ClusteredLight` (see `GpuClusteredLight`
1023/// in bevy_pbr/src/cluster/mod.rs).
1024fn unpack_clustered_rect_light(light_id: u32) -> RectLight {
1025 let light = &view_bindings::clustered_lights.data[light_id];
1026 let rotation = (*light).light_custom_data;
1027 return RectLight(
1028 vec4<f32>((*light).color_inverse_square_range.xyz, 1.0),
1029 (*light).position_radius.xyz,
1030 // width
1031 (*light).position_radius.w,
1032 quat_rotate(rotation, vec3<f32>(1.0, 0.0, 0.0)),
1033 // height
1034 (*light).color_inverse_square_range.w,
1035 quat_rotate(rotation, vec3<f32>(0.0, 1.0, 0.0)),
1036 (*light).range,
1037 );
1041@if(AREA_LIGHT_LUTS)
1042fn rect_light(
1043 light: ptr<function, RectLight>,
1044 input: ptr<function, LightingInput>,
1045 enable_diffuse: bool,
1046) -> vec3<f32> {
1047 // Unpack
1048 let diffuse_color = (*input).diffuse_color;
1049 let P = (*input).P;
1050 let N = (*input).layers[LAYER_BASE].N;
1051 let V = (*input).V;
1052 let NdotV = (*input).layers[LAYER_BASE].NdotV;
1053 let perceptual_roughness = (*input).layers[LAYER_BASE].perceptual_roughness;
1055 let light_to_frag = (*light).position - P;
1056 let distance_square = dot(light_to_frag, light_to_frag);
1057 let inverse_range_squared = 1.0 / max((*light).range * (*light).range, 0.0001);
1058 let range_falloff = getRangeFalloff(distance_square, inverse_range_squared);
1060 let light_normal = cross((*light).up, (*light).right);
1061 let hw = (*light).right * (*light).width * 0.5;
1062 let hh = (*light).up * (*light).height * 0.5;
1063 var corners: array<vec3<f32>, 4>;
1064 corners[0] = (*light).position + hw - hh;
1065 corners[1] = (*light).position - hw - hh;
1066 corners[2] = (*light).position - hw + hh;
1067 corners[3] = (*light).position + hw + hh;
1069 // Backface test
1070 if dot(light_normal, P - corners[0]) <= 0.0 {
1071 return vec3<f32>(0.0);
1074 let LUT_SCALE = 63.0 / 64.0;
1075 let LUT_BIAS = 0.5 / 64.0;
1076 let uv = vec2<f32>(perceptual_roughness, sqrt(1.0 - NdotV)) * LUT_SCALE + LUT_BIAS;
1077 let t1 = textureSampleLevel(view_bindings::area_light_luts, view_bindings::area_light_luts_sampler, uv, 0, 0.0);
1078 let t2 = textureSampleLevel(view_bindings::area_light_luts, view_bindings::area_light_luts_sampler, uv, 1, 0.0);
1080 // Reconstruct the GGX inverse-LTC matrix
1081 let Minv = mat3x3<f32>(
1082 vec3<f32>(t1.x, 0.0, t1.y),
1083 vec3<f32>(0.0, 1.0, 0.0),
1084 vec3<f32>(t1.z, 0.0, t1.w),
1085 );
1086 let spec = ltc_integrate_quad(N, V, P, Minv, corners);
1088 // Use Lambertian diffuse, Burley would require a second LUT
1089 let identity = mat3x3<f32>(
1090 vec3<f32>(1.0, 0.0, 0.0),
1091 vec3<f32>(0.0, 1.0, 0.0),
1092 vec3<f32>(0.0, 0.0, 1.0),
1093 );
1094 let diff = select(0.0, ltc_integrate_quad(N, V, P, identity, corners), enable_diffuse);
1096 // t2.x encodes the bsdf magnitude and t2.y the fresnel direction
1097 let F0 = mix((*input).F0_dielectric, (*input).F0_metallic, (*input).metallic);
1098 let spec_weight = F0 * t2.x + (1.0 - F0) * t2.y;
1100@if(STANDARD_MATERIAL_CLEARCOAT) {
1101 let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
1102 let clearcoat_strength = (*input).clearcoat_strength;
1103 let clearcoat_perceptual_roughness = (*input).layers[LAYER_CLEARCOAT].perceptual_roughness;
1104 let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV;
1106 // Sample LUTs for clearcoat layer
1107 let cc_uv = vec2<f32>(clearcoat_perceptual_roughness, sqrt(1.0 - clearcoat_NdotV)) * LUT_SCALE + LUT_BIAS;
1108 let tc1 = textureSampleLevel(view_bindings::area_light_luts, view_bindings::area_light_luts_sampler, cc_uv, 0, 0.0);
1109 let tc2 = textureSampleLevel(view_bindings::area_light_luts, view_bindings::area_light_luts_sampler, cc_uv, 1, 0.0);
1110 let Minv_cc = mat3x3<f32>(
1111 vec3<f32>(tc1.x, 0.0, tc1.y),
1112 vec3<f32>(0.0, 1.0, 0.0),
1113 vec3<f32>(tc1.z, 0.0, tc1.w),
1114 );
1115 let spec_cc = ltc_integrate_quad(clearcoat_N, V, P, Minv_cc, corners);
1117 // Clearcoat has F0=0.04
1118 let spec_weight_cc = 0.04 * tc2.x + (1.0 - 0.04) * tc2.y;
1119 let Fc = clearcoat_strength * spec_weight_cc;
1120 let inv_Fc = 1.0 - Fc;
1122 return ((spec_weight * spec * inv_Fc + diffuse_color * diff) * inv_Fc
1123 + spec_weight_cc * spec_cc * clearcoat_strength) * (*light).color.rgb * range_falloff;
1124} @else {
1125 return (spec_weight * spec + diffuse_color * diff) * (*light).color.rgb * range_falloff;
1130@if(ATMOSPHERE)
1131fn sample_transmittance_lut(r: f32, mu: f32) -> vec3<f32> {
1132 let uv = transmittance_lut_r_mu_to_uv(view_bindings::atmosphere, r, mu);
1133 return textureSampleLevel(
1134 view_bindings::atmosphere_transmittance_texture,
1135 view_bindings::atmosphere_transmittance_sampler, uv, 0.0).rgb;
Contents