rayzee 7.20.3 → 7.22.0

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 (42) hide show
  1. package/README.md +75 -4
  2. package/dist/rayzee.es.js +2821 -2854
  3. package/dist/rayzee.umd.js +60 -58
  4. package/package.json +1 -1
  5. package/src/EngineDefaults.js +9 -2
  6. package/src/PathTracerApp.js +83 -0
  7. package/src/Pipeline/BindingAudit.js +178 -0
  8. package/src/Pipeline/RenderPipeline.js +5 -2
  9. package/src/Processor/BVHBuilder.js +1 -4
  10. package/src/Processor/CameraOptimizer.js +0 -5
  11. package/src/Processor/KernelManager.js +17 -70
  12. package/src/Processor/PackedRayBuffer.js +33 -8
  13. package/src/Processor/QueueManager.js +4 -3
  14. package/src/Processor/SceneProcessor.js +10 -4
  15. package/src/Processor/TreeletOptimizer.js +0 -7
  16. package/src/RenderSettings.js +41 -6
  17. package/src/Stages/ASVGF.js +10 -10
  18. package/src/Stages/BilateralFilter.js +70 -11
  19. package/src/Stages/EdgeFilter.js +44 -6
  20. package/src/Stages/NormalDepth.js +26 -15
  21. package/src/Stages/PathTracer.js +36 -34
  22. package/src/Stages/PathTracerStage.js +15 -46
  23. package/src/TSL/BVHTraversal.js +1 -67
  24. package/src/TSL/CameraRay.js +141 -0
  25. package/src/TSL/Common.js +1 -47
  26. package/src/TSL/DebugKernel.js +4 -5
  27. package/src/TSL/EmissiveSampling.js +40 -44
  28. package/src/TSL/ExtendKernel.js +11 -3
  29. package/src/TSL/FinalWriteKernel.js +1 -14
  30. package/src/TSL/GenerateKernel.js +11 -8
  31. package/src/TSL/LightsCore.js +3 -0
  32. package/src/TSL/LightsDirect.js +4 -4
  33. package/src/TSL/LightsSampling.js +138 -327
  34. package/src/TSL/PathTracerCore.js +2 -2
  35. package/src/TSL/Random.js +2 -2
  36. package/src/TSL/ShadeKernel.js +39 -17
  37. package/src/TSL/SortGlobalKernels.js +52 -7
  38. package/src/TSL/Struct.js +0 -10
  39. package/src/index.js +5 -0
  40. package/src/managers/DenoisingManager.js +13 -0
  41. package/src/managers/EnvironmentManager.js +6 -8
  42. package/src/managers/UniformManager.js +12 -2
@@ -31,6 +31,13 @@ import {
31
31
 
32
32
  const log = createLogger( 'wavefront' );
33
33
 
34
+ // Shared by the 1D index-list kernels built inline below.
35
+ const LIST_WG_SIZE = 256;
36
+
37
+ // Resized per bounce iteration, each from its own registered workgroup size. Unregistered entries
38
+ // (the sort passes when _sortMaterials is off) are skipped by setDispatchForCount.
39
+ const BOUNCE_KERNELS = [ 'extend', 'shade', 'globalHist', 'globalScatter', 'compact', 'compactCopyback' ];
40
+
34
41
  export class PathTracer extends PathTracerStage {
35
42
 
36
43
  constructor( renderer, scene, camera, options = {} ) {
@@ -272,6 +279,7 @@ export class PathTracer extends PathTracerStage {
272
279
 
273
280
  this._updateAccumulationUniforms( frameValue, renderMode );
274
281
  this.frame.value = frameValue;
282
+ this.seedFrame.value = this._seedTick ++;
275
283
 
276
284
  this._setWfDispatch();
277
285
 
@@ -349,7 +357,7 @@ export class PathTracer extends PathTracerStage {
349
357
 
350
358
  // reset counters → compact non-frozen pixel IDs → publish count → list-driven 1D generate.
351
359
  const genSized = Math.min( maxRays, Math.ceil( activeBounce0 * 1.5 ) + 1024 );
352
- km.setDispatchCount( 'generateList', [ Math.ceil( genSized / 256 ), 1, 1 ] );
360
+ km.setDispatchForCount( 'generateList', genSized );
353
361
  km.dispatch( 'resetFrameCounters' );
354
362
  km.dispatch( 'buildActivePixels' );
355
363
  km.dispatch( 'seedEnter' );
@@ -401,23 +409,12 @@ export class PathTracer extends PathTracerStage {
401
409
  }
402
410
 
403
411
  const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
404
- const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
405
- km.setDispatchCount( 'extend', wg );
406
- km.setDispatchCount( 'shade', wg );
407
- km.setDispatchCount( 'globalHist', wg );
408
- km.setDispatchCount( 'globalScatter', wg );
409
- km.setDispatchCount( 'compact', wg );
410
- km.setDispatchCount( 'compactCopyback', wg );
412
+ for ( const k of BOUNCE_KERNELS ) km.setDispatchForCount( k, sized );
411
413
 
412
414
  } else {
413
415
 
414
416
  km.dispatch( 'enterFull' );
415
- const full = [ Math.ceil( maxRays / 256 ), 1, 1 ];
416
- km.setDispatchCount( 'extend', full );
417
- km.setDispatchCount( 'shade', full );
418
- km.setDispatchCount( 'compact', full );
419
- km.setDispatchCount( 'globalHist', full );
420
- km.setDispatchCount( 'globalScatter', full );
417
+ for ( const k of BOUNCE_KERNELS ) km.setDispatchForCount( k, maxRays );
421
418
 
422
419
  }
423
420
 
@@ -934,7 +931,7 @@ export class PathTracer extends PathTracerStage {
934
931
 
935
932
  } );
936
933
  this._kernelManager.register( 'initActiveIndices',
937
- initFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
934
+ initFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
938
935
  );
939
936
 
940
937
  const genParams = {
@@ -942,9 +939,15 @@ export class PathTracer extends PathTracerStage {
942
939
  rngBufferRW: pb.rngBuffer.rw,
943
940
  gBufferRW,
944
941
  resolution: this.resolution,
945
- frame: this.frame,
942
+ // RNG axis only (baseSeed + stratified jitter) — takes the seed counter, not the
943
+ // accumulation index. FinalWrite keeps `frame`.
944
+ frame: this.seedFrame,
946
945
  cameraWorldMatrix: this.cameraWorldMatrix,
947
946
  cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
947
+ cameraProjection: this.cameraProjection,
948
+ panoLonRange: this.panoLonRange,
949
+ panoLatRange: this.panoLatRange,
950
+ panoLevelHorizon: this.panoLevelHorizon,
948
951
  enableDOF: this.enableDOF,
949
952
  focalLength: this.focalLength,
950
953
  aperture: this.aperture,
@@ -974,13 +977,12 @@ export class PathTracer extends PathTracerStage {
974
977
  const freezeK = this.pixelFreezeStability;
975
978
  const resetFrameFn = Fn( () => {
976
979
 
977
- // ACTIVE is per-chunk (zeroed before every chunk's scatter). CONVERGED/FROZEN are per-FRAME counters
978
- // summed across chunks in FinalWrite → zero them only on the first chunk of the frame.
980
+ // ACTIVE is per-chunk (zeroed before every chunk's scatter). CONVERGED is a per-FRAME counter
981
+ // summed across chunks in FinalWrite → zero it only on the first chunk of the frame.
979
982
  atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
980
983
  If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
981
984
 
982
985
  atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
983
- atomicStore( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 0 ) );
984
986
 
985
987
  } );
986
988
 
@@ -1047,7 +1049,7 @@ export class PathTracer extends PathTracerStage {
1047
1049
 
1048
1050
  } );
1049
1051
  this._kernelManager.register( 'buildActivePixels',
1050
- buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1052
+ buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1051
1053
  );
1052
1054
 
1053
1055
  const seedEnterFn = Fn( () => {
@@ -1070,7 +1072,7 @@ export class PathTracer extends PathTracerStage {
1070
1072
  counters,
1071
1073
  } );
1072
1074
  this._kernelManager.register( 'generateList',
1073
- genListFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1075
+ genListFn().compute( [ Math.ceil( maxRays / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1074
1076
  );
1075
1077
 
1076
1078
  const freshBvh = this.bvhStorageNode;
@@ -1105,7 +1107,6 @@ export class PathTracer extends PathTracerStage {
1105
1107
  const extFn = buildExtendKernel( {
1106
1108
  bvhBuffer: freshBvh,
1107
1109
  triangleBuffer: freshTri,
1108
- materialBuffer: freshMat,
1109
1110
  rayBufferRO: pb.rayBuffer.ro,
1110
1111
  hitBufferRW: pb.hitBuffer.rw,
1111
1112
  activeIndicesRO: qm.getActiveReadRO(),
@@ -1180,7 +1181,6 @@ export class PathTracer extends PathTracerStage {
1180
1181
  environmentIntensity: this.environmentIntensity,
1181
1182
  envMatrix: this.environmentMatrix,
1182
1183
  enableEnvironmentLight: this.enableEnvironment,
1183
- useEnvMapIS: this.useEnvMapIS,
1184
1184
  groundProjectionEnabled: this.groundProjectionEnabled,
1185
1185
  groundProjectionRadius: this.groundProjectionRadius,
1186
1186
  groundProjectionHeight: this.groundProjectionHeight,
@@ -1210,7 +1210,9 @@ export class PathTracer extends PathTracerStage {
1210
1210
  cameraProjectionMatrix: this.cameraProjectionMatrix,
1211
1211
  cameraViewMatrix: this.cameraViewMatrix,
1212
1212
  fireflyThreshold: this.fireflyThreshold,
1213
- frame: this.frame,
1213
+ // RNG axis only (keys STBN via frame & 63).
1214
+ frame: this.seedFrame,
1215
+ accumFrame: this.frame,
1214
1216
  resolution: this.resolution,
1215
1217
  emissiveTriangleCount: this.emissiveTriangleCount,
1216
1218
  emissiveVec4Offset: this.emissiveVec4Offset,
@@ -1288,7 +1290,7 @@ export class PathTracer extends PathTracerStage {
1288
1290
 
1289
1291
  } );
1290
1292
  this._kernelManager.register( 'compactCopyback',
1291
- copyFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
1293
+ copyFn().compute( [ Math.ceil( maxRays / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
1292
1294
  );
1293
1295
 
1294
1296
  const fwFn = buildFinalWriteKernel( {
@@ -1320,7 +1322,6 @@ export class PathTracer extends PathTracerStage {
1320
1322
  // Tier-2 freeze (stamp + pass-through)
1321
1323
  usePixelFreeze: this.usePixelFreeze,
1322
1324
  pixelFreezeThreshold: this.pixelFreezeThreshold,
1323
- pixelFreezeStability: this.pixelFreezeStability,
1324
1325
  streakBufferRW: streakRW,
1325
1326
  frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
1326
1327
  chunkRowBase: this._wfChunkRowBase,
@@ -1347,6 +1348,10 @@ export class PathTracer extends PathTracerStage {
1347
1348
  cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
1348
1349
  cameraProjectionMatrix: this.cameraProjectionMatrix,
1349
1350
  cameraViewMatrix: this.cameraViewMatrix,
1351
+ cameraProjection: this.cameraProjection,
1352
+ panoLonRange: this.panoLonRange,
1353
+ panoLatRange: this.panoLatRange,
1354
+ panoLevelHorizon: this.panoLevelHorizon,
1350
1355
  enableDOF: this.enableDOF,
1351
1356
  focalLength: this.focalLength,
1352
1357
  aperture: this.aperture,
@@ -1392,10 +1397,7 @@ export class PathTracer extends PathTracerStage {
1392
1397
  const w = this._wfRenderWidth.value;
1393
1398
  const h = this._wfRenderHeight.value;
1394
1399
 
1395
- this._kernelManager.setDispatchCount( 'debug', [
1396
- Math.ceil( w / DEBUG_WG_SIZE ),
1397
- Math.ceil( h / DEBUG_WG_SIZE ), 1
1398
- ] );
1400
+ this._kernelManager.setDispatchForGrid( 'debug', w, h );
1399
1401
 
1400
1402
  }
1401
1403
 
@@ -1412,10 +1414,10 @@ export class PathTracer extends PathTracerStage {
1412
1414
  this._wfMaxRayCount.value = chunkPixels;
1413
1415
 
1414
1416
  const km = this._kernelManager;
1415
- km.setDispatchCount( 'generate', [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( rows / GENERATE_WG_SIZE ), 1 ] );
1416
- km.setDispatchCount( 'finalWrite', [ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil( rows / FINALWRITE_WG_SIZE ), 1 ] );
1417
- km.setDispatchCount( 'initActiveIndices', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
1418
- km.setDispatchCount( 'buildActivePixels', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
1417
+ km.setDispatchForGrid( 'generate', w, rows );
1418
+ km.setDispatchForGrid( 'finalWrite', w, rows );
1419
+ km.setDispatchForCount( 'initActiveIndices', chunkPixels );
1420
+ km.setDispatchForCount( 'buildActivePixels', chunkPixels );
1419
1421
 
1420
1422
  return chunkPixels;
1421
1423
 
@@ -51,7 +51,6 @@ const BVH_VEC4_PER_NODE = 4;
51
51
  * - camera:moved - When camera position/orientation changes
52
52
  * - asvgf:reset - Request ASVGF to reset temporal data
53
53
  * - asvgf:updateParameters - Update ASVGF parameters
54
- * - asvgf:setTemporal - Enable/disable ASVGF temporal accumulation
55
54
  *
56
55
  * Textures published to context:
57
56
  * - pathtracer:color - Main color output
@@ -143,6 +142,11 @@ export class PathTracerStage extends RenderStage {
143
142
  this.lastCameraMatrix = new Matrix4();
144
143
  this.lastProjectionMatrix = new Matrix4();
145
144
 
145
+ // Sampler seed axis — advances per rendered frame, survives accumulation resets.
146
+ // Pinned back to the accumulation index in deterministic mode.
147
+ this._seedTick = 0;
148
+ this._pinSeedToFrame = false;
149
+
146
150
  // Denoising management state
147
151
  this.lastRenderMode = - 1;
148
152
  this.renderModeChangeTimeout = null;
@@ -293,18 +297,6 @@ export class PathTracerStage extends RenderStage {
293
297
 
294
298
  }
295
299
  },
296
- useEnvMapIS: {
297
- get value() {
298
-
299
- return self.useEnvMapIS.value;
300
-
301
- },
302
- set value( v ) {
303
-
304
- self.useEnvMapIS.value = v;
305
-
306
- }
307
- },
308
300
  enableAccumulation: {
309
301
  get value() {
310
302
 
@@ -348,7 +340,6 @@ export class PathTracerStage extends RenderStage {
348
340
  enabled: DEFAULT_STATE.interactionModeEnabled,
349
341
  qualitySettings: {
350
342
  maxBounceCount: 1,
351
- useEnvMapIS: false,
352
343
  enableAccumulation: false,
353
344
  enableEmissiveTriangleSampling: false,
354
345
  },
@@ -651,10 +642,14 @@ export class PathTracerStage extends RenderStage {
651
642
  this.isComplete = false;
652
643
  this.performanceMonitor?.reset();
653
644
 
654
- this.lastRenderMode = - 1;
655
-
645
+ // lastRenderMode is deliberately NOT invalidated here — clearing it made
646
+ // manageASVGFForRenderMode see a phantom mode change 50 ms after every reset, wiping
647
+ // ASVGF's history. A real mode change is still caught by the renderMode comparison.
656
648
  this.lastInteractionModeState = false;
657
649
 
650
+ // Only deterministic mode rewinds the seed axis; see _pinSeedToFrame.
651
+ if ( this._pinSeedToFrame ) this._seedTick = 0;
652
+
658
653
  }
659
654
 
660
655
  /**
@@ -1238,7 +1233,10 @@ export class PathTracerStage extends RenderStage {
1238
1233
  if ( this.pendingRenderMode !== null && this.pendingRenderMode !== this.lastRenderMode ) {
1239
1234
 
1240
1235
  this.lastRenderMode = this.pendingRenderMode;
1241
- this._onRenderModeChanged( this.pendingRenderMode );
1236
+ // History from the previous mode is not comparable. temporalAlpha is NOT
1237
+ // touched here — it is owned by ASVGF_QUALITY_PRESETS, and overwriting it
1238
+ // with a hardcoded value meant `medium` and `high` never took effect.
1239
+ this.emit( 'asvgf:reset' );
1242
1240
 
1243
1241
  }
1244
1242
 
@@ -1249,35 +1247,6 @@ export class PathTracerStage extends RenderStage {
1249
1247
 
1250
1248
  }
1251
1249
 
1252
- this._handleFullQuadASVGF();
1253
-
1254
- }
1255
-
1256
- _onRenderModeChanged( newMode ) {
1257
-
1258
- if ( newMode === 1 ) {
1259
-
1260
- this.emit( 'asvgf:updateParameters', {
1261
- enableDebug: false,
1262
- temporalAlpha: 0.15
1263
- } );
1264
-
1265
- } else {
1266
-
1267
- this.emit( 'asvgf:updateParameters', {
1268
- temporalAlpha: 0.1,
1269
- } );
1270
-
1271
- }
1272
-
1273
- this.emit( 'asvgf:reset' );
1274
-
1275
- }
1276
-
1277
- _handleFullQuadASVGF() {
1278
-
1279
- this.emit( 'asvgf:setTemporal', { enabled: true } );
1280
-
1281
1250
  }
1282
1251
 
1283
1252
  // ===== UNIFORM & DATA SETTERS =====
@@ -14,17 +14,14 @@ import {
14
14
  min,
15
15
  normalize,
16
16
  mix,
17
- vec4,
18
17
  notEqual,
19
18
  lessThan,
20
- mat3,
21
19
  array,
22
20
  bool as tslBool,
23
21
  } from 'three/tsl';
24
22
 
25
- import { Ray, HitInfo } from './Struct.js';
23
+ import { HitInfo } from './Struct.js';
26
24
  import { getDatafromStorageBuffer } from './Common.js';
27
- import { RandomPointInCircle } from './Random.js';
28
25
 
29
26
  const MAX_STACK_DEPTH = 32;
30
27
  const MAX_BVH_ITERATIONS = 512;
@@ -559,66 +556,3 @@ export const traverseBVHShadow = Fn( ( [
559
556
  return closestHit;
560
557
 
561
558
  } );
562
-
563
- // ================================================================================
564
- // CAMERA RAY GENERATION
565
- // ================================================================================
566
-
567
- export const generateRayFromCamera = Fn( ( [
568
- screenPosition, rngState,
569
- cameraWorldMatrix, cameraProjectionMatrixInverse,
570
- enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio
571
- ] ) => {
572
-
573
- // Convert screen position to NDC
574
- const ndcPos = vec3( screenPosition.xy, 1.0 );
575
-
576
- // Convert NDC to camera space
577
- const rayDirCS = cameraProjectionMatrixInverse.mul( vec4( ndcPos, 1.0 ) );
578
-
579
- // Convert to world space
580
- const rayDirectionWorld = normalize( mat3(
581
- cameraWorldMatrix[ 0 ].xyz,
582
- cameraWorldMatrix[ 1 ].xyz,
583
- cameraWorldMatrix[ 2 ].xyz
584
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) ) ).toVar();
585
-
586
- const rayOriginWorld = vec3( cameraWorldMatrix[ 3 ] ).toVar();
587
-
588
- const resultOrigin = rayOriginWorld.toVar();
589
- const resultDirection = rayDirectionWorld.toVar();
590
-
591
- // Check if DOF is disabled or conditions make it ineffective
592
- If( enableDOF.and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ).and( focusDistance.greaterThan( 0.001 ) ), () => {
593
-
594
- // Physical aperture calculation
595
- const effectiveAperture = focalLength.div( aperture );
596
- // Apply scene scale to maintain correct physical aperture size
597
- const apertureRadius = effectiveAperture.mul( 0.001 ).mul( sceneScale ).mul( apertureScale );
598
-
599
- // Generate random point on aperture disk
600
- const randomPoint = RandomPointInCircle( rngState );
601
-
602
- // Apply anamorphic squeeze — stretch horizontally for oval bokeh
603
- const lensX = randomPoint.x.mul( anamorphicRatio.max( 0.01 ) );
604
- const lensY = randomPoint.y;
605
-
606
- // Extract camera coordinate system directly from camera matrix
607
- const cameraRight = normalize( vec3( cameraWorldMatrix[ 0 ] ) );
608
- const cameraUp = normalize( vec3( cameraWorldMatrix[ 1 ] ) );
609
-
610
- // Apply aperture offset using camera's actual coordinate system
611
- const offset = cameraRight.mul( lensX ).add( cameraUp.mul( lensY ) ).mul( apertureRadius );
612
-
613
- // Calculate new ray from offset origin to focal point
614
- resultOrigin.assign( rayOriginWorld.add( offset ) );
615
- resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusDistance ) ).sub( resultOrigin ) ) );
616
-
617
- } );
618
-
619
- return Ray( {
620
- origin: resultOrigin,
621
- direction: resultDirection,
622
- } );
623
-
624
- } );
@@ -0,0 +1,141 @@
1
+ /**
2
+ * CameraRay.js — primary camera ray generation.
3
+ *
4
+ * Two projections behind one `cameraProjection` uniform: 0 = pinhole (NDC through
5
+ * cameraProjectionMatrixInverse), 1 = equirectangular 360 panorama. Both branches live in the same
6
+ * kernel, so switching modes writes a uniform and resets accumulation — it never recompiles WGSL.
7
+ *
8
+ * Equirect mapping, for uv ∈ [0,1] with v = 0 at the top row:
9
+ * lon = mix( lonMin, lonMax, u ), lat = mix( latMax, latMin, v )
10
+ * dirCam = ( sin(lon)·cos(lat), sin(lat), -cos(lon)·cos(lat) )
11
+ * so lon = lat = 0 is camera-forward (image centre), u = 0.75 is camera-right and v = 0 is the zenith.
12
+ */
13
+
14
+ import {
15
+ Fn, vec3, vec4, float, int,
16
+ If, normalize, mat3, mix, sin, cos, cross, dot,
17
+ } from 'three/tsl';
18
+
19
+ import { Ray } from './Struct.js';
20
+ import { constructTBN } from './Common.js';
21
+ import { RandomPointInCircle } from './Random.js';
22
+
23
+ /** World-space primary ray direction for pixel uv. Origin is always cameraWorldMatrix[3]. */
24
+ export const cameraRayDirection = Fn( ( [
25
+ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
26
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon
27
+ ] ) => {
28
+
29
+ const direction = vec3( 0.0 ).toVar();
30
+
31
+ If( cameraProjection.equal( int( 1 ) ), () => {
32
+
33
+ const lon = mix( panoLonRange.x, panoLonRange.y, uv01.x ).toVar();
34
+ const lat = mix( panoLatRange.y, panoLatRange.x, uv01.y ).toVar();
35
+ const cosLat = cos( lat ).toVar();
36
+
37
+ const right = vec3( 0.0 ).toVar();
38
+ const up = vec3( 0.0 ).toVar();
39
+ const back = vec3( 0.0 ).toVar();
40
+
41
+ If( panoLevelHorizon.equal( int( 1 ) ), () => {
42
+
43
+ // Yaw-only frame — orbit pitch/roll must not tilt the panorama. The flattened column
44
+ // is normalized below, so it needs no pre-normalize.
45
+ const flat = vec3( cameraWorldMatrix[ 2 ].x, 0.0, cameraWorldMatrix[ 2 ].z ).toVar();
46
+ // Straight up/down: the camera's own up vector is the horizontal one.
47
+ If( dot( flat, flat ).lessThan( float( 1e-8 ) ), () => {
48
+
49
+ flat.assign( vec3( cameraWorldMatrix[ 1 ].x, 0.0, cameraWorldMatrix[ 1 ].z ) );
50
+
51
+ } );
52
+
53
+ back.assign( normalize( flat ) );
54
+ up.assign( vec3( 0.0, 1.0, 0.0 ) );
55
+ right.assign( cross( up, back ) );
56
+
57
+ } ).Else( () => {
58
+
59
+ right.assign( normalize( vec3( cameraWorldMatrix[ 0 ] ) ) );
60
+ up.assign( normalize( vec3( cameraWorldMatrix[ 1 ] ) ) );
61
+ back.assign( normalize( vec3( cameraWorldMatrix[ 2 ] ) ) );
62
+
63
+ } );
64
+
65
+ direction.assign( normalize(
66
+ right.mul( sin( lon ).mul( cosLat ) )
67
+ .add( up.mul( sin( lat ) ) )
68
+ .sub( back.mul( cos( lon ).mul( cosLat ) ) )
69
+ ) );
70
+
71
+ } ).Else( () => {
72
+
73
+ const ndcPos = vec3( uv01.x.mul( 2.0 ).sub( 1.0 ), float( 1.0 ).sub( uv01.y.mul( 2.0 ) ), 1.0 );
74
+ const rayDirCS = cameraProjectionMatrixInverse.mul( vec4( ndcPos, 1.0 ) );
75
+
76
+ direction.assign( normalize( mat3(
77
+ cameraWorldMatrix[ 0 ].xyz,
78
+ cameraWorldMatrix[ 1 ].xyz,
79
+ cameraWorldMatrix[ 2 ].xyz
80
+ ).mul( rayDirCS.xyz.div( rayDirCS.w ) ) ) );
81
+
82
+ } );
83
+
84
+ return direction;
85
+
86
+ } );
87
+
88
+ export const generateRayFromCamera = Fn( ( [
89
+ uv01, rngState,
90
+ cameraWorldMatrix, cameraProjectionMatrixInverse,
91
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
92
+ enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio
93
+ ] ) => {
94
+
95
+ const rayOriginWorld = vec3( cameraWorldMatrix[ 3 ] ).toVar();
96
+ // .toVar() so the two reads below don't inline the whole projection graph twice.
97
+ const rayDirectionWorld = cameraRayDirection(
98
+ uv01, cameraWorldMatrix, cameraProjectionMatrixInverse,
99
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon
100
+ ).toVar();
101
+
102
+ const resultOrigin = rayOriginWorld.toVar();
103
+ const resultDirection = rayDirectionWorld.toVar();
104
+
105
+ If( enableDOF.and( focalLength.greaterThan( 0.0 ) ).and( aperture.lessThan( 64.0 ) ).and( focusDistance.greaterThan( 0.001 ) ), () => {
106
+
107
+ const effectiveAperture = focalLength.div( aperture );
108
+ const apertureRadius = effectiveAperture.mul( 0.001 ).mul( sceneScale ).mul( apertureScale );
109
+
110
+ const randomPoint = RandomPointInCircle( rngState );
111
+ // Anamorphic squeeze — stretch horizontally for oval bokeh
112
+ const lensX = randomPoint.x.mul( anamorphicRatio.max( 0.01 ) );
113
+ const lensY = randomPoint.y;
114
+
115
+ const lensOffset = vec3( 0.0 ).toVar();
116
+
117
+ If( cameraProjection.equal( int( 1 ) ), () => {
118
+
119
+ // Every pixel points a different way, so the lens plane is built around the ray. The
120
+ // camera's right/up would skew bokeh into a slit away from the image centre.
121
+ lensOffset.assign( constructTBN( { N: rayDirectionWorld } ).mul( vec3( lensX, lensY, 0.0 ) ) );
122
+
123
+ } ).Else( () => {
124
+
125
+ const camRight = normalize( vec3( cameraWorldMatrix[ 0 ] ) );
126
+ const camUp = normalize( vec3( cameraWorldMatrix[ 1 ] ) );
127
+ lensOffset.assign( camRight.mul( lensX ).add( camUp.mul( lensY ) ) );
128
+
129
+ } );
130
+
131
+ resultOrigin.assign( rayOriginWorld.add( lensOffset.mul( apertureRadius ) ) );
132
+ resultDirection.assign( normalize( rayOriginWorld.add( rayDirectionWorld.mul( focusDistance ) ).sub( resultOrigin ) ) );
133
+
134
+ } );
135
+
136
+ return Ray( {
137
+ origin: resultOrigin,
138
+ direction: resultDirection,
139
+ } );
140
+
141
+ } );
package/src/TSL/Common.js CHANGED
@@ -1,10 +1,9 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp, bool as tslBool } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  AnisoFrame,
5
5
  DotProducts,
6
6
  MaterialClassification,
7
- MISStrategy,
8
7
  RayTracingMaterial,
9
8
  ShadowMaterial,
10
9
  } from './Struct.js';
@@ -299,51 +298,6 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
299
298
 
300
299
  } );
301
300
 
302
- // Dynamic MIS strategy based on material properties
303
- export const selectOptimalMISStrategy = Fn( ( [ roughness, metalness, bounceIndex, throughput ] ) => {
304
-
305
- const throughputStrength = maxComponent( { v: throughput } ).toVar();
306
-
307
- // Environment is now handled via deterministic NEE — not part of stochastic selection.
308
- // Only lights and BRDF are stochastically selected.
309
- const brdfWeight = float( 0.5 ).toVar();
310
- const lightWeight = float( 0.5 ).toVar();
311
- const useBRDFSampling = tslBool( true );
312
- const useLightSampling = throughputStrength.greaterThan( 0.01 );
313
-
314
- If( roughness.lessThan( 0.1 ).and( metalness.greaterThan( 0.8 ) ), () => {
315
-
316
- // Highly specular materials — favor BRDF
317
- brdfWeight.assign( 0.7 );
318
- lightWeight.assign( 0.3 );
319
-
320
- } ).ElseIf( roughness.greaterThan( 0.7 ), () => {
321
-
322
- // Diffuse materials — favor light sampling
323
- brdfWeight.assign( 0.4 );
324
- lightWeight.assign( 0.6 );
325
-
326
- } );
327
-
328
- // Gentle adjustment for very deep bounces — shift toward BRDF
329
- If( bounceIndex.greaterThan( int( 5 ) ), () => {
330
-
331
- brdfWeight.assign( 0.6 );
332
- lightWeight.assign( 0.4 );
333
-
334
- } );
335
-
336
- return MISStrategy( {
337
- brdfWeight,
338
- lightWeight,
339
- envWeight: float( 0.0 ),
340
- useBRDFSampling,
341
- useLightSampling,
342
- useEnvSampling: tslBool( false ),
343
- } );
344
-
345
- } );
346
-
347
301
  // Storage buffer access — flat 1D indexing (WebGPU native)
348
302
  // No 2D coordinate math needed: directly indexes into the buffer
349
303
  export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dataOffset ] ) => {
@@ -13,7 +13,7 @@ import {
13
13
  localId, workgroupId,
14
14
  } from 'three/tsl';
15
15
 
16
- import { generateRayFromCamera } from './BVHTraversal.js';
16
+ import { generateRayFromCamera } from './CameraRay.js';
17
17
  import { Ray } from './Struct.js';
18
18
  import { TraceDebugMode } from './Debugger.js';
19
19
  import { pcgHash, getStratifiedSample } from './Random.js';
@@ -26,6 +26,7 @@ export function buildDebugKernel( params ) {
26
26
  writeColorTex, writeNDTex, writeAlbedoTex,
27
27
  resolution, renderWidth, renderHeight,
28
28
  cameraWorldMatrix, cameraProjectionMatrixInverse, cameraProjectionMatrix, cameraViewMatrix,
29
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
29
30
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
30
31
  bvhBuffer, triangleBuffer, materialBuffer,
31
32
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
@@ -45,12 +46,10 @@ export function buildDebugKernel( params ) {
45
46
  const seed = pcgHash( { state: uint( pixelIndex ).add( uint( 1 ) ) } ).toVar();
46
47
 
47
48
  // Center-pixel primary ray (no AA jitter — debug viz wants a stable, sharp image).
48
- const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
49
- screenPosition.y.assign( screenPosition.y.negate() );
50
-
51
49
  const ray = Ray.wrap( generateRayFromCamera(
52
- screenPosition, seed,
50
+ pixelCoord.div( resolution ), seed,
53
51
  cameraWorldMatrix, cameraProjectionMatrixInverse,
52
+ cameraProjection, panoLonRange, panoLatRange, panoLevelHorizon,
54
53
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
55
54
  ) );
56
55