From 7814496ab0b0fb6d5de4a9249374221a91c2c663 Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:41:33 +0700 Subject: [PATCH 1/2] Implement iridescent layer functions, integrate them into GltfCompliantMaterial --- src/webgpu/materials/GltfCompliantMaterial.js | 20 ++- src/webgpu/nodes/material.wgsl.js | 147 +++++++++++++++++- src/webgpu/nodes/utils.wgsl.js | 32 ++++ 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/webgpu/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js index f4aa3a04..afa258bf 100644 --- a/src/webgpu/materials/GltfCompliantMaterial.js +++ b/src/webgpu/materials/GltfCompliantMaterial.js @@ -2,7 +2,7 @@ import { texture, textureStore, globalId, float } from 'three/tsl'; import { StorageTexture, RedFormat, LinearFilter, TextureLoader, HalfFloatType } from 'three/webgpu'; import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode'; import { PathtracingMaterial } from './PathtracingMaterial'; -import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc, albedoIntegralMetallic, fresnelCoatFunc } from '../nodes/material.wgsl.js'; +import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc, albedoIntegralMetallic, fresnelCoatFunc, iridescentDielectricLayerFunc, iridescentConductorLayerFunc } from '../nodes/material.wgsl.js'; import { diffuseDirectionFunc, getLobeWeightsFunc } from '../nodes/sampling.wgsl.js'; import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc } from '../nodes/ggx.wgsl.js'; import { bxdfContextStruct, scatterRecordStruct, surfaceRecordStruct } from '../nodes/structs.wgsl.js'; @@ -27,6 +27,8 @@ export class GltfCompliantMaterial extends PathtracingMaterial { fresnelMix = fresnelMixFunc, conductorFresnel = conductorFresnelFunc, fresnelCoat = fresnelCoatFunc, + iridescentDielectricLayer = iridescentDielectricLayerFunc, + iridescentConductorLayer = iridescentConductorLayerFunc, calculateTurquinTexture = false, } = options; @@ -53,6 +55,8 @@ export class GltfCompliantMaterial extends PathtracingMaterial { this.fresnelMix = fresnelMix; this.conductorFresnel = conductorFresnel( turquinNode ); this.fresnelCoat = fresnelCoat; + this.iridescentDielectricLayer = iridescentDielectricLayer; + this.iridescentConductorLayer = iridescentConductorLayer; this.calculateTurquinTexture = calculateTurquinTexture; } @@ -85,9 +89,19 @@ export class GltfCompliantMaterial extends PathtracingMaterial { let specular = ${ this.specularBrdf }( ctx.NdotL, ctx.NdotV, ctx.NdotH, alpha ); let diffuse = ${ this.diffuseBrdf }( ctx.NdotV, ctx.NdotL, ctx.VdotH, surf ); - let dielectric = ${ this.fresnelMix }( ctx.VdotH, surf.ior, diffuse, specular ); + let dielectricBase = ${ this.fresnelMix }( ctx.VdotH, surf.ior, diffuse, specular ); - let metallic = ${ this.conductorFresnel }( ctx.NdotV, ctx.VdotH, surf.color, specular, alpha ); + let dielectric = ${ this.iridescentDielectricLayer }( + dielectricBase, diffuse, specular, ctx.VdotH, /* outsideIor */ 1.0, + surf.ior, surf.iridescenceIor, surf.iridescenceThickness, surf.iridescence + ); + + let metallicBase = ${ this.conductorFresnel }( ctx.NdotV, ctx.VdotH, surf.color, specular, alpha ); + + let metallic = ${ this.iridescentConductorLayer }( + metallicBase, specular, surf.color, ctx.VdotH, /* outsideIor */ 1.0, + surf.iridescenceIor, surf.iridescenceThickness, surf.iridescence + ); let material = mix( dielectric, metallic, surf.metalness ); diff --git a/src/webgpu/nodes/material.wgsl.js b/src/webgpu/nodes/material.wgsl.js index 98b8e47c..a6f95573 100644 --- a/src/webgpu/nodes/material.wgsl.js +++ b/src/webgpu/nodes/material.wgsl.js @@ -1,4 +1,4 @@ -import { wgslFn } from 'three/tsl'; +import { mat3, wgsl, wgslFn } from 'three/tsl'; import { inverseMat3x3Func, getBasisFromNormalFunc, @@ -6,6 +6,9 @@ import { schlickFresnelFunc, schlickFresnelVecFunc, sampleTexelFunc, + iorToF0GeneralFunc, + fresnel0ToIorFunc, + iorToF0GeneralVecFunc, } from './utils.wgsl.js'; import { ggxSmithVisibilityFunc, @@ -16,6 +19,7 @@ import { import { constants, surfaceRecordStruct, scatterRecordStruct } from './structs.wgsl.js'; import { sampleSphereCosineFn } from './sampling.wgsl.js'; import { pcgInit, pcgRand2 } from './random.wgsl.js'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; export const getSurfaceRecordFunc = wgslFn( /* wgsl */ ` @@ -353,6 +357,147 @@ export const fresnelMixFunc = wgslFn( /* wgsl */ ` `, [ schlickFresnelFunc, iorToF0Func ] ); +const iridConst = wgsl( /* wgsl */ ` + +const XYZ_TO_REC709 = mat3x3( + 3.2404542, - 0.9692660, 0.0556434, + - 1.5371385, 1.8760108, - 0.2040259, + - 0.4985314, 0.0415560, 1.0572252 +); + + +` ); + +const evalSensitivityFunc = wgslTagFn` + + fn evalSensitivity( OPD: f32, shift: vec3f ) -> vec3f { + ${ [ iridConst ] } + + let phase = 2.0 * ${ Math.PI } * OPD * 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); + + var xyz = val * sqrt(2.0 * ${ Math.PI } * _var) * cos(pos * phase + shift) * exp(-phase * phase * _var); + xyz.x += 9.7470e-14 * sqrt(2.0 * ${ Math.PI } * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase); + xyz /= 1.0685e-7; + + let rgb = XYZ_TO_REC709 * xyz; + return rgb; + + } + +`; + +// Reference: Belcour/Barla, 2017 +// https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html +// This is a simplified model that ignores light polarization and uses fresnel approximation +export const iridescentFresnelFunc = wgslFn( /* wgsl */ ` + + fn iridescentFresnel( + cosTheta1: f32, baseF0: vec3f, iridescenceIor: f32, + outsideIor: f32, iridescenceThickness: f32, + ) -> vec3f { + + let sinTheta2Sq = pow( outsideIor / iridescenceIor, 2.0 ) * ( 1.0 - pow( cosTheta1, 2.0 ) ); + let cosTheta2Sq = 1.0 - sinTheta2Sq; + + // Handle total internal reflection + if ( cosTheta2Sq < 0.0 ) { + + return vec3( 1.0 ); + + } + + let cosTheta2 = sqrt( cosTheta2Sq ); + + // First interface: air -> iridescent thin film + let R0 = iorToF0General( iridescenceIor, outsideIor ); + let R12 = schlickFresnel( cosTheta1, R0 ); + let R21 = R12; + let T121 = 1.0 - R12; + let phi12 = select( 0.0, PI, iridescenceIor < outsideIor ); + let phi21 = PI - phi12; + + // Second interface: iridescent thin film -> base material + let baseIor = fresnel0ToIor( baseF0 + 0.0001 ); // guard against 1.0 + let R1 = iorToF0GeneralVec( baseIor, vec3( iridescenceIor ) ); + let R23 = schlickFresnelVec( cosTheta2, R1, vec3( 1.0 ) ); + let phi23 = select( vec3( 0.0 ), vec3( PI ), baseIor < vec3( iridescenceIor ) ); + + // Phase shift + let OPD = 2.0 * iridescenceIor * iridescenceThickness * cosTheta2; + let phi = vec3( phi21 ) + phi23; + + // Analytical integration + // Compound terms + let R123 = clamp( R12 * R23, vec3( 1e-5 ), vec3( 0.9999 ) ); + let r123 = sqrt( R123 ); + let Rs = T121 * T121 * R23 / ( vec3( 1.0 ) - R123 ); + + // Reflectance term for m = 0 (DC term amplitude) + let C0 = R12 + Rs; + var I = C0; + + // Reflectance term for m > 0 (pairs of diracs) + var Cm = Rs - T121; + for (var m = 1; m <= 2; m += 1) { + + Cm *= r123; + let Sm = 2.0 * evalSensitivity( f32( m ) * OPD, f32( m ) * phi ); + I += Cm * Sm; + + } + + return max( I, vec3(0.0) ); + + } + +`, [ iorToF0GeneralFunc, iorToF0GeneralVecFunc, schlickFresnelFunc, fresnel0ToIorFunc, evalSensitivityFunc ] ); + +const rgbMixFunc = wgslFn( /* wgsl */ ` + + fn rgbMix( base: vec3f, specular: vec3f, rgbAlpha: vec3f ) -> vec3f { + + let alphaMax = max( max( rgbAlpha.x, rgbAlpha.y ), rgbAlpha.z ); + return ( 1 - alphaMax ) * base + rgbAlpha * specular; + + } + +` ); + +export const iridescentDielectricLayerFunc = wgslFn( /* wgsl */ ` + + fn iridescentDielectricLayer( + dielectricBase: vec3f, base: vec3f, specular: vec3f, HdotL: f32, + outsideIor: f32, baseIor: f32, iridescenceIor: f32, thickness: f32, strength: f32, + ) -> vec3f { + + let baseF0 = vec3( iorToF0( baseIor ) ); + + let iridescentF = iridescentFresnel( HdotL, baseF0, iridescenceIor, outsideIor, thickness ); + + return mix( dielectricBase, rgbMix( base, specular, iridescentF ), strength ); + + } + +`, [ iorToF0Func, iridescentFresnelFunc, rgbMixFunc ] ); + +export const iridescentConductorLayerFunc = wgslFn( /* wgsl */ ` + + fn iridescentConductorLayer( + metalBase: vec3f, specular: vec3f, baseF0: vec3f, HdotL: f32, + outsideIor: f32, iridescenceIor: f32, thickness: f32, strength: f32, + ) -> vec3f { + + let iridescenceF = iridescentFresnel( HdotL, baseF0, iridescenceIor, outsideIor, thickness ); + + return mix( metalBase, specular * iridescenceF, strength ); + + } + +`, [ iridescentFresnelFunc ] ); + export const conductorFresnelFunc = ( turquinTexture ) => wgslFn( /* wgsl */ ` fn conductorFresnel( NdotV: f32, VdotH: f32, f0: vec3f, bsdf: vec3f, alpha: f32 ) -> vec3f { diff --git a/src/webgpu/nodes/utils.wgsl.js b/src/webgpu/nodes/utils.wgsl.js index 03c8c92d..ecd52de2 100644 --- a/src/webgpu/nodes/utils.wgsl.js +++ b/src/webgpu/nodes/utils.wgsl.js @@ -55,6 +55,38 @@ export const iorToF0Func = wgslFn( /* wgsl */ ` ` ); +export const iorToF0GeneralFunc = wgslFn( /* wgsl */ ` + + fn iorToF0General( transmittedIor: f32, incidentIor: f32 ) -> f32 { + + return pow( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ), 2 ); + + } + +` ); + +export const iorToF0GeneralVecFunc = wgslFn( /* wgsl */ ` + + fn iorToF0GeneralVec( transmittedIor: vec3f, incidentIor: vec3f ) -> vec3f { + + let v = ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ); + return v * v; + + } + +` ); + +export const fresnel0ToIorFunc = wgslFn( /* wgsl */ ` + + fn fresnel0ToIor( f0: vec3f ) -> vec3f { + + let sqrtF0 = sqrt( f0 ); + return ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 ); + + } + +` ); + export const schlickFresnelFunc = wgslFn( /* wgsl */ ` fn schlickFresnel( cosine: f32, f0: f32 ) -> f32 { From c3277ae1ec4bebabae9460d57301c9a7e78a17bc Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Sat, 30 May 2026 19:03:29 +0700 Subject: [PATCH 2/2] Indentation fixes, use mat3 node --- src/webgpu/nodes/material.wgsl.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/webgpu/nodes/material.wgsl.js b/src/webgpu/nodes/material.wgsl.js index a6f95573..05134e26 100644 --- a/src/webgpu/nodes/material.wgsl.js +++ b/src/webgpu/nodes/material.wgsl.js @@ -1,4 +1,4 @@ -import { mat3, wgsl, wgslFn } from 'three/tsl'; +import { mat3, wgslFn } from 'three/tsl'; import { inverseMat3x3Func, getBasisFromNormalFunc, @@ -357,33 +357,27 @@ export const fresnelMixFunc = wgslFn( /* wgsl */ ` `, [ schlickFresnelFunc, iorToF0Func ] ); -const iridConst = wgsl( /* wgsl */ ` - -const XYZ_TO_REC709 = mat3x3( +const XYZ_TO_REC709 = mat3( 3.2404542, - 0.9692660, 0.0556434, - 1.5371385, 1.8760108, - 0.2040259, - - 0.4985314, 0.0415560, 1.0572252 + - 0.4985314, 0.0415560, 1.0572252, ); - -` ); - -const evalSensitivityFunc = wgslTagFn` +const evalSensitivityFunc = wgslTagFn/* wgsl */` fn evalSensitivity( OPD: f32, shift: vec3f ) -> vec3f { - ${ [ iridConst ] } let phase = 2.0 * ${ Math.PI } * OPD * 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 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); - var xyz = val * sqrt(2.0 * ${ Math.PI } * _var) * cos(pos * phase + shift) * exp(-phase * phase * _var); - xyz.x += 9.7470e-14 * sqrt(2.0 * ${ Math.PI } * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase); - xyz /= 1.0685e-7; + var xyz = val * sqrt(2.0 * ${ Math.PI } * _var) * cos(pos * phase + shift) * exp(-phase * phase * _var); + xyz.x += 9.7470e-14 * sqrt(2.0 * ${ Math.PI } * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x) * exp(-4.5282e+09 * phase * phase); + xyz /= 1.0685e-7; - let rgb = XYZ_TO_REC709 * xyz; - return rgb; + let rgb = ${ XYZ_TO_REC709 } * xyz; + return rgb; }