three-gpu-pathtracer 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +53 -530
  2. package/build/index.module.js +262 -50
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +261 -48
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +29 -11
  7. package/src/core/WebGLPathTracer.js +8 -0
  8. package/src/core/utils/sceneUpdateUtils.js +8 -12
  9. package/src/detectors/PrecisionMaterial.js +9 -9
  10. package/src/index.js +1 -0
  11. package/src/objects/PhysicalSpotLight.js +2 -0
  12. package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
  13. package/src/textures/BlueNoiseTexture.js +6 -6
  14. package/src/textures/ProceduralEquirectTexture.js +8 -7
  15. package/src/textures/turquinMetal.png +0 -0
  16. package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
  17. package/src/uniforms/FloatAttributeTextureArray.js +11 -11
  18. package/src/uniforms/MaterialsTexture.js +9 -9
  19. package/src/webgpu/API.md +762 -0
  20. package/src/webgpu/AtlasTexture.js +471 -0
  21. package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
  22. package/src/webgpu/EquirectBackgroundInfo.js +97 -0
  23. package/src/webgpu/EquirectHdrInfoNode.js +151 -0
  24. package/src/webgpu/LightsInfoNode.js +224 -0
  25. package/src/webgpu/MegaKernelPathTracer.js +277 -0
  26. package/src/webgpu/PathTracerBackend.js +216 -0
  27. package/src/webgpu/TurquinTexture.js +169 -0
  28. package/src/webgpu/WaveFrontPathTracer.js +586 -0
  29. package/src/webgpu/WebGPUPathTracer.js +1302 -0
  30. package/src/webgpu/compute/ComputeKernel.js +92 -0
  31. package/src/webgpu/compute/CopyBufferKernel.js +40 -0
  32. package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
  33. package/src/webgpu/compute/SampleDebugKernel.js +51 -0
  34. package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
  35. package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
  36. package/src/webgpu/compute/ZeroOutKernel.js +31 -0
  37. package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
  38. package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
  39. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
  40. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
  41. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
  42. package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
  43. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
  44. package/src/webgpu/compute/wavefront/structs.js +155 -0
  45. package/src/webgpu/constants.js +62 -0
  46. package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
  47. package/src/webgpu/index.d.ts +176 -0
  48. package/src/webgpu/index.js +11 -0
  49. package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
  50. package/src/webgpu/materials/GraphMaterial.js +331 -0
  51. package/src/webgpu/materials/PathtracingMaterial.js +89 -0
  52. package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
  53. package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
  54. package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
  55. package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
  56. package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
  57. package/src/webgpu/nodes/eon.wgsl.js +262 -0
  58. package/src/webgpu/nodes/ggx.wgsl.js +190 -0
  59. package/src/webgpu/nodes/lights.wgsl.js +203 -0
  60. package/src/webgpu/nodes/material.wgsl.js +990 -0
  61. package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
  62. package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
  63. package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
  64. package/src/webgpu/nodes/random.wgsl.js +20 -0
  65. package/src/webgpu/nodes/reset.wgsl.js +21 -0
  66. package/src/webgpu/nodes/sampling.wgsl.js +193 -0
  67. package/src/webgpu/nodes/sheen.wgsl.js +148 -0
  68. package/src/webgpu/nodes/structs.wgsl.js +272 -0
  69. package/src/webgpu/nodes/utils.wgsl.js +366 -0
  70. package/src/webgpu/shims/ArrayCameraShim.js +74 -0
  71. package/src/webgpu/shims/EquirectCameraShim.js +37 -0
  72. package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
  73. package/src/webgpu/upscale/FSRUpscaler.js +108 -0
@@ -0,0 +1,608 @@
1
+ import { float } from 'three/tsl';
2
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
3
+ import { PathtracingMaterial } from './PathtracingMaterial.js';
4
+ import { specularBrdfFunc, specularBtdfFunc, fresnelMixFunc, conductorFresnelFunc, fresnelCoatFunc, iridescentFresnelFunc, isMatchedIorFunc, thinWallTransmissionRoughnessFunc, transmissionFresnelFunc } from '../nodes/material.wgsl.js';
5
+ import { eonBrdfFunc, eonDirectionFunc, eonPDFFunc } from '../nodes/eon.wgsl.js';
6
+ import { sheenColorFunc, sheenAlbedoScalingFunc } from '../nodes/sheen.wgsl.js';
7
+ import { getLobeWeightsFunc } from '../nodes/sampling.wgsl.js';
8
+ import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc, ggxRefractionAdjustedPDFFunc } from '../nodes/ggx.wgsl.js';
9
+ import { bxdfContextStruct, scatterRecordStruct, surfaceRecordStruct } from '../nodes/structs.wgsl.js';
10
+ import { rand1, rand2, RNG_INDEX_SCATTER_DIRECTION, RNG_INDEX_SCATTER_TYPE } from '../nodes/random.wgsl.js';
11
+ import { TurquinTexture } from '../TurquinTexture.js';
12
+ import { iorToF0Func, schlickFresnelFunc, schlickFresnelVecFunc, dielectricFresnelFunc } from '../nodes/utils.wgsl.js';
13
+
14
+ const CLEARCOAT_IOR = float( 1.5 );
15
+ const SINGULAR_EVENT_SCALE = float( 1e6 );
16
+
17
+ export class GltfCompliantMaterial extends PathtracingMaterial {
18
+
19
+ constructor( options = {} ) {
20
+
21
+ super();
22
+
23
+ const {
24
+ specularBrdf = specularBrdfFunc,
25
+ specularBtdf = specularBtdfFunc,
26
+ diffuseBrdf = eonBrdfFunc,
27
+ fresnelMix = fresnelMixFunc,
28
+ conductorFresnel = conductorFresnelFunc,
29
+ fresnelCoat = fresnelCoatFunc,
30
+ iridescentFresnel = iridescentFresnelFunc,
31
+ } = options;
32
+
33
+ this.turquinTexture = new TurquinTexture();
34
+ this.specularBrdf = specularBrdf;
35
+ this.specularBtdf = specularBtdf;
36
+ this.diffuseBrdf = diffuseBrdf;
37
+ this.fresnelMix = fresnelMix;
38
+ this.conductorFresnel = conductorFresnel;
39
+ this.fresnelCoat = fresnelCoat;
40
+ this.iridescentFresnel = iridescentFresnel;
41
+ this.transmissionFresnel = transmissionFresnelFunc( iridescentFresnel );
42
+
43
+ }
44
+
45
+ init( renderer ) {
46
+
47
+ this.turquinTexture.generate( renderer );
48
+
49
+ }
50
+
51
+ getBsdfNode() {
52
+
53
+ this._bsdfEvalFunc = wgslTagFn/* wgsl */`
54
+
55
+ // The material is organized as one scoped block per lobe in cascade order - clearcoat,
56
+ // sheen, transmission, specular, diffuse - each accumulating into a shared result and
57
+ // guarding its own hemisphere.
58
+ // TODO: energy only cascades downward through front faces, so the layers are always traversed
59
+ // outside-in and cannot reflect energy back into the glass from below.
60
+ fn bsdfEval( ctx: ${ bxdfContextStruct }, surf: ${ surfaceRecordStruct } ) -> vec3f {
61
+
62
+ let NdotV = ctx.V.z;
63
+ let NdotL = ctx.L.z;
64
+
65
+ // Each lobe contributes into "result" within its own scope, evaluated in cascade.
66
+ // "attenuation" carries the fraction of energy each layer passes through to
67
+ // the layers beneath it. Every lobe guards its own hemisphere.
68
+ var result = vec3f( 0.0 );
69
+ var attenuation = vec3f( 1.0 );
70
+
71
+ // clearcoat
72
+ if ( NdotL > 0.0 && surf.clearcoat > 0.0 ) {
73
+
74
+ // the clearcoat evaluates in its own frame
75
+ let toClearcoatMat = surf.clearcoatInvBasis * surf.normalBasis;
76
+ let Vc = normalize( toClearcoatMat * ctx.V );
77
+ let Lc = normalize( toClearcoatMat * ctx.L );
78
+ let Hc = normalize( toClearcoatMat * ctx.H );
79
+ let NdotVc = Vc.z;
80
+
81
+ let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
82
+
83
+ // reuse the same pattern for energy conservation used in the dielectric layer
84
+ let clearcoatEnergySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotVc, surf.clearcoatRoughness ), 1e-5 );
85
+ let clearcoatBoost = 1.0 + ${ iorToF0Func }( 1.5 ) * ( 1.0 - clearcoatEnergySS ) / clearcoatEnergySS;
86
+
87
+ let clearcoatSpecular = ${ this.specularBrdf }( Vc, Lc, Hc, vec2( clearcoatAlpha ) ) * clearcoatBoost;
88
+ let clearcoatFresnel = ${ schlickFresnelFunc }( abs( dot( Vc, Hc ) ), ${ iorToF0Func }( 1.5 ) );
89
+
90
+ // retrieve specular energy reflected by this layer
91
+ let clearcoatFresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotVc, surf.clearcoatRoughness, 1.5 ) * clearcoatBoost;
92
+
93
+ result += surf.clearcoat * clearcoatFresnel * clearcoatSpecular;
94
+ attenuation *= 1.0 - surf.clearcoat * clearcoatFresnelEnergySS;
95
+
96
+ }
97
+
98
+ // sheen
99
+ if ( NdotL > 0.0 && surf.sheen > 0.0 ) {
100
+
101
+ result += attenuation * surf.sheen * ${ sheenColorFunc }( ctx.V, ctx.L, ctx.H, surf );
102
+ attenuation *= mix( 1.0, ${ sheenAlbedoScalingFunc }( ctx.V, ctx.L, surf ), surf.sheen );
103
+
104
+ }
105
+
106
+ // transmission
107
+ // handles specular reflection and transmission
108
+ if ( surf.transmission > 0.0 ) {
109
+
110
+ let matchedIor = ${ isMatchedIorFunc }( surf.eta );
111
+
112
+ // anisotropic roughness along tangent, bitangent
113
+ let alphaB = surf.roughness * surf.roughness;
114
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
115
+ let alpha = vec2( alphaT, alphaB );
116
+
117
+ // multiscatter compensation
118
+ var glassBoost = 0.0;
119
+ if ( matchedIor ) {
120
+
121
+ glassBoost = 1.0;
122
+
123
+ } else if ( surf.thinWall ) {
124
+
125
+ // thin wall halves are reflection-shaped so each is compensated with the conductor albedo at its own roughness.
126
+ glassBoost = 1.0 / max( ${ this.turquinTexture.sampleConductorFn }( NdotV, surf.roughness ), 1e-5 );
127
+
128
+ } else {
129
+
130
+ // volumetric bsdf is scaled by its total reflected + refracted energy
131
+ glassBoost = 1.0 / max( ${ this.turquinTexture.sampleTransmissiveFn }( NdotV, surf.roughness, surf.ior, surf.frontFace ), 1e-5 );
132
+
133
+ }
134
+
135
+ let fresnelCosine = select( ctx.VdotH, NdotV, surf.thinWall );
136
+ let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
137
+
138
+ if ( NdotL < 0.0 ) {
139
+
140
+ var refraction = vec3f( 0.0 );
141
+ if ( surf.thinWall && ! matchedIor ) {
142
+
143
+ // evaluate the flipped reflection, compensated at the remapped roughness
144
+ let wiMirror = vec3f( ctx.L.xy, - ctx.L.z );
145
+ let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
146
+ let thinWallEnergySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotV, sqrt( thinWallAlpha.x ) ), 1e-5 );
147
+ refraction = fresnel.transmittance * ${ this.specularBrdf }( ctx.V, wiMirror, ctx.H, thinWallAlpha ) / thinWallEnergySS;
148
+
149
+ } else if ( ! matchedIor ) {
150
+
151
+ // The default BTDF contains the plain dielectric Fresnel complement. Replace it
152
+ // with the colored thin-film-aware coefficient shared by sampling and the PDF.
153
+ let dielectricTransmittance = 1.0 - ${ dielectricFresnelFunc }( abs( ctx.VdotH ), surf.eta );
154
+ if ( dielectricTransmittance > EPSILON ) {
155
+
156
+ refraction = ${ this.specularBtdf }( ctx.V, ctx.L, ctx.H, alpha, surf.eta ) *
157
+ fresnel.transmittance / dielectricTransmittance * glassBoost;
158
+
159
+ }
160
+
161
+ }
162
+
163
+ // the refracted half is not attenuated by the layers above
164
+ result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * surf.color;
165
+
166
+ } else {
167
+
168
+ // the raw single scatter lobe scaled by the glass compensation boost rather
169
+ // than the opaque dielectric boost
170
+ let specular = ${ this.specularBrdf }( ctx.V, ctx.L, ctx.H, alpha );
171
+ let dielectricSpecular = specular * glassBoost;
172
+
173
+ let reflection = dielectricSpecular * fresnel.reflectance;
174
+
175
+ result += attenuation * ( 1.0 - surf.metalness ) * surf.transmission * reflection;
176
+
177
+ }
178
+
179
+ }
180
+
181
+ // specular
182
+ // metallic + dielectric + iridescence
183
+ if ( NdotL > 0.0 ) {
184
+
185
+ // anisotropic roughness along tangent, bitangent
186
+ let alphaB = surf.roughness * surf.roughness;
187
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
188
+ let alpha = vec2( alphaT, alphaB );
189
+
190
+ // Sample the single scatter energy for specular at the given roughness
191
+ let energySS = max( ${ this.turquinTexture.sampleConductorFn }( NdotV, surf.roughness ), 1e-5 );
192
+ let specular = ${ this.specularBrdf }( ctx.V, ctx.L, ctx.H, alpha );
193
+
194
+ // metallic with the multiscatter comp
195
+ let metallicBoost = 1.0 + surf.color * ( 1.0 - energySS ) / energySS;
196
+ let metallicSpecular = specular * metallicBoost;
197
+ var metallicReflectance = ${ this.conductorFresnel }( ctx.VdotH, surf.color, vec3f( 1.0 ) );
198
+
199
+ // dielectric with the multiscatter comp
200
+ let dielectricBoost = 1.0 + surf.f0 * ( 1.0 - energySS ) / energySS;
201
+ let dielectricSpecular = specular * dielectricBoost;
202
+
203
+ // KHR_materials_specular: fold the specular color and intensity into the dielectric f0.
204
+ // Schlick is used on both hit sides, matching Cycles - the opaque specular carries no
205
+ // TIR so the energy removed from the base always matches the energy paid back
206
+ let dielectricF0 = min( surf.f0 * surf.specularColor, vec3f( 1.0 ) );
207
+ let dielectricFr = ${ schlickFresnelVecFunc }( ctx.VdotH, dielectricF0, vec3f( 1.0 ) );
208
+ var dielectricReflectance = surf.specularIntensity * dielectricFr;
209
+
210
+ // iridescence
211
+ var filmFresnelMax = 0.0;
212
+ if ( surf.iridescence > 0.0 ) {
213
+
214
+ // the media on either side of the film - air outside and the volume interior
215
+ // as the base on front faces, swapped on back faces so TIR can take effect
216
+ let outsideIor = select( surf.ior, 1.0, surf.frontFace );
217
+ let filmBaseIor = select( 1.0, surf.ior, surf.frontFace );
218
+
219
+ let metallicFilmFresnel = ${ this.iridescentFresnel }( ctx.VdotH, surf.color, surf.iridescenceIor, outsideIor, surf.iridescenceThickness );
220
+ metallicReflectance = mix( metallicReflectance, metallicFilmFresnel, surf.iridescence );
221
+
222
+ let dielectricFilmFresnel = ${ this.iridescentFresnel }( ctx.VdotH, vec3f( ${ iorToF0Func }( filmBaseIor ) ), surf.iridescenceIor, outsideIor, surf.iridescenceThickness );
223
+ dielectricReflectance = mix( dielectricReflectance, dielectricFilmFresnel, surf.iridescence );
224
+ filmFresnelMax = max( max( dielectricFilmFresnel.r, dielectricFilmFresnel.g ), dielectricFilmFresnel.b );
225
+
226
+ }
227
+
228
+ let metallic = metallicSpecular * metallicReflectance;
229
+ let dielectric = dielectricSpecular * dielectricReflectance;
230
+
231
+ // the energy the specular interface takes from the layers below
232
+ let fresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior ) * dielectricBoost;
233
+
234
+ result += attenuation * mix( ( 1.0 - surf.transmission ) * dielectric, metallic, surf.metalness );
235
+ attenuation *= ( 1.0 - surf.metalness ) * mix( 1.0 - fresnelEnergySS, 1.0 - filmFresnelMax, surf.iridescence );
236
+
237
+ }
238
+
239
+ // diffuse
240
+ if ( NdotL > 0.0 ) {
241
+
242
+ // the dielectric base mixes diffuse with transmission - the transmissive half
243
+ // is carried by the glass lobe above
244
+ let diffuseVdotH = sqrt( saturate( 0.5 * ( 1.0 + dot( ctx.V, ctx.L ) ) ) );
245
+ let reflection = ${ this.diffuseBrdf }( NdotV, NdotL, diffuseVdotH, surf );
246
+ let diffuse = ( 1.0 - surf.transmission ) * reflection;
247
+
248
+ result += attenuation * diffuse;
249
+
250
+ }
251
+
252
+ return result;
253
+
254
+ }
255
+
256
+ `;
257
+
258
+ const bsdfEvalPdfFn = this.getBsdfEvalPdfNode();
259
+
260
+ return wgslTagFn/* wgsl */`
261
+
262
+ fn bsdfSample( worldWo: vec3f, surf: ${ surfaceRecordStruct } ) -> ${ scatterRecordStruct } {
263
+
264
+ var result: ${ scatterRecordStruct };
265
+ result.color = vec3f( 0.0 );
266
+ result.isTransmissive = false;
267
+ result.direction = vec3f( 0.0 );
268
+ result.pdf = 0.0;
269
+
270
+ let wo = normalize( surf.normalInvBasis * worldWo );
271
+ let woClearcoat = normalize( surf.clearcoatInvBasis * worldWo );
272
+
273
+ // TODO: mirror bsdfEval's layer structure - the glass layer should own both its
274
+ // reflection and refraction halves
275
+ // lobe selection weights and cumulative bounds in cascade order:
276
+ // clearcoat, specular, transmission, diffuse
277
+ let weights = ${ getLobeWeightsFunc }( wo, wo, woClearcoat, vec3( 0, 0, 1 ), ${ CLEARCOAT_IOR }, surf );
278
+ let cdfClearcoat = weights.clearcoat;
279
+ let cdfSpecular = cdfClearcoat + weights.specular;
280
+ let cdfTransmission = cdfSpecular + weights.transmission;
281
+ let cdfTotal = cdfTransmission + weights.diffuse;
282
+
283
+ // random samples for lobes
284
+ let lobeSample = ${ rand1 }( ${ RNG_INDEX_SCATTER_TYPE } ) * cdfTotal;
285
+ let directionUV = ${ rand2 }( ${ RNG_INDEX_SCATTER_DIRECTION } );
286
+
287
+ // output
288
+ var wi: vec3f;
289
+ var wh: vec3f;
290
+
291
+ // TODO: see if we can clean up these flags so they're not necessary
292
+ var isTransmissionLobe = false;
293
+ var isDeltaTransmission = false;
294
+ var deltaTransmittance = vec3f( 1.0 );
295
+ var deltaProbability = 1.0;
296
+ var isDead = false;
297
+
298
+ if ( lobeSample <= cdfClearcoat ) {
299
+
300
+ // clearcoat
301
+ let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
302
+ let whCoat = ${ ggxDirectionFunc }( woClearcoat, vec2( clearcoatAlpha ), directionUV );
303
+ let wiCoat = - normalize( reflect( woClearcoat, whCoat ) );
304
+
305
+ wi = normalize( surf.normalInvBasis * surf.clearcoatBasis * wiCoat );
306
+ wh = normalize( surf.normalInvBasis * surf.clearcoatBasis * whCoat );
307
+
308
+ // reflected rays must leave above the geometry surface - flip rays that land
309
+ // below it due to the shading normal. Rays below the shading hemisphere are
310
+ // left as is since their loss is refunded by the energy compensation tables
311
+ let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
312
+ let geomDotDir = dot( wi, faceNormal );
313
+ if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
314
+
315
+ wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
316
+
317
+ }
318
+
319
+ } else if ( lobeSample <= cdfSpecular ) {
320
+
321
+ // specular
322
+ // anisotropic roughness along tangent, bitangent
323
+ let alphaB = surf.roughness * surf.roughness;
324
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
325
+ let alpha = vec2( alphaT, alphaB );
326
+
327
+ wh = ${ ggxDirectionFunc }( wo, alpha, directionUV );
328
+ wi = - normalize( reflect( wo, wh ) );
329
+
330
+ // reflected rays must leave above the geometry surface - flip rays that land
331
+ // below it due to the shading normal. Rays below the shading hemisphere are
332
+ // left as is since their loss is refunded by the energy compensation tables
333
+ let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
334
+ let geomDotDir = dot( wi, faceNormal );
335
+ if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
336
+
337
+ wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
338
+
339
+ }
340
+
341
+ } else if ( lobeSample <= cdfTransmission ) {
342
+
343
+ // transmission
344
+ isTransmissionLobe = true;
345
+
346
+ // anisotropic roughness along tangent, bitangent
347
+ let alphaB = surf.roughness * surf.roughness;
348
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
349
+ let alpha = vec2( alphaT, alphaB );
350
+
351
+ // sample the half vector first and select reflection or refraction by the
352
+ // facet fresnel, matching Cycles - total internal reflection drives the
353
+ // fresnel to 1 so TIR facets always reflect with a matching pdf
354
+ wh = ${ ggxDirectionFunc }( wo, alpha, directionUV );
355
+
356
+ let fresnelCosine = select( dot( wo, wh ), wo.z, surf.thinWall );
357
+ let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
358
+
359
+ let fresnelSample = ( lobeSample - cdfSpecular ) / ( cdfTransmission - cdfSpecular );
360
+ let doReflect = fresnelSample < fresnel.reflectProbability;
361
+ if ( doReflect ) {
362
+
363
+ wi = - normalize( reflect( wo, wh ) );
364
+
365
+ } else if ( ${ isMatchedIorFunc }( surf.eta ) ) {
366
+
367
+ // With no IOR boundary, rough transmission collapses to straight-through
368
+ // delta transmission independent of the sampled microfacet.
369
+ wi = - wo;
370
+ wh = vec3f( 0.0, 0.0, 1.0 );
371
+ isDeltaTransmission = true;
372
+ deltaTransmittance = fresnel.transmittance;
373
+ deltaProbability = 1.0 - fresnel.reflectProbability;
374
+
375
+ } else if ( surf.thinWall ) {
376
+
377
+ // model the double refraction as a single reflection flipped through the
378
+ // surface at the remapped thin wall roughness
379
+ let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
380
+ wh = ${ ggxDirectionFunc }( wo, thinWallAlpha, directionUV );
381
+ wi = - normalize( reflect( wo, wh ) );
382
+ wi = vec3f( wi.xy, - wi.z );
383
+
384
+ } else {
385
+
386
+ wi = refract( - wo, wh, surf.eta );
387
+
388
+ }
389
+
390
+ // the facet choice must agree with the resulting hemisphere - reflection above,
391
+ // transmission below. Mismatched samples are dropped since their loss is
392
+ // refunded by the energy compensation tables
393
+ isDead = doReflect != ( wi.z > 0.0 );
394
+
395
+ } else if ( lobeSample <= cdfTotal ) {
396
+
397
+ // diffuse
398
+ // all diffuse BRDF variants share the EON sampling proposal
399
+ wi = ${ eonDirectionFunc }( wo, surf.diffuseRoughness, directionUV );
400
+ wh = normalize( wi + wo );
401
+
402
+ // reflected rays must leave above the geometry surface - flip rays that land
403
+ // below it due to the shading normal. Rays below the shading hemisphere are
404
+ // left as is since their loss is refunded by the energy compensation tables
405
+ let faceNormal = normalize( surf.normalInvBasis * surf.faceNormal );
406
+ let geomDotDir = dot( wi, faceNormal );
407
+ if ( wi.z > 0.0 && geomDotDir < 0.0 ) {
408
+
409
+ wi = normalize( wi - 2.0 * geomDotDir * faceNormal );
410
+
411
+ }
412
+
413
+ }
414
+
415
+ let worldWi = normalize( surf.normalBasis * wi );
416
+ if ( isDeltaTransmission ) {
417
+
418
+ // Cycles represents a singular event with a large matching BSDF and PDF. Their
419
+ // ratio preserves throughput while forward MIS correctly gives the event full weight.
420
+ result.color = ( 1.0 - surf.metalness ) * surf.transmission * surf.color *
421
+ deltaTransmittance * ${ SINGULAR_EVENT_SCALE };
422
+ result.direction = worldWi;
423
+ result.pdf = weights.transmission * deltaProbability * ${ SINGULAR_EVENT_SCALE };
424
+ result.isTransmissive = dot( worldWi, surf.faceNormal ) < 0.0;
425
+
426
+ } else {
427
+
428
+ // evaluate the shared eval / pdf function for the sampled direction so the sample
429
+ // and light-sampling paths can never disagree on color or pdf
430
+ result = ${ bsdfEvalPdfFn }( worldWo, worldWi, surf );
431
+
432
+ }
433
+
434
+ // mismatched glass facets and reflection lobe samples that land below the
435
+ // hemisphere carry no energy - their loss is refunded by the compensation tables
436
+ let invalid = isDead || ( ! isTransmissionLobe && wi.z <= 0.0 );
437
+ result.color *= select( 1.0, 0.0, invalid );
438
+
439
+ return result;
440
+
441
+ }
442
+
443
+ `;
444
+
445
+ }
446
+
447
+ // Evaluates the BSDF and its sampling pdf for an arbitrary light direction ( both world space ).
448
+ // Used by next event estimation to weight a chosen light/environment direction. Shares the same
449
+ // bsdfEval and lobe-mixture pdf as bsdfSample so MIS weights stay consistent.
450
+ getBsdfEvalPdfNode() {
451
+
452
+ // bsdfSample embeds this node so both share a single instance
453
+ if ( this._bsdfEvalPdfNode ) {
454
+
455
+ return this._bsdfEvalPdfNode;
456
+
457
+ }
458
+
459
+ if ( ! this._bsdfEvalFunc ) {
460
+
461
+ this.getBsdfNode();
462
+
463
+ }
464
+
465
+ const bsdfEvalFunc = this._bsdfEvalFunc;
466
+
467
+ this._bsdfEvalPdfNode = wgslTagFn/* wgsl */`
468
+
469
+ fn bsdfEvalPdf( worldWo: vec3f, worldWi: vec3f, surf: ${ surfaceRecordStruct } ) -> ${ scatterRecordStruct } {
470
+
471
+ var result: ${ scatterRecordStruct };
472
+ result.color = vec3f( 0.0 );
473
+ result.isTransmissive = false;
474
+ result.direction = worldWi;
475
+ result.pdf = 0.0;
476
+
477
+ let wo = normalize( surf.normalInvBasis * worldWo );
478
+ let wi = normalize( surf.normalInvBasis * worldWi );
479
+ let woClearcoat = normalize( surf.clearcoatInvBasis * worldWo );
480
+
481
+ let isTransmission = wi.z < 0.0;
482
+ if ( isTransmission && ${ isMatchedIorFunc }( surf.eta ) ) {
483
+
484
+ // Delta transmission has no finite solid-angle BSDF or PDF. It is handled
485
+ // explicitly by bsdfSample, matching PBRT-v4's eta == 1 path.
486
+ return result;
487
+
488
+ }
489
+
490
+ // reconstruct the half vector bsdfSample would have used for this direction -
491
+ // reflections use the standard half vector while refractions use the generalized
492
+ // form scaled by the ior ratio, oriented into the upper hemisphere
493
+ var wh: vec3f;
494
+ if ( ! isTransmission ) {
495
+
496
+ wh = normalize( wo + wi );
497
+
498
+ } else if ( surf.thinWall ) {
499
+
500
+ // thin wall transmission is modeled as a reflection flipped through the surface
501
+ wh = normalize( wo + vec3f( wi.xy, - wi.z ) );
502
+
503
+ } else {
504
+
505
+ wh = normalize( wi + wo * surf.eta );
506
+ wh *= sign( wh.z );
507
+
508
+ }
509
+
510
+ let weights = ${ getLobeWeightsFunc }( wo, wo, woClearcoat, vec3( 0, 0, 1 ), ${ CLEARCOAT_IOR }, surf );
511
+
512
+ // TODO: mirror bsdfEval's layer structure - the glass layer should own both its
513
+ // reflection and refraction halves
514
+ // pdf mixture - every lobe that can produce the direction contributes its share,
515
+ // in the same cascade order and with the same terms as bsdfSample
516
+
517
+ // clearcoat
518
+ if ( weights.clearcoat > 0.0 ) {
519
+
520
+ // the clearcoat lobe evaluates in its own frame
521
+ let toClearcoatMat = surf.clearcoatInvBasis * surf.normalBasis;
522
+ let wiClearcoat = normalize( toClearcoatMat * wi );
523
+ if ( wiClearcoat.z > 0.0 ) {
524
+
525
+ let whClearcoat = normalize( toClearcoatMat * wh );
526
+ let clearcoatAlpha = surf.clearcoatRoughness * surf.clearcoatRoughness;
527
+ result.pdf += weights.clearcoat * ${ ggxReflectionAdjustedPDFFunc }( woClearcoat, whClearcoat, vec2( clearcoatAlpha ) );
528
+
529
+ }
530
+
531
+ }
532
+
533
+ // specular
534
+ if ( weights.specular > 0.0 && wi.z > 0.0 ) {
535
+
536
+ // anisotropic roughness along tangent, bitangent
537
+ let alphaB = surf.roughness * surf.roughness;
538
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
539
+ let alpha = vec2( alphaT, alphaB );
540
+
541
+ result.pdf += weights.specular * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, alpha );
542
+
543
+ }
544
+
545
+ // transmission
546
+ if ( weights.transmission > 0.0 ) {
547
+
548
+ // anisotropic roughness along tangent, bitangent
549
+ let alphaB = surf.roughness * surf.roughness;
550
+ let alphaT = mix( alphaB, 1.0, surf.anisotropy * surf.anisotropy );
551
+ let alpha = vec2( alphaT, alphaB );
552
+
553
+ // the glass lobe selects reflection or refraction by the facet fresnel so
554
+ // each side carries the corresponding share of the transmission pdf
555
+ let fresnelCosine = select( dot( wo, wh ), wo.z, surf.thinWall );
556
+ let fresnel = ${ this.transmissionFresnel }( fresnelCosine, surf );
557
+
558
+ if ( wi.z > 0.0 ) {
559
+
560
+ result.pdf += weights.transmission * fresnel.reflectProbability * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, alpha );
561
+
562
+ } else if ( surf.thinWall ) {
563
+
564
+ // the flipped reflection shares the reflection pdf at the remapped roughness
565
+ let thinWallAlpha = vec2f( ${ thinWallTransmissionRoughnessFunc }( alphaB, surf.ior ) );
566
+ result.pdf += weights.transmission * ( 1.0 - fresnel.reflectProbability ) * ${ ggxReflectionAdjustedPDFFunc }( wo, wh, thinWallAlpha );
567
+
568
+ } else {
569
+
570
+ result.pdf += weights.transmission * ( 1.0 - fresnel.reflectProbability ) * ${ ggxRefractionAdjustedPDFFunc }( wo, wi, wh, alpha, surf.eta );
571
+
572
+ }
573
+
574
+ }
575
+
576
+ // diffuse
577
+ if ( weights.diffuse > 0.0 ) {
578
+
579
+ result.pdf += weights.diffuse * ${ eonPDFFunc }( wo, wi, surf.diffuseRoughness );
580
+
581
+ }
582
+
583
+ //
584
+
585
+ // construct the scatter context
586
+ var ctx: ${ bxdfContextStruct };
587
+ ctx.V = wo;
588
+ ctx.L = wi;
589
+ ctx.H = wh;
590
+ ctx.VdotH = saturate( dot( wo, wh ) );
591
+
592
+ // evaluate the bsdf for the direction
593
+ result.color = ${ bsdfEvalFunc }( ctx, surf ) * abs( wi.z );
594
+
595
+ // a direction crossing below the surface enters or leaves the volume
596
+ result.isTransmissive = isTransmission && dot( worldWi, surf.faceNormal ) < 0.0;
597
+
598
+ return result;
599
+
600
+ }
601
+
602
+ `;
603
+
604
+ return this._bsdfEvalPdfNode;
605
+
606
+ }
607
+
608
+ }