rayzee 9.3.2 → 9.3.3

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.
@@ -2597,6 +2597,7 @@ export class SceneProcessor {
2597
2597
  async refitBVH( newPositions, newNormals ) {
2598
2598
 
2599
2599
  this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
2600
+ this._tlasLeafBoxesOf = null;
2600
2601
 
2601
2602
  if ( ! this.bvh || ! this.triangles || ! this.instanceTable ) {
2602
2603
 
@@ -2650,13 +2651,17 @@ export class SceneProcessor {
2650
2651
  const table = this.instanceTable;
2651
2652
  const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
2652
2653
  const triRanges = [];
2654
+ const blasRanges = [];
2655
+ let owners = 0;
2653
2656
  for ( let i = 0; i < table.count; i ++ ) {
2654
2657
 
2655
2658
  if ( ! table.isSet[ i ] || ! table.isOwner( i ) ) continue;
2659
+ owners ++;
2656
2660
 
2657
2661
  const p = positionsFor( i );
2658
2662
  if ( ! p ) continue;
2659
2663
  this._updateMeshTrianglePositions( i, p );
2664
+ blasRanges.push( table.blasOffsetOf( i ), table.blasNodeCountOf( i ) );
2660
2665
 
2661
2666
  // Smooth normals overwrite the face normals just computed, so they follow per mesh.
2662
2667
  if ( normalsFor ) {
@@ -2673,6 +2678,11 @@ export class SceneProcessor {
2673
2678
 
2674
2679
  }
2675
2680
 
2681
+ const tlasNodeCount = table.tlasNodeCount;
2682
+ const partial = tlasNodeCount > 0 && blasRanges.length / 2 < owners;
2683
+ const bvhRanges = partial ? [ this.computeTLASDirtyRange() ] : null;
2684
+ for ( let r = 0; partial && r < blasRanges.length; r += 2 ) bvhRanges.push( { offset: blasRanges[ r ] * 16, count: blasRanges[ r + 1 ] * 16 } );
2685
+
2676
2686
  return new Promise( ( resolve, reject ) => {
2677
2687
 
2678
2688
  this._refitWorker.onmessage = ( e ) => {
@@ -2680,7 +2690,7 @@ export class SceneProcessor {
2680
2690
  const msg = e.data;
2681
2691
  if ( msg.type === 'refitComplete' ) {
2682
2692
 
2683
- resolve( { refitTimeMs: msg.refitTimeMs, triRanges } );
2693
+ resolve( { refitTimeMs: msg.refitTimeMs, triRanges, bvhRanges } );
2684
2694
 
2685
2695
  } else if ( msg.type === 'error' ) {
2686
2696
 
@@ -2691,7 +2701,7 @@ export class SceneProcessor {
2691
2701
  };
2692
2702
 
2693
2703
  // Signal worker — no data transfer needed, everything is in shared memory
2694
- this._refitWorker.postMessage( { type: 'refit' } );
2704
+ this._refitWorker.postMessage( { type: 'refit', blasRanges: partial ? blasRanges : null, tlasNodeCount } );
2695
2705
 
2696
2706
  } );
2697
2707
 
@@ -2841,6 +2851,8 @@ export class SceneProcessor {
2841
2851
  const start = performance.now();
2842
2852
  const table = this.instanceTable;
2843
2853
  const composed = this._transformScratch ??= new Float32Array( 16 );
2854
+ if ( this._movedPlacements?.length !== table.count ) this._movedPlacements = new Uint8Array( table.count );
2855
+ const movedMask = this._movedPlacements;
2844
2856
  let moved = 0;
2845
2857
 
2846
2858
  for ( const meshIndex of meshIndices ) {
@@ -2877,13 +2889,15 @@ export class SceneProcessor {
2877
2889
  }
2878
2890
 
2879
2891
  this._writeLeafMatrix( p );
2892
+ movedMask[ p ] = 1;
2880
2893
  moved ++;
2881
2894
 
2882
2895
  }
2883
2896
 
2884
2897
  }
2885
2898
 
2886
- this._refitTLAS();
2899
+ this._refitTLAS( movedMask );
2900
+ movedMask.fill( 0 );
2887
2901
 
2888
2902
  return { refitTimeMs: performance.now() - start, placements: moved };
2889
2903
 
@@ -3364,6 +3378,8 @@ export class SceneProcessor {
3364
3378
  /** Keep the chunked BVH and its u32 index view in step. @private */
3365
3379
  _setBVHData( data ) {
3366
3380
 
3381
+ this._tlasLeafBoxesOf = null;
3382
+
3367
3383
  const records = ! data ? null
3368
3384
  : ( data instanceof ChunkedRecords
3369
3385
  ? data
@@ -3545,9 +3561,11 @@ export class SceneProcessor {
3545
3561
  /**
3546
3562
  * Refit TLAS AABBs in-place without rebuilding the tree structure.
3547
3563
  * O(tlasNodeCount) bottom-up pass — much faster than full SAH rebuild.
3564
+ * @param {Uint8Array} [moved] - by placement: only these leaves' world boxes are recomputed, the
3565
+ * rest kept from the last pass. Every leaf when omitted, or when nothing valid was kept.
3548
3566
  * @private
3549
3567
  */
3550
- _refitTLAS() {
3568
+ _refitTLAS( moved = null ) {
3551
3569
 
3552
3570
  const tlasNodeCount = this.instanceTable.tlasNodeCount;
3553
3571
 
@@ -3555,10 +3573,12 @@ export class SceneProcessor {
3555
3573
  if ( ! this._tlasBounds || this._tlasBounds.length < tlasNodeCount * 6 ) {
3556
3574
 
3557
3575
  this._tlasBounds = new Float32Array( tlasNodeCount * 6 );
3576
+ this._tlasLeafBoxesOf = null;
3558
3577
 
3559
3578
  }
3560
3579
 
3561
3580
  const table = this.instanceTable;
3581
+ const keep = moved && this._tlasLeafBoxesOf === table;
3562
3582
 
3563
3583
  // Bottom-up pass: reverse iteration over TLAS nodes
3564
3584
  for ( let i = tlasNodeCount - 1; i >= 0; i -- ) {
@@ -3574,7 +3594,7 @@ export class SceneProcessor {
3574
3594
  // every placement of a shared geometry onto one box, so all but one copy sat
3575
3595
  // outside its own bounds and rays walked straight past it.
3576
3596
  const entryIndex = idxChunk[ o + 1 ] & TLAS_PLACEMENT_MASK;
3577
- if ( entryIndex < table.count ) {
3597
+ if ( entryIndex < table.count && ! ( keep && ! moved[ entryIndex ] ) ) {
3578
3598
 
3579
3599
  table.writeWorldAABB( entryIndex, this._tlasBounds, i * 6 );
3580
3600
 
@@ -3615,6 +3635,8 @@ export class SceneProcessor {
3615
3635
 
3616
3636
  }
3617
3637
 
3638
+ this._tlasLeafBoxesOf = table;
3639
+
3618
3640
  }
3619
3641
 
3620
3642
  /**
@@ -56,7 +56,39 @@ fn rayzee_source( x: u32, y: u32 ) -> vec4<f32> {
56
56
  }`,
57
57
  };
58
58
 
59
- const toneMapPassWGSL = ( transformWGSL, input ) => /* wgsl */ `
59
+ // RGBA bytes for a picture, or four float planes (r, g, b, a) for a network that wants the curve
60
+ // unquantized. The planes take a 2.2 power, not the sRGB curve: what the AI upscaler has always fed
61
+ // its network, so its output does not move.
62
+ const OUTPUTS = {
63
+ rgba8: /* wgsl */ `
64
+ @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
65
+
66
+ fn rayzee_store( i: u32, linear: vec4<f32>, mapped: vec3<f32> ) {
67
+ let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
68
+ var a8 = 255u;
69
+ if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
70
+ dst[ i ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
71
+ }`,
72
+ planar: /* wgsl */ `
73
+ @group(0) @binding(1) var<storage, read_write> dst: array<f32>;
74
+
75
+ fn rayzee_store( i: u32, linear: vec4<f32>, mapped: vec3<f32> ) {
76
+ var c = clamp( mapped, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
77
+ if ( ! rayzee_output_encoded( params.mode ) ) {
78
+ let m = max( mapped, vec3<f32>( 0.0 ) );
79
+ c = select( pow( m, vec3<f32>( 1.0 / 2.2 ) ), vec3<f32>( 0.0 ), m <= vec3<f32>( 0.0 ) );
80
+ }
81
+ let n = params.width * params.height;
82
+ dst[ i ] = c.r;
83
+ dst[ n + i ] = c.g;
84
+ dst[ 2u * n + i ] = c.b;
85
+ dst[ 3u * n + i ] = linear.a;
86
+ }`,
87
+ };
88
+
89
+ const BYTES_PER_PIXEL = { rgba8: 4, planar: 16 };
90
+
91
+ const toneMapPassWGSL = ( transformWGSL, input, output ) => /* wgsl */ `
60
92
  struct Params {
61
93
  width: u32,
62
94
  height: u32,
@@ -67,11 +99,11 @@ struct Params {
67
99
  alpha: u32,
68
100
  };
69
101
 
70
- @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
71
102
  @group(0) @binding(2) var<uniform> params: Params;
72
103
  ${SOURCES[ input ]}
73
104
 
74
105
  ${transformWGSL}
106
+ ${OUTPUTS[ output ]}
75
107
 
76
108
  @compute @workgroup_size(8, 8)
77
109
  fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
@@ -81,11 +113,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
81
113
  if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
82
114
 
83
115
  let linear = rayzee_source( gid.x, srcY );
84
- let mapped = rayzee_tone_map( linear.rgb, params.mode, params.exposure, params.saturation );
85
- let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
86
- var a8 = 255u;
87
- if ( params.alpha == 1u ) { a8 = rayzee_to_u8( vec3<f32>( linear.a ) ).x; }
88
- dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( a8 << 24u );
116
+ rayzee_store( gid.y * params.width + gid.x, linear, rayzee_tone_map( linear.rgb, params.mode, params.exposure, params.saturation ) );
89
117
  }
90
118
  `;
91
119
 
@@ -94,17 +122,18 @@ const PARAMS_BYTES = 32;
94
122
 
95
123
  /**
96
124
  * Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel), or with `input: 'texture'` a
97
- * float texture read by pixel, into RGBA bytes.
125
+ * float texture read by pixel, into RGBA bytes — or with `output: 'planar'` into float planes.
98
126
  *
99
127
  * Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
100
128
  */
101
129
  export class PackedToneMapper {
102
130
 
103
- constructor( device, label = 'rayzee:tonemap', { input = 'packed' } = {} ) {
131
+ constructor( device, label = 'rayzee:tonemap', { input = 'packed', output = 'rgba8' } = {} ) {
104
132
 
105
133
  this.device = device;
106
134
  this.label = label;
107
135
  this.input = input;
136
+ this.output = output;
108
137
  this.width = 0;
109
138
  this.height = 0;
110
139
  this._pipeline = null;
@@ -139,7 +168,7 @@ export class PackedToneMapper {
139
168
  label: this.label,
140
169
  layout: 'auto',
141
170
  compute: {
142
- module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input ) } ),
171
+ module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input, this.output ) } ),
143
172
  entryPoint: 'main',
144
173
  },
145
174
  } );
@@ -202,7 +231,7 @@ export class PackedToneMapper {
202
231
 
203
232
  this._releaseBuffers();
204
233
 
205
- const bytes = width * height * 4;
234
+ const bytes = width * height * BYTES_PER_PIXEL[ this.output ];
206
235
  this._storage = this.device.createBuffer( {
207
236
  label: `${this.label}-out`,
208
237
  size: bytes,
@@ -235,7 +264,26 @@ export class PackedToneMapper {
235
264
  * @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
236
265
  * @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
237
266
  */
238
- async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
267
+ async toRGBA8( src, tone ) {
268
+
269
+ return new Uint8ClampedArray( await this._run( src, tone ) );
270
+
271
+ }
272
+
273
+ /**
274
+ * The same tone map into four float planes — r, g, b, then the source's alpha — each `width * height`.
275
+ * Needs `output: 'planar'`.
276
+ * @param {GPUBuffer|GPUTexture} src - as for toRGBA8
277
+ * @param {object} tone - as for toRGBA8; `preserveAlpha` does not apply
278
+ * @returns {Promise<Float32Array>}
279
+ */
280
+ async toPlanar( src, tone ) {
281
+
282
+ return new Float32Array( await this._run( src, tone ) );
283
+
284
+ }
285
+
286
+ async _run( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
239
287
 
240
288
  if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
241
289
  if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
@@ -246,6 +294,7 @@ export class PackedToneMapper {
246
294
  this._ensureTables();
247
295
 
248
296
  const { width, height } = this;
297
+ const bytes = width * height * BYTES_PER_PIXEL[ this.output ];
249
298
 
250
299
  this._paramU32[ 0 ] = width;
251
300
  this._paramU32[ 1 ] = height;
@@ -275,13 +324,13 @@ export class PackedToneMapper {
275
324
  pass.setBindGroup( 0, group );
276
325
  pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
277
326
  pass.end();
278
- encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, width * height * 4 );
327
+ encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, bytes );
279
328
  this.device.queue.submit( [ encoder.finish() ] );
280
329
 
281
330
  await this._map.mapAsync( GPUMapMode.READ );
282
- const bytes = new Uint8ClampedArray( this._map.getMappedRange().slice( 0 ) );
331
+ const out = this._map.getMappedRange().slice( 0 );
283
332
  this._map.unmap();
284
- return bytes;
333
+ return out;
285
334
 
286
335
  }
287
336
 
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Protocol:
5
5
  * 'init' → receives the shared triangle and node buffers (once per scene)
6
- * 'refit' → recomputes every node's AABB from the triangles already written there
6
+ * 'refit' → recomputes every node's AABB from the triangles already written there, or with
7
+ * `blasRanges` only those BLASes and the TLAS
7
8
  *
8
9
  * Positions never reach here: the main thread scatters each mesh into the shared triangle
9
10
  * records as it reads it, so no buffer the size of the scene exists on either side.
@@ -45,7 +46,9 @@ self.onmessage = function ( e ) {
45
46
 
46
47
  const startTime = performance.now();
47
48
 
48
- refitter.refit( bvhData, triData, nodeCount );
49
+ const { blasRanges, tlasNodeCount } = e.data;
50
+ if ( blasRanges ) refitter.refitPartial( bvhData, triData, blasRanges, tlasNodeCount );
51
+ else refitter.refit( bvhData, triData, nodeCount );
49
52
 
50
53
  self.postMessage( {
51
54
  type: 'refitComplete',
@@ -1102,17 +1102,34 @@ export class PathTracerStage extends RenderStage {
1102
1102
 
1103
1103
  }
1104
1104
 
1105
- /** Update BVH node data in the existing GPU buffer (full). */
1106
- updateBVHData( bvhData ) {
1105
+ /**
1106
+ * Update BVH node data in the existing GPU buffer.
1107
+ * @param {Object} [bvhData]
1108
+ * @param {Array<{offset:number,count:number}>} [ranges] - only these, in floats; every node when omitted
1109
+ */
1110
+ updateBVHData( bvhData, ranges ) {
1107
1111
 
1108
1112
  if ( this._bvhRecords ) {
1109
1113
 
1110
- uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
1114
+ if ( bvhData?.chunks?.length > 1 ) this._bvhRecords = bvhData;
1115
+ if ( ranges ) this.updateBufferRanges( [], ranges );
1116
+ else uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
1117
+ return;
1118
+
1119
+ }
1120
+
1121
+ const flat = bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData;
1122
+ if ( ! ranges ) {
1123
+
1124
+ this._updateStorageBuffer( this.bvhStorageAttr, flat );
1111
1125
  return;
1112
1126
 
1113
1127
  }
1114
1128
 
1115
- this._updateStorageBuffer( this.bvhStorageAttr, bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData );
1129
+ // The attribute keeps its own array once the refit has moved the nodes into shared memory.
1130
+ const own = this.bvhStorageAttr?.array;
1131
+ if ( flat && own && flat !== own ) for ( const r of ranges ) own.set( flat.subarray( r.offset, r.offset + r.count ), r.offset );
1132
+ this.updateBufferRanges( [], ranges );
1116
1133
 
1117
1134
  }
1118
1135
 
@@ -10,6 +10,23 @@
10
10
  import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
11
11
  import { EngineEvents } from '../EngineEvents.js';
12
12
 
13
+ /** The nodes of `nodes` with no ancestor in it, or null when updating each would cost more than one pass over `root`. */
14
+ function subtreeRoots( nodes, root ) {
15
+
16
+ const roots = [ ...nodes ].filter( node => {
17
+
18
+ for ( let o = node.parent; o; o = o.parent ) if ( nodes.has( o ) ) return false;
19
+ return true;
20
+
21
+ } );
22
+
23
+ let total = 0, covered = 0;
24
+ root.traverse?.( () => total ++ );
25
+ for ( const node of roots ) node.traverse?.( () => covered ++ );
26
+ return covered + roots.length / 2 < total ? roots : null;
27
+
28
+ }
29
+
13
30
  export class AnimationManager extends EventDispatcher {
14
31
 
15
32
  constructor() {
@@ -162,11 +179,19 @@ export class AnimationManager extends EventDispatcher {
162
179
 
163
180
  const animated = new Set();
164
181
  let visibility = false;
182
+ let unresolved = false;
165
183
  for ( const track of clip.tracks ) {
166
184
 
167
185
  const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
168
- const node = byName.get( nodeName );
169
- if ( ! node ) continue;
186
+ // As PropertyBinding.findNode: no node name binds the root itself.
187
+ const node = nodeName === '' || nodeName === '.' ? root : byName.get( nodeName );
188
+ if ( ! node ) {
189
+
190
+ unresolved = true;
191
+ continue;
192
+
193
+ }
194
+
170
195
  animated.add( node );
171
196
  if ( propertyName === 'visible' ) visibility = true;
172
197
 
@@ -191,7 +216,7 @@ export class AnimationManager extends EventDispatcher {
191
216
  const cameras = new Set();
192
217
  for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
193
218
 
194
- return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
219
+ return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes: unresolved ? null : animated, roots: unresolved ? null : subtreeRoots( animated, root ) };
195
220
 
196
221
  } );
197
222
 
@@ -218,15 +243,18 @@ export class AnimationManager extends EventDispatcher {
218
243
 
219
244
  const meshes = new Set(), cameras = new Set();
220
245
  let visibility = false;
246
+ let nodes = new Set();
221
247
  for ( const plan of this._plans ) {
222
248
 
223
249
  plan.meshes.forEach( m => meshes.add( m ) );
224
250
  plan.cameras.forEach( c => cameras.add( c ) );
225
251
  visibility ||= plan.visibility;
252
+ if ( nodes && plan.nodes ) plan.nodes.forEach( n => nodes.add( n ) );
253
+ else nodes = null;
226
254
 
227
255
  }
228
256
 
229
- this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
257
+ this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes, roots: nodes ? subtreeRoots( nodes, this._mixerRoot ) : null };
230
258
 
231
259
  }
232
260
 
@@ -491,7 +519,10 @@ export class AnimationManager extends EventDispatcher {
491
519
 
492
520
  // Bones live outside mesh subtrees so per-mesh updateMatrixWorld() misses them. Doing it
493
521
  // once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
494
- this._mixerRoot.updateMatrixWorld( true );
522
+ // A rigid clip moves only what its tracks name, so only those subtrees are updated.
523
+ const roots = this._rigid ? this._planFor( this._activeClip )?.roots : null;
524
+ if ( roots ) for ( const node of roots ) node.updateMatrixWorld( true );
525
+ else this._mixerRoot.updateMatrixWorld( true );
495
526
 
496
527
  if ( this._rigid ) {
497
528