three-gpu-pathtracer 0.0.27 → 0.0.28

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 (46) hide show
  1. package/README.md +0 -1
  2. package/build/index.module.js +5 -3
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +4 -2
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +7 -7
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  9. package/src/webgpu/API.md +167 -19
  10. package/src/webgpu/AtlasTexture.js +2 -3
  11. package/src/webgpu/EquirectHdrInfoNode.js +89 -48
  12. package/src/webgpu/LightsInfoNode.js +1 -1
  13. package/src/webgpu/MegaKernelPathTracer.js +10 -3
  14. package/src/webgpu/PathTracerBackend.js +172 -21
  15. package/src/webgpu/WaveFrontPathTracer.js +156 -71
  16. package/src/webgpu/WebGPUPathTracer.js +628 -48
  17. package/src/webgpu/compute/ComputeKernel.js +8 -0
  18. package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
  19. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  20. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  21. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  22. package/src/webgpu/compute/targets.js +55 -0
  23. package/src/webgpu/compute/wavefront/LogicKernel.js +67 -20
  24. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  25. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  26. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  27. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  28. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  29. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  30. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  31. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -25
  32. package/src/webgpu/compute/wavefront/structs.js +63 -37
  33. package/src/webgpu/constants.js +2 -2
  34. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  35. package/src/webgpu/index.d.ts +41 -6
  36. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  37. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  38. package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
  39. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  40. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  41. package/src/webgpu/nodes/material.wgsl.js +2 -1
  42. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  43. package/src/webgpu/nodes/random.wgsl.js +2 -0
  44. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  45. package/src/webgpu/nodes/structs.wgsl.js +7 -1
  46. package/src/webgpu/upscale/FSRUpscaler.js +1 -1
@@ -0,0 +1,61 @@
1
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
2
+ import { scatterRecordStruct, surfaceRecordStruct } from './structs.wgsl.js';
3
+
4
+ // The denoiser's auxiliary images, the first hit albedo and normal, accumulate in an "r32uint" array
5
+ // texture, the one kind of storage texture core WebGPU can both read and write. Each layer packs two
6
+ // halves: albedo.rg, then albedo.b and normal.z, then normal.xy. Normals are mapped to [0,1].
7
+ export const AUX_LAYERS = 3;
8
+
9
+ // the smooth metals the auxiliary images look past to what they reflect, as in mutracer
10
+ const AUX_MIRROR_MAX_ROUGHNESS = 0.02;
11
+ const AUX_MIRROR_MIN_METALNESS = 0.98;
12
+
13
+ // whether a scatter shows its surface in the auxiliary images, rather than looking past it through a
14
+ // transmission or a mirror
15
+ export const isAuxSurfaceFn = wgslTagFn/* wgsl */`
16
+
17
+ fn isAuxSurface( surface: ${ surfaceRecordStruct }, scatterRec: ${ scatterRecordStruct } ) -> bool {
18
+
19
+ let isMirror = surface.roughness <= ${ AUX_MIRROR_MAX_ROUGHNESS } && surface.metalness >= ${ AUX_MIRROR_MIN_METALNESS };
20
+ return ! scatterRec.isTransmissive && ! isMirror;
21
+
22
+ }
23
+
24
+ `;
25
+
26
+ export const unpackAuxAlbedoFn = wgslTagFn/* wgsl */`
27
+
28
+ fn unpackAuxAlbedo( packed: vec3u ) -> vec3f {
29
+
30
+ return vec3f( unpack2x16float( packed.x ), unpack2x16float( packed.y ).x );
31
+
32
+ }
33
+
34
+ `;
35
+
36
+ export const unpackAuxNormalFn = wgslTagFn/* wgsl */`
37
+
38
+ fn unpackAuxNormal( packed: vec3u ) -> vec3f {
39
+
40
+ return vec3f( unpack2x16float( packed.z ), unpack2x16float( packed.y ).y );
41
+
42
+ }
43
+
44
+ `;
45
+
46
+ // blends a sample's albedo and normal into the packed running means
47
+ export const blendAuxFn = wgslTagFn/* wgsl */`
48
+
49
+ fn blendAux( packed: vec3u, albedo: vec3f, normal: vec3f, weight: f32 ) -> vec3u {
50
+
51
+ let meanAlbedo = mix( ${ unpackAuxAlbedoFn }( packed ), albedo, weight );
52
+ let meanNormal = mix( ${ unpackAuxNormalFn }( packed ), normal * 0.5 + 0.5, weight );
53
+ return vec3u(
54
+ pack2x16float( meanAlbedo.rg ),
55
+ pack2x16float( vec2f( meanAlbedo.b, meanNormal.z ) ),
56
+ pack2x16float( meanNormal.xy ),
57
+ );
58
+
59
+ }
60
+
61
+ `;
@@ -119,7 +119,7 @@ const eonLtcBasisFunc = wgslFn( /* wgsl */ `
119
119
  fn eonLtcBasis( wo: vec3f ) -> mat3x3f {
120
120
 
121
121
  let lenSqr = dot( wo.xy, wo.xy );
122
- let invLen = inverseSqrt( max( lenSqr, 1e-7 ) );
122
+ let invLen = inverseSqrt( lenSqr );
123
123
  let x = select(
124
124
  vec3f( 1.0, 0.0, 0.0 ),
125
125
  vec3f( wo.x, wo.y, 0.0 ) * invLen,
@@ -848,7 +848,8 @@ export const transmissionFresnelFunc = ( iridescentFresnel = iridescentFresnelFu
848
848
  // Include the base-color tint in the transmission energy, as Cycles does when it
849
849
  // computes the glass reflection probability from its two tinted coefficients.
850
850
  let reflectEnergy = dot( reflectance, vec3f( 1.0 / 3.0 ) );
851
- let transmitEnergy = dot( transmittance * surf.color, vec3f( 1.0 / 3.0 ) );
851
+ let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
852
+ let transmitEnergy = dot( transmittance * transmissionTint, vec3f( 1.0 / 3.0 ) );
852
853
  let totalEnergy = reflectEnergy + transmitEnergy;
853
854
  result.reflectProbability = select( 0.0, reflectEnergy / max( totalEnergy, EPSILON ), totalEnergy > EPSILON );
854
855
 
@@ -0,0 +1,165 @@
1
+ import { StructTypeNode } from 'three/webgpu';
2
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
3
+ import { LIGHT_FAR_DISTANCE } from './lights.wgsl.js';
4
+ import { materialStruct } from './structs.wgsl.js';
5
+
6
+ // Random walk subsurface scattering, following Cycles' subsurface_random_walk.h
7
+ // https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/integrator/subsurface_random_walk.h
8
+
9
+ // the smallest attenuation color used to derive a medium's extinction, keeping the log finite
10
+ const MEDIUM_MIN_ATTENUATION = 1e-6;
11
+
12
+ // the throughput below which a walk stops, matching Cycles' VOLUME_THROUGHPUT_EPSILON
13
+ export const MEDIUM_THROUGHPUT_EPSILON = 1e-6;
14
+
15
+ // the largest phase anisotropy used, short of the +/- 1 delta phase function the scatter albedo
16
+ // inversion and the Henyey-Greenstein pdf divide by zero at
17
+ const MEDIUM_MAX_ANISOTROPY = 0.999;
18
+
19
+ export const mediumStruct = new StructTypeNode( {
20
+ inside: 'bool',
21
+ object: 'uint',
22
+ extinction: 'vec3f',
23
+ albedo: 'vec3f',
24
+ anisotropy: 'float',
25
+ }, 'Medium' );
26
+
27
+ // a medium in a storage buffer, which can't hold bools, for walks that span passes
28
+ export const mediumStateStruct = new StructTypeNode( {
29
+ extinction: 'vec3f',
30
+ object: 'uint',
31
+
32
+ albedo: 'vec3f',
33
+ anisotropy: 'float',
34
+
35
+ inside: 'uint',
36
+ }, 'MediumState' );
37
+
38
+ export const storeMediumFn = wgslTagFn/* wgsl */`
39
+
40
+ fn storeMedium( medium: ${ mediumStruct } ) -> ${ mediumStateStruct } {
41
+
42
+ var state: ${ mediumStateStruct };
43
+ state.extinction = medium.extinction;
44
+ state.object = medium.object;
45
+ state.albedo = medium.albedo;
46
+ state.anisotropy = medium.anisotropy;
47
+ state.inside = select( 0u, 1u, medium.inside );
48
+ return state;
49
+
50
+ }
51
+
52
+ `;
53
+
54
+ export const loadMediumFn = wgslTagFn/* wgsl */`
55
+
56
+ fn loadMedium( state: ${ mediumStateStruct } ) -> ${ mediumStruct } {
57
+
58
+ var medium: ${ mediumStruct };
59
+ medium.inside = state.inside != 0u;
60
+ medium.object = state.object;
61
+ medium.extinction = state.extinction;
62
+ medium.albedo = state.albedo;
63
+ medium.anisotropy = state.anisotropy;
64
+ return medium;
65
+
66
+ }
67
+
68
+ `;
69
+
70
+ // The single scatter albedo of a volume from its perceived multi scatter albedo and phase anisotropy, using the
71
+ // inversion from "KHR_materials_scatter" (after Kulla and Conty 2017). A zero multi scatter albedo maps to exactly zero
72
+ // https://github.com/KhronosGroup/glTF/pull/2579
73
+ const singleScatterAlbedoFn = wgslTagFn/* wgsl */`
74
+
75
+ fn singleScatterAlbedo( multiscatterAlbedo: vec3f, anisotropy: f32 ) -> vec3f {
76
+
77
+ let a = multiscatterAlbedo;
78
+ let s = clamp( 4.09712 + 4.20863 * a - sqrt( 9.59217 + 41.6808 * a + 17.7126 * a * a ), vec3f( 0.0 ), vec3f( 1.0 ) );
79
+ let s2 = select( s * s, vec3f( 1.0 ), a <= vec3f( 0.0 ) );
80
+ return ( 1.0 - s2 ) / ( 1.0 - anisotropy * s2 );
81
+
82
+ }
83
+
84
+ `;
85
+
86
+ // The medium entered by refracting into a material, not "inside" if the material doesn't scatter
87
+ export const enterMediumFn = wgslTagFn/* wgsl */`
88
+
89
+ fn enterMedium( material: ${ materialStruct }, object: u32 ) -> ${ mediumStruct } {
90
+
91
+ let anisotropy = clamp( material.scatterAnisotropy, - ${ MEDIUM_MAX_ANISOTROPY }, ${ MEDIUM_MAX_ANISOTROPY } );
92
+
93
+ var medium: ${ mediumStruct };
94
+ medium.extinction = - log( max( material.attenuationColor, vec3f( ${ MEDIUM_MIN_ATTENUATION } ) ) ) / material.attenuationDistance;
95
+ medium.albedo = ${ singleScatterAlbedoFn }( material.multiscatterColor * material.scatterStrength, anisotropy );
96
+ medium.inside = any( medium.albedo > vec3f( 0.0 ) ) && any( medium.extinction > vec3f( 0.0 ) );
97
+ medium.object = object;
98
+ medium.anisotropy = anisotropy;
99
+ return medium;
100
+
101
+ }
102
+
103
+ `;
104
+
105
+ // The probability of sampling each color channel's distance, in proportion to throughput times albedo
106
+ // https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/closure/volume.h#L157
107
+ export const mediumChannelPdfFn = wgslTagFn/* wgsl */`
108
+
109
+ fn mediumChannelPdf( throughput: vec3f, albedo: vec3f ) -> vec3f {
110
+
111
+ let weights = abs( throughput * albedo );
112
+ let weightSum = weights.r + weights.g + weights.b;
113
+ if ( weightSum > 0.0 ) {
114
+
115
+ return weights / weightSum;
116
+
117
+ }
118
+
119
+ return vec3f( 1.0 / 3.0 );
120
+
121
+ }
122
+
123
+ `;
124
+
125
+ // Samples the distance to the next scatter event using one color channel's extinction
126
+ export const sampleMediumDistanceFn = wgslTagFn/* wgsl */`
127
+
128
+ fn sampleMediumDistance( medium: ${ mediumStruct }, channelPdf: vec3f, u: vec2f ) -> f32 {
129
+
130
+ var channel = 2u;
131
+ if ( u.x < channelPdf.r ) {
132
+
133
+ channel = 0u;
134
+
135
+ } else if ( u.x < channelPdf.r + channelPdf.g ) {
136
+
137
+ channel = 1u;
138
+
139
+ }
140
+
141
+ // a channel with no extinction never scatters, and WGSL doesn't guarantee an infinite quotient
142
+ let extinction = medium.extinction[ channel ];
143
+ if ( extinction <= 0.0 ) {
144
+
145
+ return ${ LIGHT_FAR_DISTANCE };
146
+
147
+ }
148
+
149
+ return - log( 1.0 - u.y ) / extinction;
150
+
151
+ }
152
+
153
+ `;
154
+
155
+ // Per channel pdf of a segment: of scattering at "dist", or of passing it when it didn't scatter
156
+ export const mediumDistancePdfFn = wgslTagFn/* wgsl */`
157
+
158
+ fn mediumDistancePdf( medium: ${ mediumStruct }, dist: f32, scattered: bool ) -> vec3f {
159
+
160
+ let transmittance = exp( - medium.extinction * dist );
161
+ return select( transmittance, medium.extinction * transmittance, scattered );
162
+
163
+ }
164
+
165
+ `;
@@ -6,6 +6,8 @@ export const RNG_INDEX_APERTURE_SAMPLE = 4;
6
6
  export const RNG_INDEX_DIRECT_LIGHT_SAMPLE = 5;
7
7
  export const RNG_INDEX_RUSSIAN_ROULETTE = 6;
8
8
  export const RNG_INDEX_DISPERSION_WAVELENGTH = 7;
9
+ export const RNG_INDEX_MEDIUM_DISTANCE = 8;
10
+ export const RNG_INDEX_MEDIUM_DIRECTION = 9;
9
11
  export const RNG_INDEX_ALPHA_TEST = 50;
10
12
  import { contextProxyFn } from 'three-mesh-bvh/webgpu';
11
13
  import * as sobol from './rand/sobol.wgsl.js';
@@ -108,14 +108,55 @@ export const sampleHemisphereFn = wgslFn( /* wgsl */ `
108
108
 
109
109
  `, [ constants ] );
110
110
 
111
+ // below this anisotropy the Henyey-Greenstein phase function is sampled as isotropic, avoiding the
112
+ // division by zero in its inverted CDF
113
+ const HG_ISOTROPIC_EPSILON = 1e-3;
114
+
115
+ // Samples a new direction from the Henyey-Greenstein phase function around the propagation direction
116
+ // "d". Positive "g" scatters forward. The sample is exact, so the phase function over its pdf is one.
117
+ // https://github.com/blender/cycles/blob/a456b761034dda42c32eef9f4aae0fa5a5c9f604/src/kernel/closure/volume_util.h#L36
118
+ export const sampleHenyeyGreensteinFn = wgslFn( /* wgsl */ `
119
+
120
+ fn sampleHenyeyGreenstein( d: vec3f, g: f32, uv: vec2f ) -> vec3f {
121
+
122
+ var cosTheta = 1.0 - 2.0 * uv.x;
123
+ if ( abs( g ) > ${ HG_ISOTROPIC_EPSILON } ) {
124
+
125
+ let sqrTerm = ( 1.0 - g * g ) / ( 1.0 - g + 2.0 * g * uv.x );
126
+ cosTheta = clamp( ( 1.0 + g * g - sqrTerm * sqrTerm ) / ( 2.0 * g ), - 1.0, 1.0 );
127
+
128
+ }
129
+
130
+ // orthonormal basis around "d", from https://graphics.pixar.com/library/OrthonormalB/paper.pdf
131
+ let sign = select( sign( d.z ), 1.0, d.z == 0.0 );
132
+ let a = - 1.0 / ( sign + d.z );
133
+ let b = d.x * d.y * a;
134
+ let b1 = vec3( 1.0 + sign * d.x * d.x * a, sign * b, - sign * d.x );
135
+ let b2 = vec3( b, sign + d.y * d.y * a, - d.y );
136
+
137
+ let sinTheta = sqrt( max( 0.0, 1.0 - cosTheta * cosTheta ) );
138
+ let phi = 2.0 * PI * uv.y;
139
+ return normalize( sinTheta * cos( phi ) * b1 + sinTheta * sin( phi ) * b2 + cosTheta * d );
140
+
141
+ }
142
+
143
+ `, [ constants ] );
144
+
111
145
  // power heuristic for multiple importance sampling
112
146
  export const misHeuristicFn = wgslFn( /* wgsl */ `
113
147
 
114
148
  fn misHeuristic( a: f32, b: f32 ) -> f32 {
115
149
 
116
- let aa = a * a;
117
- let bb = b * b;
118
- return aa / ( aa + bb );
150
+ // computed as a ratio so squaring large pdfs can't overflow to Inf / Inf
151
+ if ( a >= b ) {
152
+
153
+ let r = b / a;
154
+ return 1.0 / ( 1.0 + r * r );
155
+
156
+ }
157
+
158
+ let r = a / b;
159
+ return r * r / ( 1.0 + r * r );
119
160
 
120
161
  }
121
162
 
@@ -156,7 +156,13 @@ export const materialStruct = new StructTypeNode( {
156
156
 
157
157
  // offset 264 floats
158
158
  anisotropyMapTransform: 'mat3',
159
- // total size = 276
159
+
160
+ // offset 276 floats
161
+ // the volume's scattering, from "KHR_materials_scatter"
162
+ multiscatterColor: 'vec3',
163
+ scatterStrength: 'float',
164
+ scatterAnisotropy: 'float',
165
+ // total size = 284
160
166
  }, 'Material' );
161
167
 
162
168
  export const surfaceRecordStruct = new StructTypeNode( {
@@ -1,5 +1,5 @@
1
- import { Vector2 } from 'three/webgpu';
2
1
  /** @import { Camera, Texture, WebGPURenderer } from 'three/webgpu' */
2
+ import { Vector2 } from 'three/webgpu';
3
3
 
4
4
  const _size = /*@__PURE__*/ new Vector2();
5
5