three-gpu-pathtracer 0.0.25 → 0.0.27

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.
@@ -53,12 +53,17 @@ export class LogicKernel extends ComputeKernel {
53
53
  const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
54
54
  const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
55
55
  const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
56
+ const getEnvWeight = proxy( 'envInfo.value.getWeight', params );
56
57
  const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
57
58
 
58
59
  // analytic scene lights pulled off the lightsInfo provider (LightsInfoNode)
59
60
  const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
60
61
  const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
61
62
  const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
63
+ const getLightWeightFn = proxyFn( 'lightsInfo.value.getLightWeight', params );
64
+ const getLightsWeightFn = proxyFn( 'lightsInfo.value.getLightsWeight', params );
65
+ const selectLightFn = proxyFn( 'lightsInfo.value.selectLight', params );
66
+ const isLightVisibleToCameraFn = proxyFn( 'lightsInfo.value.isLightVisibleToCamera', params );
62
67
 
63
68
  const fn = wgslTagFn/* wgsl */`
64
69
 
@@ -97,19 +102,24 @@ export class LogicKernel extends ComputeKernel {
97
102
  let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
98
103
  ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
99
104
 
100
- // one-sample NEE selection normalization (lights + env), matched with the megakernel
105
+ // one-sample NEE chooses between the analytic lights and the environment, matched with
106
+ // the megakernel
101
107
  let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
102
108
  let lightsCount = ${ lightsCountNode };
103
- var lightsDenom = f32( lightsCount );
109
+ var envWeight = 0.0;
104
110
  if ( envActive ) {
105
111
 
106
- lightsDenom += 1.0;
112
+ envWeight = ${ getEnvWeight }();
107
113
 
108
114
  }
109
115
 
110
116
  var resultColor = input.resultColor;
111
117
  var throughputColor = input.throughputColor;
112
118
 
119
+ // added last, since a camera ray miss overwrites the pixel with the background
120
+ var lightHits = vec3f( 0.0 );
121
+ var cameraHitLight = false;
122
+
113
123
  // resolve the previous surface's NEE shadow ray (pre-scatter throughput). The index
114
124
  // is negative when no shadow ray was enqueued last frame
115
125
  if ( input.shadowRayIntersectionIndex >= 0 && input.lightPdf > 0.0 ) {
@@ -118,8 +128,10 @@ export class LogicKernel extends ComputeKernel {
118
128
  let occluded = shadowHit.objectIndex >= 0;
119
129
  if ( ! occluded ) {
120
130
 
121
- // env + area lights are also bsdf-sampled, so MIS-weight them; punctual take full weight
122
- let misWeight = select( 1.0, ${ misHeuristicFn }( input.lightPdf, input.lightBsdfPdf ), ${ isMISWeightLightFn }( input.lightType ) );
131
+ // env + area lights are also bsdf-sampled, so MIS-weight them; punctual take full weight,
132
+ // or when a bsdf ray could never reach the light
133
+ let isMISWeighted = ${ isMISWeightLightFn }( input.lightType ) && shadowHit.forwardBlocked == 0u;
134
+ let misWeight = select( 1.0, ${ misHeuristicFn }( input.lightPdf, input.lightBsdfPdf ), isMISWeighted );
123
135
  let directLight = throughputColor * input.lightEmission * input.lightBsdf * misWeight / input.lightPdf;
124
136
  let contribution = ${ clampPathContributionFunc }( directLight, input.currentBounce, clampDirect, clampIndirect );
125
137
  resultColor += vec4f( contribution, 0.0 );
@@ -151,28 +163,38 @@ export class LogicKernel extends ComputeKernel {
151
163
  let didHit = hitResult.objectIndex >= 0;
152
164
  let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
153
165
 
154
- // forward hits: a bsdf-sampled segment that lands on a area light. MIS-weighted
155
- // only when NEE is also sampling the lights. The camera segment is skipped.
156
- if ( input.currentBounce > 0u ) {
166
+ // NEE's total weight at the vertex this segment left, for MIS. Camera segments have none
167
+ let isMISWeighted = misEnabled != 0u && input.currentBounce > 0u;
168
+ var lightTotalWeight = 0.0;
169
+ if ( isMISWeighted ) {
157
170
 
158
- for ( var li = 0u; li < lightsCount; li ++ ) {
171
+ lightTotalWeight = ${ getLightsWeightFn }( input.origin ) + envWeight;
159
172
 
160
- var lightRec: ${ lightRecordStruct };
161
- if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
173
+ }
162
174
 
163
- var misWeight = 1.0;
164
- if ( misEnabled != 0u ) {
175
+ // forward hits on area lights. Camera rays only see visibleToCameraRays lights, at full weight
176
+ for ( var li = 0u; li < lightsCount; li ++ ) {
165
177
 
166
- let lightPdf = lightRec.pdf / lightsDenom;
167
- misWeight = ${ misHeuristicFn }( input.scatterPdf, lightPdf );
178
+ if ( input.currentBounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
168
179
 
169
- }
180
+ continue;
181
+
182
+ }
170
183
 
171
- let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
172
- resultColor += vec4f( lightHit, 0.0 );
184
+ var lightRec: ${ lightRecordStruct };
185
+ if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
186
+
187
+ var misWeight = 1.0;
188
+ if ( isMISWeighted ) {
189
+
190
+ let selectionPdf = ${ getLightWeightFn }( li, input.origin ) / lightTotalWeight;
191
+ misWeight = ${ misHeuristicFn }( input.scatterPdf, lightRec.pdf * selectionPdf );
173
192
 
174
193
  }
175
194
 
195
+ lightHits += ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
196
+ cameraHitLight = cameraHitLight || input.currentBounce == 0u;
197
+
176
198
  }
177
199
 
178
200
  }
@@ -187,36 +209,40 @@ export class LogicKernel extends ComputeKernel {
187
209
  rayDataStorage[ index ].objectIndex = hitResult.objectIndex;
188
210
  rayDataStorage[ index ].dist = hitResult.dist;
189
211
 
190
- // next event estimation: pick one light or the environment with a single sample.
191
- // MaterialKernel evaluates the bsdf and enqueues the shadow ray.
192
- // TODO: importance-sample the selection by light intensity and solid angle
212
+ // next event estimation: choose one light or the environment by its estimated
213
+ // contribution. MaterialKernel evaluates the bsdf and enqueues the shadow ray.
193
214
  var lightPdf = 0.0;
194
- if ( misEnabled != 0u && lightsDenom > 0.0 ) {
215
+ if ( misEnabled != 0u ) {
195
216
 
196
217
  let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
197
- let lightIndex = min( u32( ruv.x * lightsDenom ), u32( lightsDenom ) - 1u );
198
- var lightRec: ${ lightRecordStruct };
199
- if ( envActive && lightIndex == lightsCount ) {
200
-
201
- // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
202
- let envSample = ${ sampleEnvDir }( ruv.yz );
203
- lightRec.direction = envSample.direction;
204
- lightRec.emission = envSample.color;
205
- lightRec.pdf = envSample.pdf;
206
- lightRec.dist = ${ LIGHT_FAR_DISTANCE };
207
- lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
218
+ var selectionPdf = 0.0;
219
+ let lightIndex = ${ selectLightFn }( hitResult.position, envWeight, ruv.x, &selectionPdf );
220
+ if ( selectionPdf > 0.0 ) {
208
221
 
209
- } else {
222
+ var lightRec: ${ lightRecordStruct };
223
+ if ( envActive && lightIndex == lightsCount ) {
210
224
 
211
- lightRec = ${ randomLightSampleFn }( lightIndex, hitResult.position, ruv.yz );
225
+ // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
226
+ let envSample = ${ sampleEnvDir }( ruv.yz );
227
+ lightRec.direction = envSample.direction;
228
+ lightRec.emission = envSample.color;
229
+ lightRec.pdf = envSample.pdf;
230
+ lightRec.dist = ${ LIGHT_FAR_DISTANCE };
231
+ lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
212
232
 
213
- }
233
+ } else {
234
+
235
+ lightRec = ${ randomLightSampleFn }( lightIndex, hitResult.position, ruv.yz );
214
236
 
215
- lightPdf = lightRec.pdf / lightsDenom;
216
- rayDataStorage[ index ].lightDirection = lightRec.direction;
217
- rayDataStorage[ index ].lightEmission = lightRec.emission;
218
- rayDataStorage[ index ].lightDist = lightRec.dist;
219
- rayDataStorage[ index ].lightType = lightRec.lightType;
237
+ }
238
+
239
+ lightPdf = lightRec.pdf * selectionPdf;
240
+ rayDataStorage[ index ].lightDirection = lightRec.direction;
241
+ rayDataStorage[ index ].lightEmission = lightRec.emission;
242
+ rayDataStorage[ index ].lightDist = lightRec.dist;
243
+ rayDataStorage[ index ].lightType = lightRec.lightType;
244
+
245
+ }
220
246
 
221
247
  }
222
248
 
@@ -229,10 +255,10 @@ export class LogicKernel extends ComputeKernel {
229
255
  if ( input.currentBounce > 0u && input.isFullyTransmissive == 0u ) {
230
256
 
231
257
  var misWeight = 1.0;
232
- if ( misEnabled != 0u && envActive ) {
258
+ if ( isMISWeighted && envActive ) {
233
259
 
234
260
  // match the env pdf scaling used by the NEE selection so the two estimators balance
235
- let envPdf = ${ getEnvDirPdf }( input.direction ) / lightsDenom;
261
+ let envPdf = ${ getEnvDirPdf }( input.direction ) * envWeight / lightTotalWeight;
236
262
  misWeight = ${ misHeuristicFn }( input.scatterPdf, envPdf );
237
263
 
238
264
  }
@@ -308,6 +334,14 @@ export class LogicKernel extends ComputeKernel {
308
334
 
309
335
  }
310
336
 
337
+ // visible lights are opaque against a transparent background
338
+ resultColor += vec4f( lightHits, 0.0 );
339
+ if ( cameraHitLight ) {
340
+
341
+ resultColor.a = 1.0;
342
+
343
+ }
344
+
311
345
  // the color rows are stored top down to match a rasterized render target
312
346
  let colorIndex = vec2u( indexUV.x, textureDimensions( ${ params.outputTarget } ).y - 1u - indexUV.y );
313
347
  let storedSamples = textureLoad( ${ params.sampleCountTarget }, indexUV ).r & ${ SAMPLE_COUNT_MASK }u;
@@ -339,7 +339,7 @@ export class MaterialKernel extends ComputeKernel {
339
339
  shadowRayQueue.elements[ shadowIndex ].currentBounce = input.currentBounce;
340
340
  shadowRayQueue.elements[ shadowIndex ].seed = input.seed;
341
341
  shadowRayQueue.elements[ shadowIndex ].alphaDepth = input.alphaDepth;
342
- shadowRayQueue.elements[ shadowIndex ].maxDist = input.lightDist - ${ LIGHT_EPSILON };
342
+ shadowRayQueue.elements[ shadowIndex ].maxDist = input.lightDist * ( 1.0 - ${ LIGHT_EPSILON } );
343
343
  rayDataStorage[ index ].shadowRayIntersectionIndex = i32( shadowIndex );
344
344
 
345
345
  } else {
@@ -5,8 +5,8 @@ import { proxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
5
  import { rngInit } from '../../nodes/random.wgsl.js';
6
6
  import { rayQueueStruct, intersectionResultStruct } from './structs.js';
7
7
 
8
- // Pure BVH traversal over the queued shadow rays. Uses the same first-hit traversal as the bounce
9
- // rays ( no dedicated any-hit traversal exists yet ); LogicKernel decides occlusion by comparing the
8
+ // Pure BVH traversal over the queued shadow rays. Uses a first-hit traversal without face culling
9
+ // ( no dedicated any-hit traversal exists yet ); LogicKernel decides occlusion by comparing the
10
10
  // hit distance against the light distance.
11
11
  export class TraceShadowRayKernel extends ComputeKernel {
12
12
 
@@ -22,7 +22,7 @@ export class TraceShadowRayKernel extends ComputeKernel {
22
22
  };
23
23
 
24
24
  const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
25
- const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params );
25
+ const raycastShadowFn = proxy( 'bvhData.value.fns.raycastShadow', params );
26
26
 
27
27
  const fn = wgslTagFn /* wgsl */`
28
28
 
@@ -44,7 +44,7 @@ export class TraceShadowRayKernel extends ComputeKernel {
44
44
 
45
45
  let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
46
46
  var hitResult: ${ raycastOutput };
47
- if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) {
47
+ if ( ${ raycastShadowFn }( ray, &hitResult ) ) {
48
48
 
49
49
  shadowRayIntersectionsStorage[ index ].objectIndex = i32( hitResult.objectIndex );
50
50
  shadowRayIntersectionsStorage[ index ].dist = hitResult.dist;
@@ -55,6 +55,8 @@ export class TraceShadowRayKernel extends ComputeKernel {
55
55
 
56
56
  }
57
57
 
58
+ shadowRayIntersectionsStorage[ index ].forwardBlocked = u32( hitResult.forwardBlocked );
59
+
58
60
  }
59
61
  `;
60
62
 
@@ -118,7 +118,9 @@ export const intersectionResultStruct = new StructTypeNode( {
118
118
  side: 'float',
119
119
 
120
120
  indices: 'vec3u',
121
- _alignment0: 'uint',
121
+
122
+ // shadow rays only: a face a bsdf ray would hit was passed
123
+ forwardBlocked: 'uint',
122
124
 
123
125
  }, 'TraceResult' );
124
126
 
@@ -1,6 +1,44 @@
1
1
  import { Camera, Scene, Texture, ExternalTexture } from 'three';
2
2
  import { WebGPURenderer } from 'three/webgpu';
3
3
 
4
+ // three.js type augmentation
5
+
6
+ declare module 'three' {
7
+
8
+ export interface Material {
9
+
10
+ matte?: boolean;
11
+ castShadow?: boolean;
12
+
13
+ }
14
+
15
+ export interface Object3D {
16
+
17
+ visibleToShadowRays?: boolean;
18
+
19
+ }
20
+
21
+ export interface Light {
22
+
23
+ visibleToCameraRays?: boolean;
24
+
25
+ }
26
+
27
+ export interface SpotLight {
28
+
29
+ radius?: number;
30
+ iesMap?: Texture | null;
31
+
32
+ }
33
+
34
+ export interface RectAreaLight {
35
+
36
+ isCircular?: boolean;
37
+
38
+ }
39
+
40
+ }
41
+
4
42
  // constants
5
43
 
6
44
  export type RandomStrategy = object;
@@ -1,6 +1,6 @@
1
1
  import { float } from 'three/tsl';
2
2
  import { wgslTagFn } from 'three-mesh-bvh/webgpu';
3
- import { PathtracingMaterial } from './PathtracingMaterial';
3
+ import { PathtracingMaterial } from './PathtracingMaterial.js';
4
4
  import { specularBrdfFunc, specularBtdfFunc, fresnelMixFunc, conductorFresnelFunc, fresnelCoatFunc, iridescentFresnelFunc, isMatchedIorFunc, thinWallTransmissionRoughnessFunc, transmissionFresnelFunc } from '../nodes/material.wgsl.js';
5
5
  import { eonBrdfFunc, eonDirectionFunc, eonPDFFunc } from '../nodes/eon.wgsl.js';
6
6
  import { sheenColorFunc, sheenAlbedoScalingFunc } from '../nodes/sheen.wgsl.js';
@@ -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,23 @@ 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, plus whether a shadow ray passed a face a bsdf ray would hit
23
+ const intersectionResultStruct = new StructTypeNode( {
24
+ indices: 'vec4u',
25
+ normal: 'vec3f',
26
+ didHit: 'bool',
27
+ barycoord: 'vec3f',
28
+ objectIndex: 'uint',
29
+ side: 'float',
30
+ dist: 'float',
31
+ forwardBlocked: 'bool',
32
+ }, 'PathTracerIntersectionResult' );
33
+
22
34
  // Pathtracer-specific version of the BVHComputeData tht includes material mapping, property structs
23
35
  export class PathtracerBVHComputeData extends BVHComputeData {
24
36
 
@@ -181,8 +193,11 @@ export class PathtracerBVHComputeData extends BVHComputeData {
181
193
  const scratchRayScalar = float( 1.0 ).toVar( 'bvh_rayScalar' );
182
194
  const baseOpacityScalar = float( 1.0 ).toVar( 'bvh_baseOpacity' );
183
195
 
184
- fns.raycastFirstHit = this.getShapecastFn( {
185
- name: 'raycastFirstHit',
196
+ // shadow rays hit both sides, and pass through the objects not visible to them
197
+ const isShadowRay = bool( false ).toVar( 'bvh_isShadowRay' );
198
+ const skipShadowObject = bool( false ).toVar( 'bvh_skipShadowObject' );
199
+
200
+ const raycastOptions = {
186
201
  shapeStruct: rayStruct,
187
202
  resultStruct: intersectionResultStruct,
188
203
 
@@ -259,7 +274,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
259
274
  let material = ${ storage.materials }[ ${ currentMaterialIndex } ];
260
275
 
261
276
  // TODO: if material is a transmissive volume we may need to assume double-sidedness
262
- if ( material.side != 0 && triResult.side != material.side ) {
277
+ // shadow rays hit both sides
278
+ if ( ! ${ isShadowRay } && material.side != 0 && triResult.side != material.side ) {
263
279
 
264
280
  continue;
265
281
 
@@ -325,6 +341,19 @@ export class PathtracerBVHComputeData extends BVHComputeData {
325
341
 
326
342
  }
327
343
 
344
+ // shadow rays pass the objects not visible to them, but a bsdf ray would hit this face
345
+ if ( ${ skipShadowObject } ) {
346
+
347
+ if ( material.side == 0 || triResult.side == material.side ) {
348
+
349
+ result.forwardBlocked = true;
350
+
351
+ }
352
+
353
+ continue;
354
+
355
+ }
356
+
328
357
  result.didHit = true;
329
358
  result.dist = triResult.dist;
330
359
  result.normal = triResult.normal;
@@ -367,6 +396,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
367
396
 
368
397
  }
369
398
 
399
+ ${ skipShadowObject } = ${ isShadowRay } && object.visibleToShadowRays == 0u;
400
+
370
401
  }
371
402
  `,
372
403
  transformResultFn: wgslTagFn/* wgsl */`
@@ -384,6 +415,30 @@ export class PathtracerBVHComputeData extends BVHComputeData {
384
415
  ${ scratchRayScalar } = 1.0;
385
416
  ${ baseOpacityScalar } = 1.0;
386
417
 
418
+ }
419
+ `,
420
+ };
421
+
422
+ fns.raycastFirstHit = this.getShapecastFn( {
423
+ ...raycastOptions,
424
+ name: 'raycastFirstHit',
425
+ prefixFn: wgslTagFn/* wgsl */`
426
+ fn initFirstHitCull() -> void {
427
+
428
+ ${ isShadowRay } = false;
429
+
430
+ }
431
+ `,
432
+ } );
433
+
434
+ fns.raycastShadow = this.getShapecastFn( {
435
+ ...raycastOptions,
436
+ name: 'raycastShadow',
437
+ prefixFn: wgslTagFn/* wgsl */`
438
+ fn initShadowCull() -> void {
439
+
440
+ ${ isShadowRay } = true;
441
+
387
442
  }
388
443
  `,
389
444
  } );
@@ -398,6 +453,9 @@ export class PathtracerBVHComputeData extends BVHComputeData {
398
453
 
399
454
  this.updateUvAttributesFromScene();
400
455
 
456
+ // the material indices are written with the transforms
457
+ this.updateMaterialsMap();
458
+
401
459
  super.update();
402
460
 
403
461
  // build the channel -> uv lookup now that the geometry struct (and its uv members) exist
@@ -423,7 +481,7 @@ export class PathtracerBVHComputeData extends BVHComputeData {
423
481
 
424
482
  if ( Array.isArray( o.material ) ) {
425
483
 
426
- o.material.forEach( m => add( m ) );
484
+ o.material.forEach( m => m && add( m ) );
427
485
 
428
486
  } else {
429
487
 
@@ -906,9 +964,17 @@ export class PathtracerBVHComputeData extends BVHComputeData {
906
964
 
907
965
  }
908
966
 
909
- // save the index
910
- const index = materialsMap.get( material ) || 0;
911
967
  const transformBufferU32 = new Uint32Array( targetBuffer );
968
+
969
+ const index = materialsMap.has( material ) ? materialsMap.get( material ) : - 1;
970
+ if ( index === - 1 ) {
971
+
972
+ // hide the object when there is no material
973
+ transformBufferU32[ writeOffset * transformStruct.getLength() + 32 ] = 0;
974
+
975
+ }
976
+
977
+ // save the material index
912
978
  transformBufferU32[ writeOffset * transformStruct.getLength() + 33 ] = index;
913
979
 
914
980
  // write color
@@ -939,6 +1005,10 @@ export class PathtracerBVHComputeData extends BVHComputeData {
939
1005
  super.writeTransformData( info, premultiplyMatrix, writeOffset, targetBuffer );
940
1006
  this.writeMaterialData( info, writeOffset, targetBuffer );
941
1007
 
1008
+ // whether shadow rays see the object, in the slot after the material index
1009
+ const transformBufferU32 = new Uint32Array( targetBuffer );
1010
+ transformBufferU32[ writeOffset * this.structs.transform.getLength() + 34 ] = info.object.visibleToShadowRays === false ? 0 : 1;
1011
+
942
1012
  }
943
1013
 
944
1014
  getBVH( object, instanceId, rangeTarget ) {
@@ -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 ] );
@@ -234,6 +234,7 @@ export const lightStruct = new StructTypeNode( {
234
234
  color: 'vec3f',
235
235
  intensity: 'float',
236
236
  u: 'vec3f',
237
+ visibleToCameraRays: 'int',
237
238
  v: 'vec3f',
238
239
  area: 'float',
239
240
  radius: 'float',
@@ -50,6 +50,7 @@ export class FSRUpscaler {
50
50
  init( renderer ) {
51
51
 
52
52
  this.renderer = renderer;
53
+ this._upscaler?.dispose();
53
54
  this._upscaler = new this.Upscaler( { renderer } );
54
55
  this._upscaler.init();
55
56
 
@@ -100,7 +101,7 @@ export class FSRUpscaler {
100
101
 
101
102
  dispose() {
102
103
 
103
- this._upscaler.dispose();
104
+ this._upscaler?.dispose();
104
105
  this._upscaler = null;
105
106
 
106
107
  }