|
@@ -3,16 +3,129 @@ import BRDF_GGX from './BSDF/BRDF_GGX.js';
|
|
|
import DFGApprox from './BSDF/DFGApprox.js';
|
|
|
import EnvironmentBRDF from './BSDF/EnvironmentBRDF.js';
|
|
|
import F_Schlick from './BSDF/F_Schlick.js';
|
|
|
+import Schlick_to_F0 from './BSDF/Schlick_to_F0.js';
|
|
|
import BRDF_Sheen from './BSDF/BRDF_Sheen.js';
|
|
|
import LightingModel from '../core/LightingModel.js';
|
|
|
-import { diffuseColor, specularColor, roughness, clearcoat, clearcoatRoughness, sheen, sheenRoughness } from '../core/PropertyNode.js';
|
|
|
+import { diffuseColor, specularColor, roughness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness } from '../core/PropertyNode.js';
|
|
|
import { transformedNormalView, transformedClearcoatNormalView } from '../accessors/NormalNode.js';
|
|
|
import { positionViewDirection } from '../accessors/PositionNode.js';
|
|
|
-import { float, vec3 } from '../shadernode/ShaderNode.js';
|
|
|
+import { float, vec3, mat3 } from '../shadernode/ShaderNode.js';
|
|
|
import { cond } from '../math/CondNode.js';
|
|
|
+import { mix, smoothstep } from '../math/MathNode.js';
|
|
|
|
|
|
-const clearcoatF0 = vec3( 0.04 );
|
|
|
-const clearcoatF90 = vec3( 1 );
|
|
|
+//
|
|
|
+// Iridescence
|
|
|
+//
|
|
|
+
|
|
|
+// XYZ to linear-sRGB color space
|
|
|
+const XYZ_TO_REC709 = mat3(
|
|
|
+ 3.2404542, - 0.9692660, 0.0556434,
|
|
|
+ - 1.5371385, 1.8760108, - 0.2040259,
|
|
|
+ - 0.4985314, 0.0415560, 1.0572252
|
|
|
+);
|
|
|
+
|
|
|
+// Assume air interface for top
|
|
|
+// Note: We don't handle the case fresnel0 == 1
|
|
|
+const Fresnel0ToIor = ( fresnel0 ) => {
|
|
|
+
|
|
|
+ const sqrtF0 = fresnel0.sqrt();
|
|
|
+ return vec3( 1.0 ).add( sqrtF0 ).div( vec3( 1.0 ).sub( sqrtF0 ) );
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+// ior is a value between 1.0 and 3.0. 1.0 is air interface
|
|
|
+const IorToFresnel0 = ( transmittedIor, incidentIor ) => {
|
|
|
+
|
|
|
+ return transmittedIor.sub( incidentIor ).div( transmittedIor.add( incidentIor ) ).pow2();
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+// Fresnel equations for dielectric/dielectric interfaces.
|
|
|
+// Ref: https://belcour.github.io/blog/research/2017/05/01/brdf-thin-film.html
|
|
|
+// Evaluation XYZ sensitivity curves in Fourier space
|
|
|
+const evalSensitivity = ( OPD, shift ) => {
|
|
|
+
|
|
|
+ const phase = OPD.mul( 2.0 * Math.PI * 1.0e-9 );
|
|
|
+ const val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );
|
|
|
+ const pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );
|
|
|
+ const VAR = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );
|
|
|
+
|
|
|
+ const x = float( 9.7470e-14 * Math.sqrt( 2.0 * Math.PI * 4.5282e+09 ) ).mul( phase.mul( 2.2399e+06 ).add( shift.x ).cos() ).mul( phase.pow2().mul( - 4.5282e+09 ).exp() );
|
|
|
+
|
|
|
+ let xyz = val.mul( VAR.mul( 2.0 * Math.PI ).sqrt() ).mul( pos.mul( phase ).add( shift ).cos() ).mul( phase.pow2().negate().mul( VAR ).exp() );
|
|
|
+ xyz = vec3( xyz.x.add( x ), xyz.y, xyz.z ).div( 1.0685e-7 );
|
|
|
+
|
|
|
+ const rgb = XYZ_TO_REC709.mul( xyz );
|
|
|
+ return rgb;
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+const evalIridescence = ( outsideIOR, eta2, cosTheta1, thinFilmThickness, baseF0 ) => {
|
|
|
+
|
|
|
+ // Force iridescenceIOR -> outsideIOR when thinFilmThickness -> 0.0
|
|
|
+ const iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );
|
|
|
+ // Evaluate the cosTheta on the base layer (Snell law)
|
|
|
+ const sinTheta2Sq = outsideIOR.div( iridescenceIOR ).pow2().mul( float( 1 ).sub( cosTheta1.pow2() ) );
|
|
|
+
|
|
|
+ // Handle TIR:
|
|
|
+ const cosTheta2Sq = float( 1 ).sub( sinTheta2Sq );
|
|
|
+ /*if ( cosTheta2Sq < 0.0 ) {
|
|
|
+
|
|
|
+ return vec3( 1.0 );
|
|
|
+
|
|
|
+ }*/
|
|
|
+
|
|
|
+ const cosTheta2 = cosTheta2Sq.sqrt();
|
|
|
+
|
|
|
+ // First interface
|
|
|
+ const R0 = IorToFresnel0( iridescenceIOR, outsideIOR );
|
|
|
+ const R12 = F_Schlick( { f0: R0, f90: 1.0, dotVH: cosTheta1 } );
|
|
|
+ //const R21 = R12;
|
|
|
+ const T121 = R12.oneMinus();
|
|
|
+ const phi12 = iridescenceIOR.lessThan( outsideIOR ).cond( Math.PI, 0.0 );
|
|
|
+ const phi21 = float( Math.PI ).sub( phi12 );
|
|
|
+
|
|
|
+ // Second interface
|
|
|
+ const baseIOR = Fresnel0ToIor( baseF0.clamp( 0.0, 0.9999 ) ); // guard against 1.0
|
|
|
+ const R1 = IorToFresnel0( baseIOR, iridescenceIOR.vec3() );
|
|
|
+ const R23 = F_Schlick( { f0: R1, f90: 1.0, dotVH: cosTheta2 } );
|
|
|
+ const phi23 = vec3(
|
|
|
+ baseIOR.x.lessThan( iridescenceIOR ).cond( Math.PI, 0.0 ),
|
|
|
+ baseIOR.y.lessThan( iridescenceIOR ).cond( Math.PI, 0.0 ),
|
|
|
+ baseIOR.z.lessThan( iridescenceIOR ).cond( Math.PI, 0.0 )
|
|
|
+ );
|
|
|
+
|
|
|
+ // Phase shift
|
|
|
+ const OPD = iridescenceIOR.mul( thinFilmThickness, cosTheta2, 2.0 );
|
|
|
+ const phi = vec3( phi21 ).add( phi23 );
|
|
|
+
|
|
|
+ // Compound terms
|
|
|
+ const R123 = R12.mul( R23 ).clamp( 1e-5, 0.9999 );
|
|
|
+ const r123 = R123.sqrt();
|
|
|
+ const Rs = T121.pow2().mul( R23 ).div( vec3( 1.0 ).sub( R123 ) );
|
|
|
+
|
|
|
+ // Reflectance term for m = 0 (DC term amplitude)
|
|
|
+ const C0 = R12.add( Rs );
|
|
|
+ let I = C0;
|
|
|
+
|
|
|
+ // Reflectance term for m > 0 (pairs of diracs)
|
|
|
+ let Cm = Rs.sub( T121 );
|
|
|
+ for ( let m = 1; m <= 2; ++ m ) {
|
|
|
+
|
|
|
+ Cm = Cm.mul( r123 );
|
|
|
+ const Sm = evalSensitivity( float( m ).mul( OPD ), float( m ).mul( phi ) ).mul( 2.0 );
|
|
|
+ I = I.add( Cm.mul( Sm ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Since out of gamut colors might be produced, negative color values are clamped to 0.
|
|
|
+ return I.max( vec3( 0.0 ) );
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+//
|
|
|
+// Sheen
|
|
|
+//
|
|
|
|
|
|
// This is a curve-fit approxmation to the "Charlie sheen" BRDF integrated over the hemisphere from
|
|
|
// Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF". The analysis can be found
|
|
@@ -41,40 +154,26 @@ const IBLSheenBRDF = ( normal, viewDir, roughness ) => {
|
|
|
|
|
|
};
|
|
|
|
|
|
-// Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
|
|
|
-// Approximates multiscattering in order to preserve energy.
|
|
|
-// http://www.jcgt.org/published/0008/01/03/
|
|
|
-const computeMultiscattering = ( singleScatter, multiScatter, specularF90 = float( 1 ) ) => {
|
|
|
-
|
|
|
- const fab = DFGApprox( { roughness } );
|
|
|
-
|
|
|
- const FssEss = specularColor.mul( fab.x ).add( specularF90.mul( fab.y ) );
|
|
|
-
|
|
|
- const Ess = fab.x.add( fab.y );
|
|
|
- const Ems = Ess.oneMinus();
|
|
|
-
|
|
|
- const Favg = specularColor.add( specularColor.oneMinus().mul( 0.047619 ) ); // 1/21
|
|
|
- const Fms = FssEss.mul( Favg ).div( Ems.mul( Favg ).oneMinus() );
|
|
|
-
|
|
|
- singleScatter.addAssign( FssEss );
|
|
|
- multiScatter.addAssign( Fms.mul( Ems ) );
|
|
|
-
|
|
|
-};
|
|
|
+const clearcoatF0 = vec3( 0.04 );
|
|
|
+const clearcoatF90 = vec3( 1 );
|
|
|
|
|
|
//
|
|
|
|
|
|
class PhysicalLightingModel extends LightingModel {
|
|
|
|
|
|
- constructor( clearcoat = true, sheen = true ) {
|
|
|
+ constructor( clearcoat = true, sheen = true, iridescence = true ) {
|
|
|
|
|
|
super();
|
|
|
|
|
|
this.clearcoat = clearcoat;
|
|
|
this.sheen = sheen;
|
|
|
+ this.iridescence = iridescence;
|
|
|
|
|
|
this.clearcoatRadiance = null;
|
|
|
this.clearcoatSpecular = null;
|
|
|
this.sheenSpecular = null;
|
|
|
+ this.iridescenceFresnel = null;
|
|
|
+ this.iridescenceF0 = null;
|
|
|
|
|
|
}
|
|
|
|
|
@@ -113,6 +212,38 @@ class PhysicalLightingModel extends LightingModel {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ if ( this.iridescence === true ) {
|
|
|
+
|
|
|
+ const dotNVi = transformedNormalView.dot( positionViewDirection ).clamp();
|
|
|
+
|
|
|
+ this.iridescenceFresnel = evalIridescence( float( 1.0 ), iridescenceIOR, dotNVi, iridescenceThickness, specularColor );
|
|
|
+ this.iridescenceF0 = Schlick_to_F0( { f: this.iridescenceFresnel, f90: 1.0, dotVH: dotNVi } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
|
|
|
+ // Approximates multiscattering in order to preserve energy.
|
|
|
+ // http://www.jcgt.org/published/0008/01/03/
|
|
|
+
|
|
|
+ computeMultiscattering( singleScatter, multiScatter, specularF90 = float( 1 ) ) {
|
|
|
+
|
|
|
+ const fab = DFGApprox( { roughness } );
|
|
|
+
|
|
|
+ const Fr = this.iridescenceF0 ? iridescence.mix( specularColor, this.iridescenceF0 ) : specularColor;
|
|
|
+
|
|
|
+ const FssEss = Fr.mul( fab.x ).add( specularF90.mul( fab.y ) );
|
|
|
+
|
|
|
+ const Ess = fab.x.add( fab.y );
|
|
|
+ const Ems = Ess.oneMinus();
|
|
|
+
|
|
|
+ const Favg = specularColor.add( specularColor.oneMinus().mul( 0.047619 ) ); // 1/21
|
|
|
+ const Fms = FssEss.mul( Favg ).div( Ems.mul( Favg ).oneMinus() );
|
|
|
+
|
|
|
+ singleScatter.addAssign( FssEss );
|
|
|
+ multiScatter.addAssign( Fms.mul( Ems ) );
|
|
|
+
|
|
|
}
|
|
|
|
|
|
direct( { lightDirection, lightColor, reflectedLight } ) {
|
|
@@ -137,7 +268,7 @@ class PhysicalLightingModel extends LightingModel {
|
|
|
|
|
|
reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseColor.rgb } ) ) );
|
|
|
|
|
|
- reflectedLight.directSpecular.addAssign( irradiance.mul( BRDF_GGX( { lightDirection, f0: specularColor, f90: 1, roughness } ) ) );
|
|
|
+ reflectedLight.directSpecular.addAssign( irradiance.mul( BRDF_GGX( { lightDirection, f0: specularColor, f90: 1, roughness, iridescence: this.iridescence, iridescenceFresnel: this.iridescenceFresnel } ) ) );
|
|
|
|
|
|
}
|
|
|
|
|
@@ -179,7 +310,7 @@ class PhysicalLightingModel extends LightingModel {
|
|
|
const multiScattering = vec3().temp();
|
|
|
const cosineWeightedIrradiance = iblIrradiance.mul( 1 / Math.PI );
|
|
|
|
|
|
- computeMultiscattering( singleScattering, multiScattering );
|
|
|
+ this.computeMultiscattering( singleScattering, multiScattering );
|
|
|
|
|
|
const totalScattering = singleScattering.add( multiScattering );
|
|
|
|