From 11bf3a44937ea942f996e6090ba07f6b4aaddeca Mon Sep 17 00:00:00 2001 From: Filament Bot Date: Wed, 28 Jan 2026 07:56:16 +0000 Subject: [PATCH] [automated] Updating /docs due to commit 7c64fb9 Full commit hash is 7c64fb9cf3e5af36b7c1b4efa5e0a514b974e62a DOCS_ALLOW_DIRECT_EDITS --- docs/main/filament.html | 398 ++++++++++++++++++++-------------------- docs/wip/sky/index.html | 4 +- docs/wip/sky/main.js | 163 ++++++++++------ 3 files changed, 310 insertions(+), 255 deletions(-) diff --git a/docs/main/filament.html b/docs/main/filament.html index d90e4c9eb9..eb304edd9c 100644 --- a/docs/main/filament.html +++ b/docs/main/filament.html @@ -504,9 +504,9 @@ D_{GGX}(h,\alpha) = \frac{\aa}{\pi ( (\NoH)^2 (\aa - 1) + 1)^2} \end{equation}$$

The GLSL implementation of the NDF, shown in listing 1, is simple and efficient. -

float D_GGX(float NoH, float roughness) {
-    float a = NoH * roughness;
-    float k = roughness / (1.0 - NoH * NoH + a * a);
+

float D_GGX(float NoH, float roughness) {
+    float a = NoH * roughness;
+    float k = roughness / (1.0 - NoH * NoH + a * a);
     return k * k * (1.0 / PI);
 }
 Listing 1: Implementation of the specular D term in GLSL

@@ -590,10 +590,10 @@ V(v,l,\alpha) = \frac{0.5}{\NoL \sqrt{(\NoV)^2 (1 - \aa) + \aa} + \NoV \sqrt{(\N \end{equation}$$

The GLSL implementation of the visibility term, shown in listing 3, is a bit more expensive than we would like since it requires two sqrt operations. -

float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
-    float a2 = roughness * roughness;
-    float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
-    float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
+

float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) {
+    float a2 = roughness * roughness;
+    float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
+    float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
     return 0.5 / (GGXV + GGXL);
 }
 Listing 3: Implementation of the specular V term in GLSL

@@ -604,10 +604,10 @@ V(v,l,\alpha) = \frac{0.5}{\NoL (\NoV (1 - \alpha) + \alpha) + \NoV (\NoL (1 - \ \end{equation}$$

This approximation is mathematically wrong but saves two square root operations and is good enough for real-time mobile applications, as shown in listing 4. -

float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) {
-    float a = roughness;
-    float GGXV = NoL * (NoV * (1.0 - a) + a);
-    float GGXL = NoV * (NoL * (1.0 - a) + a);
+

float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) {
+    float a = roughness;
+    float GGXV = NoL * (NoV * (1.0 - a) + a);
+    float GGXL = NoV * (NoL * (1.0 - a) + a);
     return 0.5 / (GGXV + GGXL);
 }
 Listing 4: Implementation of the approximated specular V term in GLSL

@@ -659,7 +659,7 @@ $$\begin{equation} \end{equation}$$

In practice, the diffuse reflectance \(\sigma\) is multiplied later, as shown in listing 8. -

float Fd_Lambert() {
+

float Fd_Lambert() {
     return 1.0 / PI;
 }
 
@@ -680,14 +680,14 @@ Where:
 $$\begin{equation}
 \fGrazing=0.5 + 2 \cdot \alpha cos^2(\theta_d)
 \end{equation}$$
-

float F_Schlick(float u, float f0, float f90) {
-    return f0 + (f90 - f0) * pow(1.0 - u, 5.0);
+

float F_Schlick(float u, float f0, float f90) {
+    return f0 + (f90 - f0) * pow(1.0 - u, 5.0);
 }
 
-float Fd_Burley(float NoV, float NoL, float LoH, float roughness) {
-    float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
-    float lightScatter = F_Schlick(NoL, 1.0, f90);
-    float viewScatter = F_Schlick(NoV, 1.0, f90);
+float Fd_Burley(float NoV, float NoL, float LoH, float roughness) {
+    float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
+    float lightScatter = F_Schlick(NoL, 1.0, f90);
+    float viewScatter = F_Schlick(NoV, 1.0, f90);
     return lightScatter * viewScatter * (1.0 / PI);
 }
 Listing 8: Implementation of the diffuse Disney BRDF in GLSL

@@ -704,47 +704,47 @@ We could allow artists/developers to choose the Disney diffuse BRDF depending on Diffuse term: a Lambertian diffuse model.

The full GLSL implementation of the standard model is shown in listing 9. -

float D_GGX(float NoH, float a) {
-    float a2 = a * a;
-    float f = (NoH * a2 - NoH) * NoH + 1.0;
+

float D_GGX(float NoH, float a) {
+    float a2 = a * a;
+    float f = (NoH * a2 - NoH) * NoH + 1.0;
     return a2 / (PI * f * f);
 }
 
-vec3 F_Schlick(float u, vec3 f0) {
-    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
+vec3 F_Schlick(float u, vec3 f0) {
+    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
 }
 
-float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
-    float a2 = a * a;
-    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
-    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
+float V_SmithGGXCorrelated(float NoV, float NoL, float a) {
+    float a2 = a * a;
+    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
+    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
     return 0.5 / (GGXV + GGXL);
 }
 
-float Fd_Lambert() {
+float Fd_Lambert() {
     return 1.0 / PI;
 }
 
-void BRDF(...) {
-    vec3 h = normalize(v + l);
+void BRDF(...) {
+    vec3 h = normalize(v + l);
 
-    float NoV = abs(dot(n, v)) + 1e-5;
-    float NoL = clamp(dot(n, l), 0.0, 1.0);
-    float NoH = clamp(dot(n, h), 0.0, 1.0);
-    float LoH = clamp(dot(l, h), 0.0, 1.0);
+    float NoV = abs(dot(n, v)) + 1e-5;
+    float NoL = clamp(dot(n, l), 0.0, 1.0);
+    float NoH = clamp(dot(n, h), 0.0, 1.0);
+    float LoH = clamp(dot(l, h), 0.0, 1.0);
 
     // perceptually linear roughness to roughness (see parameterization)
-    float roughness = perceptualRoughness * perceptualRoughness;
+    float roughness = perceptualRoughness * perceptualRoughness;
 
-    float D = D_GGX(NoH, roughness);
-    vec3  F = F_Schlick(LoH, f0);
-    float V = V_SmithGGXCorrelated(NoV, NoL, roughness);
+    float D = D_GGX(NoH, roughness);
+    vec3  F = F_Schlick(LoH, f0);
+    float V = V_SmithGGXCorrelated(NoV, NoL, roughness);
 
     // specular BRDF
-    vec3 Fr = (D * V) * F;
+    vec3 Fr = (D * V) * F;
 
     // diffuse BRDF
-    vec3 Fd = diffuseColor * Fd_Lambert();
+    vec3 Fd = diffuseColor * Fd_Lambert();
 
     // apply lighting...
 }
 Listing 9: Evaluation of the BRDF in GLSL
@@ -965,7 +965,7 @@ $$\begin{equation} \end{equation}$$

Listing 12 shows how \(\fNormal\) is computed for both dielectric and metallic materials. It shows that the color of the specular reflectance is derived from the base color in the metallic case. -

vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;
 Listing 12: Computing \(\fNormal\) for dielectric and metallic materials in GLSL
+

vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;
 Listing 12: Computing \(\fNormal\) for dielectric and metallic materials in GLSL
   

Roughness remapping and clamping

The roughness set by the user, called perceptualRoughness here, is remapped to a perceptually linear range using the following formulation:

@@ -1054,7 +1054,7 @@ V(l,h) = \frac{1}{4(\LoH)^2} This masking-shadowing function is not physically based, as shown in [Heitz14], but its simplicity makes it desirable for real-time rendering.

In summary, our clear coat BRDF is a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Kelemen visibility function, and a Schlick Fresnel function. Listing 13 shows how trivial the GLSL implementation is. -

float V_Kelemen(float LoH) {
+

float V_Kelemen(float LoH) {
     return 0.25 / (LoH * LoH);
 }
 Listing 13: Implementation of the Kelemen visibility term in GLSL

@@ -1097,18 +1097,18 @@ The clear coat roughness parameter is remapped and clamped in a similar way to t

 Figure 26: Clear coat roughness varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0, roughness to 0.8 and clear coat to 1.0

Listing 14 shows the GLSL implementation of the clear coat material model after remapping, parameterization and integration in the standard surface response. -

void BRDF(...) {
+

void BRDF(...) {
     // compute Fd and Fr from standard model
 
     // remapping and linearization of clear coat roughness
-    clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0);
+    clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0);
     clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness;
 
     // clear coat BRDF
-    float  Dc = D_GGX(clearCoatRoughness, NoH);
-    float  Vc = V_Kelemen(clearCoatRoughness, LoH);
-    float  Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength
-    float Frc = (Dc * Vc) * Fc;
+    float  Dc = D_GGX(clearCoatRoughness, NoH);
+    float  Vc = V_Kelemen(clearCoatRoughness, LoH);
+    float  Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength
+    float Frc = (Dc * Vc) * Fc;
 
     // account for energy loss in the base layer
     return color * ((Fd + Fr * (1.0 - Fc)) * (1.0 - Fc) + Frc);
@@ -1294,14 +1294,14 @@ f_{r}(v,h,\alpha) = \frac{D_{velvet}(v,h,\alpha)}{4(\NoL + \NoV - (\NoL)(\NoV))}
 \end{equation}$$
 

The implementation of the velvet NDF is presented in listing 17, optimized to properly fit in half float formats and to avoid computing a costly cotangent, relying instead on trigonometric identities. Note that we removed the Fresnel component from this BRDF. -

float D_Ashikhmin(float roughness, float NoH) {
+

float D_Ashikhmin(float roughness, float NoH) {
     // Ashikhmin 2007, "Distribution-based BRDFs"
- float a2 = roughness * roughness;
- float cos2h = NoH * NoH;
- float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
- float sin4h = sin2h * sin2h;
- float cot2 = -cos2h / (a2 * sin2h);
- return 1.0 / (PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h);
+ float a2 = roughness * roughness;
+ float cos2h = NoH * NoH;
+ float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
+ float sin4h = sin2h * sin2h;
+ float cot2 = -cos2h / (a2 * sin2h);
+ return 1.0 / (PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h);
 }
 Listing 17: Implementation of Ashikhmin's velvet NDF in GLSL

In [Estevez17] Estevez and Kulla propose a different NDF (called the “Charlie” sheen) that is based on an exponentiated sinusoidal instead of an inverted Gaussian. This NDF is appealing for several reasons: its parameterization feels more natural and intuitive, it provides a softer appearance and, as shown in equation (\ref{charlieNDF}), its implementation is simpler:

@@ -1312,11 +1312,11 @@ D(m) = \frac{(2 + \frac{1}{\alpha}) sin(\theta)^{\frac{1}{\alpha}}}{2 \pi}

[Estevez17] also presents a new shadowing term that we omit here because of its cost. We instead rely on the visibility term from [Neubelt13] (shown in equation \(\ref{clothSpecularBRDF}\) above). The implementation of this NDF is presented in listing 18, optimized to properly fit in half float formats. -

float D_Charlie(float roughness, float NoH) {
+

float D_Charlie(float roughness, float NoH) {
     // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF"
-    float invAlpha  = 1.0 / roughness;
-    float cos2h = NoH * NoH;
-    float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
+    float invAlpha  = 1.0 / roughness;
+    float cos2h = NoH * NoH;
+    float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
     return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);
 }
 Listing 18: Implementation of the “Charlie” NDF in GLSL
   

Sheen color

@@ -1745,21 +1745,21 @@ The photometric attenuation function can be easily implemented in GLSL by adding }
 Listing 22: Implementation of attenuation from photometric profiles in GLSL

The light intensity is computed CPU-side (listing 23) and depends on whether the photometric profile is used as a mask.

-

float multiplier;
+

float multiplier;
 // Photometric profile used as a mask
-if (photometricLight.isMasked()) {
+if (photometricLight.isMasked()) {
     // The desired intensity is set by the artist
     // The integrated intensity comes from a Monte-Carlo
     // integration over the unit sphere around the luminaire
-    multiplier = photometricLight.getDesiredIntensity() /
-            photometricLight.getIntegratedIntensity();
+    multiplier = photometricLight.getDesiredIntensity() /
+            photometricLight.getIntegratedIntensity();
 } else {
     // Multiplier provided for convenience, set to 1.0 by default
-    multiplier = photometricLight.getMultiplier();
+    multiplier = photometricLight.getMultiplier();
 }
 
 // The max intensity in cd comes from the IES profile
-float lightIntensity = photometricLight.getMaxIntensity() * multiplier;
 Listing 23: Computing the intensity of a photometric light on the CPU
+float lightIntensity = photometricLight.getMaxIntensity() * multiplier;
 Listing 23: Computing the intensity of a photometric light on the CPU

 4 The XArrow profile declares a luminous intensity of 1,750 lm but a Monte-Carlo integration shows an intensity of only 350 lm.
@@ -2058,19 +2058,19 @@ In practice only 4 or 9 coefficients (i.e.: 2 or 3 bands) are enough for \(\cosT
 Figure 52: 2 bands (4 coefficients)

In practice we pre-convolve \(\Lt\) with \(\cosTheta\) and pre-scale these coefficients by the basis scaling factors \(K_l^m\) so that the reconstruction code is as simple as possible in the shader: -

vec3 irradianceSH(vec3 n) {
-    // uniform vec3 sphericalHarmonics[9]
-    // We can use only the first 2 bands for better performance
-    return
-          sphericalHarmonics[0]
-        + sphericalHarmonics[1] * (n.y)
-        + sphericalHarmonics[2] * (n.z)
-        + sphericalHarmonics[3] * (n.x)
-        + sphericalHarmonics[4] * (n.y * n.x)
-        + sphericalHarmonics[5] * (n.y * n.z)
-        + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0)
-        + sphericalHarmonics[7] * (n.z * n.x)
-        + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y);
+

vec3 irradianceSH(vec3 n) {
+    // uniform vec3 sphericalHarmonics[9]
+    // We can use only the first 2 bands for better performance
+    return
+          sphericalHarmonics[0]
+        + sphericalHarmonics[1] * (n.y)
+        + sphericalHarmonics[2] * (n.z)
+        + sphericalHarmonics[3] * (n.x)
+        + sphericalHarmonics[4] * (n.y * n.x)
+        + sphericalHarmonics[5] * (n.y * n.z)
+        + sphericalHarmonics[6] * (3.0 * n.z * n.z - 1.0)
+        + sphericalHarmonics[7] * (n.z * n.x)
+        + sphericalHarmonics[8] * (n.x * n.x - n.y * n.y);
 }
 Listing 26: GLSL code to reconstruct the irradiance from the pre-scaled SH

Note that with 2 bands, the computation above becomes a single (4 \times 4) matrix-by-vector multiply.

@@ -2443,7 +2443,7 @@ LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n, $$

These two new \(DFG\) terms simply need to replace the ones used in the implementation shown in section 9.5: -

float Fc = pow(1 - VoH, 5.0f);
+

float Fc = pow(1 - VoH, 5.0f);
 r.x += Gv * Fc;
 r.y += Gv;
 Listing 29: C++ implementation of the \(L_{DFG}\) term for multiscattering

@@ -2507,11 +2507,11 @@ using an environment made of colored vertical stripes (skybox hidden).

When sampling the IBL, the clear coat layer is calculated as a second specular lobe. This specular lobe is oriented along the view direction since we cannot reasonably integrate over the hemisphere. Listing 31 demonstrates this approximation in practice. It also shows the energy conservation step. It is important to note that this second specular lobe is computed exactly the same way as the main specular lobe, using the same DFG approximation.

// clearCoat_NoV == shading_NoV if the clear coat layer doesn't have its own normal map
-float Fc = F_Schlick(0.04, 1.0, clearCoat_NoV) * clearCoat;
+float Fc = F_Schlick(0.04, 1.0, clearCoat_NoV) * clearCoat;
 // base layer attenuation for energy compensation
 iblDiffuse  *= 1.0 - Fc;
-iblSpecular *= sq(1.0 - Fc);
-iblSpecular += specularIBL(r, clearCoatPerceptualRoughness) * Fc;
 Listing 31: GLSL implementation of the clear coat specular lobe for image-based lighting
+iblSpecular *= sq(1.0 - Fc); +iblSpecular += specularIBL(r, clearCoatPerceptualRoughness) * Fc;
 Listing 31: GLSL implementation of the clear coat specular lobe for image-based lighting
   

Anisotropy

[McAuley15] describes a technique called “bent reflection vector”, based [Revie12]. The bent reflection vector is a rough approximation of anisotropic lighting but the alternative is to use importance sampling. This approximation is sufficiently cheap to compute and provides good results, as shown in figure 59 and figure 60.

@@ -2550,17 +2550,17 @@ The DG term is generated using uniform sampling as recommended in [
 Figure 62: DFG LUT with a 3rd channel encoding the DG term of the cloth BRDF

The remainder of the image-based lighting implementation follows the same steps as the implementation of regular lights, including the optional subsurface scattering term and its wrap diffuse component. Just as with the clear coat IBL implementation, we cannot integrate over the hemisphere and use the view direction as the dominant light direction to compute the wrap diffuse component. -

float diffuse = Fd_Lambert() * ambientOcclusion;
-#if defined(SHADING_MODEL_CLOTH)
-#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
-diffuse *= saturate((NoV + 0.5) / 2.25);
-#endif
-#endif
+

float diffuse = Fd_Lambert() * ambientOcclusion;
+#if defined(SHADING_MODEL_CLOTH)
+#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+diffuse *= saturate((NoV + 0.5) / 2.25);
+#endif
+#endif
 
-vec3 indirectDiffuse = irradianceIBL(n) * diffuse;
-#if defined(SHADING_MODEL_CLOTH) && defined(MATERIAL_HAS_SUBSURFACE_COLOR)
-indirectDiffuse *= saturate(subsurfaceColor + NoV);
-#endif
+vec3 indirectDiffuse = irradianceIBL(n) * diffuse;
+#if defined(SHADING_MODEL_CLOTH) && defined(MATERIAL_HAS_SUBSURFACE_COLOR)
+indirectDiffuse *= saturate(subsurfaceColor + NoV);
+#endif
 
 vec3 ibl = diffuseColor * indirectDiffuse + indirectSpecular * specularColor;
 Listing 34: GLSL implementation of the DFG approximation for the cloth NDF

@@ -2982,20 +2982,20 @@ L_{max} &= 2^{EV_{100}} \times 1.2 // aperture in f-stops // shutterSpeed in seconds // sensitivity in ISO -float exposureSettings(float aperture, float shutterSpeed, float sensitivity) { +float exposureSettings(float aperture, float shutterSpeed, float sensitivity) { return log2((aperture * aperture) / shutterSpeed * 100.0 / sensitivity); } // Computes the exposure normalization factor from // the camera's EV100 -float exposure(float ev100) { - return 1.0 / (pow(2.0, ev100) * 1.2); +float exposure(float ev100) { + return 1.0 / (pow(2.0, ev100) * 1.2); } -float ev100 = exposureSettings(aperture, shutterSpeed, sensitivity); -float exposure = exposure(ev100); +float ev100 = exposureSettings(aperture, shutterSpeed, sensitivity); +float exposure = exposure(ev100); -vec4 color = evaluateLighting(); +vec4 color = evaluateLighting(); color.rgb *= exposure;

 Listing 42: Implementation of exposure in GLSL

In practice the exposure factor can be pre-computed on the CPU to save shader instructions.

@@ -3466,9 +3466,9 @@ Our implementation is presented in listing& // Data source: // http://cvrl.ioo.ucl.ac.uk/cmfs.htm // http://cvrl.ioo.ucl.ac.uk/database/text/cmfs/ciexyz31.htm -const size_t CIE_XYZ_START = 360; -const size_t CIE_XYZ_COUNT = 471; -const float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... }; +const size_t CIE_XYZ_START = 360; +const size_t CIE_XYZ_COUNT = 471; +const float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... }; // CIE Standard Illuminant D65 relative spectral power distribution, // from 300nm to 830, at 5nm intervals @@ -3476,51 +3476,51 @@ Our implementation is presented in listing& // Data source: // https://en.wikipedia.org/wiki/Illuminant_D65 // https://cielab.xyz/pdf/CIE_sel_colorimetric_tables.xls -const size_t CIE_D65_INTERVAL = 5; -const size_t CIE_D65_START = 300; -const size_t CIE_D65_END = 830; -const size_t CIE_D65_COUNT = 107; -const float CIE_D65[CIE_D65_COUNT] = { ... }; +const size_t CIE_D65_INTERVAL = 5; +const size_t CIE_D65_START = 300; +const size_t CIE_D65_END = 830; +const size_t CIE_D65_COUNT = 107; +const float CIE_D65[CIE_D65_COUNT] = { ... }; -struct Sample { - float w = 0.0f; // wavelength - std::complex<float> ior; // complex IOR, n + ik +struct Sample { + float w = 0.0f; // wavelength + std::complex<float> ior; // complex IOR, n + ik }; -static float illuminantD65(float w) { - auto i0 = size_t((w - CIE_D65_START) / CIE_D65_INTERVAL); - uint2 indexBounds{i0, std::min(i0 + 1, CIE_D65_END)}; +static float illuminantD65(float w) { + auto i0 = size_t((w - CIE_D65_START) / CIE_D65_INTERVAL); + uint2 indexBounds{i0, std::min(i0 + 1, CIE_D65_END)}; float2 wavelengthBounds = CIE_D65_START + float2{indexBounds} * CIE_D65_INTERVAL; - float t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x); - return lerp(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t); + float t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x); + return lerp(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t); } // For std::lower_bound -bool operator<(const Sample& lhs, const Sample& rhs) { +bool operator<(const Sample& lhs, const Sample& rhs) { return lhs.w < rhs.w; } // The wavelength w must be between 360nm and 830nm -static std::complex<float> findSample(const std::vector<sample>& samples, float w) { - auto i1 = std::lower_bound( - samples.begin(), samples.end(), Sample{w, 0.0f + 0.0if}); +static std::complex<float> findSample(const std::vector<sample>& samples, float w) { + auto i1 = std::lower_bound( + samples.begin(), samples.end(), Sample{w, 0.0f + 0.0if}); auto i0 = i1 - 1; // Interpolate the complex IORs - float t = (w - i0->w) / (i1->w - i0->w); - float n = lerp(i0->ior.real(), i1->ior.real(), t); - float k = lerp(i0->ior.imag(), i1->ior.imag(), t); + float t = (w - i0->w) / (i1->w - i0->w); + float n = lerp(i0->ior.real(), i1->ior.real(), t); + float k = lerp(i0->ior.imag(), i1->ior.imag(), t); return { n, k }; } -static float fresnel(const std::complex<float>& sample) { - return (((sample - (1.0f + 0if)) * (std::conj(sample) - (1.0f + 0if))) / - ((sample + (1.0f + 0if)) * (std::conj(sample) + (1.0f + 0if)))).real(); +static float fresnel(const std::complex<float>& sample) { + return (((sample - (1.0f + 0if)) * (std::conj(sample) - (1.0f + 0if))) / + ((sample + (1.0f + 0if)) * (std::conj(sample) + (1.0f + 0if)))).real(); } -static float3 XYZ_to_sRGB(const float3& v) { - const mat3f XYZ_sRGB{ +static float3 XYZ_to_sRGB(const float3& v) { + const mat3f XYZ_sRGB{ 3.2404542f, -0.9692660f, 0.0556434f, -1.5371385f, 1.8760108f, -0.2040259f, -0.4985314f, 0.0415560f, 1.0572252f @@ -3529,21 +3529,21 @@ Our implementation is presented in listing& } // Outputs a linear sRGB color -static float3 computeColor(const std::vector<sample>& samples) { +static float3 computeColor(const std::vector<sample>& samples) { float3 xyz{0.0f}; - float y = 0.0f; + float y = 0.0f; - for (size_t i = 0; i < CIE_XYZ_COUNT; i++) { + for (size_t i = 0; i < CIE_XYZ_COUNT; i++) { // Current wavelength - float w = CIE_XYZ_START + i; + float w = CIE_XYZ_START + i; // Find most appropriate CIE XYZ sample for the wavelength - auto sample = findSample(samples, w); + auto sample = findSample(samples, w); // Compute Fresnel reflectance at normal incidence - float f0 = fresnel(sample); + float f0 = fresnel(sample); // We need to multiply by the spectral power distribution of the illuminant - float d65 = illuminantD65(w); + float d65 = illuminantD65(w); xyz += f0 * CIE_XYZ[i] * d65; y += CIE_XYZ[i].y * d65; @@ -3552,10 +3552,10 @@ Our implementation is presented in listing& // Normalize so that 100% reflectance at every wavelength yields Y=1 xyz /= y; - float3 linear = XYZ_to_sRGB(xyz); + float3 linear = XYZ_to_sRGB(xyz); // Normalize out-of-gamut values - if (any(greaterThan(linear, float3{1.0f}))) linear *= 1.0f / max(linear); + if (any(greaterThan(linear, float3{1.0f}))) linear *= 1.0f / max(linear); return linear; }
 Listing 46: C++ implementation to compute the base color of a metallic surface from spectral data
@@ -3735,47 +3735,47 @@ l &= \{ cos\phi sin\theta,sin\phi sin\theta,cos\theta \}
vec2f hammersley(uint i, float numSamples) {
     uint bits = i;
     bits = (bits << 16) | (bits >> 16);
-    bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
-    bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
-    bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
-    bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
-    return vec2f(i / numSamples, bits / exp2(32));
+    bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
+    bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
+    bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
+    bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
+    return vec2f(i / numSamples, bits / exp2(32));
 }
C++ implementation of a Hammersley sequence generator
   

Precomputing L for image-based lighting

The term ( L_{DFG} ) is only dependent on ( \NoV ). Below, the normal is arbitrarily set to ( n=\left[0, 0, 1\right] ) and (v) is chosen to satisfy ( \NoV ). The vector ( h_i ) is the ( D_{GGX}(\alpha) ) important direction sample (i).

-

float GDFG(float NoV, float NoL, float a) {
-    float a2 = a * a;
-    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
-    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
+

float GDFG(float NoV, float NoL, float a) {
+    float a2 = a * a;
+    float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
+    float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
     return (2 * NoL) / (GGXV + GGXL);
 }
 
-float2 DFG(float NoV, float a) {
+float2 DFG(float NoV, float a) {
     float3 V;
-    V.x = sqrt(1.0f - NoV*NoV);
-    V.y = 0.0f;
+    V.x = sqrt(1.0f - NoV*NoV);
+    V.y = 0.0f;
     V.z = NoV;
 
-    float2 r = 0.0f;
-    for (uint i = 0; i < sampleCount; i++) {
-        float2 Xi = hammersley(i, sampleCount);
-        float3 H = importanceSampleGGX(Xi, a, N);
-        float3 L = 2.0f * dot(V, H) * H - V;
+    float2 r = 0.0f;
+    for (uint i = 0; i < sampleCount; i++) {
+        float2 Xi = hammersley(i, sampleCount);
+        float3 H = importanceSampleGGX(Xi, a, N);
+        float3 L = 2.0f * dot(V, H) * H - V;
 
-        float VoH = saturate(dot(V, H));
-        float NoL = saturate(L.z);
-        float NoH = saturate(H.z);
+        float VoH = saturate(dot(V, H));
+        float NoL = saturate(L.z);
+        float NoH = saturate(H.z);
 
-        if (NoL > 0.0f) {
-            float G = GDFG(NoV, NoL, a);
-            float Gv = G * VoH / NoH;
-            float Fc = pow(1 - VoH, 5.0f);
+        if (NoL > 0.0f) {
+            float G = GDFG(NoV, NoL, a);
+            float Gv = G * VoH / NoH;
+            float Fc = pow(1 - VoH, 5.0f);
             r.x += Gv * (1 - Fc);
             r.y += Gv * Fc;
         }
     }
-    return r * (1.0f / sampleCount);
+    return r * (1.0f / sampleCount);
 }
C++ implementation of the \( L_{DFG} \) term
   

Spherical Harmonics

@@ -3851,56 +3851,56 @@ sin(m \phi + \phi) &= sin(m \phi) cos(\phi) + cos(m \phi) sin(\phi) \Leftrightar \end{align*}$$

Listing 47 shows the C++ code to compute the non-normalized SH basis \(\frac{y^m_l(s)}{\sqrt{2} K^m_l}\): -

static inline size_t SHindex(ssize_t m, size_t l) {
+

static inline size_t SHindex(ssize_t m, size_t l) {
     return l * (l + 1) + m;
 }
 
-void computeShBasis(
-        double* const SHb,
-        size_t numBands,
-        const vec3& s)
+void computeShBasis(
+        double* const SHb,
+        size_t numBands,
+        const vec3& s)
 {
     // handle m=0 separately, since it produces only one coefficient
-    double Pml_2 = 0;
-    double Pml_1 = 1;
+    double Pml_2 = 0;
+    double Pml_1 = 1;
     SHb[0] =  Pml_1;
-    for (ssize_t l = 1; l < numBands; l++) {
-        double Pml = ((2 * l - 1) * Pml_1 * s.z - (l - 1) * Pml_2) / l;
+    for (ssize_t l = 1; l < numBands; l++) {
+        double Pml = ((2 * l - 1) * Pml_1 * s.z - (l - 1) * Pml_2) / l;
         Pml_2 = Pml_1;
         Pml_1 = Pml;
-        SHb[SHindex(0, l)] = Pml;
+        SHb[SHindex(0, l)] = Pml;
     }
-    double Pmm = 1;
-    for (ssize_t m = 1; m < numBands ; m++) {
+    double Pmm = 1;
+    for (ssize_t m = 1; m < numBands ; m++) {
         Pmm = (1 - 2 * m) * Pmm;
-        double Pml_2 = Pmm;
-        double Pml_1 = (2 * m + 1)*Pmm*s.z;
+        double Pml_2 = Pmm;
+        double Pml_1 = (2 * m + 1)*Pmm*s.z;
         // l == m
-        SHb[SHindex(-m, m)] = Pml_2;
-        SHb[SHindex( m, m)] = Pml_2;
+        SHb[SHindex(-m, m)] = Pml_2;
+        SHb[SHindex( m, m)] = Pml_2;
         if (m + 1 < numBands) {
             // l == m+1
-            SHb[SHindex(-m, m + 1)] = Pml_1;
-            SHb[SHindex( m, m + 1)] = Pml_1;
-            for (ssize_t l = m + 2; l < numBands; l++) {
-                double Pml = ((2 * l - 1) * Pml_1 * s.z - (l + m - 1) * Pml_2)
+            SHb[SHindex(-m, m + 1)] = Pml_1;
+            SHb[SHindex( m, m + 1)] = Pml_1;
+            for (ssize_t l = m + 2; l < numBands; l++) {
+                double Pml = ((2 * l - 1) * Pml_1 * s.z - (l + m - 1) * Pml_2)
                         / (l - m);
                 Pml_2 = Pml_1;
                 Pml_1 = Pml;
-                SHb[SHindex(-m, l)] = Pml;
-                SHb[SHindex( m, l)] = Pml;
+                SHb[SHindex(-m, l)] = Pml;
+                SHb[SHindex( m, l)] = Pml;
             }
         }
     }
-    double Cm = s.x;
-    double Sm = s.y;
-    for (ssize_t m = 1; m <= numBands ; m++) {
-        for (ssize_t l = m; l < numBands ; l++) {
-            SHb[SHindex(-m, l)] *= Sm;
-            SHb[SHindex( m, l)] *= Cm;
+    double Cm = s.x;
+    double Sm = s.y;
+    for (ssize_t m = 1; m <= numBands ; m++) {
+        for (ssize_t l = m; l < numBands ; l++) {
+            SHb[SHindex(-m, l)] *= Sm;
+            SHb[SHindex( m, l)] *= Cm;
         }
-        double Cm1 = Cm * s.x - Sm * s.y;
-        double Sm1 = Sm * s.x + Cm * s.y;
+        double Cm1 = Cm * s.x - Sm * s.y;
+        double Sm1 = Sm * s.x + Cm * s.y;
         Cm = Cm1;
         Sm = Sm1;
     }
@@ -3979,10 +3979,10 @@ $$\begin{equation}
 \end{equation}$$
 

Here is the C++ code to compute \(\hat{C}_l\): -

static double factorial(size_t n, size_t d = 1);
+

static double factorial(size_t n, size_t d = 1);
 
 // < cos(theta) > SH coefficients pre-multiplied by 1 / K(0,l)
-double computeTruncatedCosSh(size_t l) {
+double computeTruncatedCosSh(size_t l) {
     if (l == 0) {
         return M_PI;
     } else if (l == 1) {
@@ -3990,17 +3990,17 @@ Here is the C++ code to compute \(\hat{C}_l\):
     } else if (l & 1) {
         return 0;
     }
-    const size_t l_2 = l / 2;
-    double A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1));
-    double A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l));
+    const size_t l_2 = l / 2;
+    double A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1));
+    double A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l));
     return 2 * M_PI * A0 * A1;
 }
 
 // returns n! / d!
-double factorial(size_t n, size_t d ) {
-   d = std::max(size_t(1), d);
-   n = std::max(size_t(1), n);
-   double r = 1.0;
+double factorial(size_t n, size_t d ) {
+   d = std::max(size_t(1), d);
+   n = std::max(size_t(1), n);
+   double r = 1.0;
    if (n == d) {
        // intentionally left blank
    } else if (n > d) {
diff --git a/docs/wip/sky/index.html b/docs/wip/sky/index.html
index 8717228449..c8207efc64 100644
--- a/docs/wip/sky/index.html
+++ b/docs/wip/sky/index.html
@@ -32,8 +32,8 @@
   
 
   
-  
-  
+  
+  
 
 
 
\ No newline at end of file
diff --git a/docs/wip/sky/main.js b/docs/wip/sky/main.js
index c64c97a772..82b0bf1c2c 100644
--- a/docs/wip/sky/main.js
+++ b/docs/wip/sky/main.js
@@ -130,8 +130,8 @@ class App {
 
     const sunFolder = gui.addFolder('Sun');
     // Helper for "Sun Height" cosine slider like C++
-    const sunHeightParam = { height: Math.cos(this.params.sunTheta) };
-    sunFolder.add(sunHeightParam, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
+    this.sunHeightParam = { height: Math.cos(this.params.sunTheta) };
+    sunFolder.add(this.sunHeightParam, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
       this.params.sunTheta = Math.acos(v);
       updateSun();
     });
@@ -141,14 +141,14 @@ class App {
 
     const sunDisk = sunFolder.addFolder('Disk');
     // We need local proxy for sunRadius due to conversion
-    const diskParams = {
+    this.diskParams = {
       radius: 1.2,
       enabled: true // Enable sun disk
     };
     sky.setSunDiskEnabled(true);
     sky.setSunRadius(1.2);
-    sunDisk.add(diskParams, 'enabled').onChange(v => sky.setSunDiskEnabled(v));
-    sunDisk.add(diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
+    sunDisk.add(this.diskParams, 'enabled').onChange(v => sky.setSunDiskEnabled(v));
+    sunDisk.add(this.diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
     sunDisk.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
     sunDisk.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
 
@@ -313,67 +313,122 @@ class App {
   }
 
   getURLState() {
-    // Serialize current state
+    // Serialize current state (Minified)
+    // Mapping:
+    // p: params (Camera) -> a:aperture, ss:shutterSpeed, i:iso, st:sunTheta, sp:sunPhi, fl:focalLength, si:sunIntensity
+    // c: cParams (Clouds) -> v:volumetrics, co:coverage, d:density, h:height, s:speed, e:evolution
+    // w: wParams (Water) -> dt:derivativeTrick, st:strength, s:speed, o:octaves
+    // s: sParams (Stars) -> e:enabled, d:density
+    // b: bParams (Bloom) -> e:enabled, lf:lensFlare
+    // k: sky (Skybox) -> t:turbidity, r:rayleigh, mc:mieCoefficient, mg:mieG, o:ozone, ms:msFactors, co:contrast, nc:nightColor, sh:shimmerControl, hl:sunHalo
+
+    const p = this.params;
+    const c = this.cParams;
+    const w = this.wParams;
+    const s = this.sParams;
+    const b = this.bParams;
+    const sk = this.skybox;
+
     return {
-      params: { ...this.params },
-      cParams: { ...this.cParams },
-      wParams: { ...this.wParams },
-      sParams: { ...this.sParams },
-      bParams: { ...this.bParams },
-      // Skybox direct parameters
-      sky: {
-        turbidity: this.skybox.turbidity,
-        rayleigh: this.skybox.rayleigh,
-        mieCoefficient: this.skybox.mieCoefficient,
-        mieG: this.skybox.mieG,
-        ozone: this.skybox.ozone,
-        msFactors: [...this.skybox.msFactors],
-        contrast: this.skybox.contrast,
-        nightColor: [...this.skybox.nightColor],
-        shimmerControl: [...this.skybox.shimmerControl],
-        sunHalo: [...this.skybox.sunHalo],
-        sunDirection: [...this.skybox.sunDirection]
+      p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
+      c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
+      w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
+      s: { e: s.enabled, d: s.density },
+      b: { e: b.enabled, lf: b.lensFlare },
+      k: {
+        t: sk.turbidity,
+        r: sk.rayleigh,
+        mc: sk.mieCoefficient,
+        mg: sk.mieG,
+        o: sk.ozone,
+        ms: [...sk.msFactors],
+        co: sk.contrast,
+        nc: [...sk.nightColor],
+        sh: [...sk.shimmerControl],
+        hl: [...sk.sunHalo]
       }
     };
   }
 
   applyURLState(state) {
-    if (state.params) Object.assign(this.params, state.params);
-    if (state.cParams) Object.assign(this.cParams, state.cParams);
-    if (state.wParams) Object.assign(this.wParams, state.wParams);
-    if (state.sParams) Object.assign(this.sParams, state.sParams);
-    if (state.bParams) Object.assign(this.bParams, state.bParams);
+    const p = state.p;
+    const c = state.c;
+    const w = state.w;
+    const s = state.s;
+    const b = state.b;
+    const k = state.k;
+
+    if (p) {
+      if (p.a !== undefined) this.params.aperture = p.a;
+      if (p.ss !== undefined) this.params.shutterSpeed = p.ss;
+      if (p.i !== undefined) this.params.iso = p.i;
+      if (p.st !== undefined) this.params.sunTheta = p.st;
+      if (p.sp !== undefined) this.params.sunPhi = p.sp;
+      if (p.fl !== undefined) this.params.focalLength = p.fl;
+      if (p.si !== undefined) this.params.sunIntensity = p.si;
+    }
+
+    if (c) {
+      if (c.v !== undefined) this.cParams.volumetrics = c.v;
+      if (c.co !== undefined) this.cParams.coverage = c.co;
+      if (c.d !== undefined) this.cParams.density = c.d;
+      if (c.h !== undefined) this.cParams.height = c.h;
+      if (c.s !== undefined) this.cParams.speed = c.s;
+      if (c.e !== undefined) this.cParams.evolution = c.e;
+    }
+
+    if (w) {
+      if (w.dt !== undefined) this.wParams.derivativeTrick = w.dt;
+      if (w.st !== undefined) this.wParams.strength = w.st;
+      if (w.s !== undefined) this.wParams.speed = w.s;
+      if (w.o !== undefined) this.wParams.octaves = w.o;
+    }
+
+    if (s) {
+      if (s.e !== undefined) this.sParams.enabled = s.e;
+      if (s.d !== undefined) this.sParams.density = s.d;
+    }
+
+    if (b) {
+      if (b.e !== undefined) this.bParams.enabled = b.e;
+      if (b.lf !== undefined) this.bParams.lensFlare = b.lf;
+    }
 
     const sky = this.skybox;
-    if (state.sky) {
-      sky.setTurbidity(state.sky.turbidity);
-      sky.setRayleigh(state.sky.rayleigh);
-      sky.setMieCoefficient(state.sky.mieCoefficient);
-      sky.setMieG(state.sky.mieG);
-      sky.setOzone(state.sky.ozone);
-      sky.setMultiScattering(state.sky.msFactors[0], state.sky.msFactors[1]);
-      sky.setHorizonGlow(state.sky.msFactors[2]);
-      sky.setContrast(state.sky.contrast);
-      sky.setNightColor(state.sky.nightColor);
-      sky.setShimmerControl(state.sky.shimmerControl[0], state.sky.shimmerControl[1], state.sky.shimmerControl[2]);
+    if (k) {
+      if (k.t !== undefined) sky.setTurbidity(k.t);
+      if (k.r !== undefined) sky.setRayleigh(k.r);
+      if (k.mc !== undefined) sky.setMieCoefficient(k.mc);
+      if (k.mg !== undefined) sky.setMieG(k.mg);
+      if (k.o !== undefined) sky.setOzone(k.o);
+
+      if (k.ms) { sky.setMultiScattering(k.ms[0], k.ms[1]); sky.setHorizonGlow(k.ms[2]); }
+      if (k.co !== undefined) sky.setContrast(k.co);
+      if (k.nc) sky.setNightColor(k.nc);
+      if (k.sh) sky.setShimmerControl(k.sh[0], k.sh[1], k.sh[2]);
+
+      // Sun Halo
+      const savedHalo = k.hl;
+      if (savedHalo && savedHalo.length === 4) {
+        sky.sunHalo[0] = savedHalo[0];
+        sky.sunHalo[1] = savedHalo[1];
+        sky.sunHalo[2] = savedHalo[2];
+        sky.sunHalo[3] = savedHalo[3];
+
+        // Update derived UI params for Sun Disk
+        const rad = Math.acos(Math.max(-1.0, Math.min(1.0, savedHalo[0])));
+        this.diskParams.radius = rad * (180.0 / Math.PI);
+        this.diskParams.enabled = savedHalo[3] > 0.5;
+      }
 
-      // Sun Halo (Radius, Limb, Intensity, Enabled)
-      sky.sunHalo = state.sky.sunHalo; // Direct assign or setters? 
-      // Setters are better but we have composite array. 
-      // sky.setSunDiskEnabled(sky.sunHalo[3] > 0.5); 
-      // Actually existing setters update individual components.
-      sky.setSunLimbDarkening(state.sky.sunHalo[1]);
-      sky.setSunDiskIntensity(state.sky.sunHalo[2]);
-      sky.setSunDiskEnabled(state.sky.sunHalo[3] > 0.5);
-      // Radius is trickier, it was set via setSunRadius(degrees). 
-      // We can just send the updated halo directly if we want, but setSunRadius is nice.
-      // Inverse cos to get rads? 
-      // Let's just trust the float array if we updateCoefficients.
-      // BUT updateCoefficients uses sky.sunHalo. So direct assign is fine + update.
-      sky.sunHalo = [...state.sky.sunHalo];
       sky.updateCoefficients();
     }
 
+    // Update derived Sun Height param for UI
+    if (this.sunHeightParam) {
+      this.sunHeightParam.height = Math.cos(this.params.sunTheta);
+    }
+
     // Apply Local Params via Setters
     sky.setCloudControl(this.cParams.coverage, this.cParams.density, this.cParams.height, this.cParams.speed);
     sky.setCloudVolumetricLighting(this.cParams.volumetrics);