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,228 @@
1
+ import { Matrix4, StructTypeNode } from 'three/webgpu';
2
+ import { uniform, uint, float } from 'three/tsl';
3
+ import { wgslTagFn, ndcToCameraRay, rayStruct, bvhNodeBoundsStruct, bvhNodeStruct, intersectsTriangle } from 'three-mesh-bvh/webgpu';
4
+
5
+ // blue -> cyan -> green -> yellow -> red heat ramp shared by the debug visualizations
6
+ export const heatColorFn = wgslTagFn/* wgsl */`
7
+ fn debugHeatColor( t: f32 ) -> vec3f {
8
+
9
+ let r = clamp( 1.5 - abs( 4.0 * t - 3.0 ), 0.0, 1.0 );
10
+ let g = clamp( 1.5 - abs( 4.0 * t - 2.0 ), 0.0, 1.0 );
11
+ let b = clamp( 1.5 - abs( 4.0 * t - 1.0 ), 0.0, 1.0 );
12
+ return vec3f( r, g, b );
13
+
14
+ }
15
+ `;
16
+
17
+ // Result struct holding a running tally of the TLAS and BLAS bounding boxes the ray crosses
18
+ const debugBoundsResultStruct = new StructTypeNode( {
19
+ tlasCount: 'uint',
20
+ blasCount: 'uint',
21
+ didHit: 'bool',
22
+ dist: 'float',
23
+ }, 'DebugBoundsResult' );
24
+
25
+ // Builds the full-screen "bvh bounds heatmap" fragment function for the given bvh data. The uniforms
26
+ // driving it are attached as `.uniforms` so the caller can update them each frame.
27
+ export function getDebugBoundsFunction( bvhData ) {
28
+
29
+ const { transforms, index, attributes, materials } = bvhData.storage;
30
+
31
+ // uniforms
32
+
33
+ // camera transforms used to build the ray
34
+ const cameraToModelMatrix = uniform( new Matrix4() );
35
+ const inverseProjectionMatrix = uniform( new Matrix4() );
36
+
37
+ // 1 / 0 flags toggling which bvh levels contribute to the count
38
+ const displayTLAS = uniform( 1 );
39
+ const displayBLAS = uniform( 1 );
40
+
41
+ // 1 / 0 flag, stop counting boxes past the nearest hit surface
42
+ const stopAtSurface = uniform( 0 );
43
+
44
+ // node count that saturates to full heat
45
+ const saturationCount = uniform( 64 );
46
+
47
+ // shared traversal state, mirroring the path tracer's raycast:
48
+ // - level: 0 while walking the TLAS, 1 while walking a BLAS ( picks which counter to bump )
49
+ // - rayScalar: factor converting a local-space hit distance back to world units
50
+ // - materialSide: side of the current object's material, for front / backface culling
51
+ const level = uint( 0 ).toVar( 'bvh_debugBoundsLevel' );
52
+ const rayScalar = float( 1.0 ).toVar( 'bvh_debugRayScalar' );
53
+ const materialSide = float( 0.0 ).toVar( 'bvh_debugMaterialSide' );
54
+
55
+ // front-to-back child ordering: descend the child the ray reaches first
56
+ const getBoundsOrder = wgslTagFn/* wgsl */`
57
+ fn debugGetBoundsOrder( ray: ${ rayStruct }, splitAxis: u32, node: ${ bvhNodeStruct } ) -> bool {
58
+
59
+ return ray.direction[ splitAxis ] >= 0.0;
60
+
61
+ }
62
+ `;
63
+
64
+ // bounds hook: tally the box (into the level's counter) and keep descending, unless it sits
65
+ // entirely behind the nearest hit surface found so far
66
+ const intersectsBounds = wgslTagFn/* wgsl */`
67
+ fn debugIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct }, result: ptr<function, ${ debugBoundsResultStruct }> ) -> u32 {
68
+
69
+ let boundsMin = vec3( bounds.min[ 0 ], bounds.min[ 1 ], bounds.min[ 2 ] );
70
+ let boundsMax = vec3( bounds.max[ 0 ], bounds.max[ 1 ], bounds.max[ 2 ] );
71
+
72
+ let invDir = 1.0 / ray.direction;
73
+ let tMinPlane = ( boundsMin - ray.origin ) * invDir;
74
+ let tMaxPlane = ( boundsMax - ray.origin ) * invDir;
75
+
76
+ let tMinHit = min( tMinPlane, tMaxPlane );
77
+ let tMaxHit = max( tMinPlane, tMaxPlane );
78
+
79
+ let t0 = max( max( tMinHit.x, tMinHit.y ), tMinHit.z );
80
+ let t1 = min( min( tMaxHit.x, tMaxHit.y ), tMaxHit.z );
81
+
82
+ let dist = max( t0, 0.0 );
83
+ if ( t1 < dist ) {
84
+
85
+ return 0u;
86
+
87
+ } else if ( result.didHit && dist * ${ rayScalar } >= result.dist ) {
88
+
89
+ return 0u;
90
+
91
+ }
92
+
93
+ if ( ${ level } == 1u ) {
94
+
95
+ result.blasCount = result.blasCount + 1u;
96
+
97
+ } else {
98
+
99
+ result.tlasCount = result.tlasCount + 1u;
100
+
101
+ }
102
+
103
+ return 1u;
104
+
105
+ }
106
+ `;
107
+
108
+ // range hook: intersect the leaf triangles to track the nearest surface
109
+ const intersectRange = wgslTagFn/* wgsl */`
110
+ fn debugIntersectRange( ray: ${ rayStruct }, offset: u32, count: u32, result: ptr<function, ${ debugBoundsResultStruct }> ) -> bool {
111
+
112
+ var didHit = false;
113
+ if ( ${ stopAtSurface } > 0.5 ) {
114
+
115
+ for ( var ti = offset; ti < offset + count; ti = ti + 1u ) {
116
+
117
+ let i0 = ${ index }[ ti * 3u ];
118
+ let i1 = ${ index }[ ti * 3u + 1u ];
119
+ let i2 = ${ index }[ ti * 3u + 2u ];
120
+
121
+ let a = ${ attributes }[ i0 ].position.xyz;
122
+ let b = ${ attributes }[ i1 ].position.xyz;
123
+ let c = ${ attributes }[ i2 ].position.xyz;
124
+
125
+ var triResult = ${ intersectsTriangle }( ray, a, b, c );
126
+ triResult.dist *= ${ rayScalar };
127
+ if ( triResult.didHit && ( ! result.didHit || triResult.dist < result.dist ) ) {
128
+
129
+ // cull faces that don't match the material side
130
+ if ( ${ materialSide } != 0.0 && triResult.side != ${ materialSide } ) {
131
+
132
+ continue;
133
+
134
+ }
135
+
136
+ result.didHit = true;
137
+ result.dist = triResult.dist;
138
+ didHit = true;
139
+
140
+ }
141
+
142
+ }
143
+
144
+ }
145
+
146
+ return didHit;
147
+
148
+ }
149
+ `;
150
+
151
+ // transform hook: move the ray into object-local space ( normalizing the direction and storing
152
+ // the scale factor, exactly like the raycast ), record the material side, and mark that we are
153
+ // now descending a BLAS
154
+ const transformRay = wgslTagFn/* wgsl */`
155
+ fn debugTransformRay( ray: ptr<function, ${ rayStruct }>, objectIndex: u32 ) -> void {
156
+
157
+ let object = ${ transforms }[ objectIndex ];
158
+ let toLocal = object.inverseMatrixWorld;
159
+ ray.origin = ( toLocal * vec4f( ray.origin, 1.0 ) ).xyz;
160
+ ray.direction = ( toLocal * vec4f( ray.direction, 0.0 ) ).xyz;
161
+
162
+ let len = length( ray.direction );
163
+ ray.direction = ray.direction / len;
164
+ ${ rayScalar } = 1.0 / len;
165
+
166
+ ${ materialSide } = ${ materials }[ object.materialIndex ].side;
167
+ ${ level } = 1u;
168
+
169
+ }
170
+ `;
171
+
172
+ // reset hook: back to the TLAS once a BLAS has been fully traversed
173
+ const resetLevel = wgslTagFn/* wgsl */`
174
+ fn debugResetLevel( objectIndex: u32 ) -> void {
175
+
176
+ ${ rayScalar } = 1.0;
177
+ ${ level } = 0u;
178
+
179
+ }
180
+ `;
181
+
182
+ // reuse the path tracer's traversal to count boxes across the TLAS and every BLAS it references
183
+ const countBounds = bvhData.getShapecastFn( {
184
+ name: 'debugBoundsCount',
185
+ shapeStruct: rayStruct,
186
+ resultStruct: debugBoundsResultStruct,
187
+
188
+ boundsOrderFn: getBoundsOrder,
189
+ intersectsBoundsFn: intersectsBounds,
190
+ intersectRangeFn: intersectRange,
191
+ transformShapeFn: transformRay,
192
+ resetShapeFn: resetLevel,
193
+ } );
194
+
195
+ const debugBounds = wgslTagFn/* wgsl */`
196
+ fn debugBounds( vUv: vec2f ) -> vec4f {
197
+
198
+ let ndc = vUv * 2.0 - vec2f( 1.0 );
199
+ var ray = ${ ndcToCameraRay }( ndc, ${ cameraToModelMatrix } * ${ inverseProjectionMatrix } );
200
+ ray.direction = normalize( ray.direction );
201
+
202
+ // count the boxes the ray crosses, pruning those behind the nearest hit surface
203
+ var result: ${ debugBoundsResultStruct };
204
+ ${ countBounds }( ray, &result );
205
+
206
+ let tlas = select( 0u, result.tlasCount, ${ displayTLAS } > 0.5 );
207
+ let blas = select( 0u, result.blasCount, ${ displayBLAS } > 0.5 );
208
+ let count = tlas + blas;
209
+
210
+ let t = clamp( f32( count ) / max( ${ saturationCount }, 1.0 ), 0.0, 1.0 );
211
+
212
+ return vec4f( ${ heatColorFn }( t ), 1.0 );
213
+
214
+ }
215
+ `;
216
+
217
+ debugBounds.uniforms = {
218
+ cameraToModelMatrix,
219
+ inverseProjectionMatrix,
220
+ displayTLAS,
221
+ displayBLAS,
222
+ stopAtSurface,
223
+ saturationCount,
224
+ };
225
+
226
+ return debugBounds;
227
+
228
+ }
@@ -0,0 +1,262 @@
1
+ import { wgslFn } from 'three/tsl';
2
+ import { constants, surfaceRecordStruct } from './structs.wgsl.js';
3
+
4
+ const eonDirectionalAlbedoFunc = wgslFn( /* wgsl */ `
5
+
6
+ fn eonDirectionalAlbedo( mu: f32, roughness: f32, A: f32 ) -> f32 {
7
+
8
+ let muComp = 1.0 - mu;
9
+ let gOverPi = muComp * (
10
+ 0.0571085289 + muComp * (
11
+ 0.491881867 + muComp * (
12
+ - 0.332181442 + muComp * 0.0714429953
13
+ )
14
+ )
15
+ );
16
+ return A * ( 1.0 + roughness * gOverPi );
17
+
18
+ }
19
+
20
+ ` );
21
+
22
+ const fonSingleScatterFunc = wgslFn( /* wgsl */ `
23
+
24
+ fn fonSingleScatter( NdotV: f32, NdotL: f32, VdotH: f32, roughness: f32, A: f32, rho: vec3f ) -> vec3f {
25
+
26
+ let VdotL = 2.0 * VdotH * VdotH - 1.0;
27
+ let s = VdotL - NdotV * NdotL;
28
+ let sOverT = select( s, s / max( NdotV, NdotL ), s > 0.0 );
29
+
30
+ return ( rho / PI ) * A * ( 1.0 + roughness * sOverT );
31
+
32
+ }
33
+
34
+ `, [ constants ] );
35
+
36
+ // Fujii's improved Oren-Nayar single-scatter diffuse BRDF.
37
+ // Based on: https://jcgt.org/published/0014/01/06/ (Section 2, Listing 1)
38
+ export const fonBrdfFunc = wgslFn( /* wgsl */ `
39
+
40
+ fn fonBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
41
+
42
+ let roughness = surf.diffuseRoughness;
43
+ if ( roughness < 1e-5 ) {
44
+
45
+ return surf.color / PI;
46
+
47
+ }
48
+
49
+ let rho = saturate( surf.color );
50
+ let A = 1.0 / ( 1.0 + ( 0.5 - 2.0 / ( 3.0 * PI ) ) * roughness );
51
+
52
+ return fonSingleScatter( NdotV, NdotL, VdotH, roughness, A, rho );
53
+
54
+ }
55
+
56
+ `, [ constants, surfaceRecordStruct, fonSingleScatterFunc ] );
57
+
58
+ // Energy-preserving Oren-Nayar diffuse BRDF (EON).
59
+ // Based on: https://jcgt.org/published/0014/01/06/ (Section 3, Listing 2)
60
+ export const eonBrdfFunc = wgslFn( /* wgsl */ `
61
+
62
+ fn eonBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
63
+
64
+ let roughness = surf.diffuseRoughness;
65
+ if ( roughness < 1e-5 ) {
66
+
67
+ return surf.color / PI;
68
+
69
+ }
70
+
71
+ let rho = saturate( surf.color );
72
+ let A = 1.0 / ( 1.0 + ( 0.5 - 2.0 / ( 3.0 * PI ) ) * roughness );
73
+ let singleScatter = fonSingleScatter( NdotV, NdotL, VdotH, roughness, A, rho );
74
+
75
+ let averageDirectionalAlbedo = A * ( 1.0 + ( 2.0 / 3.0 - 28.0 / ( 15.0 * PI ) ) * roughness );
76
+ let directionalAlbedoV = eonDirectionalAlbedo( NdotV, roughness, A );
77
+ let directionalAlbedoL = eonDirectionalAlbedo( NdotL, roughness, A );
78
+
79
+ let rhoMultiScatter = rho * rho * averageDirectionalAlbedo /
80
+ max( vec3f( 1e-7 ), vec3f( 1.0 ) - rho * ( 1.0 - averageDirectionalAlbedo ) );
81
+ let multiScatter = ( rhoMultiScatter / PI ) *
82
+ max( 1e-7, 1.0 - directionalAlbedoV ) *
83
+ max( 1e-7, 1.0 - directionalAlbedoL ) /
84
+ max( 1e-7, 1.0 - averageDirectionalAlbedo );
85
+
86
+ return singleScatter + multiScatter;
87
+
88
+ }
89
+
90
+ `, [ constants, surfaceRecordStruct, eonDirectionalAlbedoFunc, fonSingleScatterFunc ] );
91
+
92
+ // EON importance sampling: a clipped linearly transformed cosine (CLTC) lobe
93
+ // mixed with a uniform hemisphere lobe. Based on Listing 3 and Listing 4:
94
+ // https://jcgt.org/published/0014/01/06/
95
+ const eonLtcCoeffsFunc = wgslFn( /* wgsl */ `
96
+
97
+ fn eonLtcCoeffs( mu: f32, roughness: f32 ) -> vec4f {
98
+
99
+ let a = 1.0 + roughness * (
100
+ 0.303392 + ( - 0.518982 + 0.111709 * mu ) * mu +
101
+ ( - 0.276266 + 0.335918 * mu ) * roughness
102
+ );
103
+ let b = roughness * (
104
+ - 1.16407 + 1.15859 * mu + ( 0.150815 - 0.150105 * mu ) * roughness
105
+ ) / ( mu * mu * mu - 1.43545 );
106
+ let c = 1.0 + roughness * ( 0.20013 + ( - 0.506373 + 0.261777 * mu ) * mu );
107
+ let d = roughness * (
108
+ 0.540852 + ( - 1.01625 + 0.475392 * mu ) * mu
109
+ ) / ( - 1.0743 + ( 0.0725628 + mu ) * mu );
110
+
111
+ return vec4f( a, b, c, d );
112
+
113
+ }
114
+
115
+ ` );
116
+
117
+ const eonLtcBasisFunc = wgslFn( /* wgsl */ `
118
+
119
+ fn eonLtcBasis( wo: vec3f ) -> mat3x3f {
120
+
121
+ let lenSqr = dot( wo.xy, wo.xy );
122
+ let invLen = inverseSqrt( max( lenSqr, 1e-7 ) );
123
+ let x = select(
124
+ vec3f( 1.0, 0.0, 0.0 ),
125
+ vec3f( wo.x, wo.y, 0.0 ) * invLen,
126
+ lenSqr > 0.0,
127
+ );
128
+ let y = vec3f( - x.y, x.x, 0.0 );
129
+
130
+ return mat3x3f( x, y, vec3f( 0.0, 0.0, 1.0 ) );
131
+
132
+ }
133
+
134
+ ` );
135
+
136
+ const eonCltcPdfFunc = wgslFn( /* wgsl */ `
137
+
138
+ fn eonCltcPdf( wo: vec3f, wi: vec3f, roughness: f32 ) -> f32 {
139
+
140
+ let toLtc = transpose( eonLtcBasis( wo ) );
141
+ let wiLtc = toLtc * wi;
142
+ let coeffs = eonLtcCoeffs( saturate( wo.z ), roughness );
143
+ let a = coeffs.x;
144
+ let b = coeffs.y;
145
+ let c = coeffs.z;
146
+ let d = coeffs.w;
147
+ let detM = c * ( a - b * d );
148
+ let wh = vec3f(
149
+ c * ( wiLtc.x - b * wiLtc.z ),
150
+ ( a - b * d ) * wiLtc.y,
151
+ - c * ( d * wiLtc.x - a * wiLtc.z ),
152
+ );
153
+ let lenSqr = max( dot( wh, wh ), 1e-7 );
154
+ let vz = inverseSqrt( d * d + 1.0 );
155
+ let s = 0.5 * ( 1.0 + vz );
156
+ let jacobian = detM * detM / ( lenSqr * lenSqr );
157
+
158
+ return jacobian * max( wh.z, 0.0 ) / ( PI * s );
159
+
160
+ }
161
+
162
+ `, [ constants, eonLtcCoeffsFunc, eonLtcBasisFunc ] );
163
+
164
+ const eonCltcDirectionFunc = wgslFn( /* wgsl */ `
165
+
166
+ fn eonCltcDirection( wo: vec3f, roughness: f32, uv: vec2f ) -> vec3f {
167
+
168
+ let coeffs = eonLtcCoeffs( saturate( wo.z ), roughness );
169
+ let a = coeffs.x;
170
+ let b = coeffs.y;
171
+ let c = coeffs.z;
172
+ let d = coeffs.w;
173
+ let radius = sqrt( uv.x );
174
+ let phi = 2.0 * PI * uv.y;
175
+ let y = radius * sin( phi );
176
+ let vz = inverseSqrt( d * d + 1.0 );
177
+ let s = 0.5 * ( 1.0 + vz );
178
+ let x = - mix( sqrt( max( 1.0 - y * y, 0.0 ) ), radius * cos( phi ), s );
179
+ let wh = vec3f( x, y, sqrt( max( 1.0 - x * x - y * y, 0.0 ) ) );
180
+ let wiUnnormalized = vec3f(
181
+ a * wh.x + b * wh.z,
182
+ c * wh.y,
183
+ d * wh.x + wh.z,
184
+ );
185
+ let wi = normalize( eonLtcBasis( wo ) * wiUnnormalized );
186
+
187
+ return wi;
188
+
189
+ }
190
+
191
+ `, [ constants, eonLtcCoeffsFunc, eonLtcBasisFunc ] );
192
+
193
+ const eonUniformHemisphereSampleFunc = wgslFn( /* wgsl */ `
194
+
195
+ fn eonUniformHemisphereSample( uv: vec2f ) -> vec3f {
196
+
197
+ let sinTheta = sqrt( max( 1.0 - uv.x * uv.x, 0.0 ) );
198
+ let phi = 2.0 * PI * uv.y;
199
+ return vec3f( sinTheta * cos( phi ), sinTheta * sin( phi ), uv.x );
200
+
201
+ }
202
+
203
+ `, [ constants ] );
204
+
205
+ const eonUniformProbabilityFunc = wgslFn( /* wgsl */ `
206
+
207
+ fn eonUniformProbability( wo: vec3f, roughness: f32 ) -> f32 {
208
+
209
+ let mu = saturate( wo.z );
210
+ return clamp( pow( roughness, 0.1 ), 0.0, 1.0 ) * (
211
+ 0.162925 + ( - 0.372058 + ( 0.538233 - 0.290822 * mu ) * mu ) * mu
212
+ );
213
+
214
+ }
215
+
216
+ ` );
217
+
218
+ // EON's CLTC + uniform-hemisphere sampler. The PDF is evaluated separately by
219
+ // eonPdf so direction sampling does not pay for a discarded Jacobian evaluation.
220
+ export const eonDirectionFunc = wgslFn( /* wgsl */ `
221
+
222
+ fn eonDirection( wo: vec3f, roughness: f32, uv: vec2f ) -> vec3f {
223
+
224
+ let uniformProbability = eonUniformProbability( wo, roughness );
225
+ if ( uv.x <= uniformProbability && uniformProbability > 0.0 ) {
226
+
227
+ return eonUniformHemisphereSample( vec2f( uv.x / uniformProbability, uv.y ) );
228
+
229
+ }
230
+
231
+ let cltcProbability = 1.0 - uniformProbability;
232
+ let cltcUv = vec2f(
233
+ ( uv.x - uniformProbability ) / max( cltcProbability, 1e-7 ),
234
+ uv.y,
235
+ );
236
+ return eonCltcDirection( wo, roughness, cltcUv );
237
+
238
+ }
239
+
240
+ `, [ eonCltcDirectionFunc, eonUniformHemisphereSampleFunc, eonUniformProbabilityFunc ] );
241
+
242
+ export const eonPDFFunc = wgslFn( /* wgsl */ `
243
+
244
+ fn eonPdf( wo: vec3f, wi: vec3f, roughness: f32 ) -> f32 {
245
+
246
+ // The EON proposal is defined over the positive hemisphere.
247
+ if ( wo.z <= 0.0 || wi.z <= 0.0 ) {
248
+
249
+ return 0.0;
250
+
251
+ }
252
+
253
+ let uniformProbability = eonUniformProbability( wo, roughness );
254
+ let cltcProbability = 1.0 - uniformProbability;
255
+ let uniformPdf = 1.0 / ( 2.0 * PI );
256
+
257
+ return uniformProbability * uniformPdf +
258
+ cltcProbability * eonCltcPdf( wo, wi, roughness );
259
+
260
+ }
261
+
262
+ `, [ constants, eonCltcPdfFunc, eonUniformProbabilityFunc ] );
@@ -0,0 +1,190 @@
1
+ import { wgslFn } from 'three/tsl';
2
+ import { constants } from './structs.wgsl.js';
3
+
4
+ // See sampling.wgsl for vector shorthand explanations
5
+ // The GGX functions provide sampling and distribution information for normals as output so
6
+ // in order to get probability of scatter direction the half vector must be computed and
7
+ // provided. Anisotropic surfaces are represented with a 2-dimensional alpha storing the
8
+ // roughness along the tangent (x) and bitangent (y) in the TBN frame.
9
+
10
+ // [0] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
11
+ // [1] https://hal.archives-ouvertes.fr/hal-01509746/document
12
+ // [2] http://jcgt.org/published/0007/04/01/
13
+ // [4] http://jcgt.org/published/0003/02/03/
14
+ // [5] https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
15
+ // [6] https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md
16
+ // [7] https://google.github.io/filament/Filament.md.html#materialsystem/anisotropicmodel/anisotropicspecularbrdf
17
+
18
+ // trowbridge-reitz === GGX === GTR
19
+ export const ggxDirectionFunc = wgslFn( /* wgsl */ `
20
+
21
+ fn ggxDirection( incidentDir: vec3f, alpha: vec2f, uv: vec2f ) -> vec3f {
22
+
23
+ // Implementation from reference [1]
24
+ // stretch view
25
+ let V = normalize( vec3f( alpha * incidentDir.xy, incidentDir.z ) );
26
+
27
+ // orthonormal basis
28
+ var T1: vec3f;
29
+ if ( V.z < 0.9999 ) {
30
+
31
+ T1 = normalize( cross( V, vec3( 0.0, 0.0, 1.0 ) ) );
32
+
33
+ } else {
34
+
35
+ T1 = vec3( 1.0, 0.0, 0.0 );
36
+
37
+ }
38
+
39
+ let T2 = cross( T1, V );
40
+
41
+ // sample point with polar coordinates (r, phi)
42
+ let a = 1.0 / ( 1.0 + V.z );
43
+ let r = sqrt( uv.x );
44
+ var phi: f32;
45
+ if ( uv.y < a ) {
46
+
47
+ phi = uv.y / a * PI;
48
+
49
+ } else {
50
+
51
+ phi = PI + ( uv.y - a ) / ( 1.0 - a ) * PI;
52
+
53
+ }
54
+
55
+ let P1 = r * cos( phi );
56
+ var P2 = r * sin( phi );
57
+ if ( uv.y >= a ) {
58
+
59
+ P2 *= V.z;
60
+
61
+ }
62
+
63
+ // compute normal
64
+ var N = P1 * T1 + P2 * T2 + V * sqrt( max( 0.0, 1.0 - P1 * P1 - P2 * P2 ) );
65
+
66
+ // unstretch
67
+ N = normalize( vec3( alpha * N.xy, max( 0.0, N.z ) ) );
68
+
69
+ return N;
70
+
71
+ }
72
+
73
+ `, [ constants ] );
74
+
75
+ // Smith masking lambda for a single direction, used to build the G1 term for the pdf
76
+ // See equation (34) from [0] and equation (43) from [7]
77
+ export const ggxLambdaFunc = wgslFn( /* wgsl */ `
78
+
79
+ fn ggxLambda( V: vec3f, alpha: vec2f ) -> f32 {
80
+
81
+ let alphaT = alpha.x;
82
+ let alphaB = alpha.y;
83
+
84
+ // abs v.z to support transmitted rays
85
+ let NdotV = max( abs( V.z ), MIN_INCIDENT_COS );
86
+ let cos2 = NdotV * NdotV;
87
+ let t = ( alphaT * alphaT * V.x * V.x + alphaB * alphaB * V.y * V.y ) / cos2;
88
+ let numerator = - 1.0 + sqrt( 1.0 + t );
89
+ return numerator / 2.0;
90
+
91
+ }
92
+
93
+ `, [ constants ] );
94
+
95
+ // Based on equation (2) from reference [1]
96
+ export const ggxShadowMaskG1Func = wgslFn( /* wgsl */ `
97
+
98
+ fn ggxShadowMaskG1( V: vec3f, alpha: vec2f ) -> f32 {
99
+
100
+ // TODO: this could be collapsed to a simpler form as an optimization
101
+ return 1.0 / ( 1.0 + ggxLambda( V, alpha ) );
102
+
103
+ }
104
+
105
+ `, [ ggxLambdaFunc ] );
106
+
107
+ // Smith height-correlated visibility term = G / ( 4 * NdotV * NdotL )
108
+ // See (listing 16) in [7] and from [6]
109
+ export const ggxSmithVisibilityFunc = wgslFn( /* wgsl */ `
110
+
111
+ fn ggxSmithVisibility( V: vec3f, L: vec3f, alpha: vec2f ) -> f32 {
112
+
113
+ let alphaT = alpha.x;
114
+ let alphaB = alpha.y;
115
+
116
+ let NdotV = max( V.z, MIN_INCIDENT_COS );
117
+ let NdotL = max( L.z, MIN_INCIDENT_COS );
118
+
119
+ let TdotV = V.x;
120
+ let TdotL = L.x;
121
+
122
+ let BdotV = V.y;
123
+ let BdotL = L.y;
124
+
125
+ let GGXV = NdotL * length( vec3f( alphaT * TdotV, alphaB * BdotV, NdotV ) );
126
+ let GGXL = NdotV * length( vec3f( alphaT * TdotL, alphaB * BdotL, NdotL ) );
127
+
128
+ return 0.5 / ( GGXV + GGXL );
129
+
130
+ }
131
+
132
+ `, [ constants ] );
133
+
134
+ // Trowbridge-Reitz ( GGX ) normal distribution
135
+ // See (listing 15) in [7]
136
+ export const ggxDistributionFunc = wgslFn( /* wgsl */ `
137
+
138
+ fn ggxDistribution( H: vec3f, alpha: vec2f ) -> f32 {
139
+
140
+ let alphaT = alpha.x;
141
+ let alphaB = alpha.y;
142
+
143
+ let NdotH = H.z;
144
+ let TdotH = H.x;
145
+ let BdotH = H.y;
146
+
147
+ let a2 = alphaT * alphaB;
148
+ let v = vec3f( alphaB * TdotH, alphaT * BdotH, a2 * NdotH );
149
+ let v2 = dot( v, v );
150
+ let w2 = a2 / v2;
151
+
152
+ return a2 * w2 * w2 / PI;
153
+
154
+ }
155
+
156
+ `, [ constants ] );
157
+
158
+ // ggxPDF, divided by the Jacobian of reflection operation
159
+ // PDF: See equation (17) from reference [2]
160
+ // Note: HdotV cancel out bc its guaranteed to be > 0
161
+ export const ggxReflectionAdjustedPDFFunc = wgslFn( /* wgsl */ `
162
+
163
+ fn ggxReflectionAdjustedPDF( V: vec3f, H: vec3f, alpha: vec2f ) -> f32 {
164
+
165
+ let NdotV = max( V.z, MIN_INCIDENT_COS );
166
+ let D = ggxDistribution( H, alpha );
167
+ let G1 = ggxShadowMaskG1( V, alpha );
168
+
169
+ return D * G1 / ( 4 * NdotV );
170
+
171
+ }
172
+ `, [ ggxDistributionFunc, ggxShadowMaskG1Func ] );
173
+
174
+ // ggxPDF, divided by the Jacobian of refraction operation
175
+ // See equation (3) from [2] for pdf and (17) from [0] for Jacobian
176
+ export const ggxRefractionAdjustedPDFFunc = wgslFn( /* wgsl */ `
177
+
178
+ fn ggxRefractionAdjustedPDF( V: vec3f, L: vec3f, H: vec3f, alpha: vec2f, eta: f32 ) -> f32 {
179
+
180
+ let NdotV = max( V.z, MIN_INCIDENT_COS );
181
+ let HdotV = dot( V, H );
182
+ let HdotL = dot( L, H );
183
+ let D = ggxDistribution( H, alpha );
184
+ let G1 = ggxShadowMaskG1( V, alpha );
185
+
186
+ let denom = eta * HdotV + HdotL;
187
+ return D * G1 * abs( HdotV ) * abs( HdotL ) / ( NdotV * denom * denom );
188
+
189
+ }
190
+ `, [ ggxDistributionFunc, ggxShadowMaskG1Func ] );