three-gpu-pathtracer 0.0.23 → 0.0.25

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 (83) hide show
  1. package/README.md +53 -528
  2. package/build/index.module.js +319 -66
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +318 -64
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +35 -18
  7. package/src/core/PathTracingSceneGenerator.js +21 -2
  8. package/src/core/WebGLPathTracer.js +16 -7
  9. package/src/core/utils/mergeGeometries.js +14 -1
  10. package/src/core/utils/sceneUpdateUtils.js +8 -12
  11. package/src/detectors/PrecisionMaterial.js +9 -9
  12. package/src/index.d.ts +2 -1
  13. package/src/index.js +1 -0
  14. package/src/materials/pathtracing/PhysicalPathTracingMaterial.js +2 -1
  15. package/src/materials/pathtracing/glsl/attenuate_hit_function.glsl.js +2 -1
  16. package/src/materials/pathtracing/glsl/camera_util_functions.glsl.js +1 -1
  17. package/src/materials/pathtracing/glsl/get_surface_record_function.glsl.js +2 -1
  18. package/src/materials/surface/AmbientOcclusionMaterial.js +2 -0
  19. package/src/objects/PhysicalSpotLight.js +2 -0
  20. package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
  21. package/src/shader/bvh/inside_fog_volume_function.glsl.js +1 -1
  22. package/src/shader/structs/material_struct.glsl.js +3 -1
  23. package/src/textures/BlueNoiseTexture.js +6 -6
  24. package/src/textures/ProceduralEquirectTexture.js +8 -7
  25. package/src/textures/turquinMetal.png +0 -0
  26. package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
  27. package/src/uniforms/FloatAttributeTextureArray.js +11 -11
  28. package/src/uniforms/MaterialsTexture.js +13 -10
  29. package/src/webgpu/API.md +762 -0
  30. package/src/webgpu/AtlasTexture.js +471 -0
  31. package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
  32. package/src/webgpu/EquirectBackgroundInfo.js +97 -0
  33. package/src/webgpu/EquirectHdrInfoNode.js +151 -0
  34. package/src/webgpu/LightsInfoNode.js +224 -0
  35. package/src/webgpu/MegaKernelPathTracer.js +277 -0
  36. package/src/webgpu/PathTracerBackend.js +216 -0
  37. package/src/webgpu/TurquinTexture.js +169 -0
  38. package/src/webgpu/WaveFrontPathTracer.js +586 -0
  39. package/src/webgpu/WebGPUPathTracer.js +1302 -0
  40. package/src/webgpu/compute/ComputeKernel.js +92 -0
  41. package/src/webgpu/compute/CopyBufferKernel.js +40 -0
  42. package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
  43. package/src/webgpu/compute/SampleDebugKernel.js +51 -0
  44. package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
  45. package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
  46. package/src/webgpu/compute/ZeroOutKernel.js +31 -0
  47. package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
  48. package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
  49. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
  50. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
  51. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
  52. package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
  53. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
  54. package/src/webgpu/compute/wavefront/structs.js +155 -0
  55. package/src/webgpu/constants.js +62 -0
  56. package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
  57. package/src/webgpu/index.d.ts +176 -0
  58. package/src/webgpu/index.js +11 -0
  59. package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
  60. package/src/webgpu/materials/GraphMaterial.js +331 -0
  61. package/src/webgpu/materials/PathtracingMaterial.js +89 -0
  62. package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
  63. package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
  64. package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
  65. package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
  66. package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
  67. package/src/webgpu/nodes/eon.wgsl.js +262 -0
  68. package/src/webgpu/nodes/ggx.wgsl.js +190 -0
  69. package/src/webgpu/nodes/lights.wgsl.js +203 -0
  70. package/src/webgpu/nodes/material.wgsl.js +990 -0
  71. package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
  72. package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
  73. package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
  74. package/src/webgpu/nodes/random.wgsl.js +20 -0
  75. package/src/webgpu/nodes/reset.wgsl.js +21 -0
  76. package/src/webgpu/nodes/sampling.wgsl.js +193 -0
  77. package/src/webgpu/nodes/sheen.wgsl.js +148 -0
  78. package/src/webgpu/nodes/structs.wgsl.js +272 -0
  79. package/src/webgpu/nodes/utils.wgsl.js +366 -0
  80. package/src/webgpu/shims/ArrayCameraShim.js +74 -0
  81. package/src/webgpu/shims/EquirectCameraShim.js +37 -0
  82. package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
  83. package/src/webgpu/upscale/FSRUpscaler.js +108 -0
@@ -0,0 +1,990 @@
1
+ import { wgslFn, mat3 } from 'three/tsl';
2
+ import {
3
+ inverseMat3x3Func,
4
+ getBasisFromNormalFunc,
5
+ iorToF0Func,
6
+ schlickFresnelFunc,
7
+ schlickFresnelVecFunc,
8
+ iorToF0GeneralFunc,
9
+ fresnel0ToIorFunc,
10
+ iorToF0GeneralVecFunc,
11
+ dielectricFresnelFunc,
12
+ totalInternalReflectionFunc,
13
+ totalInternalReflectionVecFunc,
14
+ } from './utils.wgsl.js';
15
+ import {
16
+ ggxSmithVisibilityFunc,
17
+ ggxDistributionFunc,
18
+ ggxDirectionFunc,
19
+ ggxReflectionAdjustedPDFFunc,
20
+ ggxRefractionAdjustedPDFFunc,
21
+ ggxShadowMaskG1Func,
22
+ } from './ggx.wgsl.js';
23
+ import { constants, surfaceRecordStruct, transmissionFresnelStruct } from './structs.wgsl.js';
24
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
25
+
26
+ // Correct the shading normal to prevent scattering rays below the geometry surface by bending
27
+ // the normal towards the geometry normal such that the perfect reflection ray is just above the
28
+ // geometry surface. Ported verbatim from Blender Cycles "ensure_valid_specular_reflection"
29
+ // (src/kernel/closure/bsdf_util.h), which is derived from from the Iray paper (Keller et al. 2017, A.3).
30
+ //
31
+ // This only guarantees perfect reflection rays are above surface - other diffuse rays, for example,
32
+ // flipped when sampling.
33
+ const ensureValidReflectionNormal = wgslTagFn/* wgsl */`
34
+
35
+ fn ensureValidReflectionNormal( n: vec3f, ng: vec3f, i: vec3f ) -> vec3f {
36
+
37
+ // reflected ray
38
+ let R = 2.0 * dot( n, i ) * n - i;
39
+
40
+ let Iz = dot( i, ng );
41
+
42
+ // reflection rays may always be at least as shallow as the incoming ray
43
+ let threshold = min( 0.9 * Iz, 0.01 );
44
+ if ( dot( ng, R ) >= threshold ) {
45
+
46
+ return n;
47
+
48
+ }
49
+
50
+ // Form coordinate system with Ng as the Z axis and N inside the X-Z-plane.
51
+ // The X axis is found by normalizing the component of N that's orthogonal to Ng.
52
+ // The Y axis isn't actually needed.
53
+ let orthoN = n - dot( n, ng ) * ng;
54
+ let orthoLen = length( orthoN );
55
+ let X = select( n, orthoN / orthoLen, orthoLen != 0.0 );
56
+
57
+ // Calculate N.z and N.x in the local coordinate system.
58
+ //
59
+ // The goal of this computation is to find a N' that is rotated towards Ng just enough
60
+ // to lift R' above the threshold (here called t), therefore dot(R', Ng) = t.
61
+ //
62
+ // See the Blender implementation for a full description of the solution.
63
+ let Ix = dot( i, X );
64
+
65
+ let a = Ix * Ix + Iz * Iz;
66
+ let b = 2.0 * ( a + Iz * threshold );
67
+ let c = ( threshold + Iz ) * ( threshold + Iz );
68
+
69
+ // only one root is valid; the sign of Ix selects it
70
+ var Nz2: f32;
71
+ if ( Ix < 0.0 ) {
72
+
73
+ Nz2 = 0.25 * ( b + sqrt( max( b * b - 4.0 * a * c, 0.0 ) ) ) / a;
74
+
75
+ } else {
76
+
77
+ Nz2 = 0.25 * ( b - sqrt( max( b * b - 4.0 * a * c, 0.0 ) ) ) / a;
78
+
79
+ }
80
+
81
+ let Nx = sqrt( max( 1.0 - Nz2, 0.0 ) );
82
+ let Nz = sqrt( max( Nz2, 0.0 ) );
83
+
84
+ return Nx * X + Nz * ng;
85
+
86
+ }
87
+
88
+ `;
89
+
90
+ // Adjusts the shading normal such that the provided view normal is guaranteed to be in
91
+ // positive hemisphere by rotating it towards the view direction just enough so view is
92
+ // just glancing the surface as a small epsilon if needed.
93
+ const ensureValidViewNormal = wgslTagFn/* wgsl */`
94
+
95
+ fn ensureValidViewNormal( n: vec3f, ng: vec3f, view: vec3f ) -> vec3f {
96
+
97
+ // ensure the view is at least slightly above the surface normal hemisphere
98
+ const MIN_VIEW_COS = 1e-6;
99
+
100
+ // if we're positive already then early out
101
+ let c = dot( n, view );
102
+ if ( c > MIN_VIEW_COS ) {
103
+
104
+ return n;
105
+
106
+ }
107
+
108
+ // perpendicular vector to normal and view
109
+ var perp = n - c * view;
110
+ let perpLen = length( perp );
111
+
112
+ // if we reach here that means that he view is nearly the opposite direction and
113
+ // we can't derive plan to rotate on towards the view, so just use the geometry
114
+ // normal. This could likely only really happen with a severe normal map application.
115
+ if ( perpLen <= 1e-6 ) {
116
+
117
+ return ng;
118
+
119
+ }
120
+
121
+ perp = perp / perpLen;
122
+
123
+ // rotate the normal to be at larger than the above threshold - pythagorean
124
+ // theorem for generating normal with threshold*view component
125
+ return MIN_VIEW_COS * view + sqrt( 1.0 - MIN_VIEW_COS * MIN_VIEW_COS ) * perp;
126
+
127
+ }
128
+
129
+ `;
130
+
131
+ // Builds getSurfaceRecord using the given per-instance sampleTexel and uv channel lookup
132
+ // Sentinel for a disabled glossy filter ( FLT_MAX, mirroring Cycles ): the blur term
133
+ // clamps to zero for any path pdf when the inverted filter value is this large.
134
+ export const FILTER_GLOSSY_DISABLED = 3.402823466e38;
135
+
136
+ export const getSurfaceRecordFunc = ( sampleTexel, getUvFromChannel, getColor ) => wgslFn( /* wgsl */ `
137
+
138
+ fn getSurfaceRecord(
139
+ material: Material,
140
+ vertexData: bvh_GeometryStruct,
141
+ side: f32,
142
+ faceNormal: vec3f,
143
+ view: vec3f,
144
+ blurRoughness: f32,
145
+ ) -> SurfaceRecord {
146
+
147
+ // "faceNormal" is provided on the hit side so flip it back to the geometric side
148
+ var normal = faceNormal * side;
149
+ if ( material.flatShading == 0 ) {
150
+
151
+ normal = normalize( vertexData.normal.xyz );
152
+
153
+ }
154
+
155
+ var baseNormal = normal;
156
+ if ( material.normalMap != -1 ) {
157
+
158
+ // some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
159
+ // resulting in NaNs and slow path tracing.
160
+ if ( length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
161
+
162
+ // TODO: consider re-orthonormalizing against the normal here since attribute
163
+ // interpolation could result in drift.
164
+ let tangent = normalize( vertexData.tangent.xyz );
165
+ let bitangent = normalize( cross( baseNormal, tangent ) * vertexData.tangent.w );
166
+ let vTBN = mat3x3f( tangent, bitangent, baseNormal );
167
+
168
+ let uvPrime = material.normalMapTransform * vec3( getUvFromChannel( vertexData, material.normalMap ), 1.0 );
169
+ var texNormal = sampleTexel( uvPrime.xy, material.normalMap, 0 ).xyz;
170
+ texNormal = texNormal * 2.0 - 1.0;
171
+ texNormal = texNormal * vec3f( material.normalScale, 1.0 );
172
+ normal = normalize( vTBN * texNormal );
173
+
174
+ }
175
+
176
+ }
177
+
178
+ normal *= side;
179
+
180
+ normal = ensureValidViewNormal( normal, faceNormal, view );
181
+
182
+ normal = ensureValidReflectionNormal( normal, faceNormal, view );
183
+
184
+ var albedo = vec4( material.color, material.opacity );
185
+ if ( material.vertexColors == 1 ) {
186
+
187
+ albedo *= getColor( vertexData );
188
+
189
+ }
190
+
191
+ if ( material.map != -1 ) {
192
+
193
+ let uvPrime = material.mapTransform * vec3f( getUvFromChannel( vertexData, material.map ), 1 );
194
+ albedo *= sampleTexel( uvPrime.xy, material.map, 0 );
195
+
196
+ }
197
+
198
+ if ( material.alphaMap != -1 ) {
199
+
200
+ let uvPrime = material.alphaMapTransform * vec3f( getUvFromChannel( vertexData, material.alphaMap ), 1 );
201
+ albedo.a *= sampleTexel( uvPrime.xy, material.alphaMap, 0 ).g;
202
+
203
+ }
204
+
205
+ var roughness = material.roughness;
206
+ if ( material.roughnessMap != -1 ) {
207
+
208
+ let uvPrime = material.roughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.roughnessMap ), 1 );
209
+ let texColor = sampleTexel( uvPrime.xy, material.roughnessMap, 0 );
210
+ roughness *= texColor.g;
211
+
212
+ }
213
+
214
+ var metalness = material.metalness;
215
+ if ( material.metalnessMap != -1 ) {
216
+
217
+ let uvPrime = material.metalnessMapTransform * vec3f( getUvFromChannel( vertexData, material.metalnessMap ), 1 );
218
+ let texColor = sampleTexel( uvPrime.xy, material.metalnessMap, 0 );
219
+ metalness *= texColor.b;
220
+
221
+ }
222
+
223
+ var emission = material.emissiveIntensity * material.emissive;
224
+ if ( material.emissiveMap != -1 ) {
225
+
226
+ let uvPrime = material.emissiveMapTransform * vec3f( getUvFromChannel( vertexData, material.emissiveMap ), 1 );
227
+ let texColor = sampleTexel( uvPrime.xy, material.emissiveMap, 0 );
228
+ emission *= texColor.rgb;
229
+
230
+ }
231
+
232
+ var transmission = material.transmission;
233
+ if ( material.transmissionMap != -1 ) {
234
+
235
+ let uvPrime = material.transmissionMapTransform * vec3f( getUvFromChannel( vertexData, material.transmissionMap ), 1 );
236
+ let texColor = sampleTexel( uvPrime.xy, material.transmissionMap, 0 );
237
+ transmission *= texColor.r;
238
+
239
+ }
240
+
241
+ var clearcoat = material.clearcoat;
242
+ if ( material.clearcoatMap != -1 ) {
243
+
244
+ let uvPrime = material.clearcoatMapTransform * vec3f( getUvFromChannel( vertexData, material.clearcoatMap ), 1 );
245
+ let texColor = sampleTexel( uvPrime.xy, material.clearcoatMap, 0 );
246
+ clearcoat *= texColor.r;
247
+
248
+ }
249
+
250
+ var clearcoatRoughness = material.clearcoatRoughness;
251
+ if ( material.clearcoatRoughnessMap != -1 ) {
252
+
253
+ let uvPrime = material.clearcoatRoughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.clearcoatRoughnessMap ), 1 );
254
+ let texColor = sampleTexel( uvPrime.xy, material.clearcoatRoughnessMap, 0 );
255
+ clearcoatRoughness *= texColor.g;
256
+
257
+ }
258
+
259
+ var clearcoatNormal = baseNormal;
260
+ if ( material.clearcoatNormalMap != -1 ) {
261
+
262
+ // some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
263
+ // resulting in NaNs and slow path tracing.
264
+ if ( length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
265
+
266
+ // TODO: consider re-orthonormalizing against the normal here since attribute
267
+ // interpolation could result in drift.
268
+ let tangent = normalize( vertexData.tangent.xyz );
269
+ let bitangent = normalize( cross( baseNormal, tangent ) * vertexData.tangent.w );
270
+ let vTBN = mat3x3f( tangent, bitangent, baseNormal );
271
+
272
+ let uvPrime = material.clearcoatNormalMapTransform * vec3( getUvFromChannel( vertexData, material.clearcoatNormalMap ), 1.0 );
273
+ var texNormal = sampleTexel( uvPrime.xy, material.clearcoatNormalMap, 0 ).xyz;
274
+ texNormal = texNormal * 2.0 - 1.0;
275
+ texNormal = texNormal * vec3f( material.clearcoatNormalScale, 1.0 );
276
+ clearcoatNormal = normalize( vTBN * texNormal );
277
+
278
+ }
279
+
280
+ }
281
+
282
+ clearcoatNormal *= side;
283
+
284
+ clearcoatNormal = ensureValidViewNormal( clearcoatNormal, faceNormal, view );
285
+
286
+ clearcoatNormal = ensureValidReflectionNormal( clearcoatNormal, faceNormal, view );
287
+
288
+ var sheenColor = material.sheenColor;
289
+ if ( material.sheenColorMap != -1 ) {
290
+
291
+ let uvPrime = material.sheenColorMapTransform * vec3f( getUvFromChannel( vertexData, material.sheenColorMap ), 1 );
292
+ let texColor = sampleTexel( uvPrime.xy, material.sheenColorMap, 0 );
293
+ sheenColor *= texColor.rgb;
294
+
295
+ }
296
+
297
+ var sheenRoughness = material.sheenRoughness;
298
+ if ( material.sheenRoughnessMap != -1 ) {
299
+
300
+ let uvPrime = material.sheenRoughnessMapTransform * vec3f( getUvFromChannel( vertexData, material.sheenRoughnessMap ), 1 );
301
+ let texColor = sampleTexel( uvPrime.xy, material.sheenRoughnessMap, 0 );
302
+ sheenRoughness *= texColor.r;
303
+
304
+ }
305
+
306
+ var iridescence = material.iridescence;
307
+ if ( material.iridescenceMap != -1 ) {
308
+
309
+ let uvPrime = material.iridescenceMapTransform * vec3f( getUvFromChannel( vertexData, material.iridescenceMap ), 1 );
310
+ let texColor = sampleTexel( uvPrime.xy, material.iridescenceMap, 0 );
311
+ iridescence *= texColor.r;
312
+
313
+ }
314
+
315
+ var iridescenceThickness = material.iridescenceThicknessMaximum;
316
+ if ( material.iridescenceThicknessMap != -1 ) {
317
+
318
+ let uvPrime = material.iridescenceThicknessMapTransform * vec3f( getUvFromChannel( vertexData, material.iridescenceThicknessMap ), 1 );
319
+ let texColor = sampleTexel( uvPrime.xy, material.iridescenceThicknessMap, 0 );
320
+
321
+ iridescenceThickness = mix(
322
+ material.iridescenceThicknessMinimum,
323
+ material.iridescenceThicknessMaximum,
324
+ texColor.g,
325
+ );
326
+
327
+ }
328
+
329
+ var specularColor = material.specularColor;
330
+ if ( material.specularColorMap != -1 ) {
331
+
332
+ let uvPrime = material.specularColorMapTransform * vec3f( getUvFromChannel( vertexData, material.specularColorMap ), 1 );
333
+ let texColor = sampleTexel( uvPrime.xy, material.specularColorMap, 0 );
334
+ specularColor *= texColor.rgb;
335
+
336
+ }
337
+
338
+ var specularIntensity = material.specularIntensity;
339
+ if ( material.specularIntensityMap != -1 ) {
340
+
341
+ let uvPrime = material.specularIntensityMapTransform * vec3f( getUvFromChannel( vertexData, material.specularIntensityMap ), 1 );
342
+ let texColor = sampleTexel( uvPrime.xy, material.specularIntensityMap, 0 );
343
+ specularIntensity *= texColor.a;
344
+
345
+ }
346
+
347
+ // extract the anisotropy magnitude vector in tangent space
348
+ var anisotropyDirVec = material.anisotropy * vec2f( cos( material.anisotropyRotation ), sin( material.anisotropyRotation ) );
349
+ if ( material.anisotropyMap != -1 ) {
350
+
351
+ let uvPrime = material.anisotropyMapTransform * vec3f( getUvFromChannel( vertexData, material.anisotropyMap ), 1.0 );
352
+ let aniTex = sampleTexel( uvPrime.xy, material.anisotropyMap, 0 );
353
+
354
+ // map rg encode the direction ([-1,1]), b the strength; rotate + scale by the material anisotropy.
355
+ let mapDir = aniTex.rg * 2.0 - vec2f( 1.0 );
356
+ let mapStr = aniTex.b;
357
+ let mapLen = length( mapDir );
358
+ if ( mapLen > EPSILON ) {
359
+
360
+ anisotropyDirVec = mat2x2f(
361
+ anisotropyDirVec.x, anisotropyDirVec.y,
362
+ - anisotropyDirVec.y, anisotropyDirVec.x
363
+ ) * mapStr * mapDir / mapLen;
364
+
365
+ }
366
+
367
+ }
368
+
369
+ // adjust the surface basis to be oriented along the anisotropic vector
370
+ let anisotropyStrength = length( anisotropyDirVec );
371
+ var surfaceBasis = getBasisFromNormal( normal );
372
+ if ( anisotropyStrength > 0.0 && length( vertexData.tangent.xyz ) > 0.0 && vertexData.tangent.w != 0.0 ) {
373
+
374
+ let anisotropyDir = anisotropyDirVec / anisotropyStrength;
375
+
376
+ // re-orthonormalize the tangent against the shading normal so the frame stays orthonormal -
377
+ // normal map adjustments will cause the normal and tangent to become non-orthonormal.
378
+ let tangent = normalize( vertexData.tangent.xyz - normal * dot( normal, vertexData.tangent.xyz ) );
379
+ let bitangent = cross( normal, tangent ) * vertexData.tangent.w;
380
+
381
+ surfaceBasis = mat3x3f(
382
+ tangent * anisotropyDir.x + bitangent * anisotropyDir.y,
383
+ bitangent * anisotropyDir.x - tangent * anisotropyDir.y,
384
+ normal
385
+ );
386
+
387
+ }
388
+
389
+ var surf: SurfaceRecord;
390
+
391
+ surf.volumeParticle = false;
392
+ surf.faceNormal = faceNormal;
393
+ surf.normal = normal;
394
+
395
+ surf.metalness = metalness;
396
+ surf.diffuseRoughness = material.diffuseRoughness;
397
+ surf.color = albedo.rgb;
398
+ surf.emission = emission;
399
+
400
+ surf.opacity = select( 1.0, albedo.a, material.transparent != 0 );
401
+
402
+ surf.ior = material.ior;
403
+ surf.transmission = transmission;
404
+ surf.thinWall = material.thinWall == 1;
405
+ surf.attenuationColor = material.attenuationColor;
406
+ surf.attenuationDistance = material.attenuationDistance;
407
+
408
+ surf.clearcoatNormal = clearcoatNormal;
409
+ surf.clearcoat = clearcoat;
410
+
411
+ surf.iridescence = iridescence;
412
+ surf.iridescenceIor = material.iridescenceIor;
413
+ surf.iridescenceThickness = iridescenceThickness;
414
+
415
+ surf.specularColor = specularColor;
416
+ surf.specularIntensity = specularIntensity;
417
+
418
+ surf.sheen = material.sheen;
419
+ surf.sheenColor = sheenColor;
420
+
421
+ let minRoughness = max( MIN_ROUGHNESS, blurRoughness );
422
+ surf.roughness = clamp( roughness, minRoughness, 1.0 );
423
+ surf.clearcoatRoughness = clamp( clearcoatRoughness, minRoughness, 1.0 );
424
+ surf.sheenRoughness = clamp( sheenRoughness, minRoughness, 1.0 );
425
+ surf.anisotropy = anisotropyStrength;
426
+
427
+ // frontFace is used to determine transmissive properties and PDF. If no transmission is used
428
+ // then we can just always assume this is a front face.
429
+ surf.frontFace = side == 1.0 || transmission == 0.0;
430
+ if ( material.thinWall == 1 || surf.frontFace ) {
431
+
432
+ surf.eta = 1.0 / material.ior;
433
+
434
+ } else {
435
+
436
+ surf.eta = material.ior;
437
+
438
+ }
439
+ surf.f0 = iorToF0( surf.eta );
440
+
441
+ // get the normal frames
442
+ surf.normalBasis = surfaceBasis;
443
+ surf.normalInvBasis = inverse( surf.normalBasis );
444
+
445
+ surf.clearcoatBasis = getBasisFromNormal( surf.clearcoatNormal );
446
+ surf.clearcoatInvBasis = inverse( surf.clearcoatBasis );
447
+
448
+ return surf;
449
+ }
450
+
451
+ `, [
452
+ inverseMat3x3Func,
453
+ iorToF0Func,
454
+ getBasisFromNormalFunc,
455
+ sampleTexel,
456
+ getUvFromChannel,
457
+ getColor,
458
+ surfaceRecordStruct,
459
+ constants,
460
+ ensureValidReflectionNormal,
461
+ ensureValidViewNormal,
462
+ ] );
463
+
464
+ /*
465
+ *
466
+ * N : Macronormal of the surface
467
+ * V ( wo ) : View direction
468
+ * L ( wi ) : Light direction
469
+ * H ( wh ) : Halfvector between V and L, micronormal of the surface in ggx
470
+ * f0 : Amount of light reflected when looking at a surface head on - "fresnel 0"
471
+ * f90 : Amount of light reflected at grazing angles
472
+ *
473
+ */
474
+
475
+ // Disney Diffuse BRDF without subsurface approximation
476
+ export const diffuseBrdfFunc = wgslFn( /* wgsl */ `
477
+
478
+ fn diffuseBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
479
+
480
+ // https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
481
+ // See equation (4)
482
+
483
+ let fl = schlickFresnel( NdotL, 0.0 );
484
+ let fv = schlickFresnel( NdotV, 0.0 );
485
+
486
+ let alpha = surf.roughness * surf.roughness;
487
+ let bias = mix( 0.0, 0.5, alpha ) - 1;
488
+ let energyFactor = mix( 1.0, 1.0 / 1.51, alpha );
489
+
490
+ let rr = 2.0 * alpha * VdotH * VdotH;
491
+ let retro = rr * ( fl + fv + fl * fv * ( rr + 2.0 * bias ) );
492
+ let fresnel = ( 1.0 + bias * fl ) * ( 1.0f + bias * fv );
493
+
494
+ // TODO: subsurface approx?
495
+
496
+ return energyFactor * ( surf.color / PI ) * ( retro + fresnel );
497
+
498
+ }
499
+
500
+ `, [ constants, schlickFresnelFunc, surfaceRecordStruct ] );
501
+
502
+ // Lambertian diffuse BRDF with cosine distribution
503
+ export const lambertBrdfFunc = wgslFn( /* wgsl */ `
504
+
505
+ fn lambertBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
506
+
507
+ return surf.color / PI;
508
+
509
+ }
510
+
511
+ `, [ constants, surfaceRecordStruct ] );
512
+
513
+ export const specularBrdfFunc = wgslFn( /* wgsl */ `
514
+
515
+ fn specularBrdf( V: vec3f, L: vec3f, H: vec3f, alpha: vec2f ) -> vec3f {
516
+
517
+ let alphaT = alpha.x;
518
+ let alphaB = alpha.y;
519
+
520
+ let Vis = ggxSmithVisibility( V, L, alpha );
521
+ let D = ggxDistribution( H, alpha );
522
+
523
+ return vec3f( D * Vis );
524
+
525
+ }
526
+
527
+ `, [ ggxSmithVisibilityFunc, ggxDistributionFunc ] );
528
+
529
+ // PBRT-v4 treats matched media as perfect specular transmission because the rough dielectric
530
+ // half-vector and its Jacobian are undefined when eta is one:
531
+ // https://github.com/mmp/pbrt-v4/blob/master/src/pbrt/bxdfs.cpp
532
+ export const isMatchedIorFunc = wgslFn( /* wgsl */ `
533
+
534
+ fn isMatchedIor( eta: f32 ) -> bool {
535
+
536
+ return abs( eta - 1.0 ) < EPSILON;
537
+
538
+ }
539
+
540
+ `, [ constants ] );
541
+
542
+ export const specularBtdfFunc = wgslFn( /* wgsl */`
543
+
544
+ fn specularBtdf( V: vec3f, L: vec3f, H: vec3f, alpha: vec2f, eta: f32 ) -> vec3f {
545
+
546
+ let NdotV = V.z;
547
+ let NdotL = L.z;
548
+ let HdotV = dot( V, H );
549
+ let HdotL = dot( L, H );
550
+
551
+ // Heaviside function for G term
552
+ if ( NdotV * HdotV < 0.0 || NdotL * HdotL < 0.0 || HdotV * HdotL > 0.0 ) {
553
+
554
+ return vec3f( 0.0 );
555
+
556
+ }
557
+
558
+ let G1_i = ggxShadowMaskG1( V, alpha );
559
+ let G1_o = ggxShadowMaskG1( L, alpha );
560
+
561
+ // separable G2 product, no height correlation
562
+ let denom = eta * HdotV + HdotL;
563
+ let Vis = G1_i * G1_o * abs( HdotV ) * abs( HdotL ) /
564
+ ( abs( NdotV ) * abs( NdotL ) * denom * denom );
565
+
566
+ let F = dielectricFresnel( abs( HdotV ), eta );
567
+
568
+ let D = ggxDistribution( H, alpha );
569
+
570
+ return vec3f( ( 1 - F ) * D * Vis );
571
+
572
+ }
573
+
574
+ `, [ ggxShadowMaskG1Func, ggxDistributionFunc, dielectricFresnelFunc ] );
575
+
576
+ // Effective roughness for thin walled transmission, from page 40 (note the slides' 3.7 is a typo for 3.4) -
577
+ // referenced from Blender Cycles:
578
+ // https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
579
+ export const thinWallTransmissionRoughnessFunc = wgslFn( /* wgsl */ `
580
+
581
+ fn thinWallTransmissionRoughness( alpha: f32, eta: f32 ) -> f32 {
582
+
583
+ let t = 3.4 * ( eta - 1.0 ) * ( eta - 0.5 ) * ( eta - 0.5 ) / ( eta * eta * eta );
584
+ return saturate( alpha * sqrt( max( t, 0.0 ) ) );
585
+
586
+ }
587
+
588
+ ` );
589
+
590
+ // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume/README.md#attenuation
591
+ export const transmissionAttenuationFunc = wgslFn( /* wgsl */ `
592
+
593
+ fn transmissionAttenuation( dist: f32, attColor: vec3f, attDist: f32 ) -> vec3f {
594
+
595
+ return pow( attColor, vec3f( dist / attDist ) );
596
+
597
+ }
598
+
599
+ ` );
600
+
601
+ // Continuous Cauchy IOR approximation derived from the Fraunhofer C, d, and F spectral lines:
602
+ // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_dispersion
603
+ // https://github.com/bsdorra/slang-pbr/blob/c12046ec3b9628bcfc384ed2c275c92d007e5d7e/src/models/enterprise/components/transmission.slang#L20-L27
604
+ export const DISPERSION_MIN_WAVELENGTH = 360;
605
+ export const DISPERSION_MAX_WAVELENGTH = 830;
606
+
607
+ export const dispersionIorFunc = wgslFn( /* wgsl */ `
608
+
609
+ fn dispersionIor( ior: f32, dispersion: f32, wavelength: f32 ) -> f32 {
610
+
611
+ let nd = max( ior, 1.0 );
612
+ let scale = ( nd - 1.0 ) * max( dispersion, 0.0 ) / 20.0;
613
+ let lambda = clamp( wavelength, ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0 );
614
+ return max( nd + scale * ( 523655.0 / ( lambda * lambda ) - 1.5168 ), 1.0 );
615
+
616
+ }
617
+
618
+ ` );
619
+
620
+ export const applyDispersionFunc = wgslTagFn/* wgsl */ `
621
+
622
+ fn applyDispersion( surf: ptr<function, ${ surfaceRecordStruct }>, dispersion: f32, wavelength: f32 ) -> void {
623
+
624
+ surf.ior = ${ dispersionIorFunc }( surf.ior, dispersion, wavelength );
625
+ surf.eta = select( surf.ior, 1.0 / surf.ior, surf.thinWall || surf.frontFace );
626
+ surf.f0 = ${ iorToF0Func }( surf.eta );
627
+
628
+ }
629
+
630
+ `;
631
+
632
+ // A path keeps one uniformly sampled hero wavelength after its first dispersive interaction.
633
+ // The CIE 1931 fit is converted to linear Rec. 709, clamped to the RGB gamut, and normalized so
634
+ // every channel has an expected weight of one over the uniformly sampled wavelength interval.
635
+ // This preserves the expected value of the renderer's existing RGB throughput while providing
636
+ // continuous hues. The renderer still uses RGB material and light data rather than spectral data.
637
+ // CIE fit: https://jcgt.org/published/0002/02/01/
638
+ // Hero wavelength sampling:
639
+ // https://pbr-book.org/4ed/Textures_and_Materials/Material_Interface_and_Implementations.html#sec:dielectric-material
640
+ // https://doi.org/10.1111/cgf.12419
641
+ export const dispersionColorWeightFunc = wgslFn( /* wgsl */ `
642
+
643
+ fn dispersionColorWeight( wavelength: f32 ) -> vec3f {
644
+
645
+ let lambda = clamp( wavelength, ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0 );
646
+
647
+ let x1 = ( lambda - 442.0 ) * select( 0.0374, 0.0624, lambda < 442.0 );
648
+ let x2 = ( lambda - 599.8 ) * select( 0.0323, 0.0264, lambda < 599.8 );
649
+ let x3 = ( lambda - 501.1 ) * select( 0.0382, 0.0490, lambda < 501.1 );
650
+ let x = 0.362 * exp( - 0.5 * x1 * x1 ) + 1.056 * exp( - 0.5 * x2 * x2 ) - 0.065 * exp( - 0.5 * x3 * x3 );
651
+
652
+ let y1 = ( lambda - 568.8 ) * select( 0.0247, 0.0213, lambda < 568.8 );
653
+ let y2 = ( lambda - 530.9 ) * select( 0.0322, 0.0613, lambda < 530.9 );
654
+ let y = 0.821 * exp( - 0.5 * y1 * y1 ) + 0.286 * exp( - 0.5 * y2 * y2 );
655
+
656
+ let z1 = ( lambda - 437.0 ) * select( 0.0278, 0.0845, lambda < 437.0 );
657
+ let z2 = ( lambda - 459.0 ) * select( 0.0725, 0.0385, lambda < 459.0 );
658
+ let z = 1.217 * exp( - 0.5 * z1 * z1 ) + 0.681 * exp( - 0.5 * z2 * z2 );
659
+
660
+ let linearRgb = max( vec3f(
661
+ 3.2404542 * x - 1.5371385 * y - 0.4985314 * z,
662
+ - 0.9692660 * x + 1.8760108 * y + 0.0415560 * z,
663
+ 0.0556434 * x - 0.2040259 * y + 1.0572252 * z,
664
+ ), vec3f( 0.0 ) );
665
+
666
+ return linearRgb * vec3f( 2.6699448, 4.0708066, 4.2980494 );
667
+
668
+ }
669
+
670
+ ` );
671
+
672
+ // Dielectric layer fresnel operator that supports custom f0 color, specular weight.
673
+ // Based on the specular color specification:
674
+ // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular
675
+ export const fresnelMixFunc = wgslFn( /* wgsl */ `
676
+
677
+ fn fresnelMix( VdotH: f32, f0Color: vec3f, ior: f32, eta: f32, weight: f32, base: vec3f, layer: vec3f ) -> vec3f {
678
+
679
+ var f0 = iorToF0( ior ) * f0Color;
680
+ f0 = min( f0, vec3f( 1.0 ) );
681
+
682
+ // reflect all light on total internal reflection, matching the WebGL evaluateFresnel function
683
+ var fr = vec3f( 1.0 );
684
+ if ( ! totalInternalReflection( abs( VdotH ), eta ) ) {
685
+
686
+ fr = schlickFresnelVec( abs( VdotH ), f0, vec3f( 1.0 ) );
687
+
688
+ }
689
+
690
+ let maxFr = max( max( fr.r, fr.g ), fr.b );
691
+
692
+ return ( 1.0 - weight * maxFr ) * base + weight * fr * layer;
693
+
694
+ }
695
+
696
+ `, [ schlickFresnelVecFunc, iorToF0Func, totalInternalReflectionFunc ] );
697
+
698
+ const XYZ_TO_REC709 = mat3(
699
+ 3.2404542, - 0.9692660, 0.0556434,
700
+ - 1.5371385, 1.8760108, - 0.2040259,
701
+ - 0.4985314, 0.0415560, 1.0572252,
702
+ );
703
+
704
+ const evalSensitivityFunc = wgslTagFn/* wgsl */`
705
+
706
+ fn evalSensitivity( OPD: f32, shift: vec3f ) -> vec3f {
707
+
708
+ let phase = 2.0 * ${ Math.PI } * OPD * 1.0e-9;
709
+ const val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13);
710
+ const pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06);
711
+ const _var = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09);
712
+
713
+ var xyz = val * sqrt(2.0 * ${ Math.PI } * _var) * cos(pos * phase + shift) * exp(-phase * phase * _var);
714
+ 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);
715
+ xyz /= 1.0685e-7;
716
+
717
+ let rgb = ${ XYZ_TO_REC709 } * xyz;
718
+ return rgb;
719
+
720
+ }
721
+
722
+ `;
723
+
724
+ // Reference: Belcour/Barla, 2017
725
+ // https://belcour.github.io/blog/research/publication/2017/05/01/brdf-thin-film.html
726
+ // This is a simplified model that ignores light polarization and uses fresnel approximation
727
+ export const iridescentFresnelFunc = wgslFn( /* wgsl */ `
728
+
729
+ fn iridescentFresnel(
730
+ cosTheta1: f32, baseF0: vec3f, iridescenceIor: f32,
731
+ outsideIor: f32, iridescenceThickness: f32,
732
+ ) -> vec3f {
733
+
734
+ let sinTheta2Sq = pow( outsideIor / iridescenceIor, 2.0 ) * ( 1.0 - pow( cosTheta1, 2.0 ) );
735
+ let cosTheta2Sq = 1.0 - sinTheta2Sq;
736
+
737
+ // Handle total internal reflection
738
+ if ( cosTheta2Sq < 0.0 ) {
739
+
740
+ return vec3( 1.0 );
741
+
742
+ }
743
+
744
+ let cosTheta2 = sqrt( cosTheta2Sq );
745
+
746
+ // First interface: air -> iridescent thin film
747
+ let R0 = iorToF0General( iridescenceIor, outsideIor );
748
+ let R12 = schlickFresnel( cosTheta1, R0 );
749
+ let R21 = R12;
750
+ let T121 = 1.0 - R12;
751
+ let phi12 = select( 0.0, PI, iridescenceIor < outsideIor );
752
+ let phi21 = PI - phi12;
753
+
754
+ // Second interface: iridescent thin film -> base material
755
+ let baseIor = fresnel0ToIor( clamp( baseF0, vec3( 0.0 ), vec3( 0.9999 ) ) ); // guard against 1.0
756
+ let R1 = iorToF0GeneralVec( baseIor, vec3( iridescenceIor ) );
757
+ var R23 = schlickFresnelVec( cosTheta2, R1, vec3( 1.0 ) );
758
+
759
+ // Handle total internal reflection at the film -> base interface
760
+ // NOTE: Added separately from the original implementation. Is this correct?
761
+ R23 = select( R23, vec3( 1.0 ), totalInternalReflectionVec( cosTheta2, vec3( iridescenceIor ) / baseIor ) );
762
+ let phi23 = select( vec3( 0.0 ), vec3( PI ), baseIor < vec3( iridescenceIor ) );
763
+
764
+ // Phase shift
765
+ let OPD = 2.0 * iridescenceIor * iridescenceThickness * cosTheta2;
766
+ let phi = vec3( phi21 ) + phi23;
767
+
768
+ // Analytical integration
769
+ // Compound terms
770
+ let R123 = clamp( R12 * R23, vec3( 1e-5 ), vec3( 0.9999 ) );
771
+ let r123 = sqrt( R123 );
772
+ let Rs = T121 * T121 * R23 / ( vec3( 1.0 ) - R123 );
773
+
774
+ // Reflectance term for m = 0 (DC term amplitude)
775
+ let C0 = R12 + Rs;
776
+ var I = C0;
777
+
778
+ // Reflectance term for m > 0 (pairs of diracs)
779
+ var Cm = Rs - T121;
780
+ for (var m = 1; m <= 2; m += 1) {
781
+
782
+ Cm *= r123;
783
+ let Sm = 2.0 * evalSensitivity( f32( m ) * OPD, f32( m ) * phi );
784
+ I += Cm * Sm;
785
+
786
+ }
787
+
788
+ return max( I, vec3(0.0) );
789
+
790
+ }
791
+
792
+ `, [ iorToF0GeneralFunc, iorToF0GeneralVecFunc, schlickFresnelFunc, fresnel0ToIorFunc, evalSensitivityFunc, totalInternalReflectionVecFunc ] );
793
+
794
+ // Reflection and transmission coefficients for the transmissive dielectric layer. The scalar
795
+ // branch probability follows Cycles: choose between the two colored coefficients by their mean
796
+ // energy while retaining the full RGB coefficient in the path throughput.
797
+ export const transmissionFresnelFunc = ( iridescentFresnel = iridescentFresnelFunc ) => wgslTagFn/* wgsl */ `
798
+
799
+ fn transmissionFresnel( cosTheta: f32, surf: ${ surfaceRecordStruct } ) -> ${ transmissionFresnelStruct } {
800
+
801
+ var result: ${ transmissionFresnelStruct };
802
+ let cosine = saturate( abs( cosTheta ) );
803
+ let matchedIor = ${ isMatchedIorFunc }( surf.eta );
804
+ let airIncident = surf.thinWall || surf.frontFace;
805
+ let dielectricFresnel = ${ dielectricFresnelFunc }( cosine, surf.eta );
806
+
807
+ // Keep matched media transparent unless a real thin-film layer is present. This avoids
808
+ // introducing the non-physical Schlick F90 reflection produced by KHR_materials_specular
809
+ // when the substrate itself has no IOR boundary.
810
+ var reflectance = vec3f( 0.0 );
811
+ if ( ! matchedIor ) {
812
+
813
+ let specularF0 = min( surf.f0 * surf.specularColor, vec3f( 1.0 ) );
814
+ let dielectricReflectance = select(
815
+ vec3f( dielectricFresnel ),
816
+ ${ schlickFresnelVecFunc }( cosine, specularF0, vec3f( 1.0 ) ),
817
+ airIncident,
818
+ );
819
+ reflectance = surf.specularIntensity * dielectricReflectance;
820
+
821
+ }
822
+
823
+ reflectance = clamp( reflectance, vec3f( 0.0 ), vec3f( 1.0 ) );
824
+ var transmittance = vec3f( 1.0 ) - reflectance;
825
+
826
+ if ( surf.iridescence > 0.0 ) {
827
+
828
+ // The media on either side of the film are air outside and the volume interior as
829
+ // the base. Swap them on interior hits so film-interface TIR remains possible.
830
+ let outsideIor = select( surf.ior, 1.0, airIncident );
831
+ let filmBaseIor = select( 1.0, surf.ior, airIncident );
832
+ let filmReflectance = clamp( ${ iridescentFresnel }(
833
+ cosine,
834
+ vec3f( ${ iorToF0Func }( filmBaseIor ) ),
835
+ surf.iridescenceIor,
836
+ outsideIor,
837
+ surf.iridescenceThickness,
838
+ ), vec3f( 0.0 ), vec3f( 1.0 ) );
839
+
840
+ reflectance = mix( reflectance, filmReflectance, surf.iridescence );
841
+ transmittance = mix( transmittance, vec3f( 1.0 ) - filmReflectance, surf.iridescence );
842
+
843
+ }
844
+
845
+ result.reflectance = reflectance;
846
+ result.transmittance = transmittance;
847
+
848
+ // Include the base-color tint in the transmission energy, as Cycles does when it
849
+ // computes the glass reflection probability from its two tinted coefficients.
850
+ let reflectEnergy = dot( reflectance, vec3f( 1.0 / 3.0 ) );
851
+ let transmitEnergy = dot( transmittance * surf.color, vec3f( 1.0 / 3.0 ) );
852
+ let totalEnergy = reflectEnergy + transmitEnergy;
853
+ result.reflectProbability = select( 0.0, reflectEnergy / max( totalEnergy, EPSILON ), totalEnergy > EPSILON );
854
+
855
+ return result;
856
+
857
+ }
858
+
859
+ `;
860
+
861
+ export const conductorFresnelFunc = wgslFn( /* wgsl */ `
862
+
863
+ fn conductorFresnel( VdotH: f32, f0: vec3f, specular: vec3f ) -> vec3f {
864
+
865
+ return specular * schlickFresnelVec( abs( VdotH ), f0, vec3f( 1 ) );
866
+
867
+ }
868
+
869
+ `, [ schlickFresnelVecFunc ] );
870
+
871
+ export const fresnelCoatFunc = wgslFn( /* wgsl */ `
872
+
873
+ fn fresnelCoat( VdotNc: f32, ior: f32, base: vec3f, layer: vec3f, weight: f32 ) -> vec3f {
874
+
875
+ let f0 = iorToF0( ior );
876
+ let F = schlickFresnel( abs( VdotNc ), f0 );
877
+
878
+ return mix( base, layer, weight * F );
879
+
880
+ }
881
+
882
+ `, [ iorToF0Func, schlickFresnelFunc ] );
883
+
884
+ // GGX Multibounce compensation using Turquin's method. Bakes the directional albedo of the
885
+ // reflection lobe, optionally fresnel-weighted, and optionally including the refraction lobe
886
+ // for the full transmissive bsdf. "eta" is the incident over transmitted ior ratio.
887
+ export const turquinIntegralFn = outputTarget => wgslTagFn/* wgsl */ `
888
+
889
+ fn albedo(
890
+ eta: f32,
891
+ includeFresnel: bool,
892
+ includeRefraction: bool,
893
+ globalId: vec3u,
894
+ layer: u32,
895
+ ) -> void {
896
+
897
+ // The matched-IOR limit is a unit-energy delta transmission event, so it cannot
898
+ // be integrated with the rough BTDF below.
899
+ if ( includeRefraction && ${ isMatchedIorFunc }( eta ) ) {
900
+
901
+ textureStore( ${ outputTarget }, vec3( globalId.xy, layer ), vec4( 1.0 ) );
902
+ return;
903
+
904
+ }
905
+
906
+ // sample the brdf directions in a grid pattern
907
+ const GRID_SIZE = 64u;
908
+
909
+ // TODO: this sampling means that energy at 0.0 & 1.0 roughness (and 0 and 90deg cos) are never
910
+ // written to the texture due to the half texel inset, resulting in small, though possibly noticeable,
911
+ // error in common cases.
912
+ let dimensions = textureDimensions( ${ outputTarget } ).xy;
913
+ let uv = ( vec2f( globalId.xy ) + vec2f( 0.5 ) ) / vec2f( dimensions );
914
+
915
+ let cosThetaO = uv.x;
916
+ let roughness = uv.y;
917
+ let alpha = roughness * roughness;
918
+
919
+ let wo = vec3( sqrt( 1 - cosThetaO * cosThetaO ), 0 , cosThetaO );
920
+
921
+ let f0 = ${ iorToF0Func }( eta );
922
+
923
+ var result = 0.0;
924
+ for ( var x = 0u; x < GRID_SIZE; x++ ) {
925
+
926
+ for ( var y = 0u; y < GRID_SIZE; y++ ) {
927
+
928
+ // calculate the half vector to sample
929
+ let gridPoint = vec2f( vec2u( x, y ) ) + vec2f( 0.5 );
930
+ let sampleUv = gridPoint / f32( GRID_SIZE );
931
+ let wh = ${ ggxDirectionFunc }( wo, vec2( alpha ), sampleUv );
932
+
933
+ var f = 1.0;
934
+ if ( includeFresnel ) {
935
+
936
+ // use the exact dielectric fresnel so the table agrees with the fresnel used
937
+ // for lobe sampling
938
+ f = ${ dielectricFresnelFunc }( dot( wo, wh ), eta );
939
+
940
+ }
941
+
942
+ // air-incident reflection is evaluated with schlick in bsdfEval - only the
943
+ // volume-incident ( TIR ) glass reflection uses the exact fresnel
944
+ var reflectionF = f;
945
+ if ( eta < 1.0 ) {
946
+
947
+ reflectionF = ${ schlickFresnelFunc }( dot( wo, wh ), f0 );
948
+
949
+ }
950
+
951
+ // reflection lobe - samples landing below the surface are lost
952
+ let wi = - reflect( wo, wh );
953
+ let reflectionPdf = ${ ggxReflectionAdjustedPDFFunc }( wo, wh, vec2f( alpha ) );
954
+ if ( wi.z > 0.0 && reflectionPdf != 0.0 ) {
955
+
956
+ let specular = ${ specularBrdfFunc }( wo, wi, wh, vec2f( alpha ) );
957
+ result += reflectionF * specular.x * wi.z / reflectionPdf;
958
+
959
+ }
960
+
961
+ // refraction lobe - the btdf carries its own ( 1 - F ) fresnel weight, and
962
+ // samples landing back above the surface are lost
963
+ if ( includeRefraction ) {
964
+
965
+ let wiRefract = refract( - wo, wh, eta );
966
+ if ( wiRefract.z < 0.0 ) {
967
+
968
+ let transmission = ${ specularBtdfFunc }( wo, wiRefract, wh, vec2f( alpha ), eta );
969
+ let pdf = ${ ggxRefractionAdjustedPDFFunc }( wo, wiRefract, wh, vec2f( alpha ), eta );
970
+ if ( pdf != 0.0 ) {
971
+
972
+ result += transmission.x * abs( wiRefract.z ) / pdf;
973
+
974
+ }
975
+
976
+ }
977
+
978
+ }
979
+
980
+ }
981
+
982
+ }
983
+
984
+ result /= f32( GRID_SIZE * GRID_SIZE );
985
+
986
+ textureStore( ${ outputTarget }, vec3( globalId.xy, layer ), vec4( result ) );
987
+
988
+ }
989
+
990
+ `;