rayzee 8.6.1 → 8.8.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.
@@ -1,7 +1,7 @@
1
1
  import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace, LinearSRGBColorSpace } from 'three/webgpu';
2
2
  import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
3
3
  import {
4
- Scene, EventDispatcher, Box3
4
+ Scene, EventDispatcher, Box3, Vector3
5
5
  } from 'three';
6
6
  import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
7
7
  import { SceneHelpers } from './SceneHelpers.js';
@@ -195,6 +195,7 @@ export class PathTracerApp extends EventDispatcher {
195
195
  this.assetLoader = null;
196
196
  this._sdf = null;
197
197
  this._animRefitInFlight = false;
198
+ this._emittersMoved = false;
198
199
  // Max material-texture dimension (longest edge); applied on each scene build.
199
200
  this._maxTextureSize = DEFAULT_STATE.maxTextureSize;
200
201
 
@@ -446,6 +447,7 @@ export class PathTracerApp extends EventDispatcher {
446
447
  }
447
448
 
448
449
  this.pipeline.render();
450
+ this.denoisingManager?.afterTrace();
449
451
 
450
452
  if ( ! this.stages.pathTracer.isComplete ) {
451
453
 
@@ -566,8 +568,15 @@ export class PathTracerApp extends EventDispatcher {
566
568
  /**
567
569
  * Resets the accumulation buffer.
568
570
  * @param {boolean} soft - When true, preserves ASVGF temporal history
571
+ * @param {Object} [options]
572
+ * @param {boolean} [options.motion] - only object placements or geometry moved; keeps the OIDN
573
+ * motion history, which rejects what moved per pixel
569
574
  */
570
- reset( soft = false ) {
575
+ reset( soft = false, { motion = false } = {} ) {
576
+
577
+ // Objects moving is the view changing, as far as the live denoiser's refresh cadence goes.
578
+ if ( motion ) this.stages.pathTracer?.noteViewChanged();
579
+ this.denoisingManager?.beforeReset( { keepHistory: soft || motion } );
571
580
 
572
581
  if ( this.pipeline ) {
573
582
 
@@ -581,6 +590,8 @@ export class PathTracerApp extends EventDispatcher {
581
590
 
582
591
  }
583
592
 
593
+ this.denoisingManager?.afterReset();
594
+
584
595
  // Whatever is on screen stays until its replacement is ready, including while the camera
585
596
  // moves: the denoising manager decides, since only it knows something is coming.
586
597
  this._abortPostProcess( { keepDisplay: true } );
@@ -1165,6 +1176,7 @@ export class PathTracerApp extends EventDispatcher {
1165
1176
  // Stop any running animation before rebuilding scene data
1166
1177
  this.animationManager.dispose();
1167
1178
  this._animRefitInFlight = false;
1179
+ this._emittersMoved = false;
1168
1180
 
1169
1181
  // Tag the primary (replace-loaded) model so it appears in the scene-object list.
1170
1182
  this._tagPrimarySceneObject();
@@ -1615,7 +1627,7 @@ export class PathTracerApp extends EventDispatcher {
1615
1627
 
1616
1628
  this.stages.pathTracer.updateTriangleData( this._sdf.triangles );
1617
1629
  this.stages.pathTracer.updateBVHData( this._sdf.bvh );
1618
- this.reset();
1630
+ this.reset( false, { motion: true } );
1619
1631
 
1620
1632
  return result;
1621
1633
 
@@ -1637,7 +1649,7 @@ export class PathTracerApp extends EventDispatcher {
1637
1649
 
1638
1650
  const { triRanges, bvhRanges } = this._sdf.computeBLASDirtyRanges( affectedMeshIndices );
1639
1651
  this.stages.pathTracer.updateBufferRanges( triRanges, bvhRanges );
1640
- this.reset();
1652
+ this.reset( false, { motion: true } );
1641
1653
 
1642
1654
  // Kick off background rebuild for optimal SAH quality
1643
1655
  this._sdf.scheduleBackgroundRebuild( affectedMeshIndices, ( meshIndex ) => {
@@ -1646,7 +1658,7 @@ export class PathTracerApp extends EventDispatcher {
1646
1658
  // re-upload here is gigabytes on a large scene, for one mesh's worth of change.
1647
1659
  const dirty = this._sdf.computeBLASDirtyRanges( [ meshIndex ] );
1648
1660
  this.stages.pathTracer.updateBufferRanges( dirty.triRanges, dirty.bvhRanges );
1649
- this.reset();
1661
+ this.reset( false, { motion: true } );
1650
1662
 
1651
1663
  } );
1652
1664
 
@@ -1654,6 +1666,113 @@ export class PathTracerApp extends EventDispatcher {
1654
1666
 
1655
1667
  }
1656
1668
 
1669
+ /** Apply a pose from AnimationManager: placements, visibility, and a followed camera. @private */
1670
+ _applyAnimationPose( { meshIndices, visibilityChanged, cameras } ) {
1671
+
1672
+ if ( ! this._sdf?.instanceTable ) return;
1673
+ let changed = false;
1674
+
1675
+ if ( meshIndices.length > 0 ) {
1676
+
1677
+ this._notePlacementsMoving( meshIndices );
1678
+ this._sdf.updateMeshTransforms( meshIndices );
1679
+ this.stages.pathTracer?.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
1680
+ if ( this._sdf.movesEmitters( meshIndices ) ) {
1681
+
1682
+ this._emittersMoved = true;
1683
+ // Deferred while playing: the rebuild uploads a scene-sized map, and sampling
1684
+ // already reads each emitter through its placement.
1685
+ if ( ! this.animationManager.isPlaying ) this._refreshMovedEmitters();
1686
+
1687
+ }
1688
+
1689
+ changed = true;
1690
+
1691
+ }
1692
+
1693
+ if ( visibilityChanged ) {
1694
+
1695
+ this.stages.pathTracer?.updateAllMeshVisibility();
1696
+ this._refreshEmissiveForVisibility();
1697
+ changed = true;
1698
+
1699
+ }
1700
+
1701
+ if ( cameras.length > 0 && this._followAnimatedCamera( cameras ) ) changed = true;
1702
+
1703
+ if ( changed ) this.reset( false, { motion: true } );
1704
+
1705
+ }
1706
+
1707
+ /** @private */
1708
+ _notePlacementsMoving( meshIndices ) {
1709
+
1710
+ const dm = this.denoisingManager;
1711
+ const table = this._sdf?.instanceTable;
1712
+ if ( ! dm?.historyActive || ! table ) return;
1713
+
1714
+ for ( const meshIndex of meshIndices ) {
1715
+
1716
+ const run = table.placementRunOf( meshIndex );
1717
+ if ( ! run ) continue;
1718
+ for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], table.world, p * 16 );
1719
+
1720
+ }
1721
+
1722
+ }
1723
+
1724
+ /** @private */
1725
+ _refreshMovedEmitters() {
1726
+
1727
+ if ( ! this._emittersMoved ) return;
1728
+ this._emittersMoved = false;
1729
+ this._uploadEmissivePayload( this._sdf?.refreshEmissiveTransforms() ?? null );
1730
+
1731
+ }
1732
+
1733
+ /**
1734
+ * Move the view with the selected camera's animated original — the camera list holds copies.
1735
+ * @returns {boolean} whether the view moved
1736
+ * @private
1737
+ */
1738
+ _followAnimatedCamera( cameras ) {
1739
+
1740
+ const cm = this.cameraManager;
1741
+ const selected = cm.currentCameraIndex > 0 ? cm.cameras[ cm.currentCameraIndex ] : null;
1742
+ const uuid = selected?.userData?.__rayzeeSourceUuid;
1743
+ const source = uuid && cameras.find( c => c.uuid === uuid );
1744
+ if ( ! source ) return false;
1745
+
1746
+ source.updateWorldMatrix( true, false );
1747
+ const worldScale = new Vector3();
1748
+ source.matrixWorld.decompose( selected.position, selected.quaternion, worldScale );
1749
+ // Only the mirror survives.
1750
+ selected.scale.set( Math.sign( worldScale.x ) || 1, Math.sign( worldScale.y ) || 1, Math.sign( worldScale.z ) || 1 );
1751
+ if ( source.isPerspectiveCamera ) selected.fov = source.fov;
1752
+
1753
+ const camera = cm.camera;
1754
+ const controls = cm.controls;
1755
+ const distance = controls ? controls.target.distanceTo( camera.position ) : 0;
1756
+
1757
+ camera.position.copy( selected.position );
1758
+ camera.quaternion.copy( selected.quaternion );
1759
+ camera.scale.copy( selected.scale );
1760
+ if ( camera.isPerspectiveCamera && selected.isPerspectiveCamera ) camera.fov = selected.fov;
1761
+ camera.updateProjectionMatrix();
1762
+ camera.updateMatrixWorld( true );
1763
+
1764
+ // Pivot ahead at the same distance, so controls.update() keeps the pose.
1765
+ if ( controls ) {
1766
+
1767
+ const forward = new Vector3( 0, 0, - 1 ).applyQuaternion( camera.quaternion );
1768
+ controls.target.copy( camera.position ).addScaledVector( forward, distance || 1 );
1769
+
1770
+ }
1771
+
1772
+ return true;
1773
+
1774
+ }
1775
+
1657
1776
  /**
1658
1777
  * Apply new transforms for objects that moved, without touching their geometry.
1659
1778
  *
@@ -1667,10 +1786,11 @@ export class PathTracerApp extends EventDispatcher {
1667
1786
  */
1668
1787
  updateMeshTransforms( meshIndices ) {
1669
1788
 
1789
+ this._notePlacementsMoving( meshIndices );
1670
1790
  const result = this._sdf.updateMeshTransforms( meshIndices );
1671
1791
 
1672
1792
  this.stages.pathTracer.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
1673
- this.reset();
1793
+ this.reset( false, { motion: true } );
1674
1794
 
1675
1795
  return result;
1676
1796
 
@@ -2873,10 +2993,11 @@ export class PathTracerApp extends EventDispatcher {
2873
2993
  const table = this._sdf?.instanceTable;
2874
2994
  if ( ! table ) return;
2875
2995
 
2996
+ // The emitter list names meshes, the table placements.
2876
2997
  const hidden = new Set();
2877
2998
  for ( let i = 0; i < table.count; i ++ ) {
2878
2999
 
2879
- if ( table.isSet[ i ] && ! table.visible[ i ] ) hidden.add( i );
3000
+ if ( table.isSet[ i ] && ! table.visible[ i ] ) hidden.add( table.sourceMesh[ i ] );
2880
3001
 
2881
3002
  }
2882
3003
 
@@ -3410,6 +3531,7 @@ export class PathTracerApp extends EventDispatcher {
3410
3531
 
3411
3532
  // Animation lifecycle → wake + refit flag
3412
3533
  this.animationManager.wakeCallback = () => this.wake();
3534
+ this.animationManager.applyPoseCallback = ( pose ) => this._applyAnimationPose( pose );
3413
3535
  this._forwardEvents( this.animationManager, [
3414
3536
  EngineEvents.ANIMATION_STARTED,
3415
3537
  EngineEvents.ANIMATION_PAUSED,
@@ -3418,6 +3540,7 @@ export class PathTracerApp extends EventDispatcher {
3418
3540
  this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_PAUSED, () => {
3419
3541
 
3420
3542
  this._animRefitInFlight = false;
3543
+ this._refreshMovedEmitters();
3421
3544
 
3422
3545
  } );
3423
3546
  this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_STOPPED, () => {
@@ -3538,6 +3661,7 @@ export class PathTracerApp extends EventDispatcher {
3538
3661
  this.animationManager.onFinished = () => {
3539
3662
 
3540
3663
  this._animRefitInFlight = false;
3664
+ this._refreshMovedEmitters();
3541
3665
  this.dispatchEvent( { type: EngineEvents.ANIMATION_FINISHED } );
3542
3666
 
3543
3667
  };
@@ -787,10 +787,11 @@ export class AssetLoader extends EventDispatcher {
787
787
  };
788
788
 
789
789
  const pbrtStart = performance.now();
790
- const { group, environment, report, warnings, meshCount, entryPath: loadedEntry, candidates,
791
- parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
790
+ const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
791
+ frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
792
792
  droppedNoTemplate } = await loadPBRTScene( {
793
793
  vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes,
794
+ animation: options.animation,
794
795
  maxTriangles: options.maxTriangles,
795
796
  maxPlacements: options.maxPlacements,
796
797
  mergeShapesAbove: options.mergeShapesAbove,
@@ -802,15 +803,17 @@ export class AssetLoader extends EventDispatcher {
802
803
  // SceneProcessor.performanceMetrics.
803
804
  this.lastPBRTStats = {
804
805
  parseMs, buildMs, loaderMs: performance.now() - pbrtStart,
805
- triangleCount, placementCount, mergedShapes, skippedForBudget, droppedNoTemplate, meshCount
806
+ triangleCount, placementCount, mergedShapes, skippedForBudget, droppedNoTemplate, meshCount,
807
+ frames: frames?.length ?? 1
806
808
  };
807
809
 
808
- // An archive can hold several independent scenes (transparent-machines ships five
809
- // animation frames). Only one is loaded, so name it and the alternatives rather
810
- // than leave the user comparing against a reference of a different scene.
811
- if ( candidates.length > 1 ) {
810
+ // An archive can hold several independent scenes (bistro ships three views). Only one is
811
+ // loaded, so name it and the alternatives rather than leave the user comparing against a
812
+ // reference of a different scene. A frame sequence loads whole, as its animation.
813
+ const loaded = new Set( frames ?? [ loadedEntry ] );
814
+ const others = candidates.filter( p => ! loaded.has( p ) );
815
+ if ( others.length > 0 ) {
812
816
 
813
- const others = candidates.filter( p => p !== loadedEntry );
814
817
  console.warn( `PBRT archive holds ${candidates.length} scenes; loaded "${loadedEntry}". Others: ${others.join( ', ' )}` );
815
818
  this._issues?.record(
816
819
  ISSUE_CODES.ASSET_AMBIGUOUS_ENTRY,
@@ -849,6 +852,7 @@ export class AssetLoader extends EventDispatcher {
849
852
  group.name = loadedEntry || filename;
850
853
  this.releaseTargetModel();
851
854
  this.targetModel = group;
855
+ this.animations = animations ?? [];
852
856
 
853
857
  // The light's own orientation and `scale` are already baked into the texture, so the
854
858
  // scene is only correct at rotation 0 / intensity 1 — pinned, whatever the host's defaults.
@@ -2032,6 +2036,8 @@ export class AssetLoader extends EventDispatcher {
2032
2036
 
2033
2037
  // Clone the camera to avoid modifying the original
2034
2038
  const camera = object.clone();
2039
+ // The clip animates the original; this lets the copy follow it.
2040
+ camera.userData.__rayzeeSourceUuid = object.uuid;
2035
2041
 
2036
2042
  // Apply world transforms — cameras may be children of
2037
2043
  // transformed nodes, so local position/quaternion != world.
@@ -72,6 +72,7 @@ export class EmissiveTriangleBuilder {
72
72
  extractEmissiveTriangles( triangleData, materials, triangleCount, table = null ) {
73
73
 
74
74
  this.emissiveTriangles = [];
75
+ this._emissiveMeshes = null;
75
76
  this.totalEmissivePower = 0;
76
77
  this._totalTriangleCount = triangleCount;
77
78
 
@@ -172,6 +173,7 @@ export class EmissiveTriangleBuilder {
172
173
  triangleIndex: i,
173
174
  materialIndex: materialIndex,
174
175
  meshIndex: meshIndex,
176
+ sideSign,
175
177
  instanceLeaf: hasInstance ? table.tlasLeafIndex[ placement ] : - 1,
176
178
  power: power,
177
179
  area: area,
@@ -203,6 +205,74 @@ export class EmissiveTriangleBuilder {
203
205
 
204
206
  }
205
207
 
208
+ /** Mesh indices owning at least one emissive triangle. */
209
+ get emissiveMeshes() {
210
+
211
+ return this._emissiveMeshes ??= new Set( this.emissiveTriangles.map( t => t.meshIndex ) );
212
+
213
+ }
214
+
215
+ /**
216
+ * Re-measure every emitter at its placement's current matrix. Call buildLightBVH() after.
217
+ * @param {Uint32Array|object} triangleData - packed triangle records, flat or chunked
218
+ * @param {import('./InstanceTable.js').InstanceTable} table
219
+ */
220
+ refreshTransforms( triangleData, table ) {
221
+
222
+ const chunked = triangleData && triangleData.chunks ? triangleData : null;
223
+ const chunkedF = chunked ? chunked.viewAs( Float32Array ) : null;
224
+ const flatF = chunked ? null : new Float32Array( triangleData.buffer, triangleData.byteOffset, triangleData.length );
225
+ const FLOATS_PER_TRIANGLE = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
226
+ const m = table.world;
227
+
228
+ this.totalEmissivePower = 0;
229
+
230
+ for ( const tri of this.emissiveTriangles ) {
231
+
232
+ const f = chunked ? chunkedF.chunkFor( tri.triangleIndex ) : flatF;
233
+ const b = chunked ? chunked.baseOf( tri.triangleIndex ) : tri.triangleIndex * FLOATS_PER_TRIANGLE;
234
+ const v = [ f[ b ], f[ b + 1 ], f[ b + 2 ], f[ b + 4 ], f[ b + 5 ], f[ b + 6 ], f[ b + 8 ], f[ b + 9 ], f[ b + 10 ] ];
235
+
236
+ // As extraction resolves it.
237
+ const placement = table.placementRunOf?.( tri.meshIndex )?.start ?? - 1;
238
+ if ( placement >= 0 && table.isSet[ placement ] ) {
239
+
240
+ const o = placement * 16;
241
+ for ( let k = 0; k < 9; k += 3 ) {
242
+
243
+ const x = v[ k ], y = v[ k + 1 ], z = v[ k + 2 ];
244
+ v[ k ] = m[ o ] * x + m[ o + 4 ] * y + m[ o + 8 ] * z + m[ o + 12 ];
245
+ v[ k + 1 ] = m[ o + 1 ] * x + m[ o + 5 ] * y + m[ o + 9 ] * z + m[ o + 13 ];
246
+ v[ k + 2 ] = m[ o + 2 ] * x + m[ o + 6 ] * y + m[ o + 10 ] * z + m[ o + 14 ];
247
+
248
+ }
249
+
250
+ }
251
+
252
+ const [ v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z ] = v;
253
+ const area = this._calculateTriangleArea( v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z );
254
+ const e = tri.emissive;
255
+ tri.power = ( 0.2126 * e.r + 0.7152 * e.g + 0.0722 * e.b ) * tri.emissiveIntensity * area;
256
+ tri.area = area;
257
+
258
+ const nx = ( v1y - v0y ) * ( v2z - v0z ) - ( v1z - v0z ) * ( v2y - v0y );
259
+ const ny = ( v1z - v0z ) * ( v2x - v0x ) - ( v1x - v0x ) * ( v2z - v0z );
260
+ const nz = ( v1x - v0x ) * ( v2y - v0y ) - ( v1y - v0y ) * ( v2x - v0x );
261
+ const nl = ( Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1 ) / tri.sideSign;
262
+ tri.nx = nx / nl; tri.ny = ny / nl; tri.nz = nz / nl;
263
+
264
+ tri.cx = ( v0x + v1x + v2x ) / 3; tri.cy = ( v0y + v1y + v2y ) / 3; tri.cz = ( v0z + v1z + v2z ) / 3;
265
+ tri.bMinX = Math.min( v0x, v1x, v2x ); tri.bMinY = Math.min( v0y, v1y, v2y ); tri.bMinZ = Math.min( v0z, v1z, v2z );
266
+ tri.bMaxX = Math.max( v0x, v1x, v2x ); tri.bMaxY = Math.max( v0y, v1y, v2y ); tri.bMaxZ = Math.max( v0z, v1z, v2z );
267
+
268
+ this.totalEmissivePower += tri.power;
269
+
270
+ }
271
+
272
+ this._buildDataArrays();
273
+
274
+ }
275
+
206
276
  /**
207
277
  * Calculate triangle area using cross product
208
278
  */