Prechádzať zdrojové kódy

Merge pull request #6202 from sebavan/master

Fix IBL Diffuse
David Catuhe 6 rokov pred
rodič
commit
0c93dfc0d5

+ 1 - 0
dist/preview release/what's new.md

@@ -37,6 +37,7 @@
   - Added anisotropy support to PBR ([Sebavan](https://github.com/Sebavan)) **** NEED DEMO or DOC LINK)
   - Added sheen support to PBR ([Sebavan](https://github.com/Sebavan)) **** NEED DEMO or DOC LINK)
   - Added sub-surface support to PBR ([Sebavan](https://github.com/Sebavan)) **** NEED DEMO or DOC LINK)
+  - Added SH Harmonics support to PBR ([Sebavan](https://github.com/Sebavan)) **** NEED DEMO or DOC LINK)
 - Added a STL exporter ([pryme8](https://github.com/pryme8))
 
 ## Optimizations

+ 3 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/pbrMaterialPropertyGridComponent.tsx

@@ -259,6 +259,9 @@ export class PBRMaterialPropertyGridComponent extends React.Component<IPBRMateri
                     <CheckBoxLineComponent label="Energy Conservation" target={material.brdf} propertyName="useEnergyConservation"
                         onValueChanged={() => this.forceUpdate()}
                         onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    <CheckBoxLineComponent label="Spherical Harmonics" target={material.brdf} propertyName="useSphericalHarmonics"
+                        onValueChanged={() => this.forceUpdate()}
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <CheckBoxLineComponent label="Radiance occlusion" target={material} propertyName="useRadianceOcclusion" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <CheckBoxLineComponent label="Horizon occlusion " target={material} propertyName="useHorizonOcclusion" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <CheckBoxLineComponent label="Unlit" target={material} propertyName="unlit" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />

+ 1 - 1
loaders/src/glTF/2.0/Extensions/EXT_lights_image_based.ts

@@ -130,7 +130,7 @@ export class EXT_lights_image_based implements IGLTFLoaderExtension {
                 }
 
                 const sphericalHarmonics = SphericalHarmonics.FromArray(light.irradianceCoefficients);
-                sphericalHarmonics.scale(light.intensity);
+                sphericalHarmonics.scaleInPlace(light.intensity);
 
                 sphericalHarmonics.convertIrradianceToLambertianRadiance();
                 const sphericalPolynomial = SphericalPolynomial.FromHarmonics(sphericalHarmonics);

+ 22 - 1
src/Materials/PBR/pbrBRDFConfiguration.ts

@@ -6,6 +6,7 @@ import { SerializationHelper, serialize, expandToProperty } from "../../Misc/dec
 export interface IMaterialBRDFDefines {
     BRDF_V_HEIGHT_CORRELATED: boolean;
     MS_BRDF_ENERGY_CONSERVATION: boolean;
+    SPHERICAL_HARMONICS: boolean;
 
     /** @hidden */
     _areMiscDirty: boolean;
@@ -28,6 +29,13 @@ export class PBRBRDFConfiguration {
      */
     public static DEFAULT_USE_SMITH_VISIBILITY_HEIGHT_CORRELATED = true;
 
+    /**
+     * Default value used for the IBL diffuse part.
+     * This can help switching back to the polynomials mode globally which is a tiny bit
+     * less GPU intensive at the drawback of a lower quality.
+     */
+    public static DEFAULT_USE_SPHERICAL_HARMONICS = true;
+
     @serialize()
     private _useEnergyConservation = PBRBRDFConfiguration.DEFAULT_USE_ENERGY_CONSERVATION;
     /**
@@ -49,6 +57,18 @@ export class PBRBRDFConfiguration {
     @expandToProperty("_markAllSubMeshesAsMiscDirty")
     public useSmithVisibilityHeightCorrelated = PBRBRDFConfiguration.DEFAULT_USE_SMITH_VISIBILITY_HEIGHT_CORRELATED;
 
+    @serialize()
+    private _useSphericalHarmonics = PBRBRDFConfiguration.DEFAULT_USE_SPHERICAL_HARMONICS;
+    /**
+     * LEGACY Mode set to false
+     * Defines if the material uses spherical harmonics vs spherical polynomials for the
+     * diffuse part of the IBL.
+     * The harmonics despite a tiny bigger cost has been proven to provide closer results
+     * to the ground truth.
+     */
+    @expandToProperty("_markAllSubMeshesAsMiscDirty")
+    public useSphericalHarmonics = PBRBRDFConfiguration.DEFAULT_USE_SPHERICAL_HARMONICS;
+
     /** @hidden */
     private _internalMarkAllSubMeshesAsMiscDirty: () => void;
 
@@ -72,6 +92,7 @@ export class PBRBRDFConfiguration {
     public prepareDefines(defines: IMaterialBRDFDefines): void {
         defines.BRDF_V_HEIGHT_CORRELATED = this._useSmithVisibilityHeightCorrelated;
         defines.MS_BRDF_ENERGY_CONSERVATION = this._useEnergyConservation && this._useSmithVisibilityHeightCorrelated;
+        defines.SPHERICAL_HARMONICS = this._useSphericalHarmonics;
     }
 
     /**
@@ -79,7 +100,7 @@ export class PBRBRDFConfiguration {
     * @returns "PBRClearCoatConfiguration"
     */
     public getClassName(): string {
-        return "PBRClearCoatConfiguration";
+        return "PBRBRDFConfiguration";
     }
 
     /**

+ 31 - 13
src/Materials/PBR/pbrBaseMaterial.ts

@@ -129,6 +129,7 @@ export class PBRMaterialDefines extends MaterialDefines
     public REFLECTIONMAP_MIRROREDEQUIRECTANGULAR_FIXED = false;
     public INVERTCUBICMAP = false;
     public USESPHERICALFROMREFLECTIONMAP = false;
+    public SPHERICAL_HARMONICS = false;
     public USESPHERICALINVERTEX = false;
     public REFLECTIONMAP_OPPOSITEZ = false;
     public LODINREFLECTIONALPHA = false;
@@ -1171,6 +1172,9 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             "vSphericalX", "vSphericalY", "vSphericalZ",
             "vSphericalXX", "vSphericalYY", "vSphericalZZ",
             "vSphericalXY", "vSphericalYZ", "vSphericalZX",
+            "vSphericalL00",
+            "vSphericalL1_1", "vSphericalL10", "vSphericalL11",
+            "vSphericalL2_2", "vSphericalL2_1", "vSphericalL20", "vSphericalL21", "vSphericalL22",
             "vReflectionMicrosurfaceInfos",
             "vTangentSpaceParams", "boneTextureWidth",
             "vDebugMode"
@@ -1669,19 +1673,33 @@ export abstract class PBRBaseMaterial extends PushMaterial {
 
                         var polynomials = reflectionTexture.sphericalPolynomial;
                         if (defines.USESPHERICALFROMREFLECTIONMAP && polynomials) {
-                            this._activeEffect.setFloat3("vSphericalX", polynomials.x.x, polynomials.x.y, polynomials.x.z);
-                            this._activeEffect.setFloat3("vSphericalY", polynomials.y.x, polynomials.y.y, polynomials.y.z);
-                            this._activeEffect.setFloat3("vSphericalZ", polynomials.z.x, polynomials.z.y, polynomials.z.z);
-                            this._activeEffect.setFloat3("vSphericalXX_ZZ", polynomials.xx.x - polynomials.zz.x,
-                                polynomials.xx.y - polynomials.zz.y,
-                                polynomials.xx.z - polynomials.zz.z);
-                            this._activeEffect.setFloat3("vSphericalYY_ZZ", polynomials.yy.x - polynomials.zz.x,
-                                polynomials.yy.y - polynomials.zz.y,
-                                polynomials.yy.z - polynomials.zz.z);
-                            this._activeEffect.setFloat3("vSphericalZZ", polynomials.zz.x, polynomials.zz.y, polynomials.zz.z);
-                            this._activeEffect.setFloat3("vSphericalXY", polynomials.xy.x, polynomials.xy.y, polynomials.xy.z);
-                            this._activeEffect.setFloat3("vSphericalYZ", polynomials.yz.x, polynomials.yz.y, polynomials.yz.z);
-                            this._activeEffect.setFloat3("vSphericalZX", polynomials.zx.x, polynomials.zx.y, polynomials.zx.z);
+                            if (defines.SPHERICAL_HARMONICS) {
+                                const preScaledHarmonics = polynomials.preScaledHarmonics;
+                                this._activeEffect.setVector3("vSphericalL00", preScaledHarmonics.l00);
+                                this._activeEffect.setVector3("vSphericalL1_1", preScaledHarmonics.l1_1);
+                                this._activeEffect.setVector3("vSphericalL10", preScaledHarmonics.l10);
+                                this._activeEffect.setVector3("vSphericalL11", preScaledHarmonics.l11);
+                                this._activeEffect.setVector3("vSphericalL2_2", preScaledHarmonics.l2_2);
+                                this._activeEffect.setVector3("vSphericalL2_1", preScaledHarmonics.l2_1);
+                                this._activeEffect.setVector3("vSphericalL20", preScaledHarmonics.l20);
+                                this._activeEffect.setVector3("vSphericalL21", preScaledHarmonics.l21);
+                                this._activeEffect.setVector3("vSphericalL22", preScaledHarmonics.l22);
+                            }
+                            else {
+                                this._activeEffect.setFloat3("vSphericalX", polynomials.x.x, polynomials.x.y, polynomials.x.z);
+                                this._activeEffect.setFloat3("vSphericalY", polynomials.y.x, polynomials.y.y, polynomials.y.z);
+                                this._activeEffect.setFloat3("vSphericalZ", polynomials.z.x, polynomials.z.y, polynomials.z.z);
+                                this._activeEffect.setFloat3("vSphericalXX_ZZ", polynomials.xx.x - polynomials.zz.x,
+                                    polynomials.xx.y - polynomials.zz.y,
+                                    polynomials.xx.z - polynomials.zz.z);
+                                this._activeEffect.setFloat3("vSphericalYY_ZZ", polynomials.yy.x - polynomials.zz.x,
+                                    polynomials.yy.y - polynomials.zz.y,
+                                    polynomials.yy.z - polynomials.zz.z);
+                                this._activeEffect.setFloat3("vSphericalZZ", polynomials.zz.x, polynomials.zz.y, polynomials.zz.z);
+                                this._activeEffect.setFloat3("vSphericalXY", polynomials.xy.x, polynomials.xy.y, polynomials.xy.z);
+                                this._activeEffect.setFloat3("vSphericalYZ", polynomials.yz.x, polynomials.yz.y, polynomials.yz.z);
+                                this._activeEffect.setFloat3("vSphericalZX", polynomials.zx.x, polynomials.zx.y, polynomials.zx.z);
+                            }
                         }
 
                         ubo.updateFloat3("vReflectionMicrosurfaceInfos",

+ 308 - 168
src/Maths/sphericalPolynomial.ts

@@ -1,130 +1,105 @@
 import { Vector3, Color3 } from "../Maths/math";
-/**
- * Class representing spherical polynomial coefficients to the 3rd degree
- */
-export class SphericalPolynomial {
-    /**
-     * The x coefficients of the spherical polynomial
-     */
-    public x: Vector3 = Vector3.Zero();
-
-    /**
-     * The y coefficients of the spherical polynomial
-     */
-    public y: Vector3 = Vector3.Zero();
-
-    /**
-     * The z coefficients of the spherical polynomial
-     */
-    public z: Vector3 = Vector3.Zero();
-
-    /**
-     * The xx coefficients of the spherical polynomial
-     */
-    public xx: Vector3 = Vector3.Zero();
-
-    /**
-     * The yy coefficients of the spherical polynomial
-     */
-    public yy: Vector3 = Vector3.Zero();
-
-    /**
-     * The zz coefficients of the spherical polynomial
-     */
-    public zz: Vector3 = Vector3.Zero();
-
-    /**
-     * The xy coefficients of the spherical polynomial
-     */
-    public xy: Vector3 = Vector3.Zero();
-
-    /**
-     * The yz coefficients of the spherical polynomial
-     */
-    public yz: Vector3 = Vector3.Zero();
-
-    /**
-     * The zx coefficients of the spherical polynomial
-     */
-    public zx: Vector3 = Vector3.Zero();
-
-    /**
-     * Adds an ambient color to the spherical polynomial
-     * @param color the color to add
-     */
-    public addAmbient(color: Color3): void {
-        var colorVector = new Vector3(color.r, color.g, color.b);
-        this.xx = this.xx.add(colorVector);
-        this.yy = this.yy.add(colorVector);
-        this.zz = this.zz.add(colorVector);
-    }
-
-    /**
-     * Scales the spherical polynomial by the given amount
-     * @param scale the amount to scale
-     */
-    public scale(scale: number) {
-        this.x = this.x.scale(scale);
-        this.y = this.y.scale(scale);
-        this.z = this.z.scale(scale);
-        this.xx = this.xx.scale(scale);
-        this.yy = this.yy.scale(scale);
-        this.zz = this.zz.scale(scale);
-        this.yz = this.yz.scale(scale);
-        this.zx = this.zx.scale(scale);
-        this.xy = this.xy.scale(scale);
-    }
-
-    /**
-     * Gets the spherical polynomial from harmonics
-     * @param harmonics the spherical harmonics
-     * @returns the spherical polynomial
-     */
-    public static FromHarmonics(harmonics: SphericalHarmonics): SphericalPolynomial {
-        var result = new SphericalPolynomial();
-
-        result.x = harmonics.l11.scale(1.02333);
-        result.y = harmonics.l1_1.scale(1.02333);
-        result.z = harmonics.l10.scale(1.02333);
-
-        result.xx = harmonics.l00.scale(0.886277).subtract(harmonics.l20.scale(0.247708)).add(harmonics.lL22.scale(0.429043));
-        result.yy = harmonics.l00.scale(0.886277).subtract(harmonics.l20.scale(0.247708)).subtract(harmonics.lL22.scale(0.429043));
-        result.zz = harmonics.l00.scale(0.886277).add(harmonics.l20.scale(0.495417));
-
-        result.yz = harmonics.l2_1.scale(0.858086);
-        result.zx = harmonics.l21.scale(0.858086);
-        result.xy = harmonics.l2_2.scale(0.858086);
-
-        result.scale(1.0 / Math.PI);
-
-        return result;
-    }
-
-    /**
-     * Constructs a spherical polynomial from an array.
-     * @param data defines the 9x3 coefficients (x, y, z, xx, yy, zz, yz, zx, xy)
-     * @returns the spherical polynomial
-     */
-    public static FromArray(data: ArrayLike<ArrayLike<number>>): SphericalPolynomial {
-        const sp = new SphericalPolynomial();
-        Vector3.FromArrayToRef(data[0], 0, sp.x);
-        Vector3.FromArrayToRef(data[1], 0, sp.y);
-        Vector3.FromArrayToRef(data[2], 0, sp.z);
-        Vector3.FromArrayToRef(data[3], 0, sp.xx);
-        Vector3.FromArrayToRef(data[4], 0, sp.yy);
-        Vector3.FromArrayToRef(data[5], 0, sp.zz);
-        Vector3.FromArrayToRef(data[6], 0, sp.yz);
-        Vector3.FromArrayToRef(data[7], 0, sp.zx);
-        Vector3.FromArrayToRef(data[8], 0, sp.xy);
-        return sp;
-    }
-}
+import { Nullable } from "../types";
+
+// https://dickyjim.wordpress.com/2013/09/04/spherical-harmonics-for-beginners/
+// http://silviojemma.com/public/papers/lighting/spherical-harmonic-lighting.pdf
+// https://www.ppsloan.org/publications/StupidSH36.pdf
+// http://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf
+// https://www.ppsloan.org/publications/SHJCGT.pdf
+// https://www.ppsloan.org/publications/shdering.pdf
+// https://google.github.io/filament/Filament.md.html#annex/sphericalharmonics
+// https://patapom.com/blog/SHPortal/
+// https://imdoingitwrong.wordpress.com/2011/04/14/spherical-harmonics-wtf/
+
+// Using real SH basis:
+//  m>0             m   m
+// y   = sqrt(2) * K * P * cos(m*phi) * cos(theta)
+//  l               l   l
+//
+//  m<0             m   |m|
+// y   = sqrt(2) * K * P * sin(m*phi) * cos(theta)
+//  l               l   l
+//
+//  m=0   0   0
+// y   = K * P * trigono terms
+//  l     l   l
+//
+//  m       (2l + 1)(l - |m|)!
+// K = sqrt(------------------)
+//  l           4pi(l + |m|)!
+//
+// and P by recursion:
+//
+// P00(x) = 1
+// P01(x) = x
+// Pll(x) = (-1^l)(2l - 1)!!(1-x*x)^(1/2)
+//          ((2l - 1)x[Pl-1/m]-(l + m - 1)[Pl-2/m])
+// Plm(x) = ---------------------------------------
+//                         l - m
+// Leaving the trigonometric terms aside we can precompute the constants to :
+const SH3ylmBasisConstants = [
+     Math.sqrt(1  /  (4 * Math.PI)), // l00
+
+    -Math.sqrt(3  /  (4 * Math.PI)), // l1_1
+     Math.sqrt(3  /  (4 * Math.PI)), // l10
+    -Math.sqrt(3  /  (4 * Math.PI)), // l11
+
+     Math.sqrt(15 /  (4 * Math.PI)), // l2_2
+    -Math.sqrt(15 /  (4 * Math.PI)), // l2_1
+     Math.sqrt(5  / (16 * Math.PI)), // l20
+    -Math.sqrt(15 /  (4 * Math.PI)), // l21
+     Math.sqrt(15 / (16 * Math.PI)), // l22
+];
+
+// cm = cos(m * phi)
+// sm = sin(m * phi)
+// {x,y,z} = {cos(phi)sin(theta), sin(phi)sin(theta), cos(theta)}
+// By recursion on using trigo identities:
+const SH3ylmBasisTrigonometricTerms = [
+    (direction: Vector3) => 1, // l00
+
+    (direction: Vector3) => direction.y, // l1_1
+    (direction: Vector3) => direction.z, // l10
+    (direction: Vector3) => direction.x, // l11
+
+    (direction: Vector3) => direction.x * direction.y, // l2_2
+    (direction: Vector3) => direction.y * direction.z, // l2_1
+    (direction: Vector3) => 3 * direction.z * direction.z - 1, // l20
+    (direction: Vector3) => direction.x * direction.z, // l21
+    (direction: Vector3) => direction.x * direction.x - direction.y * direction.y, // l22
+];
+
+// Wrap the full compute
+const applySH3 = (lm: number, direction: Vector3) => {
+    return SH3ylmBasisConstants[lm] * SH3ylmBasisTrigonometricTerms[lm](direction);
+};
+
+// Derived from the integration of the a kernel convolution to SH.
+// Great explanation here: https://patapom.com/blog/SHPortal/#about-distant-radiance-and-irradiance-environments
+const SHCosKernelConvolution = [
+    Math.PI,
+
+    2 * Math.PI / 3,
+    2 * Math.PI / 3,
+    2 * Math.PI / 3,
+
+    Math.PI / 4,
+    Math.PI / 4,
+    Math.PI / 4,
+    Math.PI / 4,
+    Math.PI / 4,
+];
 
 /**
  * Class representing spherical harmonics coefficients to the 3rd degree
  */
 export class SphericalHarmonics {
     /**
+     * Defines whether or not the harmonics have been prescaled for rendering.
+     */
+    public preScaled = false;
+
+    /**
      * The l0,0 coefficients of the spherical harmonics
      */
     public l00: Vector3 = Vector3.Zero();
@@ -167,7 +142,7 @@ export class SphericalHarmonics {
     /**
      * The l2,2 coefficients of the spherical harmonics
      */
-    public lL22: Vector3 = Vector3.Zero();
+    public l22: Vector3 = Vector3.Zero();
 
     /**
      * Adds a light to the spherical harmonics
@@ -179,34 +154,33 @@ export class SphericalHarmonics {
         var colorVector = new Vector3(color.r, color.g, color.b);
         var c = colorVector.scale(deltaSolidAngle);
 
-        this.l00 = this.l00.add(c.scale(0.282095));
+        this.l00 = this.l00.add(c.scale(applySH3(0, direction)));
 
-        this.l1_1 = this.l1_1.add(c.scale(0.488603 * direction.y));
-        this.l10 = this.l10.add(c.scale(0.488603 * direction.z));
-        this.l11 = this.l11.add(c.scale(0.488603 * direction.x));
+        this.l1_1 = this.l1_1.add(c.scale(applySH3(1, direction)));
+        this.l10 = this.l10.add(c.scale(applySH3(2, direction)));
+        this.l11 = this.l11.add(c.scale(applySH3(3, direction)));
 
-        this.l2_2 = this.l2_2.add(c.scale(1.092548 * direction.x * direction.y));
-        this.l2_1 = this.l2_1.add(c.scale(1.092548 * direction.y * direction.z));
-        this.l21 = this.l21.add(c.scale(1.092548 * direction.x * direction.z));
-
-        this.l20 = this.l20.add(c.scale(0.315392 * (3.0 * direction.z * direction.z - 1.0)));
-        this.lL22 = this.lL22.add(c.scale(0.546274 * (direction.x * direction.x - direction.y * direction.y)));
+        this.l2_2 = this.l2_2.add(c.scale(applySH3(4, direction)));
+        this.l2_1 = this.l2_1.add(c.scale(applySH3(5, direction)));
+        this.l20 = this.l20.add(c.scale(applySH3(6, direction)));
+        this.l21 = this.l21.add(c.scale(applySH3(7, direction)));
+        this.l22 = this.l22.add(c.scale(applySH3(8, direction)));
     }
 
     /**
      * Scales the spherical harmonics by the given amount
      * @param scale the amount to scale
      */
-    public scale(scale: number): void {
-        this.l00 = this.l00.scale(scale);
-        this.l1_1 = this.l1_1.scale(scale);
-        this.l10 = this.l10.scale(scale);
-        this.l11 = this.l11.scale(scale);
-        this.l2_2 = this.l2_2.scale(scale);
-        this.l2_1 = this.l2_1.scale(scale);
-        this.l20 = this.l20.scale(scale);
-        this.l21 = this.l21.scale(scale);
-        this.lL22 = this.lL22.scale(scale);
+    public scaleInPlace(scale: number): void {
+        this.l00.scaleInPlace(scale);
+        this.l1_1.scaleInPlace(scale);
+        this.l10.scaleInPlace(scale);
+        this.l11.scaleInPlace(scale);
+        this.l2_2.scaleInPlace(scale);
+        this.l2_1.scaleInPlace(scale);
+        this.l20.scaleInPlace(scale);
+        this.l21.scaleInPlace(scale);
+        this.l22.scaleInPlace(scale);
     }
 
     /**
@@ -222,19 +196,19 @@ export class SphericalHarmonics {
      */
     public convertIncidentRadianceToIrradiance(): void {
         // Constant (Band 0)
-        this.l00 = this.l00.scale(3.141593);
+        this.l00.scaleInPlace(SHCosKernelConvolution[0]);
 
         // Linear (Band 1)
-        this.l1_1 = this.l1_1.scale(2.094395);
-        this.l10 = this.l10.scale(2.094395);
-        this.l11 = this.l11.scale(2.094395);
+        this.l1_1.scaleInPlace(SHCosKernelConvolution[1]);
+        this.l10.scaleInPlace(SHCosKernelConvolution[2]);
+        this.l11.scaleInPlace(SHCosKernelConvolution[3]);
 
         // Quadratic (Band 2)
-        this.l2_2 = this.l2_2.scale(0.785398);
-        this.l2_1 = this.l2_1.scale(0.785398);
-        this.l20 = this.l20.scale(0.785398);
-        this.l21 = this.l21.scale(0.785398);
-        this.lL22 = this.lL22.scale(0.785398);
+        this.l2_2.scaleInPlace(SHCosKernelConvolution[4]);
+        this.l2_1.scaleInPlace(SHCosKernelConvolution[5]);
+        this.l20.scaleInPlace(SHCosKernelConvolution[6]);
+        this.l21.scaleInPlace(SHCosKernelConvolution[7]);
+        this.l22.scaleInPlace(SHCosKernelConvolution[8]);
     }
 
     /**
@@ -247,33 +221,33 @@ export class SphericalHarmonics {
      * This is done by an additional scale by 1/pi, so is a fairly trivial operation but important conceptually.
      */
     public convertIrradianceToLambertianRadiance(): void {
-        this.scale(1.0 / Math.PI);
+        this.scaleInPlace(1.0 / Math.PI);
 
         // The resultant SH now represents outgoing radiance, so includes the Lambert 1/pi normalisation factor but without albedo (rho) applied
         // (The pixel shader must apply albedo after texture fetches, etc).
     }
 
     /**
-     * Gets the spherical harmonics from polynomial
-     * @param polynomial the spherical polynomial
-     * @returns the spherical harmonics
+     * Integrates the reconstruction coefficients directly in to the SH preventing further
+     * required operations at run time.
+     *
+     * This is simply done by scaling back the SH with Ylm constants parameter.
+     * The trigonometric part being applied by the shader at run time.
      */
-    public static FromPolynomial(polynomial: SphericalPolynomial): SphericalHarmonics {
-        var result = new SphericalHarmonics();
+    public preScaleForRendering(): void {
+        this.preScaled = true;
 
-        result.l00 = polynomial.xx.scale(0.376127).add(polynomial.yy.scale(0.376127)).add(polynomial.zz.scale(0.376126));
-        result.l1_1 = polynomial.y.scale(0.977204);
-        result.l10 = polynomial.z.scale(0.977204);
-        result.l11 = polynomial.x.scale(0.977204);
-        result.l2_2 = polynomial.xy.scale(1.16538);
-        result.l2_1 = polynomial.yz.scale(1.16538);
-        result.l20 = polynomial.zz.scale(1.34567).subtract(polynomial.xx.scale(0.672834)).subtract(polynomial.yy.scale(0.672834));
-        result.l21 = polynomial.zx.scale(1.16538);
-        result.lL22 = polynomial.xx.scale(1.16538).subtract(polynomial.yy.scale(1.16538));
+        this.l00.scaleInPlace(SH3ylmBasisConstants[0]);
 
-        result.scale(Math.PI);
+        this.l1_1.scaleInPlace(SH3ylmBasisConstants[1]);
+        this.l10.scaleInPlace(SH3ylmBasisConstants[2]);
+        this.l11.scaleInPlace(SH3ylmBasisConstants[3]);
 
-        return result;
+        this.l2_2.scaleInPlace(SH3ylmBasisConstants[4]);
+        this.l2_1.scaleInPlace(SH3ylmBasisConstants[5]);
+        this.l20.scaleInPlace(SH3ylmBasisConstants[6]);
+        this.l21.scaleInPlace(SH3ylmBasisConstants[7]);
+        this.l22.scaleInPlace(SH3ylmBasisConstants[8]);
     }
 
     /**
@@ -291,7 +265,173 @@ export class SphericalHarmonics {
         Vector3.FromArrayToRef(data[5], 0, sh.l2_1);
         Vector3.FromArrayToRef(data[6], 0, sh.l20);
         Vector3.FromArrayToRef(data[7], 0, sh.l21);
-        Vector3.FromArrayToRef(data[8], 0, sh.lL22);
+        Vector3.FromArrayToRef(data[8], 0, sh.l22);
         return sh;
     }
+
+    // Keep for references.
+    /**
+     * Gets the spherical harmonics from polynomial
+     * @param polynomial the spherical polynomial
+     * @returns the spherical harmonics
+     */
+    public static FromPolynomial(polynomial: SphericalPolynomial): SphericalHarmonics {
+        var result = new SphericalHarmonics();
+
+        result.l00 = polynomial.xx.scale(0.376127).add(polynomial.yy.scale(0.376127)).add(polynomial.zz.scale(0.376126));
+        result.l1_1 = polynomial.y.scale(0.977204);
+        result.l10 = polynomial.z.scale(0.977204);
+        result.l11 = polynomial.x.scale(0.977204);
+        result.l2_2 = polynomial.xy.scale(1.16538);
+        result.l2_1 = polynomial.yz.scale(1.16538);
+        result.l20 = polynomial.zz.scale(1.34567).subtract(polynomial.xx.scale(0.672834)).subtract(polynomial.yy.scale(0.672834));
+        result.l21 = polynomial.zx.scale(1.16538);
+        result.l22 = polynomial.xx.scale(1.16538).subtract(polynomial.yy.scale(1.16538));
+
+        result.l1_1.scaleInPlace(-1);
+        result.l11.scaleInPlace(-1);
+        result.l2_1.scaleInPlace(-1);
+        result.l21.scaleInPlace(-1);
+
+        result.scaleInPlace(Math.PI);
+
+        return result;
+    }
+}
+
+/**
+ * Class representing spherical polynomial coefficients to the 3rd degree
+ */
+export class SphericalPolynomial {
+    private _harmonics: Nullable<SphericalHarmonics>;
+
+    /**
+     * The spherical harmonics used to create the polynomials.
+     */
+    public get preScaledHarmonics(): SphericalHarmonics {
+        if (!this._harmonics) {
+            this._harmonics = SphericalHarmonics.FromPolynomial(this);
+        }
+        if (!this._harmonics.preScaled) {
+            this._harmonics.preScaleForRendering();
+        }
+        return this._harmonics;
+    }
+
+    /**
+     * The x coefficients of the spherical polynomial
+     */
+    public x: Vector3 = Vector3.Zero();
+
+    /**
+     * The y coefficients of the spherical polynomial
+     */
+    public y: Vector3 = Vector3.Zero();
+
+    /**
+     * The z coefficients of the spherical polynomial
+     */
+    public z: Vector3 = Vector3.Zero();
+
+    /**
+     * The xx coefficients of the spherical polynomial
+     */
+    public xx: Vector3 = Vector3.Zero();
+
+    /**
+     * The yy coefficients of the spherical polynomial
+     */
+    public yy: Vector3 = Vector3.Zero();
+
+    /**
+     * The zz coefficients of the spherical polynomial
+     */
+    public zz: Vector3 = Vector3.Zero();
+
+    /**
+     * The xy coefficients of the spherical polynomial
+     */
+    public xy: Vector3 = Vector3.Zero();
+
+    /**
+     * The yz coefficients of the spherical polynomial
+     */
+    public yz: Vector3 = Vector3.Zero();
+
+    /**
+     * The zx coefficients of the spherical polynomial
+     */
+    public zx: Vector3 = Vector3.Zero();
+
+    /**
+     * Adds an ambient color to the spherical polynomial
+     * @param color the color to add
+     */
+    public addAmbient(color: Color3): void {
+        var colorVector = new Vector3(color.r, color.g, color.b);
+        this.xx = this.xx.add(colorVector);
+        this.yy = this.yy.add(colorVector);
+        this.zz = this.zz.add(colorVector);
+    }
+
+    /**
+     * Scales the spherical polynomial by the given amount
+     * @param scale the amount to scale
+     */
+    public scaleInPlace(scale: number) {
+        this.x.scaleInPlace(scale);
+        this.y.scaleInPlace(scale);
+        this.z.scaleInPlace(scale);
+        this.xx.scaleInPlace(scale);
+        this.yy.scaleInPlace(scale);
+        this.zz.scaleInPlace(scale);
+        this.yz.scaleInPlace(scale);
+        this.zx.scaleInPlace(scale);
+        this.xy.scaleInPlace(scale);
+    }
+
+    /**
+     * Gets the spherical polynomial from harmonics
+     * @param harmonics the spherical harmonics
+     * @returns the spherical polynomial
+     */
+    public static FromHarmonics(harmonics: SphericalHarmonics): SphericalPolynomial {
+        var result = new SphericalPolynomial();
+        result._harmonics = harmonics;
+
+        result.x = harmonics.l11.scale(1.02333).scale(-1);
+        result.y = harmonics.l1_1.scale(1.02333).scale(-1);
+        result.z = harmonics.l10.scale(1.02333);
+
+        result.xx = harmonics.l00.scale(0.886277).subtract(harmonics.l20.scale(0.247708)).add(harmonics.l22.scale(0.429043));
+        result.yy = harmonics.l00.scale(0.886277).subtract(harmonics.l20.scale(0.247708)).subtract(harmonics.l22.scale(0.429043));
+        result.zz = harmonics.l00.scale(0.886277).add(harmonics.l20.scale(0.495417));
+
+        result.yz = harmonics.l2_1.scale(0.858086).scale(-1);
+        result.zx = harmonics.l21.scale(0.858086).scale(-1);
+        result.xy = harmonics.l2_2.scale(0.858086);
+
+        result.scaleInPlace(1.0 / Math.PI);
+
+        return result;
+    }
+
+    /**
+     * Constructs a spherical polynomial from an array.
+     * @param data defines the 9x3 coefficients (x, y, z, xx, yy, zz, yz, zx, xy)
+     * @returns the spherical polynomial
+     */
+    public static FromArray(data: ArrayLike<ArrayLike<number>>): SphericalPolynomial {
+        const sp = new SphericalPolynomial();
+        Vector3.FromArrayToRef(data[0], 0, sp.x);
+        Vector3.FromArrayToRef(data[1], 0, sp.y);
+        Vector3.FromArrayToRef(data[2], 0, sp.z);
+        Vector3.FromArrayToRef(data[3], 0, sp.xx);
+        Vector3.FromArrayToRef(data[4], 0, sp.yy);
+        Vector3.FromArrayToRef(data[5], 0, sp.zz);
+        Vector3.FromArrayToRef(data[6], 0, sp.yz);
+        Vector3.FromArrayToRef(data[7], 0, sp.zx);
+        Vector3.FromArrayToRef(data[8], 0, sp.xy);
+        return sp;
+    }
 }

+ 1 - 1
src/Misc/HighDynamicRange/cubemapToSphericalPolynomial.ts

@@ -173,7 +173,7 @@ export class CubeMapToSphericalPolynomialTools {
         // small angle approximation of solid angle for each texel (see deltaSolidAngle),
         // and also to compensate for accumulative error due to float precision in the summation.
         var correctionFactor = expectedSolidAngle / totalSolidAngle;
-        sphericalHarmonics.scale(correctionFactor);
+        sphericalHarmonics.scaleInPlace(correctionFactor);
 
         sphericalHarmonics.convertIncidentRadianceToIrradiance();
         sphericalHarmonics.convertIrradianceToLambertianRadiance();

+ 81 - 54
src/Shaders/ShadersInclude/harmonicsFunctions.fx

@@ -1,56 +1,83 @@
 #ifdef USESPHERICALFROMREFLECTIONMAP
-    uniform vec3 vSphericalX;
-    uniform vec3 vSphericalY;
-    uniform vec3 vSphericalZ;
-    uniform vec3 vSphericalXX_ZZ;
-    uniform vec3 vSphericalYY_ZZ;
-    uniform vec3 vSphericalZZ;
-    uniform vec3 vSphericalXY;
-    uniform vec3 vSphericalYZ;
-    uniform vec3 vSphericalZX;
-
-    vec3 quaternionVectorRotation_ScaledSqrtTwo(vec4 Q, vec3 V){
-        vec3 T = cross(Q.xyz, V);
-        T += Q.www * V;
-        return cross(Q.xyz, T) + V;
-    }
-
-    vec3 environmentIrradianceJones(vec3 normal)
-    {
-        // Fast method for evaluating a fixed spherical harmonics function on the sphere (e.g. irradiance or radiance).
-        // Cost: 24 scalar operations on modern GPU "scalar" shader core, or 8 multiply-adds of 3D vectors:
-        // "Function Cost 24	24x mad"
-
-        // Note: the lower operation count compared to other methods (e.g. Sloan) is by further
-        // taking advantage of the input 'normal' being normalised, which affords some further algebraic simplification.
-        // Namely, the SH coefficients are first converted to spherical polynomial (SP) basis, then 
-        // a substitution is performed using Z^2 = (1 - X^2 - Y^2).
-
-        // As with other methods for evaluation spherical harmonic, the input 'normal' is assumed to be normalised (or near normalised).
-        // This isn't as critical as it is with other calculations (e.g. specular highlight), but the result will be slightly incorrect nonetheless.
-        float Nx = normal.x;
-        float Ny = normal.y;
-        float Nz = normal.z;
-
-        vec3 C1 = vSphericalZZ.rgb;
-        vec3 Cx = vSphericalX.rgb;
-        vec3 Cy = vSphericalY.rgb;
-        vec3 Cz = vSphericalZ.rgb;
-        vec3 Cxx_zz = vSphericalXX_ZZ.rgb;
-        vec3 Cyy_zz = vSphericalYY_ZZ.rgb;
-        vec3 Cxy = vSphericalXY.rgb;
-        vec3 Cyz = vSphericalYZ.rgb;
-        vec3 Czx = vSphericalZX.rgb;
-
-        vec3 a1 = Cyy_zz * Ny + Cy;
-        vec3 a2 = Cyz * Nz + a1;
-        vec3 b1 = Czx * Nz + Cx;
-        vec3 b2 = Cxy * Ny + b1;
-        vec3 b3 = Cxx_zz * Nx + b2;
-        vec3 t1 = Cz  * Nz + C1;
-        vec3 t2 = a2  * Ny + t1;
-        vec3 t3 = b3  * Nx + t2;
-
-        return t3;
-    }
+    #ifdef SPHERICAL_HARMONICS
+        uniform vec3 vSphericalL00;
+        uniform vec3 vSphericalL1_1;
+        uniform vec3 vSphericalL10;
+        uniform vec3 vSphericalL11;
+        uniform vec3 vSphericalL2_2;
+        uniform vec3 vSphericalL2_1;
+        uniform vec3 vSphericalL20;
+        uniform vec3 vSphericalL21;
+        uniform vec3 vSphericalL22;
+
+        // Please note the the coefficient have been prescaled.
+        //
+        // This uses the theory from both Sloan and Ramamoothi:
+        //   https://www.ppsloan.org/publications/SHJCGT.pdf
+        //   http://www-graphics.stanford.edu/papers/envmap/
+        // The only difference is the integration of the reconstruction coefficients direcly
+        // into the vectors as well as the 1 / pi multiplication to simulate a lambertian diffuse.
+        vec3 computeEnvironmentIrradiance(vec3 normal) {
+            return vSphericalL00
+
+                + vSphericalL1_1 * (normal.y)
+                + vSphericalL10 * (normal.z)
+                + vSphericalL11 * (normal.x)
+
+                + vSphericalL2_2 * (normal.y * normal.x)
+                + vSphericalL2_1 * (normal.y * normal.z)
+                + vSphericalL20 * ((3.0 * normal.z * normal.z) - 1.0)
+                + vSphericalL21 * (normal.z * normal.x)
+                + vSphericalL22 * (normal.x * normal.x - (normal.y * normal.y));
+        }
+    #else
+        uniform vec3 vSphericalX;
+        uniform vec3 vSphericalY;
+        uniform vec3 vSphericalZ;
+        uniform vec3 vSphericalXX_ZZ;
+        uniform vec3 vSphericalYY_ZZ;
+        uniform vec3 vSphericalZZ;
+        uniform vec3 vSphericalXY;
+        uniform vec3 vSphericalYZ;
+        uniform vec3 vSphericalZX;
+
+        // By Matthew Jones.
+        vec3 computeEnvironmentIrradiance(vec3 normal) {
+            // Fast method for evaluating a fixed spherical harmonics function on the sphere (e.g. irradiance or radiance).
+            // Cost: 24 scalar operations on modern GPU "scalar" shader core, or 8 multiply-adds of 3D vectors:
+            // "Function Cost 24	24x mad"
+
+            // Note: the lower operation count compared to other methods (e.g. Sloan) is by further
+            // taking advantage of the input 'normal' being normalised, which affords some further algebraic simplification.
+            // Namely, the SH coefficients are first converted to spherical polynomial (SP) basis, then 
+            // a substitution is performed using Z^2 = (1 - X^2 - Y^2).
+
+            // As with other methods for evaluation spherical harmonic, the input 'normal' is assumed to be normalised (or near normalised).
+            // This isn't as critical as it is with other calculations (e.g. specular highlight), but the result will be slightly incorrect nonetheless.
+            float Nx = normal.x;
+            float Ny = normal.y;
+            float Nz = normal.z;
+
+            vec3 C1 = vSphericalZZ.rgb;
+            vec3 Cx = vSphericalX.rgb;
+            vec3 Cy = vSphericalY.rgb;
+            vec3 Cz = vSphericalZ.rgb;
+            vec3 Cxx_zz = vSphericalXX_ZZ.rgb;
+            vec3 Cyy_zz = vSphericalYY_ZZ.rgb;
+            vec3 Cxy = vSphericalXY.rgb;
+            vec3 Cyz = vSphericalYZ.rgb;
+            vec3 Czx = vSphericalZX.rgb;
+
+            vec3 a1 = Cyy_zz * Ny + Cy;
+            vec3 a2 = Cyz * Nz + a1;
+            vec3 b1 = Czx * Nz + Cx;
+            vec3 b2 = Cxy * Ny + b1;
+            vec3 b3 = Cxx_zz * Nx + b2;
+            vec3 t1 = Cz  * Nz + C1;
+            vec3 t2 = a2  * Ny + t1;
+            vec3 t3 = b3  * Nx + t2;
+
+            return t3;
+        }
+    #endif
 #endif

+ 2 - 2
src/Shaders/pbr.fragment.fx

@@ -496,7 +496,7 @@ void main(void) {
                     irradianceVector.z *= -1.0;
                 #endif
 
-                environmentIrradiance = environmentIrradianceJones(irradianceVector);
+                environmentIrradiance = computeEnvironmentIrradiance(irradianceVector);
             #endif
         #endif
 
@@ -981,7 +981,7 @@ void main(void) {
             #endif
         #endif
 
-        vec3 refractionIrradiance = environmentIrradianceJones(-irradianceVector);
+        vec3 refractionIrradiance = computeEnvironmentIrradiance(-irradianceVector);
         refractionIrradiance *= transmittance;
     #endif
 

+ 1 - 1
src/Shaders/pbr.vertex.fx

@@ -195,7 +195,7 @@ void main(void) {
         #ifdef REFLECTIONMAP_OPPOSITEZ
             reflectionVector.z *= -1.0;
         #endif
-        vEnvironmentIrradiance = environmentIrradianceJones(reflectionVector);
+        vEnvironmentIrradiance = computeEnvironmentIrradiance(reflectionVector);
     #endif
 #endif
 

BIN
tests/validation/ReferenceImages/highlights.png


BIN
tests/validation/ReferenceImages/simulatePointer.png