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
11const LAYER_BASE: u32 = 0;
12const LAYER_CLEARCOAT: u32 = 1;
14// From the Filament design doc
15// https://google.github.io/filament/Filament.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 // Specular reflectance at the normal incidence angle.
82 //
83 // This should be read F₀, but due to Naga limitations we can't name it that.
84 F0_: 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#ifdef STANDARD_MATERIAL_CLEARCOAT
93 // The strength of the clearcoat layer.
94 clearcoat_strength: f32,
95#endif // STANDARD_MATERIAL_CLEARCOAT
97#ifdef STANDARD_MATERIAL_ANISOTROPY
98 // The anisotropy strength, reflecting the amount of increased roughness in
99 // the tangent direction.
100 anisotropy: f32,
101 // The tangent direction for anisotropy: i.e. the direction in which
102 // roughness increases.
103 Ta: vec3<f32>,
104 // The bitangent direction, which is the cross product of the normal with
105 // the tangent direction.
106 Ba: vec3<f32>,
107#endif // STANDARD_MATERIAL_ANISOTROPY
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 let factor = distanceSquare * inverseRangeSquared;
130 let smoothFactor = saturate(1.0 - factor * factor);
131 let attenuation = smoothFactor * smoothFactor;
132 return attenuation * 1.0 / max(distanceSquare, 0.0001);
135// Normal distribution function (specular D)
136// Based on https://google.github.io/filament/Filament.html#citation-walter07
138// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α2−1) + 1)^2 }
140// Simple implementation, has precision problems when using fp16 instead of fp32
141// see https://google.github.io/filament/Filament.html#listing_speculardfp16
142fn D_GGX(roughness: f32, NdotH: f32) -> f32 {
143 let oneMinusNdotHSquared = 1.0 - NdotH * NdotH;
144 let a = NdotH * roughness;
145 let k = roughness / (oneMinusNdotHSquared + a * a);
146 let d = k * k * (1.0 / PI);
147 return d;
150// An approximation of the anisotropic GGX distribution function.
151//
152// 1
153// D(𝐡) = ───────────────────────────────────────────────────
154// παₜα_b((𝐡 ⋅ 𝐭)² / αₜ²) + (𝐡 ⋅ 𝐛)² / α_b² + (𝐡 ⋅ 𝐧)²)²
155//
156// * `T` = 𝐭 = the tangent direction = the direction of increased roughness.
157//
158// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness.
159//
160// * `at` = αₜ = the alpha-roughness in the tangent direction.
161//
162// * `ab` = α_b = the alpha-roughness in the bitangent direction.
163//
164// This is from the `KHR_materials_anisotropy` spec:
165// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#individual-lights>
166fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 {
167 let a2 = at * ab;
168 let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH);
169 let w2 = a2 / dot(f, f);
170 let d = a2 * w2 * w2 * (1.0 / PI);
171 return d;
174// Visibility function (Specular G)
175// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) }
176// such that f_r becomes
177// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0)
178// where
179// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) }
180// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv
181fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 {
182 let a2 = roughness * roughness;
183 let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2);
184 let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2);
185 let v = 0.5 / (lambdaV + lambdaL);
186 return v;
189// The visibility function, anisotropic variant.
190fn V_GGX_anisotropic(
191 at: f32,
192 ab: f32,
193 NdotL: f32,
194 NdotV: f32,
195 BdotV: f32,
196 TdotV: f32,
197 TdotL: f32,
198 BdotL: f32,
199) -> f32 {
200 let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV));
201 let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL));
202 let v = 0.5 / (GGX_V + GGX_L);
203 return saturate(v);
206// Probability-density function that matches the bounded VNDF sampler
207// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 2)
208fn ggx_vndf_pdf(i: vec3<f32>, NdotH: f32, roughness: f32) -> f32 {
209 let ndf = D_GGX(roughness, NdotH);
211 // Common terms
212 let ai = roughness * i.xy;
213 let len2 = dot(ai, ai);
214 let t = sqrt(len2 + i.z * i.z);
215 if i.z >= 0.0 {
216 let a = roughness;
217 let s = 1.0 + length(i.xy);
218 let a2 = a * a;
219 let s2 = s * s;
220 let k = (1.0 - a2) * s2 / (s2 + a2 * i.z * i.z);
221 return ndf / (2.0 * (k * i.z + t));
222 }
224 // Backfacing case
225 return ndf * (t - i.z) / (2.0 * len2);
228// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1)
229fn sample_visible_ggx(
230 xi: vec2<f32>,
231 roughness: f32,
232 normal: vec3<f32>,
233 view: vec3<f32>,
234) -> vec3<f32> {
235 let n = normal;
236 let alpha = roughness;
238 // Decompose view into components parallel/perpendicular to the normal
239 let wi_n = dot(view, n);
240 let wi_z = -n * wi_n;
241 let wi_xy = view + wi_z;
243 // Warp view vector to the unit-roughness configuration
244 let wi_std = -normalize(alpha * wi_xy + wi_z);
246 // Compute wi_std.z once for reuse
247 let wi_std_z = dot(wi_std, n);
249 // Bounded VNDF sampling
250 // Compute the bound parameter k (Eq. 5) and the scaled z–limit b (Eq. 6)
251 let s = 1.0 + length(wi_xy);
252 let a = clamp(alpha, 0.0, 1.0);
253 let a2 = a * a;
254 let s2 = s * s;
255 let k = (1.0 - a2) * s2 / (s2 + a2 * wi_n * wi_n);
256 let b = select(wi_std_z, k * wi_std_z, wi_n > 0.0);
258 // Sample a spherical cap in (-b, 1]
259 let z = 1.0 - xi.y * (1.0 + b);
260 let sin_theta = sqrt(max(0.0, 1.0 - z * z));
261 let phi = 2.0 * PI * xi.x - PI;
262 let x = sin_theta * cos(phi);
263 let y = sin_theta * sin(phi);
264 let c_std = vec3f(x, y, z);
266 // Reflect the sample so that the normal aligns with +Z
267 let up = vec3f(0.0, 0.0, 1.0);
268 let wr = n + up;
269 let c = dot(wr, c_std) * wr / wr.z - c_std;
271 // Half-vector in the standard frame
272 let wm_std = c + wi_std;
273 let wm_std_z = n * dot(n, wm_std);
274 let wm_std_xy = wm_std_z - wm_std;
276 // Unwarp back to original roughness and compute microfacet normal
277 let H = normalize(alpha * wm_std_xy + wm_std_z);
279 // Reflect view to obtain the outgoing (light) direction
280 return reflect(-view, H);
283// Smith geometric shadowing function
284fn G_Smith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 {
285 let k = roughness / 2.0;
286 let GGXL = NdotL / (NdotL * (1.0 - k) + k);
287 let GGXV = NdotV / (NdotV * (1.0 - k) + k);
288 return GGXL * GGXV;
291// A simpler, but nonphysical, alternative to Smith-GGX. We use this for
292// clearcoat, per the Filament spec.
293//
294// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1
295fn V_Kelemen(LdotH: f32) -> f32 {
296 return 0.25 / (LdotH * LdotH);
299// Fresnel function
300// see https://google.github.io/filament/Filament.html#citation-schlick94
301// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5
302fn F_Schlick_vec(f0: vec3<f32>, f90: f32, VdotH: f32) -> vec3<f32> {
303 // not using mix to keep the vec3 and float versions identical
304 return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
307fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 {
308 // not using mix to keep the vec3 and float versions identical
309 return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0);
312fn fresnel(f0: vec3<f32>, LdotH: f32) -> vec3<f32> {
313 // f_90 suitable for ambient occlusion
314 // see https://google.github.io/filament/Filament.html#lighting/occlusion
315 let f90 = saturate(dot(f0, vec3<f32>(50.0 * 0.33)));
316 return F_Schlick_vec(f0, f90, LdotH);
319// Given distribution, visibility, and Fresnel term, calculates the final
320// specular light.
321//
322// Multiscattering approximation:
323// <https://google.github.io/filament/Filament.html#listing_energycompensationimpl>
324fn specular_multiscatter(
325 D: f32,
326 V: f32,
327 F: vec3<f32>,
328 F0: vec3<f32>,
329 F_ab: vec2<f32>,
330 specular_intensity: f32,
331) -> vec3<f32> {
332 var Fr = (specular_intensity * D * V) * F;
333 Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0);
334 return Fr;
337// Specular BRDF
338// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf
340// N, V, and L must all be normalized.
341fn derive_lighting_input(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>) -> DerivedLightingInput {
342 var input: DerivedLightingInput;
343 var H: vec3<f32> = normalize(L + V);
344 input.H = H;
345 input.NdotL = saturate(dot(N, L));
346 input.NdotH = saturate(dot(N, H));
347 input.LdotH = saturate(dot(L, H));
348 return input;
351// Returns L in the `xyz` components and the modified roughness in the `w` component.
352fn compute_specular_layer_values_for_point_light(
353 input: ptr<function, LightingInput>,
354 layer: u32,
355 V: vec3<f32>,
356 light_to_frag: vec3<f32>,
357 light_radius: f32,
358 distance: f32,
359) -> vec4<f32> {
360 // Unpack.
361 let R = (*input).layers[layer].R;
362 let a = (*input).layers[layer].roughness;
364 // Representative Point Area Lights.
365 // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
366 var LtFdotR = dot(light_to_frag, R);
368 // HACK: the following line is an amendment to fix a discontinuity when a surface
369 // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318
370 //
371 // This sentence in the reference is crux of the problem: "We approximate finding the point with the
372 // smallest angle to the reflection ray by finding the point with the smallest distance to the ray."
373 // This approximation turns out to be completely wrong for points inside or near the sphere.
374 // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it.
375 // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero.
376 // However, this is still far from physically accurate. Deriving an exact solution would help,
377 // but really we should adopt a superior solution to area lighting, such as:
378 // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al.
379 LtFdotR = max(0.0001, LtFdotR);
381 let centerToRay = LtFdotR * R - light_to_frag;
382 let closestPoint = light_to_frag + centerToRay * saturate(
383 light_radius * inverseSqrt(dot(centerToRay, centerToRay)));
384 let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
386 // Karis 2013, page 14. The constant 2 (or 3) is hand tuned to fit reference.
387 // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
388 let a_prime = saturate(a + light_radius / (2.0 * distance));
390 let L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
391 return vec4(L, a_prime);
394// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
395// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) }
396fn specular(
397 input: ptr<function, LightingInput>,
398 derived_input: ptr<function, DerivedLightingInput>,
399 roughness: f32,
400 specular_intensity: f32,
401) -> vec3<f32> {
402 // Unpack.
403 let NdotV = (*input).layers[LAYER_BASE].NdotV;
404 let F0 = (*input).F0_;
405 let NdotL = (*derived_input).NdotL;
406 let NdotH = (*derived_input).NdotH;
407 let LdotH = (*derived_input).LdotH;
409 // Calculate distribution.
410 let D = D_GGX(roughness, NdotH);
411 // Calculate visibility.
412 let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL);
413 // Calculate the Fresnel term.
414 let F = fresnel(F0, LdotH);
416 // Calculate the specular light.
417 let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity);
418 return Fr;
421// Calculates the specular light for the clearcoat layer. Returns Fc, the
422// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in
423// the second channel.
424//
425// <https://google.github.io/filament/Filament.html#listing_clearcoatbrdf>
426fn specular_clearcoat(
427 input: ptr<function, LightingInput>,
428 derived_input: ptr<function, DerivedLightingInput>,
429 clearcoat_strength: f32,
430 roughness: f32,
431 specular_intensity: f32,
432) -> vec2<f32> {
433 // Unpack.
434 let NdotH = (*derived_input).NdotH;
435 let LdotH = (*derived_input).LdotH;
437 // Calculate distribution.
438 let Dc = D_GGX(roughness, NdotH);
439 // Calculate visibility.
440 let Vc = V_Kelemen(LdotH);
441 // Calculate the Fresnel term.
442 let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength;
443 // Calculate the specular light.
444 let Frc = (specular_intensity * Dc * Vc) * Fc;
445 return vec2(Fc, Frc);
448#ifdef STANDARD_MATERIAL_ANISOTROPY
450fn specular_anisotropy(
451 input: ptr<function, LightingInput>,
452 derived_input: ptr<function, DerivedLightingInput>,
453 L: vec3<f32>,
454 roughness: f32,
455 specular_intensity: f32,
456) -> vec3<f32> {
457 // Unpack.
458 let NdotV = (*input).layers[LAYER_BASE].NdotV;
459 let V = (*input).V;
460 let F0 = (*input).F0_;
461 let anisotropy = (*input).anisotropy;
462 let Ta = (*input).Ta;
463 let Ba = (*input).Ba;
464 let H = (*derived_input).H;
465 let NdotL = (*derived_input).NdotL;
466 let NdotH = (*derived_input).NdotH;
467 let LdotH = (*derived_input).LdotH;
469 let TdotL = dot(Ta, L);
470 let BdotL = dot(Ba, L);
471 let TdotH = dot(Ta, H);
472 let BdotH = dot(Ba, H);
473 let TdotV = dot(Ta, V);
474 let BdotV = dot(Ba, V);
476 let ab = roughness * roughness;
477 let at = mix(ab, 1.0, anisotropy * anisotropy);
479 let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH);
480 let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL);
481 let Fa = fresnel(F0, LdotH);
483 // Calculate the specular light.
484 let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity);
485 return Fr;
488#endif // STANDARD_MATERIAL_ANISOTROPY
490// Diffuse BRDF
491// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf
492// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm
493//
494// simplest approximation
495// float Fd_Lambert() {
496// return 1.0 / PI;
497// }
498//
499// vec3 Fd = diffuseColor * Fd_Lambert();
500//
501// Disney approximation
502// See https://google.github.io/filament/Filament.html#citation-burley12
503// minimal quality difference
504fn Fd_Burley(
505 input: ptr<function, LightingInput>,
506 derived_input: ptr<function, DerivedLightingInput>,
507) -> f32 {
508 // Unpack.
509 let roughness = (*input).layers[LAYER_BASE].roughness;
510 let NdotV = (*input).layers[LAYER_BASE].NdotV;
511 let NdotL = (*derived_input).NdotL;
512 let LdotH = (*derived_input).LdotH;
514 let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH;
515 let lightScatter = F_Schlick(1.0, f90, NdotL);
516 let viewScatter = F_Schlick(1.0, f90, NdotV);
517 return lightScatter * viewScatter * (1.0 / PI);
520// Scale/bias approximation
521// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
522// TODO: Use a LUT (more accurate)
523fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
524 let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
525 let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
526 let r = perceptual_roughness * c0 + c1;
527 let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
528 return vec2<f32>(-1.04, 1.04) * a004 + r.zw;
531fn EnvBRDFApprox(F0: vec3<f32>, F_ab: vec2<f32>) -> vec3<f32> {
532 return F0 * F_ab.x + F_ab.y;
535fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 {
536 // clamp perceptual roughness to prevent precision problems
537 // According to Filament design 0.089 is recommended for mobile
538 // Filament uses 0.045 for non-mobile
539 let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0);
540 return clampedPerceptualRoughness * clampedPerceptualRoughness;
543// this must align with CubemapLayout in decal/clustered.rs
544const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0;
545const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1;
546const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2;
547const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3;
549const X_PLUS: u32 = 0;
550const X_MINUS: u32 = 1;
551const Y_PLUS: u32 = 2;
552const Y_MINUS: u32 = 3;
553const Z_MINUS: u32 = 4;
554const Z_PLUS: u32 = 5;
556fn cubemap_uv(direction: vec3<f32>, cubemap_type: u32) -> vec2<f32> {
557 let abs_direction = abs(direction);
558 let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z));
560 let face_index = select(
561 select(X_PLUS, X_MINUS, direction.x < 0.0),
562 select(
563 select(Y_PLUS, Y_MINUS, direction.y < 0.0),
564 select(Z_PLUS, Z_MINUS, direction.z < 0.0),
565 max_axis != abs_direction.y
566 ),
567 max_axis != abs_direction.x
568 );
570 var face_uv: vec2<f32>;
571 var divisor: f32;
572 var corner_uv: vec2<u32> = vec2(0, 0);
573 var face_size: vec2<f32>;
575 switch face_index {
576 case X_PLUS: { face_uv = vec2<f32>(direction.z, -direction.y); divisor = direction.x; }
577 case X_MINUS: { face_uv = vec2<f32>(-direction.z, -direction.y); divisor = -direction.x; }
578 case Y_PLUS: { face_uv = vec2<f32>(direction.x, -direction.z); divisor = direction.y; }
579 case Y_MINUS: { face_uv = vec2<f32>(direction.x, direction.z); divisor = -direction.y; }
580 case Z_PLUS: { face_uv = vec2<f32>(direction.x, direction.y); divisor = direction.z; }
581 case Z_MINUS: { face_uv = vec2<f32>(direction.x, -direction.y); divisor = -direction.z; }
582 default: {}
583 }
584 face_uv = (face_uv / divisor) * 0.5 + 0.5;
586 switch cubemap_type {
587 case CUBEMAP_TYPE_CROSS_VERTICAL: {
588 face_size = vec2(1.0/3.0, 1.0/4.0);
589 corner_uv = vec2<u32>((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu);
590 }
591 case CUBEMAP_TYPE_CROSS_HORIZONTAL: {
592 face_size = vec2(1.0/4.0, 1.0/3.0);
593 corner_uv = vec2<u32>((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu);
594 }
595 case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: {
596 face_size = vec2(1.0/6.0, 1.0);
597 corner_uv.x = face_index;
598 }
599 case CUBEMAP_TYPE_SEQUENCE_VERTICAL: {
600 face_size = vec2(1.0, 1.0/6.0);
601 corner_uv.y = face_index;
602 }
603 default: {}
604 }
606 return (vec2<f32>(corner_uv) + face_uv) * face_size;
609// This is a modification to Karis 2013 for area lights to fix an issue where the specular
610// reflection on smooth materials looks too rough and dim. We lerp between the base roughness
611// and Karis2013 roughness with a lerp factor tuned by looking at reference renders. The goal
612// is to preserve sharp specular highlights on smooth materials, without blowing out specular
613// highlights on rough materials.
614//
615// The ideal solution is to switch to Linearly Transformed Cosines, which is more accurate in
616// all cases, and a popular choice for realtime.
617fn specular_fix_remap(a: f32) -> f32 {
618 let inv_a_sq = (1.0 - a) * (1.0 - a);
619 return 1.0 - inv_a_sq * inv_a_sq;
622fn point_light(
623 light_id: u32,
624 input: ptr<function, LightingInput>,
625 enable_diffuse: bool,
626 enable_texture: bool,
627) -> vec3<f32> {
628 // Unpack.
629 let diffuse_color = (*input).diffuse_color;
630 let P = (*input).P;
631 let N = (*input).layers[LAYER_BASE].N;
632 let V = (*input).V;
634 let light = &view_bindings::clusterable_objects.data[light_id];
635 let light_to_frag = (*light).position_radius.xyz - P;
636 let L = normalize(light_to_frag);
637 let distance_square = dot(light_to_frag, light_to_frag);
638 let distance = sqrt(distance_square);
639 let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w);
641 // Base layer
643 let a = (*input).layers[LAYER_BASE].roughness;
644 let specular_L_a_prime = compute_specular_layer_values_for_point_light(
645 input,
646 LAYER_BASE,
647 V,
648 light_to_frag,
649 (*light).position_radius.w,
650 distance,
651 );
652 let L_spec = specular_L_a_prime.xyz;
653 let a_prime = specular_L_a_prime.w;
654 var specular_derived_input = derive_lighting_input(N, V, L_spec);
656 let normalizationFactor = a / a_prime;
657 let specular_intensity = normalizationFactor * normalizationFactor;
659 let brdf_roughness = mix(a, a_prime, specular_fix_remap(a));
661#ifdef STANDARD_MATERIAL_ANISOTROPY
662 var specular_light = specular_anisotropy(input, &specular_derived_input, L, brdf_roughness, specular_intensity);
663#else // STANDARD_MATERIAL_ANISOTROPY
664 var specular_light = specular(input, &specular_derived_input, brdf_roughness, specular_intensity);
665#endif // STANDARD_MATERIAL_ANISOTROPY
667 // Sphere area light visibility (solid-angle attenuation)
668 let light_radius = (*light).position_radius.w;
669 if light_radius > 0.0 {
670 let solid_angle = light_radius * light_radius / (distance * distance);
671 specular_light *= saturate(specular_derived_input.NdotL / max(specular_derived_input.NdotL + solid_angle, 1e-4));
672 }
674 // Clearcoat
676#ifdef STANDARD_MATERIAL_CLEARCOAT
677 // Unpack.
678 let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
679 let clearcoat_strength = (*input).clearcoat_strength;
680 let clearcoat_a = (*input).layers[LAYER_CLEARCOAT].roughness;
682 // Perform specular input calculations again for the clearcoat layer. We
683 // can't reuse the above because the clearcoat normal might be different
684 // from the main layer normal.
685 let clearcoat_specular_L_a_prime = compute_specular_layer_values_for_point_light(
686 input,
687 LAYER_CLEARCOAT,
688 V,
689 light_to_frag,
690 (*light).position_radius.w,
691 distance,
692 );
693 let L_clearcoat_spec = clearcoat_specular_L_a_prime.xyz;
694 let clearcoat_a_prime = clearcoat_specular_L_a_prime.w;
695 var clearcoat_specular_derived_input =
696 derive_lighting_input(clearcoat_N, V, L_clearcoat_spec);
698 // Calculate the specular light.
699 let clearcoat_normalizationFactor = clearcoat_a / clearcoat_a_prime;
701 let clearcoat_specular_intensity = clearcoat_normalizationFactor * clearcoat_normalizationFactor;
703 let clearcoat_brdf_roughness = mix(clearcoat_a, clearcoat_a_prime, specular_fix_remap(clearcoat_a));
705 let Fc_Frc = specular_clearcoat(
706 input,
707 &clearcoat_specular_derived_input,
708 clearcoat_strength,
709 clearcoat_brdf_roughness,
710 clearcoat_specular_intensity
711 );
712 let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term.
713 var Frc = Fc_Frc.g; // Clearcoat light.
715 // Sphere area light visibility (solid-angle attenuation) for clearcoat
716 if light_radius > 0.0 {
717 let solid_angle = light_radius * light_radius / (distance * distance);
718 Frc *= saturate(clearcoat_specular_derived_input.NdotL / max(clearcoat_specular_derived_input.NdotL + solid_angle, 1e-4));
719 }
720#endif // STANDARD_MATERIAL_CLEARCOAT
722 // Diffuse.
723 // Comes after specular since its N⋅L is used in the lighting equation.
724 var derived_input = derive_lighting_input(N, V, L);
725 var diffuse = vec3(0.0);
726 if (enable_diffuse) {
727 diffuse = diffuse_color * Fd_Burley(input, &derived_input);
728 }
730 // See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation
731 // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩
732 // where
733 // f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color
734 // Φ is luminous power in lumens
735 // our rangeAttenuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius
737 // For a point light, luminous intensity, I, in lumens per steradian is given by:
738 // I = Φ / 4 π
739 // The derivation of this can be seen here: https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
741 // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU
743 var color_times_NdotL: vec3<f32>;
744#ifdef STANDARD_MATERIAL_CLEARCOAT
745 // Account for the Fresnel term from the clearcoat darkening the main layer.
746 //
747 // <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
748 color_times_NdotL = (diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL * inv_Fc) * inv_Fc + Frc * clearcoat_specular_derived_input.NdotL;
749#else // STANDARD_MATERIAL_CLEARCOAT
750 color_times_NdotL = diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL;
751#endif // STANDARD_MATERIAL_CLEARCOAT
753 var texture_sample = 1f;
755#ifdef LIGHT_TEXTURES
756 if enable_texture && (*light).decal_index != 0xFFFFFFFFu {
757 let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz;
758 let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag;
759 let decal_uv = cubemap_uv(relative_position, cubemap_type);
760 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
762 texture_sample = textureSampleLevel(
763 view_bindings::clustered_decal_textures[image_index],
764 view_bindings::clustered_decal_sampler,
765 decal_uv,
766 0.0
767 ).r;
768 }
769#endif
771 return color_times_NdotL * (*light).color_inverse_square_range.rgb *
772 rangeAttenuation * texture_sample;
775fn spot_light(
776 light_id: u32,
777 input: ptr<function, LightingInput>,
778 enable_diffuse: bool
779) -> vec3<f32> {
780 // reuse the point light calculations
781 let point_light = point_light(light_id, input, enable_diffuse, false);
783 let light = &view_bindings::clusterable_objects.data[light_id];
785 // reconstruct spot dir from x/z and y-direction flag
786 var spot_dir = vec3<f32>((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y);
787 spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z));
788 if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u {
789 spot_dir.y = -spot_dir.y;
790 }
791 let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz;
793 // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight
794 // spot_scale and spot_offset have been precomputed
795 // note we normalize here to get "l" from the filament listing. spot_dir is already normalized
796 let cd = dot(-spot_dir, normalize(light_to_frag));
797 let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w);
798 let spot_attenuation = attenuation * attenuation;
800 var texture_sample = 1f;
802#ifdef LIGHT_TEXTURES
803 if (*light).decal_index != 0xFFFFFFFFu {
804 let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world *
805 vec4((*input).P, 1.0)).xyz;
806 if local_position.z < 0.0 {
807 let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5;
808 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
810 texture_sample = textureSampleLevel(
811 view_bindings::clustered_decal_textures[image_index],
812 view_bindings::clustered_decal_sampler,
813 decal_uv,
814 0.0
815 ).r;
816 }
817 }
818#endif
820 return point_light * spot_attenuation * texture_sample;
823fn directional_light(
824 light_id: u32,
825 input: ptr<function, LightingInput>,
826 enable_diffuse: bool
827) -> vec3<f32> {
828 // Unpack.
829 let diffuse_color = (*input).diffuse_color;
830 let NdotV = (*input).layers[LAYER_BASE].NdotV;
831 let N = (*input).layers[LAYER_BASE].N;
832 let V = (*input).V;
833 let roughness = (*input).layers[LAYER_BASE].roughness;
835 let light = &view_bindings::lights.directional_lights[light_id];
837 let L = (*light).direction_to_light.xyz;
838 var derived_input = derive_lighting_input(N, V, L);
840 var diffuse = vec3(0.0);
841 if (enable_diffuse) {
842 diffuse = diffuse_color * Fd_Burley(input, &derived_input);
843 }
845#ifdef STANDARD_MATERIAL_ANISOTROPY
846 let specular_light = specular_anisotropy(input, &derived_input, L, roughness, 1.0);
847#else // STANDARD_MATERIAL_ANISOTROPY
848 let specular_light = specular(input, &derived_input, roughness, 1.0);
849#endif // STANDARD_MATERIAL_ANISOTROPY
851#ifdef STANDARD_MATERIAL_CLEARCOAT
852 let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
853 let clearcoat_strength = (*input).clearcoat_strength;
854 let clearcoat_roughness = (*input).layers[LAYER_CLEARCOAT].roughness;
856 // Perform specular input calculations again for the clearcoat layer. We
857 // can't reuse the above because the clearcoat normal might be different
858 // from the main layer normal.
859 var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L);
861 let Fc_Frc =
862 specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, clearcoat_roughness, 1.0);
863 let inv_Fc = 1.0 - Fc_Frc.r;
864 let Frc = Fc_Frc.g;
865#endif // STANDARD_MATERIAL_CLEARCOAT
867 var color: vec3<f32>;
868#ifdef STANDARD_MATERIAL_CLEARCOAT
869 // Account for the Fresnel term from the clearcoat darkening the main layer.
870 //
871 // <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
872 color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL +
873 Frc * derived_clearcoat_input.NdotL;
874#else // STANDARD_MATERIAL_CLEARCOAT
875 color = (diffuse + specular_light) * derived_input.NdotL;
876#endif // STANDARD_MATERIAL_CLEARCOAT
878 var texture_sample = 1f;
880#ifdef LIGHT_TEXTURES
881 if (*light).decal_index != 0xFFFFFFFFu {
882 let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world *
883 vec4((*input).P, 1.0)).xyz;
884 let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5;
886 // if tiled or within tile
887 if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u)
888 || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv)
889 {
890 let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index;
892 texture_sample = textureSampleLevel(
893 view_bindings::clustered_decal_textures[image_index],
894 view_bindings::clustered_decal_sampler,
895 decal_uv - floor(decal_uv),
896 0.0
897 ).r;
898 } else {
899 texture_sample = 0f;
900 }
901 }
902#endif
904color *= (*light).color.rgb * texture_sample;
906#ifdef ATMOSPHERE
907 let P = (*input).P;
908 let atmosphere = view_bindings::atmosphere_data.atmosphere;
909 let O = vec3(0.0, atmosphere.bottom_radius, 0.0);
910 let P_scaled = P * vec3(view_bindings::atmosphere_data.settings.scene_units_to_m);
911 let P_as = P_scaled + O;
912 let P_clamped = clamp_to_surface(atmosphere, P_as);
913 let r = length(P_clamped);
914 let local_up = normalize(P_clamped);
915 let mu_light = dot(L, local_up);
917 // Sample atmosphere
918 let transmittance = sample_transmittance_lut(r, mu_light);
919 let sun_visibility = calculate_visible_sun_ratio(atmosphere, r, mu_light, (*light).sun_disk_angular_size);
921 // Apply atmospheric effects
922 color *= transmittance * sun_visibility;
923#endif
925 return color;
928#ifdef ATMOSPHERE
929fn sample_transmittance_lut(r: f32, mu: f32) -> vec3<f32> {
930 let uv = transmittance_lut_r_mu_to_uv(view_bindings::atmosphere_data.atmosphere, r, mu);
931 return textureSampleLevel(
932 view_bindings::atmosphere_transmittance_texture,
933 view_bindings::atmosphere_transmittance_sampler, uv, 0.0).rgb;
935#endif // ATMOSPHERE
Contents