three-gpu-pathtracer 0.0.26 → 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 (50) hide show
  1. package/README.md +4 -2
  2. package/build/index.module.js +23 -5
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +22 -4
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +8 -8
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/core/utils/sceneUpdateUtils.js +2 -1
  9. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  10. package/src/uniforms/LightsInfoUniformStruct.js +16 -1
  11. package/src/webgpu/API.md +167 -19
  12. package/src/webgpu/AtlasTexture.js +32 -12
  13. package/src/webgpu/EquirectHdrInfoNode.js +97 -46
  14. package/src/webgpu/LightsInfoNode.js +142 -4
  15. package/src/webgpu/MegaKernelPathTracer.js +11 -3
  16. package/src/webgpu/PathTracerBackend.js +174 -21
  17. package/src/webgpu/TurquinTexture.js +2 -0
  18. package/src/webgpu/WaveFrontPathTracer.js +169 -71
  19. package/src/webgpu/WebGPUPathTracer.js +637 -51
  20. package/src/webgpu/compute/ComputeKernel.js +18 -0
  21. package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
  22. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  23. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  24. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  25. package/src/webgpu/compute/targets.js +55 -0
  26. package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
  27. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  28. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  29. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  30. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  31. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  32. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  33. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  34. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
  35. package/src/webgpu/compute/wavefront/structs.js +63 -35
  36. package/src/webgpu/constants.js +2 -2
  37. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  38. package/src/webgpu/index.d.ts +79 -6
  39. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  40. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  41. package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
  42. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  43. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  44. package/src/webgpu/nodes/lights.wgsl.js +21 -1
  45. package/src/webgpu/nodes/material.wgsl.js +2 -1
  46. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  47. package/src/webgpu/nodes/random.wgsl.js +2 -0
  48. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  49. package/src/webgpu/nodes/structs.wgsl.js +8 -1
  50. package/src/webgpu/upscale/FSRUpscaler.js +3 -2
@@ -1,5 +1,53 @@
1
- import { Camera, Scene, Texture, ExternalTexture } from 'three';
2
- import { WebGPURenderer } from 'three/webgpu';
1
+ import { Camera, Color, EventDispatcher, Scene, Texture, ExternalTexture } from 'three';
2
+ import { StorageArrayTexture, WebGPURenderer } from 'three/webgpu';
3
+ import { BVHWorker } from '../index.js';
4
+
5
+ // three.js type augmentation
6
+
7
+ declare module 'three' {
8
+
9
+ export interface Material {
10
+
11
+ matte?: boolean;
12
+ castShadow?: boolean;
13
+
14
+ }
15
+
16
+ // volume scattering from "KHR_materials_scatter"
17
+ export interface MeshPhysicalMaterial {
18
+
19
+ scatterStrength?: number;
20
+ multiscatterColor?: Color;
21
+ scatterAnisotropy?: number;
22
+
23
+ }
24
+
25
+ export interface Object3D {
26
+
27
+ visibleToShadowRays?: boolean;
28
+
29
+ }
30
+
31
+ export interface Light {
32
+
33
+ visibleToCameraRays?: boolean;
34
+
35
+ }
36
+
37
+ export interface SpotLight {
38
+
39
+ radius?: number;
40
+ iesMap?: Texture | null;
41
+
42
+ }
43
+
44
+ export interface RectAreaLight {
45
+
46
+ isCircular?: boolean;
47
+
48
+ }
49
+
50
+ }
3
51
 
4
52
  // constants
5
53
 
@@ -64,7 +112,14 @@ export interface OIDNDenoiserOptions {
64
112
 
65
113
  }
66
114
 
67
- export class OIDNDenoiser {
115
+ export interface OIDNDenoiserEventMap {
116
+
117
+ 'error': { error: Error };
118
+ 'progress': { progress: number };
119
+
120
+ }
121
+
122
+ export class OIDNDenoiser extends EventDispatcher<OIDNDenoiserEventMap> {
68
123
 
69
124
  constructor( options: OIDNDenoiserOptions );
70
125
 
@@ -80,8 +135,7 @@ export class OIDNDenoiser {
80
135
  readonly running: boolean;
81
136
 
82
137
  init( renderer: WebGPURenderer ): void;
83
- setScene( scene: Scene, camera: Camera ): void;
84
- update( target: Texture ): Texture | null;
138
+ update( target: Texture, aux?: StorageArrayTexture | null ): Texture | null;
85
139
  denoise( color: Texture, albedo?: Texture | null, normal?: Texture | null ): Promise<void>;
86
140
  reset(): void;
87
141
  dispose(): void;
@@ -117,13 +171,23 @@ export interface DebugBoundsOptions {
117
171
 
118
172
  }
119
173
 
120
- export class WebGPUPathTracer {
174
+ export interface WebGPUPathTracerEventMap {
175
+
176
+ 'complete': {};
177
+ 'progress': { stage: 'building' | 'compiling' | 'rendering' | 'denoising', progress: number | null, samples?: SampleCounts };
178
+
179
+ }
180
+
181
+ export class WebGPUPathTracer extends EventDispatcher<WebGPUPathTracerEventMap> {
121
182
 
122
183
  constructor( renderer: WebGPURenderer );
123
184
 
124
185
  maxBounces: number;
125
186
  frameBudget: number;
187
+ adaptiveFrameBudget: boolean;
188
+ targetFrameTime: number;
126
189
  maxTransparentBounces: number;
190
+ maxSubsurfaceBounces: number;
127
191
  maxSamples: number;
128
192
  transmissiveBackground: TransmissiveBackgroundMode;
129
193
  filterGlossyFactor: number;
@@ -136,17 +200,23 @@ export class WebGPUPathTracer {
136
200
  fadeDuration: number;
137
201
  dynamicLowRes: boolean;
138
202
  lowResScale: number;
203
+ adaptiveDynamicRes: boolean;
139
204
  renderScale: number;
140
205
  synchronizeRenderSize: boolean;
206
+ renderToCanvas: boolean;
207
+ asyncCompile: boolean;
141
208
  generateMissingAttributes: boolean;
142
209
  commonAttributes: Array<string>;
143
210
  stableNoise: boolean;
144
211
  pause: boolean;
145
212
 
146
213
  readonly target: Texture | null;
214
+ readonly frameTime: number;
215
+ readonly complete: boolean;
147
216
  readonly fadeState: number;
148
217
  readonly lowResTarget: Texture;
149
218
  readonly lowResMode: boolean;
219
+ readonly compiling: boolean;
150
220
  readonly textureAtlas: AtlasTexture;
151
221
 
152
222
  getSampleCountsAsync(): Promise<SampleCounts>;
@@ -155,6 +225,8 @@ export class WebGPUPathTracer {
155
225
  setUpscaler( upscaler: FSRUpscaler | null ): void;
156
226
 
157
227
  setScene( scene: Scene, camera: Camera ): void;
228
+ setBVHWorker( WorkerConstructor: new () => BVHWorker & { dispose(): void } ): void;
229
+ setSceneAsync( scene: Scene, camera: Camera ): Promise<void>;
158
230
  setRandom( random: RandomStrategy ): void;
159
231
  setCamera( camera: Camera ): void;
160
232
  setSize( x: number, y: number ): void;
@@ -170,6 +242,7 @@ export class WebGPUPathTracer {
170
242
  renderDebugBounds( options?: DebugBoundsOptions ): void;
171
243
  renderTextureAtlas( layer?: number ): void;
172
244
  renderSampleDensity(): void;
245
+ renderAuxBuffer( buffer?: 'albedo' | 'normal' ): void;
173
246
  getRenderTime(): number;
174
247
  dispose(): void;
175
248
 
@@ -160,8 +160,10 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
160
160
 
161
161
  }
162
162
 
163
- // the refracted half is not attenuated by the layers above
164
- result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * surf.color;
163
+ // the refracted half is not attenuated by the layers above. A volume is entered and exited,
164
+ // so each crossing takes the square root of the color, as in Cycles
165
+ let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
166
+ result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * transmissionTint;
165
167
 
166
168
  } else {
167
169
 
@@ -228,11 +230,15 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
228
230
  let metallic = metallicSpecular * metallicReflectance;
229
231
  let dielectric = dielectricSpecular * dielectricReflectance;
230
232
 
231
- // the energy the specular interface takes from the layers below
232
- let fresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior ) * dielectricBoost;
233
+ // The energy the specular interface takes from the layers below, at the lobe's specular color
234
+ // and intensity. The table holds the albedo at the ior's f0, and the Schlick albedo is linear
235
+ // in f0, so the tinted albedo slides from it toward the conductor albedo at f0 = 1
236
+ let iorEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior );
237
+ let tintedEnergySS = iorEnergySS + ( dielectricF0 - surf.f0 ) * ( energySS - iorEnergySS ) / ( 1.0 - surf.f0 );
238
+ let fresnelEnergySS = surf.specularIntensity * tintedEnergySS * dielectricBoost;
233
239
 
234
240
  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 );
241
+ attenuation *= ( 1.0 - surf.metalness ) * mix( 1.0 - fresnelEnergySS, vec3f( 1.0 - filmFresnelMax ), surf.iridescence );
236
242
 
237
243
  }
238
244
 
@@ -385,6 +391,13 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
385
391
 
386
392
  wi = refract( - wo, wh, surf.eta );
387
393
 
394
+ // "refract" returns zero on total internal reflection, so reflect instead and let "isDead" drop it
395
+ if ( dot( wi, wi ) == 0.0 ) {
396
+
397
+ wi = - normalize( reflect( wo, wh ) );
398
+
399
+ }
400
+
388
401
  }
389
402
 
390
403
  // the facet choice must agree with the resulting hemisphere - reflection above,
@@ -417,7 +430,8 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
417
430
 
418
431
  // Cycles represents a singular event with a large matching BSDF and PDF. Their
419
432
  // ratio preserves throughput while forward MIS correctly gives the event full weight.
420
- result.color = ( 1.0 - surf.metalness ) * surf.transmission * surf.color *
433
+ let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
434
+ result.color = ( 1.0 - surf.metalness ) * surf.transmission * transmissionTint *
421
435
  deltaTransmittance * ${ SINGULAR_EVENT_SCALE };
422
436
  result.direction = worldWi;
423
437
  result.pdf = weights.transmission * deltaProbability * ${ SINGULAR_EVENT_SCALE };
@@ -0,0 +1,54 @@
1
+ import { MeshBasicNodeMaterial, NoBlending } from 'three/webgpu';
2
+ import { uv, varying, texture, uniform } from 'three/tsl';
3
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
4
+ import { createAuxTarget } from '../../compute/targets.js';
5
+ import { unpackAuxAlbedoFn, unpackAuxNormalFn, AUX_LAYERS } from '../../nodes/aux.wgsl.js';
6
+
7
+ // Shows the albedo or normal the path tracer accumulates for the denoiser
8
+ export class AuxDebugMaterial extends MeshBasicNodeMaterial {
9
+
10
+ set texture( v ) {
11
+
12
+ this._texNode.value = v;
13
+
14
+ }
15
+
16
+ set showNormal( v ) {
17
+
18
+ this._showNormalUniform.value = v ? 1 : 0;
19
+
20
+ }
21
+
22
+ constructor() {
23
+
24
+ super();
25
+
26
+ this.blending = NoBlending;
27
+
28
+ this._texNode = texture( createAuxTarget() );
29
+ this._showNormalUniform = uniform( 0, 'uint' );
30
+
31
+ const auxDebug = wgslTagFn/* wgsl */`
32
+ fn auxDebug( tex: texture_2d_array<u32>, coord: vec2f, showNormal: u32 ) -> vec4f {
33
+
34
+ // the rows are stored top down, so the quad's bottom up uv is flipped
35
+ let texel = vec2u( vec2f( coord.x, 1.0 - coord.y ) * vec2f( textureDimensions( tex, 0 ) ) );
36
+ var packed: vec3u;
37
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
38
+
39
+ packed[ layer ] = textureLoad( tex, texel, layer, 0 ).r;
40
+
41
+ }
42
+
43
+ let albedo = ${ unpackAuxAlbedoFn }( packed );
44
+ let normal = ${ unpackAuxNormalFn }( packed );
45
+ return vec4f( select( albedo, normal, showNormal == 1u ), 1.0 );
46
+
47
+ }
48
+ `;
49
+
50
+ this.colorNode = auxDebug( this._texNode, varying( uv() ), this._showNormalUniform );
51
+
52
+ }
53
+
54
+ }
@@ -1,6 +1,6 @@
1
1
  import { BackSide, FrontSide, DoubleSide, BufferAttribute, BufferGeometry, StorageBufferAttribute, StructTypeNode, Vector4, SkinnedMesh, RepeatWrapping, ClampToEdgeWrapping, MirroredRepeatWrapping, NearestFilter } from 'three/webgpu';
2
- import { BVHComputeData, intersectRayTriangle, bvhNodeBoundsStruct, bvhNodeStruct, rayStruct, rayIntersectionResultStruct as intersectionResultStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
3
- import { storage, float, texture, uniformArray, uint } from 'three/tsl';
2
+ import { BVHComputeData, intersectRayTriangle, bvhNodeBoundsStruct, bvhNodeStruct, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
3
+ import { storage, float, bool, texture, uniformArray, uint } from 'three/tsl';
4
4
  import { SkinnedMeshBVH, MeshBVH, SAH } from 'three-mesh-bvh';
5
5
  import { materialStruct } from './structs.wgsl.js';
6
6
  import { getTextureHash } from '../../core/utils/sceneUpdateUtils.js';
@@ -14,11 +14,25 @@ const transformStruct = new StructTypeNode( {
14
14
  inverseMatrixWorld: 'mat4x4f',
15
15
  visible: 'uint',
16
16
  materialIndex: 'uint',
17
+ visibleToShadowRays: 'uint',
17
18
  _alignment0: 'uint',
18
- _alignment1: 'uint',
19
19
  color: 'vec4f',
20
20
  }, 'TransformStruct' );
21
21
 
22
+ // three-mesh-bvh's ray hit result. Shadow rays also record the share of bsdf rays the faces they skipped
23
+ // would stop, and the share of light the partially transparent surfaces they passed block
24
+ const intersectionResultStruct = new StructTypeNode( {
25
+ indices: 'vec4u',
26
+ normal: 'vec3f',
27
+ didHit: 'bool',
28
+ barycoord: 'vec3f',
29
+ objectIndex: 'uint',
30
+ side: 'float',
31
+ dist: 'float',
32
+ skippedOcclusion: 'float',
33
+ occlusion: 'float',
34
+ }, 'PathTracerIntersectionResult' );
35
+
22
36
  // Pathtracer-specific version of the BVHComputeData tht includes material mapping, property structs
23
37
  export class PathtracerBVHComputeData extends BVHComputeData {
24
38
 
@@ -167,7 +181,7 @@ export class PathtracerBVHComputeData extends BVHComputeData {
167
181
 
168
182
  const { textureAtlas, storage, fns } = this;
169
183
  const textures = textureAtlas.texture;
170
- const textureInfo = uniformArray( textureAtlas.textureInfo, 'uvec4' );
184
+ const textureInfo = uniformArray( textureAtlas.textureInfo, 'uvec4' ).setName( 'bvh_textureInfo' );
171
185
 
172
186
  // build the single sampleTexel bound to this instance's textureInfo node
173
187
  const sampleTexel = sampleTexelFunc( textureInfo, texture( textures ) );
@@ -181,8 +195,12 @@ export class PathtracerBVHComputeData extends BVHComputeData {
181
195
  const scratchRayScalar = float( 1.0 ).toVar( 'bvh_rayScalar' );
182
196
  const baseOpacityScalar = float( 1.0 ).toVar( 'bvh_baseOpacity' );
183
197
 
184
- fns.raycastFirstHit = this.getShapecastFn( {
185
- name: 'raycastFirstHit',
198
+ // shadow rays hit both sides, pass through the objects not visible to them and partially transparent
199
+ // surfaces, and stop at the first opaque hit
200
+ const isShadowRay = bool( false ).toVar( 'bvh_isShadowRay' );
201
+ const skipShadowObject = bool( false ).toVar( 'bvh_skipShadowObject' );
202
+
203
+ const raycastOptions = {
186
204
  shapeStruct: rayStruct,
187
205
  resultStruct: intersectionResultStruct,
188
206
 
@@ -196,8 +214,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
196
214
  intersectsBoundsFn: wgslTagFn/* wgsl */`
197
215
  fn rayIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct }, result: ptr<function, ${ intersectionResultStruct }> ) -> u32 {
198
216
 
199
- // early-out if our object is completely transparent
200
- if ( ${ baseOpacityScalar } == 0.0 ) {
217
+ // early-out if our object is completely transparent, or a shadow ray is already occluded
218
+ if ( ${ baseOpacityScalar } == 0.0 || ( ${ isShadowRay } && result.didHit ) ) {
201
219
 
202
220
  return 0u;
203
221
 
@@ -259,16 +277,16 @@ export class PathtracerBVHComputeData extends BVHComputeData {
259
277
  let material = ${ storage.materials }[ ${ currentMaterialIndex } ];
260
278
 
261
279
  // TODO: if material is a transmissive volume we may need to assume double-sidedness
262
- if ( material.side != 0 && triResult.side != material.side ) {
280
+ // shadow rays hit both sides
281
+ if ( ! ${ isShadowRay } && material.side != 0 && triResult.side != material.side ) {
263
282
 
264
283
  continue;
265
284
 
266
285
  }
267
286
 
287
+ var opacity = ${ baseOpacityScalar };
268
288
  if ( material.transparent != 0 || material.alphaTest > 0.0 ) {
269
289
 
270
- var opacity = ${ baseOpacityScalar };
271
-
272
290
  // add support for vertex color opacity
273
291
  if ( material.vertexColors == 1 ) {
274
292
 
@@ -325,6 +343,29 @@ export class PathtracerBVHComputeData extends BVHComputeData {
325
343
 
326
344
  }
327
345
 
346
+ // shadow rays skip the objects not visible to them. Record the share of bsdf rays this face
347
+ // would stop: all of them if it's opaque, its opacity if it's partially transparent
348
+ if ( ${ skipShadowObject } ) {
349
+
350
+ if ( material.side == 0 || triResult.side == material.side ) {
351
+
352
+ result.skippedOcclusion += ( 1.0 - result.skippedOcclusion ) * select( 1.0, opacity, material.transparent != 0 );
353
+
354
+ }
355
+
356
+ continue;
357
+
358
+ }
359
+
360
+ // shadow rays pass partially transparent surfaces, each blocking its opacity's share of the
361
+ // light still getting through, just as bsdf rays pass them by chance
362
+ if ( ${ isShadowRay } && material.transparent != 0 && opacity < 1.0 ) {
363
+
364
+ result.occlusion += ( 1.0 - result.occlusion ) * opacity;
365
+ continue;
366
+
367
+ }
368
+
328
369
  result.didHit = true;
329
370
  result.dist = triResult.dist;
330
371
  result.normal = triResult.normal;
@@ -367,6 +408,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
367
408
 
368
409
  }
369
410
 
411
+ ${ skipShadowObject } = ${ isShadowRay } && object.visibleToShadowRays == 0u;
412
+
370
413
  }
371
414
  `,
372
415
  transformResultFn: wgslTagFn/* wgsl */`
@@ -384,6 +427,30 @@ export class PathtracerBVHComputeData extends BVHComputeData {
384
427
  ${ scratchRayScalar } = 1.0;
385
428
  ${ baseOpacityScalar } = 1.0;
386
429
 
430
+ }
431
+ `,
432
+ };
433
+
434
+ fns.raycastFirstHit = this.getShapecastFn( {
435
+ ...raycastOptions,
436
+ name: 'raycastFirstHit',
437
+ prefixFn: wgslTagFn/* wgsl */`
438
+ fn initFirstHitCull() -> void {
439
+
440
+ ${ isShadowRay } = false;
441
+
442
+ }
443
+ `,
444
+ } );
445
+
446
+ fns.raycastShadow = this.getShapecastFn( {
447
+ ...raycastOptions,
448
+ name: 'raycastShadow',
449
+ prefixFn: wgslTagFn/* wgsl */`
450
+ fn initShadowCull() -> void {
451
+
452
+ ${ isShadowRay } = true;
453
+
387
454
  }
388
455
  `,
389
456
  } );
@@ -398,6 +465,9 @@ export class PathtracerBVHComputeData extends BVHComputeData {
398
465
 
399
466
  this.updateUvAttributesFromScene();
400
467
 
468
+ // the material indices are written with the transforms
469
+ this.updateMaterialsMap();
470
+
401
471
  super.update();
402
472
 
403
473
  // build the channel -> uv lookup now that the geometry struct (and its uv members) exist
@@ -423,7 +493,7 @@ export class PathtracerBVHComputeData extends BVHComputeData {
423
493
 
424
494
  if ( Array.isArray( o.material ) ) {
425
495
 
426
- o.material.forEach( m => add( m ) );
496
+ o.material.forEach( m => m && add( m ) );
427
497
 
428
498
  } else {
429
499
 
@@ -716,9 +786,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
716
786
  floatArray[ index ++ ] = getField( m, 'iridescenceIOR', 1.3 );
717
787
 
718
788
  // offset 32
719
- const iridescenceThicknessRange = getField( m, 'iridescenceThicknessRange', [ 100, 400 ] );
720
- floatArray[ index ++ ] = iridescenceThicknessRange[ 0 ];
721
- floatArray[ index ++ ] = iridescenceThicknessRange[ 1 ];
789
+ floatArray[ index ++ ] = getField( m, 'iridescenceThicknessMinimum', 100 );
790
+ floatArray[ index ++ ] = getField( m, 'iridescenceThicknessMaximum', 400 );
722
791
  floatArray[ index ++ ] = getField( m, 'diffuseRoughness', 0.0 );
723
792
  floatArray[ index ++ ] = getField( m, 'dispersion', 0.0 );
724
793
 
@@ -885,6 +954,17 @@ export class PathtracerBVHComputeData extends BVHComputeData {
885
954
  // anisotropyMap transform - offset 264
886
955
  index += writeTextureMatrixToArray( m, 'anisotropyMap', floatArray, index );
887
956
 
957
+ // scattering volume - offset 276
958
+ const multiscatterColor = getField( m, 'multiscatterColor', null );
959
+ floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.r : 1.0;
960
+ floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.g : 1.0;
961
+ floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.b : 1.0;
962
+ floatArray[ index ++ ] = getField( m, 'scatterStrength', 0.0 );
963
+ floatArray[ index ++ ] = getField( m, 'scatterAnisotropy', 0.0 );
964
+
965
+ // padding to the struct's 16 byte alignment
966
+ index += 3;
967
+
888
968
  }
889
969
 
890
970
  return { materialData: intArray, textures };
@@ -906,9 +986,17 @@ export class PathtracerBVHComputeData extends BVHComputeData {
906
986
 
907
987
  }
908
988
 
909
- // save the index
910
- const index = materialsMap.get( material ) || 0;
911
989
  const transformBufferU32 = new Uint32Array( targetBuffer );
990
+
991
+ const index = materialsMap.has( material ) ? materialsMap.get( material ) : - 1;
992
+ if ( index === - 1 ) {
993
+
994
+ // hide the object when there is no material
995
+ transformBufferU32[ writeOffset * transformStruct.getLength() + 32 ] = 0;
996
+
997
+ }
998
+
999
+ // save the material index
912
1000
  transformBufferU32[ writeOffset * transformStruct.getLength() + 33 ] = index;
913
1001
 
914
1002
  // write color
@@ -939,6 +1027,10 @@ export class PathtracerBVHComputeData extends BVHComputeData {
939
1027
  super.writeTransformData( info, premultiplyMatrix, writeOffset, targetBuffer );
940
1028
  this.writeMaterialData( info, writeOffset, targetBuffer );
941
1029
 
1030
+ // whether shadow rays see the object, in the slot after the material index
1031
+ const transformBufferU32 = new Uint32Array( targetBuffer );
1032
+ transformBufferU32[ writeOffset * this.structs.transform.getLength() + 34 ] = info.object.visibleToShadowRays === false ? 0 : 1;
1033
+
942
1034
  }
943
1035
 
944
1036
  getBVH( object, instanceId, rangeTarget ) {
@@ -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,
@@ -11,7 +11,7 @@ export const POINT_LIGHT_TYPE = 4;
11
11
  export const ENVIRONMENT_LIGHT_TYPE = 5;
12
12
  export const LIGHT_FAR_DISTANCE = 1e30;
13
13
 
14
- // tolerance for comparing a shadow hit distance to the sampled light distance
14
+ // fraction of the light distance a shadow ray stops short of the light
15
15
  export const LIGHT_EPSILON = 1e-5;
16
16
 
17
17
  // light kinds that are also bsdf-sampled and so take MIS-weighted NEE - punctual lights take full weight
@@ -201,3 +201,23 @@ export const randomSpotLightSampleFn = wgslFn( /* wgsl */ `
201
201
  }
202
202
 
203
203
  `, [ lightStruct, lightRecordStruct, constants, getDistanceAttenuationFn ] );
204
+
205
+ // The cosine to the spot axis and the distance from a position to the nearest point on the spot
206
+ // light's disc, where the disc lights the position the most
207
+ export const getSpotLightNearestFn = wgslFn( /* wgsl */ `
208
+
209
+ fn getSpotLightNearest( light: Light, position: vec3f ) -> vec2f {
210
+
211
+ let normal = normalize( cross( light.u, light.v ) );
212
+ let startDistance = light.radius / max( tan( acos( light.coneCos ) ), EPSILON );
213
+ let discCenter = light.position - normal * startDistance;
214
+
215
+ let toPosition = position - discCenter;
216
+ let along = dot( toPosition, - normal );
217
+ let across = max( length( toPosition + normal * along ) - light.radius, 0.0 );
218
+ let dist = length( vec2f( along, across ) );
219
+ return vec2f( along / max( dist, EPSILON ), dist );
220
+
221
+ }
222
+
223
+ `, [ lightStruct, constants ] );
@@ -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