rayzee 8.6.1 → 8.7.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,12 +1,13 @@
1
1
  /**
2
- * AnimationManager — Drives GLTF skeletal/morph animations for BVH refit.
2
+ * AnimationManager — Drives animation clips for the path tracer.
3
3
  *
4
- * Owns the Three.js AnimationMixer, advances clips each frame,
5
- * extracts deformed vertex positions via CPU skinning, and returns
6
- * a per-mesh reader in the shape PathTracerApp.refitBVH() accepts.
4
+ * Deforming clips (a SkinnedMesh or morph track) return a per-mesh position reader for
5
+ * PathTracerApp.refitBVH(). Rigid clips return nothing and hand the moved meshes to
6
+ * `applyPoseCallback`, which updates placements only — a refit would bake world positions into
7
+ * triangles that other placements share.
7
8
  */
8
9
 
9
- import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
10
+ import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
10
11
  import { EngineEvents } from '../EngineEvents.js';
11
12
 
12
13
  export class AnimationManager extends EventDispatcher {
@@ -31,9 +32,20 @@ export class AnimationManager extends EventDispatcher {
31
32
  this._savedTimeScale = 1;
32
33
  this.onFinished = null; // callback when a non-looping clip ends
33
34
 
35
+ this._rigid = false;
36
+ this._plans = null; // per clip: { meshes: Int32Array, visibility: boolean, cameras: Camera[] }
37
+ this._allPlan = null;
38
+ this._activeClip = 0;
39
+ this._slotOf = null; // meshIndex -> row in the two caches below, or -1
40
+ this._lastWorld = null; // world matrix last handed to applyPoseCallback, 16 per row
41
+ this._lastVisible = null; // 0 / 1, or 2 before the first pose
42
+
34
43
  /** Injected by PathTracerApp — wakes the render loop after play/resume. */
35
44
  this.wakeCallback = null;
36
45
 
46
+ /** Injected by PathTracerApp — receives `{ meshIndices, visibilityChanged, cameras }`. */
47
+ this.applyPoseCallback = null;
48
+
37
49
  }
38
50
 
39
51
  /**
@@ -88,6 +100,10 @@ export class AnimationManager extends EventDispatcher {
88
100
 
89
101
  this.actions = actions;
90
102
 
103
+ this._rigid = ! meshes.some( m => m?.isSkinnedMesh ) &&
104
+ ! animations.some( clip => clip.tracks.some( t => t.name.includes( 'morphTargetInfluences' ) ) );
105
+ this._planClips( animations );
106
+
91
107
  // Listen for non-looping clip completion
92
108
  this.mixer.addEventListener( 'finished', () => {
93
109
 
@@ -121,7 +137,154 @@ export class AnimationManager extends EventDispatcher {
121
137
  this._meshPositions = [];
122
138
 
123
139
  const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
124
- console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles` );
140
+ console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles, ${this._rigid ? 'rigid' : 'deforming'}` );
141
+
142
+ }
143
+
144
+ /** Which meshes and cameras each clip moves, directly or through an ancestor. @private */
145
+ _planClips( animations ) {
146
+
147
+ const root = this._mixerRoot;
148
+ const byName = new Map();
149
+ // First name in pre-order wins, as in PropertyBinding.findNode.
150
+ root.traverse?.( node => {
151
+
152
+ if ( ! byName.has( node.name ) ) byName.set( node.name, node );
153
+ byName.set( node.uuid, node );
154
+
155
+ } );
156
+
157
+ this._plans = animations.map( clip => {
158
+
159
+ const animated = new Set();
160
+ let visibility = false;
161
+ for ( const track of clip.tracks ) {
162
+
163
+ const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
164
+ const node = byName.get( nodeName );
165
+ if ( ! node ) continue;
166
+ animated.add( node );
167
+ if ( propertyName === 'visible' ) visibility = true;
168
+
169
+ }
170
+
171
+ const meshes = [];
172
+ for ( let m = 0; m < this._meshes.length; m ++ ) {
173
+
174
+ for ( let o = this._meshes[ m ]; o; o = o.parent ) {
175
+
176
+ if ( animated.has( o ) ) {
177
+
178
+ meshes.push( m );
179
+ break;
180
+
181
+ }
182
+
183
+ }
184
+
185
+ }
186
+
187
+ const cameras = new Set();
188
+ for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
189
+
190
+ return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
191
+
192
+ } );
193
+
194
+ this._allPlan = null;
195
+ if ( ! this._rigid ) return;
196
+
197
+ this._slotOf = new Int32Array( this._meshes.length ).fill( - 1 );
198
+ let rows = 0;
199
+ for ( const plan of this._plans ) for ( const m of plan.meshes ) if ( this._slotOf[ m ] < 0 ) this._slotOf[ m ] = rows ++;
200
+
201
+ // Unknown until the first pose, which therefore sends every animated mesh.
202
+ this._lastWorld = new Float64Array( rows * 16 ).fill( NaN );
203
+ this._lastVisible = new Uint8Array( rows ).fill( 2 );
204
+
205
+ }
206
+
207
+ /** The plan for a clip index, or the union of every clip for -1. @private */
208
+ _planFor( clipIndex ) {
209
+
210
+ if ( ! this._plans ) return null;
211
+ if ( clipIndex >= 0 ) return this._plans[ clipIndex ] ?? null;
212
+
213
+ if ( ! this._allPlan ) {
214
+
215
+ const meshes = new Set(), cameras = new Set();
216
+ let visibility = false;
217
+ for ( const plan of this._plans ) {
218
+
219
+ plan.meshes.forEach( m => meshes.add( m ) );
220
+ plan.cameras.forEach( c => cameras.add( c ) );
221
+ visibility ||= plan.visibility;
222
+
223
+ }
224
+
225
+ this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
226
+
227
+ }
228
+
229
+ return this._allPlan;
230
+
231
+ }
232
+
233
+ /** Send meshes whose world matrix or visibility changed since the last pose. @private */
234
+ _applyRigidPose( clipIndex ) {
235
+
236
+ const plan = this._planFor( clipIndex );
237
+ if ( ! plan ) return;
238
+
239
+ const moved = [];
240
+ let visibilityChanged = false;
241
+ const last = this._lastWorld, lastVisible = this._lastVisible;
242
+
243
+ for ( const m of plan.meshes ) {
244
+
245
+ const mesh = this._meshes[ m ];
246
+ const row = this._slotOf[ m ];
247
+ const e = mesh.matrixWorld.elements;
248
+ const o = row * 16;
249
+
250
+ for ( let i = 0; i < 16; i ++ ) {
251
+
252
+ if ( last[ o + i ] !== e[ i ] ) {
253
+
254
+ for ( let j = 0; j < 16; j ++ ) last[ o + j ] = e[ j ];
255
+ moved.push( m );
256
+ break;
257
+
258
+ }
259
+
260
+ }
261
+
262
+ if ( plan.visibility || lastVisible[ row ] === 2 ) {
263
+
264
+ let visible = 1;
265
+ for ( let n = mesh; n; n = n.parent ) if ( ! n.visible ) {
266
+
267
+ visible = 0;
268
+ break;
269
+
270
+ }
271
+
272
+ if ( lastVisible[ row ] !== visible ) {
273
+
274
+ lastVisible[ row ] = visible;
275
+ visibilityChanged = true;
276
+
277
+ }
278
+
279
+ }
280
+
281
+ }
282
+
283
+ if ( moved.length || visibilityChanged || plan.cameras.length ) {
284
+
285
+ this.applyPoseCallback?.( { meshIndices: moved, visibilityChanged, cameras: plan.cameras } );
286
+
287
+ }
125
288
 
126
289
  }
127
290
 
@@ -145,6 +308,7 @@ export class AnimationManager extends EventDispatcher {
145
308
 
146
309
  }
147
310
 
311
+ this._activeClip = clipIndex;
148
312
  this.timer.reset();
149
313
  this.isPlaying = true;
150
314
  this.wakeCallback?.();
@@ -188,10 +352,19 @@ export class AnimationManager extends EventDispatcher {
188
352
 
189
353
  if ( ! this.mixer ) return;
190
354
 
355
+ // Restores every animated property to its loaded value.
191
356
  this.mixer.stopAllAction();
192
357
  this.mixer.timeScale = this._savedTimeScale || 1;
193
358
  this.timer.reset();
194
359
  this.isPlaying = false;
360
+
361
+ if ( this._rigid ) {
362
+
363
+ this._mixerRoot.updateMatrixWorld( true );
364
+ this._applyRigidPose( - 1 );
365
+
366
+ }
367
+
195
368
  this.dispatchEvent( { type: EngineEvents.ANIMATION_STOPPED } );
196
369
 
197
370
  }
@@ -229,12 +402,15 @@ export class AnimationManager extends EventDispatcher {
229
402
  *
230
403
  * @param {number} time - Absolute time in seconds
231
404
  * @param {number} [clipIndex=0] - Clip to evaluate, or -1 for all active
232
- * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no mixer
405
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no
406
+ * mixer or the clips are rigid
233
407
  */
234
408
  seekTo( time, clipIndex = 0 ) {
235
409
 
236
410
  if ( ! this.mixer || this.actions.length === 0 ) return null;
237
411
 
412
+ this._activeClip = clipIndex;
413
+
238
414
  // Ensure the target action(s) are active so setTime evaluates them.
239
415
  // Actions must NOT be paused — setTime() calls update() internally,
240
416
  // which skips paused actions entirely.
@@ -279,7 +455,8 @@ export class AnimationManager extends EventDispatcher {
279
455
  * Advance animation and prepare deformed positions.
280
456
  * Call once per frame from the animate loop.
281
457
  *
282
- * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not playing
458
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not
459
+ * playing or the clips are rigid
283
460
  */
284
461
  update() {
285
462
 
@@ -312,6 +489,16 @@ export class AnimationManager extends EventDispatcher {
312
489
  // once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
313
490
  this._mixerRoot.updateMatrixWorld( true );
314
491
 
492
+ if ( this._rigid ) {
493
+
494
+ this._applyRigidPose( this._activeClip );
495
+ return null;
496
+
497
+ }
498
+
499
+ const cameras = this._planFor( this._activeClip )?.cameras;
500
+ if ( cameras?.length ) this.applyPoseCallback?.( { meshIndices: [], visibilityChanged: false, cameras } );
501
+
315
502
  return m => this._computeMeshPositions( m );
316
503
 
317
504
  }
@@ -446,6 +633,13 @@ export class AnimationManager extends EventDispatcher {
446
633
  this._meshPositions = null;
447
634
  this._skinnedCache = null;
448
635
  this._clipsCache = null;
636
+ this._rigid = false;
637
+ this._plans = null;
638
+ this._allPlan = null;
639
+ this._activeClip = 0;
640
+ this._slotOf = null;
641
+ this._lastWorld = null;
642
+ this._lastVisible = null;
449
643
 
450
644
  }
451
645