rayzee 8.8.2 → 9.1.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 (104) hide show
  1. package/README.md +210 -20
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10795 -5324
  6. package/dist/rayzee.umd.js +279 -268
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +60 -76
  10. package/src/EngineEvents.js +7 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/Passes/OIDNTemporalHistory.js +25 -7
  16. package/src/PathTracerApp.js +313 -25
  17. package/src/Processor/ArchiveCache.js +120 -0
  18. package/src/Processor/ArchiveReader.js +179 -32
  19. package/src/Processor/AssetLoader.js +523 -76
  20. package/src/Processor/AtmosphereModel.js +465 -0
  21. package/src/Processor/BVHBuilder.js +4 -1
  22. package/src/Processor/BVHLeafFold.js +100 -0
  23. package/src/Processor/BVHRefitter.js +98 -214
  24. package/src/Processor/ChunkedRecords.js +189 -4
  25. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  26. package/src/Processor/GeometryExtractor.js +238 -40
  27. package/src/Processor/InstanceTable.js +55 -37
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +49 -8
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/MotionVector.js +8 -42
  48. package/src/Stages/NRD.js +30 -21
  49. package/src/Stages/NormalDepth.js +12 -23
  50. package/src/Stages/PathTracer.js +114 -6
  51. package/src/Stages/PathTracerStage.js +251 -104
  52. package/src/Storage/BLASCache.js +190 -0
  53. package/src/Storage/CDFCache.js +77 -0
  54. package/src/Storage/DownloadCache.js +266 -0
  55. package/src/Storage/GeometrySpill.js +217 -0
  56. package/src/Storage/SceneGraphCodec.js +503 -0
  57. package/src/Storage/SpillStore.js +112 -0
  58. package/src/Storage/StorageManager.js +934 -0
  59. package/src/Storage/StorageOps.js +137 -0
  60. package/src/Storage/StorageWorker.js +37 -0
  61. package/src/Storage/events.js +29 -0
  62. package/src/Storage/identity.js +77 -0
  63. package/src/Storage/index.js +9 -0
  64. package/src/Storage/inlineTransport.js +24 -0
  65. package/src/Storage/locks.js +99 -0
  66. package/src/Storage/openStorage.js +87 -0
  67. package/src/Storage/sceneCachePolicy.js +12 -0
  68. package/src/Storage/shared.js +9 -0
  69. package/src/Storage/transport.js +62 -0
  70. package/src/TSL/Atmosphere.js +684 -0
  71. package/src/TSL/BVHTraversal.js +288 -239
  72. package/src/TSL/CameraRay.js +99 -14
  73. package/src/TSL/Common.js +11 -2
  74. package/src/TSL/DebugKernel.js +2 -2
  75. package/src/TSL/Displacement.js +6 -6
  76. package/src/TSL/EmissiveSampling.js +5 -5
  77. package/src/TSL/EnvironmentCDF.js +176 -0
  78. package/src/TSL/GenerateKernel.js +5 -5
  79. package/src/TSL/HitFacet.js +4 -4
  80. package/src/TSL/LightsDirect.js +8 -8
  81. package/src/TSL/LightsSampling.js +55 -37
  82. package/src/TSL/Random.js +1 -0
  83. package/src/TSL/ShadeKernel.js +75 -19
  84. package/src/TSL/Sun.js +40 -0
  85. package/src/TSL/TextureSampling.js +6 -7
  86. package/src/index.js +22 -0
  87. package/src/managers/CameraManager.js +412 -56
  88. package/src/managers/DenoisingManager.js +4 -3
  89. package/src/managers/EnvironmentManager.js +263 -33
  90. package/src/managers/GoboManager.js +19 -8
  91. package/src/managers/IESManager.js +3 -3
  92. package/src/managers/InteractionManager.js +30 -2
  93. package/src/managers/LightManager.js +98 -5
  94. package/src/managers/MaterialDataManager.js +22 -2
  95. package/src/managers/TransformManager.js +26 -0
  96. package/src/managers/UniformManager.js +11 -7
  97. package/src/managers/VideoRenderManager.js +38 -17
  98. package/src/managers/ViewCamera.js +77 -0
  99. package/src/managers/WalkControls.js +262 -0
  100. package/src/managers/helpers/OutlineHelper.js +22 -2
  101. package/src/managers/timeline/CameraTrack.js +235 -0
  102. package/src/managers/timeline/TimelineManager.js +146 -0
  103. package/src/version.js +3 -0
  104. package/src/Processor/ProceduralSky.js +0 -291
@@ -0,0 +1,52 @@
1
+ import { Color, Quaternion, Vector2, Vector3, Vector4 } from 'three';
2
+
3
+ /**
4
+ * JSON-safe copies of setting and material values: colours, vectors and non-finite numbers are
5
+ * tagged so {@link fromPortable} gives back the same types. Anything else that is not plain data
6
+ * comes back undefined, so a caller can leave it out.
7
+ */
8
+ export function toPortable( value ) {
9
+
10
+ if ( value === null || typeof value === 'boolean' || typeof value === 'string' ) return value;
11
+ if ( typeof value === 'number' ) return Number.isFinite( value ) ? value : { $num: String( value ) };
12
+ if ( typeof value !== 'object' ) return undefined;
13
+ if ( value.isColor ) return { $color: [ value.r, value.g, value.b ] };
14
+ if ( value.isQuaternion ) return { $quat: [ value.x, value.y, value.z, value.w ] };
15
+ if ( value.isVector2 || value.isVector3 || value.isVector4 ) return { $vec: value.toArray() };
16
+ if ( ArrayBuffer.isView( value ) ) return Array.from( value );
17
+ if ( Array.isArray( value ) ) return value.map( v => toPortable( v ) ?? null );
18
+
19
+ const proto = Object.getPrototypeOf( value );
20
+ if ( proto !== Object.prototype && proto !== null ) return undefined;
21
+
22
+ const out = {};
23
+ for ( const [ key, v ] of Object.entries( value ) ) {
24
+
25
+ const p = toPortable( v );
26
+ if ( p !== undefined ) out[ key ] = p;
27
+
28
+ }
29
+
30
+ return out;
31
+
32
+ }
33
+
34
+ export function fromPortable( value ) {
35
+
36
+ if ( value === null || typeof value !== 'object' ) return value;
37
+ if ( Array.isArray( value ) ) return value.map( fromPortable );
38
+ if ( '$num' in value ) return Number( value.$num );
39
+ if ( value.$color ) return new Color( ...value.$color );
40
+ if ( value.$quat ) return new Quaternion( ...value.$quat );
41
+ if ( value.$vec ) {
42
+
43
+ const Vector = [ null, null, Vector2, Vector3, Vector4 ][ value.$vec.length ];
44
+ return new Vector( ...value.$vec );
45
+
46
+ }
47
+
48
+ const out = {};
49
+ for ( const [ key, v ] of Object.entries( value ) ) out[ key ] = fromPortable( v );
50
+ return out;
51
+
52
+ }
@@ -1,9 +1,10 @@
1
- import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, normalize, mat3,
1
+ import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If,
2
2
  textureLoad, textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
+ import { cameraRayPoint, cameraRayUniforms } from '../TSL/CameraRay.js';
7
8
 
8
9
  /**
9
10
  * WebGPU Motion Vector Stage (Compute Shader)
@@ -20,8 +21,8 @@ import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
20
21
  *
21
22
  * Algorithm:
22
23
  * 1. Read normalDepth from NormalDepth (linear depth in alpha)
23
- * 2. Reconstruct camera ray from pixel coords via inverse projection
24
- * 3. World position = cameraPos + normalize(rayDir) * linearDepth
24
+ * 2. Reconstruct the camera ray NormalDepth traced for the pixel
25
+ * 3. World position = rayOrigin + rayDir * linearDepth
25
26
  * 4. Project to previous frame: prevVP * worldPos
26
27
  * 5. Motion = currentUV - prevUV
27
28
  *
@@ -78,6 +79,7 @@ export class MotionVector extends RenderStage {
78
79
  // Synced from PathTracer each frame (same source as NormalDepth)
79
80
  this.cameraWorldMatrix = uniform( new Matrix4(), 'mat4' );
80
81
  this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
82
+ this.cameraRay = cameraRayUniforms();
81
83
  this.prevVP = uniform( new Matrix4(), 'mat4' );
82
84
  this.isFirstFrameU = uniform( 1.0 ); // 1.0 = true, 0.0 = false
83
85
  this.deltaTime = uniform( 1.0 / 60.0 );
@@ -175,29 +177,7 @@ export class MotionVector extends RenderStage {
175
177
  // Sky / background (depth >= 65504 sentinel) — no motion
176
178
  If( linearDepth.lessThan( float( 6e4 ) ), () => {
177
179
 
178
- // Pixel coordinate → NDC
179
- // Negate Y to match PathTracer convention
180
- const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
181
- const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
182
- const ndcPos = vec3( ndcX, ndcY, 1.0 );
183
-
184
- // Camera-space ray direction via inverse projection
185
- const rayDirCS = cpi.mul( vec4( ndcPos, 1.0 ) );
186
-
187
- // Transform to world space (rotation only, via mat3 of world matrix)
188
- const rayDirWorld = normalize(
189
- mat3(
190
- cwm[ 0 ].xyz,
191
- cwm[ 1 ].xyz,
192
- cwm[ 2 ].xyz
193
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) )
194
- );
195
-
196
- // Camera position (translation column of world matrix)
197
- const camPos = vec3( cwm[ 3 ] );
198
-
199
- // World position = camera origin + ray direction * linear depth
200
- const worldPos = camPos.add( rayDirWorld.mul( linearDepth ) );
180
+ const worldPos = cameraRayPoint( currentUV, linearDepth, cwm, cpi, this.cameraRay );
201
181
 
202
182
  // Project to previous frame
203
183
  const prevClip = prevVP.mul( vec4( worldPos, 1.0 ) );
@@ -278,27 +258,12 @@ export class MotionVector extends RenderStage {
278
258
  // Skip first frame and sky (depth >= 65504 sentinel)
279
259
  If( isFirstFrameU.lessThan( 0.5 ).and( linearDepth.lessThan( float( 6e4 ) ) ), () => {
280
260
 
281
- // Pixel coordinate → NDC
282
- // Negate Y to match PathTracer convention
283
- const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
284
- const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
285
- const ndcPos = vec3( ndcX, ndcY, 1.0 );
286
- const rayDirCS = cpi.mul( vec4( ndcPos, 1.0 ) );
287
- const rayDirWorld = normalize(
288
- mat3(
289
- cwm[ 0 ].xyz,
290
- cwm[ 1 ].xyz,
291
- cwm[ 2 ].xyz
292
- ).mul( rayDirCS.xyz.div( rayDirCS.w ) )
293
- );
294
- const camPos = vec3( cwm[ 3 ] );
295
- const worldPos = camPos.add( rayDirWorld.mul( linearDepth ) );
296
-
297
261
  // Current pixel UV
298
262
  const currentUV = vec2(
299
263
  float( gx ).add( 0.5 ).div( resW ),
300
264
  float( gy ).add( 0.5 ).div( resH )
301
265
  );
266
+ const worldPos = cameraRayPoint( currentUV, linearDepth, cwm, cpi, this.cameraRay );
302
267
 
303
268
  // Project to previous frame
304
269
  const prevClip = prevVP.mul( vec4( worldPos, 1.0 ) );
@@ -370,6 +335,7 @@ export class MotionVector extends RenderStage {
370
335
  if ( pt && pt.uniforms ) {
371
336
 
372
337
  // Sync from PathTracer (same source as NormalDepth)
338
+ this.cameraRay.sync( pt.uniforms );
373
339
  worldMatrix = pt.uniforms.get( 'cameraWorldMatrix' ).value;
374
340
  viewMatrix = pt.uniforms.get( 'cameraViewMatrix' ).value;
375
341
  projMatrix = pt.uniforms.get( 'cameraProjectionMatrix' ).value;
package/src/Stages/NRD.js CHANGED
@@ -6,7 +6,7 @@ import { DataTexture, HalfFloatType, FloatType, RGBAFormat, LinearFilter, Matrix
6
6
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
7
7
  import { createStorageTexture } from '../Processor/StorageTexturePool.js';
8
8
  import { sanitizeRGB, sanitize1, FP16_MAX } from '../TSL/Common.js';
9
- import { cameraRayDirection } from '../TSL/CameraRay.js';
9
+ import { cameraRayPoint, cameraRayUniforms, isOrthographic } from '../TSL/CameraRay.js';
10
10
  import {
11
11
  ALBEDO_EPS, GBUFFER_MISS_THRESHOLD as MISS_THRESHOLD, NRD_DEFAULTS,
12
12
  NRD_HIT_DIST_A, NRD_HIT_DIST_B,
@@ -168,10 +168,10 @@ export class NRD extends RenderStage {
168
168
  this.prevCamView = uniform( new Matrix4(), 'mat4' );
169
169
  this.camPos = uniform( new Vector3(), 'vec3' );
170
170
  this.prevCamPos = uniform( new Vector3(), 'vec3' );
171
- this.cameraProjection = uniform( 0, 'int' );
172
- this.panoLonRange = uniform( new Vector2(), 'vec2' );
173
- this.panoLatRange = uniform( new Vector2(), 'vec2' );
174
- this.panoLevelHorizon = uniform( 1, 'int' );
171
+ // Surface to camera while orthographic, where it is the same for every pixel.
172
+ this.viewVector = uniform( new Vector3(), 'vec3' );
173
+ this.prevViewVector = uniform( new Vector3(), 'vec3' );
174
+ this.cameraRay = cameraRayUniforms();
175
175
 
176
176
  // Declared here, valued by _syncUniforms below — which is the only place a setting becomes a
177
177
  // uniform, so there is no second copy of the defaults to keep in step.
@@ -322,11 +322,20 @@ export class NRD extends RenderStage {
322
322
  // Same ray the depth was measured along — NormalDepth generates it with this helper too.
323
323
  _worldPosAt( uv, dist, cwm, cpi ) {
324
324
 
325
- const dir = cameraRayDirection(
326
- uv, cwm, cpi,
327
- this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
328
- );
329
- return vec3( cwm[ 3 ] ).add( dir.mul( dist ) );
325
+ return cameraRayPoint( uv, dist, cwm, cpi, this.cameraRay );
326
+
327
+ }
328
+
329
+ // NRD's gOrthoMode: a constant view vector, and a pixel's footprint that no longer grows with depth.
330
+ _viewVector( X, camPos, orthoV ) {
331
+
332
+ return select( isOrthographic( this.cameraRay.cameraProjection ), orthoV, normalize( camPos.sub( X ) ) );
333
+
334
+ }
335
+
336
+ _frustumSize( viewZ ) {
337
+
338
+ return this.minRectDimMulUnproject.mul( select( isOrthographic( this.cameraRay.cameraProjection ), float( 1.0 ), viewZ ) );
330
339
 
331
340
  }
332
341
 
@@ -547,9 +556,9 @@ export class NRD extends RenderStage {
547
556
 
548
557
  If( maxRadiusU.greaterThan( 0.0 ), () => {
549
558
 
550
- const V = normalize( camPos.sub( X ) );
559
+ const V = this._viewVector( X, camPos, this.viewVector );
551
560
  const NoV = dot( N, V ).abs().toVar();
552
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
561
+ const frustumSize = this._frustumSize( viewZ ).toVar();
553
562
 
554
563
  const k = inputFrames.add( 1.0 );
555
564
  const accumSpeedEff = isPre
@@ -701,9 +710,9 @@ export class NRD extends RenderStage {
701
710
  const pixelUv = this._pixelUv( gx, gy );
702
711
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
703
712
  const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs().toVar();
704
- const V = normalize( camPos.sub( X ) );
713
+ const V = this._viewVector( X, camPos, this.viewVector );
705
714
  const NoV = dot( N, V ).abs().toVar();
706
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
715
+ const frustumSize = this._frustumSize( viewZ ).toVar();
707
716
 
708
717
  // Unnormalized, as NRD requires — its length carries the normal variance. A miss neighbour's
709
718
  // shadingNormal is a literal (0,0,0), which decodes to a fixed fake direction, so substitute
@@ -753,7 +762,7 @@ export class NRD extends RenderStage {
753
762
 
754
763
  const accumSpeed = this._applyCustomWeights( prevAccum, occW, float( 0.0 ) ).toVar();
755
764
 
756
- const Vprev = normalize( prevCamPos.sub( X ) );
765
+ const Vprev = this._viewVector( X, prevCamPos, this.prevViewVector );
757
766
  const NoVprev = dot( N, Vprev ).abs();
758
767
  const sizeQuality = NoVprev.add( 1e-3 ).div( NoV.add( 1e-3 ) ).toVar();
759
768
  sizeQuality.assign( mix( float( 0.1 ), float( 1.0 ), saturate( sizeQuality.mul( sizeQuality ) ) ) );
@@ -853,7 +862,7 @@ export class NRD extends RenderStage {
853
862
  const pixelUv = this._pixelUv( gx, gy );
854
863
  const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
855
864
  const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs();
856
- const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
865
+ const frustumSize = this._frustumSize( viewZ ).toVar();
857
866
 
858
867
  const diff = textureLoad( inTex, coord ).toVar();
859
868
  const nonLinear = float( 1.0 ).div( frameNumEff.add( 1.0 ) ).toVar();
@@ -1200,6 +1209,7 @@ export class NRD extends RenderStage {
1200
1209
 
1201
1210
  this.prevCamView.value.copy( this.camView.value );
1202
1211
  this.prevCamPos.value.copy( this.camPos.value );
1212
+ this.prevViewVector.value.copy( this.viewVector.value );
1203
1213
 
1204
1214
  }
1205
1215
 
@@ -1207,20 +1217,19 @@ export class NRD extends RenderStage {
1207
1217
  this.camProjInv.value.copy( projInv );
1208
1218
  this.camView.value.copy( view );
1209
1219
  this.camPos.value.setFromMatrixPosition( world );
1210
- this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
1211
- this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
1212
- this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
1213
- this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
1220
+ this.viewVector.value.setFromMatrixColumn( world, 2 ).normalize();
1221
+ this.cameraRay.sync( pt.uniforms );
1214
1222
 
1215
1223
  if ( ! this._camInitialized ) {
1216
1224
 
1217
1225
  this.prevCamView.value.copy( view );
1218
1226
  this.prevCamPos.value.copy( this.camPos.value );
1227
+ this.prevViewVector.value.copy( this.viewVector.value );
1219
1228
  this._camInitialized = true;
1220
1229
 
1221
1230
  }
1222
1231
 
1223
- // NRD gUnproject; frustum size follows the shorter edge.
1232
+ // NRD gUnproject; frustum size follows the shorter edge. Orthographic, it is world units per pixel.
1224
1233
  const projY = proj.elements[ 5 ] || 1;
1225
1234
  const unproject = 1 / ( 0.5 * this.resH.value * projY );
1226
1235
  this.minRectDimMulUnproject.value = Math.min( this.resW.value, this.resH.value ) * unproject;
@@ -1,4 +1,4 @@
1
- import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
1
+ import { Fn, vec2, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
2
2
  textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, RedIntegerFormat, UnsignedIntType, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
@@ -6,7 +6,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
7
  import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
- import { cameraRayDirection } from '../TSL/CameraRay.js';
9
+ import { cameraRayOf, cameraRayUniforms } from '../TSL/CameraRay.js';
10
10
  import { getMaterial } from '../TSL/Common.js';
11
11
  import { computeUVCache, processNormal, processBump, processMetalnessRoughness, triangleUVTangent, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
12
12
 
@@ -53,10 +53,7 @@ export class NormalDepth extends RenderStage {
53
53
  this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
54
54
  // Mirrored from the path tracer each frame — these rays must use the same camera model
55
55
  // as the colour buffer or the denoiser's normal/depth edge-stops fight the image.
56
- this.cameraProjection = uniform( 0, 'int' );
57
- this.panoLonRange = uniform( new Vector2(), 'vec2' );
58
- this.panoLatRange = uniform( new Vector2(), 'vec2' );
59
- this.panoLevelHorizon = uniform( 1, 'int' );
56
+ this.cameraRay = cameraRayUniforms();
60
57
  this.resolutionWidth = uniform( options.width || 1 );
61
58
  this.resolutionHeight = uniform( options.height || 1 );
62
59
 
@@ -151,7 +148,7 @@ export class NormalDepth extends RenderStage {
151
148
  if ( ! pt ) return false;
152
149
 
153
150
  const matAttr = pt.materialData?.materialStorageAttr;
154
- const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
151
+ const triSwapped = pt.triangleGeoAttr && pt.triangleGeoAttr !== this._lastTriAttr;
155
152
  const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
156
153
  const matSwapped = matAttr && matAttr !== this._lastMatAttr;
157
154
 
@@ -170,12 +167,13 @@ export class NormalDepth extends RenderStage {
170
167
 
171
168
  }
172
169
 
173
- if ( pt.triangleStorageAttr && ! this._triStorageNode ) {
170
+ if ( pt.triangleGeoAttr && ! this._triStorageNode ) {
174
171
 
175
172
  // uvec4, matching PathTracerStage: packed lanes must keep their exact bit pattern.
176
- this._triStorageNode = storage(
177
- pt.triangleStorageAttr, 'uvec4', pt.triangleStorageAttr.count
178
- ).toReadOnly();
173
+ this._triStorageNode = {
174
+ geo: storage( pt.triangleGeoAttr, 'uvec4', pt.triangleGeoAttr.count ).toReadOnly(),
175
+ shade: storage( pt.triangleShadeAttr, 'uvec4', pt.triangleShadeAttr.count ).toReadOnly(),
176
+ };
179
177
 
180
178
  }
181
179
 
@@ -196,7 +194,7 @@ export class NormalDepth extends RenderStage {
196
194
  // In-place bucket swaps (model change) — graph closes over the nodes, only .value changes.
197
195
  if ( this._linearBuckets ) refreshBucketTextureNodes( this._linearBuckets, pt.materialData?.linearBuckets );
198
196
 
199
- this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
197
+ this._lastTriAttr = pt.triangleGeoAttr || this._lastTriAttr;
200
198
  this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
201
199
  this._lastMatAttr = matAttr || this._lastMatAttr;
202
200
 
@@ -236,13 +234,7 @@ export class NormalDepth extends RenderStage {
236
234
  // No jitter — deterministic per-pixel ray so the temporal gate
237
235
  // sees stable per-pixel normals across frames.
238
236
  const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
239
- const rayDirWorld = cameraRayDirection(
240
- uv, camWorldMat, camProjInvMat,
241
- this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
242
- );
243
- const rayOrigin = vec3( camWorldMat[ 3 ] );
244
-
245
- const ray = Ray( { origin: rayOrigin, direction: rayDirWorld } );
237
+ const ray = Ray( cameraRayOf( uv, camWorldMat, camProjInvMat, this.cameraRay ) );
246
238
  const hit = HitInfo.wrap( traverseBVH( ray, bvhStorage, triStorage ) );
247
239
 
248
240
  const encodedNormal = hit.normal.mul( 0.5 ).add( 0.5 );
@@ -339,10 +331,7 @@ export class NormalDepth extends RenderStage {
339
331
 
340
332
  this.cameraWorldMatrix.value.copy( pt.uniforms.get( 'cameraWorldMatrix' ).value );
341
333
  this.cameraProjectionMatrixInverse.value.copy( pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value );
342
- this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
343
- this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
344
- this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
345
- this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
334
+ this.cameraRay.sync( pt.uniforms );
346
335
 
347
336
  }
348
337
 
@@ -188,7 +188,7 @@ export class PathTracer extends PathTracerStage {
188
188
  } );
189
189
 
190
190
  // Scene geometry (triangle data, two-level BVH, light BVH + emissive)
191
- t.register( 'geometry', () => [ this.triangleStorageAttr, this.bvhStorageAttr, this.lightStorageAttr ] );
191
+ t.register( 'geometry', () => [ this.triangleGeoAttr, this.triangleShadeAttr, this.bvhStorageAttr, this.lightStorageAttr ] );
192
192
 
193
193
  // Material storage buffer + per-property texture arrays
194
194
  t.register( 'materials', () => {
@@ -547,6 +547,107 @@ export class PathTracer extends PathTracerStage {
547
547
 
548
548
  }
549
549
 
550
+ /**
551
+ * The accumulation as it stands, for a checkpoint: the three MRT attachments (aux only while it
552
+ * holds samples), the per-pixel convergence buffers, and the counters that decide the next
553
+ * sample — together what a later frame reads, so restoring them continues the same sequence.
554
+ * @returns {Promise<Object>} typed arrays, tight rows, top row first as the textures hold them
555
+ */
556
+ async captureAccumulation() {
557
+
558
+ const pool = this.storageTextures;
559
+ const width = pool.renderWidth;
560
+ const height = pool.renderHeight;
561
+ const pixels = width * height;
562
+
563
+ const readAttachment = async index => {
564
+
565
+ const padded = await this.renderer.readRenderTargetPixelsAsync( pool.readTarget, 0, 0, width, height, index );
566
+ const stride = Math.ceil( width * 16 / 256 ) * 64;
567
+ if ( stride === width * 4 ) return new Float32Array( padded.buffer, padded.byteOffset, pixels * 4 ).slice();
568
+ const tight = new Float32Array( pixels * 4 );
569
+ for ( let y = 0; y < height; y ++ ) tight.set( padded.subarray( y * stride, y * stride + width * 4 ), y * width * 4 );
570
+ return tight;
571
+
572
+ };
573
+
574
+ const readBuffer = async ( attr, Type ) => attr ? new Type( await this.renderer.getArrayBufferAsync( attr, null, 0, pixels * 4 ) ) : null;
575
+ const aux = this._auxGBufferEnabled && this._auxSamples > 0;
576
+
577
+ // Every copy is submitted here, in this task, before any await: a frame rendered between two of
578
+ // them would give a checkpoint whose parts disagree.
579
+ const state = {
580
+ frameCount: this.frameCount,
581
+ seedTick: this._seedTick,
582
+ auxSamples: aux ? this._auxSamples : 0,
583
+ convergedFraction: this._convergedFraction,
584
+ convergedGeometryFraction: this._convergedGeometryFraction,
585
+ isComplete: this.isComplete,
586
+ };
587
+ const reads = [
588
+ readAttachment( 0 ),
589
+ aux ? readAttachment( 1 ) : null,
590
+ aux ? readAttachment( 2 ) : null,
591
+ readBuffer( this._m2Attr, Float32Array ),
592
+ readBuffer( this._streakAttr, Uint32Array ),
593
+ readBuffer( this._frozenMaskAttr, Uint32Array ),
594
+ ];
595
+ const [ color, normalDepth, albedo, m2, streak, frozenMask ] = await Promise.all( reads );
596
+
597
+ return { width, height, state, color, normalDepth, albedo, m2, streak, frozenMask };
598
+
599
+ }
600
+
601
+ /**
602
+ * Writes a {@link captureAccumulation} back, so the next frame continues it. The render size must
603
+ * match; nothing may reset accumulation after this.
604
+ */
605
+ restoreAccumulation( checkpoint ) {
606
+
607
+ const pool = this.storageTextures;
608
+ const { width, height, state } = checkpoint;
609
+ if ( pool.renderWidth !== width || pool.renderHeight !== height ) {
610
+
611
+ throw new Error( `checkpoint is ${width}×${height}; the render is ${pool.renderWidth}×${pool.renderHeight}` );
612
+
613
+ }
614
+
615
+ const backend = this.renderer.backend;
616
+ const device = backend.device;
617
+ if ( ! backend.get( pool.readTarget.textures[ 0 ] ).texture ) this.renderer.initRenderTarget( pool.readTarget );
618
+
619
+ const writeAttachment = ( index, data ) => device.queue.writeTexture(
620
+ { texture: backend.get( pool.readTarget.textures[ index ] ).texture },
621
+ data,
622
+ { bytesPerRow: width * 16, rowsPerImage: height },
623
+ { width, height },
624
+ );
625
+
626
+ const writeBuffer = ( attr, data ) => {
627
+
628
+ if ( attr && data ) device.queue.writeBuffer( backend.get( attr ).buffer, 0, data );
629
+
630
+ };
631
+
632
+ writeAttachment( 0, checkpoint.color );
633
+ if ( checkpoint.normalDepth ) writeAttachment( 1, checkpoint.normalDepth );
634
+ if ( checkpoint.albedo ) writeAttachment( 2, checkpoint.albedo );
635
+ writeBuffer( this._m2Attr, checkpoint.m2 );
636
+ writeBuffer( this._streakAttr, checkpoint.streak );
637
+ writeBuffer( this._frozenMaskAttr, checkpoint.frozenMask );
638
+
639
+ this.frameCount = state.frameCount;
640
+ this.frame.value = state.frameCount;
641
+ this._seedTick = state.seedTick;
642
+ this.hasPreviousAccumulated.value = state.frameCount > 0 ? 1 : 0;
643
+ this._auxSamples = checkpoint.normalDepth ? state.auxSamples : 0;
644
+ this._auxSeedPending = this._auxGBufferEnabled && ! checkpoint.normalDepth;
645
+ this._convergedFraction = state.convergedFraction ?? 0;
646
+ this._convergedGeometryFraction = state.convergedGeometryFraction ?? 0;
647
+ this.isComplete = false;
648
+
649
+ }
650
+
550
651
  reset() {
551
652
 
552
653
  super.reset();
@@ -1147,7 +1248,7 @@ export class PathTracer extends PathTracerStage {
1147
1248
 
1148
1249
  const genParams = {
1149
1250
  rayBufferRW: pb.rayBuffer.rw,
1150
- rngBufferRW: pb.rngBuffer.rw,
1251
+ hitBufferRW: pb.hitBuffer.rw,
1151
1252
  gBufferRW,
1152
1253
  resolution: this.resolution,
1153
1254
  // RNG axis only (baseSeed + stratified jitter) — takes the seed counter, not the
@@ -1163,9 +1264,11 @@ export class PathTracer extends PathTracerStage {
1163
1264
  focalLength: this.focalLength,
1164
1265
  aperture: this.aperture,
1165
1266
  focusDistance: this.focusDistance,
1166
- sceneScale: this.sceneScale,
1267
+ unitsPerMetre: this.unitsPerMetre,
1167
1268
  apertureScale: this.apertureScale,
1168
1269
  anamorphicRatio: this.anamorphicRatio,
1270
+ dofMode: this.dofMode,
1271
+ dofBlur: this.dofBlur,
1169
1272
  renderWidth: this._wfRenderWidth,
1170
1273
  renderHeight: this._wfRenderHeight,
1171
1274
  chunkRowBase: this._wfChunkRowBase,
@@ -1456,8 +1559,7 @@ export class PathTracer extends PathTracerStage {
1456
1559
  envCDFTexture: freshEnvCDF,
1457
1560
  lightBuffer: freshLight,
1458
1561
  rayBufferRW: pb.rayBuffer.rw,
1459
- rngBufferRW: pb.rngBuffer.rw,
1460
- hitBufferRO: pb.hitBuffer.ro,
1562
+ hitBufferRW: pb.hitBuffer.rw,
1461
1563
  counters,
1462
1564
  activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
1463
1565
  envTexture: freshEnvTex,
@@ -1510,6 +1612,10 @@ export class PathTracer extends PathTracerStage {
1510
1612
  maxRayCount: this._wfMaxRayCount,
1511
1613
  chunkRowBase: this._wfChunkRowBase,
1512
1614
  auxGBufferEnabled: this._auxGBufferUniform,
1615
+ hasSun: this.hasSun,
1616
+ sunDirection: this.sunDirection,
1617
+ sunRadiance: this.sunRadiance,
1618
+ sunParams: this.sunParams,
1513
1619
  } );
1514
1620
  this._kernelManager.register( 'shade',
1515
1621
  shadeFn().compute(
@@ -1643,9 +1749,11 @@ export class PathTracer extends PathTracerStage {
1643
1749
  focalLength: this.focalLength,
1644
1750
  aperture: this.aperture,
1645
1751
  focusDistance: this.focusDistance,
1646
- sceneScale: this.sceneScale,
1752
+ unitsPerMetre: this.unitsPerMetre,
1647
1753
  apertureScale: this.apertureScale,
1648
1754
  anamorphicRatio: this.anamorphicRatio,
1755
+ dofMode: this.dofMode,
1756
+ dofBlur: this.dofBlur,
1649
1757
  bvhBuffer: freshBvh,
1650
1758
  triangleBuffer: freshTri,
1651
1759
  materialBuffer: freshMat,