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