rayzee 7.31.1 → 8.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 (53) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6728 -4735
  3. package/dist/rayzee.umd.js +121 -73
  4. package/package.json +2 -2
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +5 -0
  7. package/src/PathTracerApp.js +151 -25
  8. package/src/Processor/ArchiveReader.js +538 -0
  9. package/src/Processor/AssetLoader.js +265 -23
  10. package/src/Processor/BVHBuilder.js +48 -32
  11. package/src/Processor/BVHRefitter.js +244 -99
  12. package/src/Processor/ChunkedRecords.js +302 -0
  13. package/src/Processor/EmissiveTriangleBuilder.js +48 -21
  14. package/src/Processor/GeometryExtractor.js +580 -93
  15. package/src/Processor/HostMemory.js +222 -0
  16. package/src/Processor/InstanceTable.js +548 -97
  17. package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
  18. package/src/Processor/PBRT/PBRTCurves.js +249 -0
  19. package/src/Processor/PBRT/PBRTMath.js +19 -0
  20. package/src/Processor/PBRT/PBRTParser.js +187 -41
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
  22. package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
  23. package/src/Processor/PBRT/index.js +257 -50
  24. package/src/Processor/PackedRayBuffer.js +29 -18
  25. package/src/Processor/ParallelBVHBuilder.js +9 -8
  26. package/src/Processor/QueueManager.js +5 -3
  27. package/src/Processor/SceneProcessor.js +1046 -365
  28. package/src/Processor/TLASBuilder.js +546 -201
  29. package/src/Processor/TextureCreator.js +11 -9
  30. package/src/Processor/VRAMTracker.js +12 -3
  31. package/src/Processor/Workers/BVHRefitWorker.js +16 -11
  32. package/src/Processor/Workers/BVHWorker.js +12 -12
  33. package/src/Processor/Workers/TLASWorker.js +37 -0
  34. package/src/Processor/utils.js +8 -20
  35. package/src/Stages/NormalDepth.js +6 -2
  36. package/src/Stages/PathTracer.js +5 -5
  37. package/src/Stages/PathTracerStage.js +163 -41
  38. package/src/TSL/BVHTraversal.js +208 -90
  39. package/src/TSL/Common.js +135 -2
  40. package/src/TSL/Displacement.js +21 -11
  41. package/src/TSL/EmissiveSampling.js +49 -25
  42. package/src/TSL/ExtendKernel.js +3 -2
  43. package/src/TSL/LightBVHSampling.js +7 -4
  44. package/src/TSL/LightsDirect.js +30 -29
  45. package/src/TSL/ShadeKernel.js +8 -6
  46. package/src/TSL/SortGlobalKernels.js +3 -4
  47. package/src/TSL/Struct.js +3 -0
  48. package/src/TSL/TextureSampling.js +24 -9
  49. package/src/TSL/patches.js +149 -84
  50. package/src/index.js +3 -0
  51. package/src/managers/AnimationManager.js +86 -76
  52. package/src/managers/MaterialDataManager.js +29 -23
  53. package/src/managers/TransformManager.js +9 -192
@@ -7,11 +7,15 @@
7
7
  * loop) and `initTimestampQuery` (enlarges the stats-gl timestamp query pool so
8
8
  * the wavefront tracer's high per-frame compute-pass count doesn't overflow it).
9
9
  *
10
- * Export: `struct()` — drop-in replacement for TSL's `struct()` returning
11
- * a proxy factory that supports GLSL-style dot-notation field access.
10
+ * Also overrides `ContextNode.prototype.setup` to undo an r186 codegen regression
11
+ * (§4) — that one is temporary and goes away at r187.
12
+ *
13
+ * Exports: `struct()` — drop-in replacement for TSL's `struct()` returning
14
+ * a proxy factory that supports GLSL-style dot-notation field access — and
15
+ * `gpuOnlyStorageAttribute()`, a storage attribute with no CPU backing array.
12
16
  */
13
17
 
14
- import { WebGPUBackend } from 'three/webgpu';
18
+ import { ContextNode, StorageInstancedBufferAttribute, WebGPUBackend } from 'three/webgpu';
15
19
  import { struct as _struct } from 'three/tsl';
16
20
 
17
21
  // ---------------------------------------------------------------------------
@@ -61,14 +65,134 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
61
65
  configurable: true,
62
66
  } );
63
67
 
64
- // Install the workgroup-atomic-array codegen patch (section 4) lazily off the
65
- // first builder, so we don't need to import WGSLNodeBuilder directly.
66
- _installScopedArrayAtomicPatch( builder );
67
-
68
68
  return builder;
69
69
 
70
70
  };
71
71
 
72
+ // ---------------------------------------------------------------------------
73
+ // 1b. GPU-only storage attributes
74
+ // ---------------------------------------------------------------------------
75
+ // `StorageBufferAttribute( count, ... )` always allocates a CPU TypedArray, even
76
+ // for a buffer only compute shaders ever touch — three.js has no GPU-only path
77
+ // (upstream request open). The wavefront ray/hit/rng state and the per-pixel
78
+ // G-buffer are exactly that: every lane is written by a kernel before anything
79
+ // reads it, nothing is ever read back, and WebGPU zero-initialises a new buffer,
80
+ // so the CPU copy is pure overhead — 704 MB at a 4M path budget.
81
+ //
82
+ // `gpuOnlyStorageAttribute()` builds the attribute over a zero-length array and
83
+ // records the size the GPU buffer needs; `createStorageAttribute` below allocates
84
+ // that size unmapped, and `updateAttribute` becomes a no-op since there is
85
+ // nothing to upload. Everything else (binding, dispose, readback plumbing) is
86
+ // untouched — a storage binding takes the whole buffer with no size argument.
87
+
88
+ export function gpuOnlyStorageAttribute( count, itemSize, typeClass = Float32Array ) {
89
+
90
+ const attr = new StorageInstancedBufferAttribute( new typeClass( 0 ), itemSize );
91
+ attr.count = count;
92
+ attr.gpuByteLength = count * itemSize * typeClass.BYTES_PER_ELEMENT;
93
+ attr.isGPUOnly = true;
94
+ return attr;
95
+
96
+ }
97
+
98
+ const _origCreateStorageAttribute = WebGPUBackend.prototype.createStorageAttribute;
99
+
100
+ WebGPUBackend.prototype.createStorageAttribute = function ( attribute ) {
101
+
102
+ const bufferAttribute = this.attributeUtils._getBufferAttribute( attribute );
103
+ if ( ! bufferAttribute.isGPUOnly ) return _origCreateStorageAttribute.call( this, attribute );
104
+
105
+ const data = this.get( bufferAttribute );
106
+ if ( data.buffer !== undefined ) return;
107
+
108
+ data.buffer = this.device.createBuffer( {
109
+ label: bufferAttribute.name,
110
+ size: bufferAttribute.gpuByteLength,
111
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
112
+ } );
113
+
114
+ };
115
+
116
+ /**
117
+ * Upload a storage attribute's contents from several CPU chunks.
118
+ *
119
+ * A GPU buffer may be up to `maxBufferSize` (4 GB here) while V8 caps a single ArrayBuffer at
120
+ * ~2 GB, so anything larger has to be staged in pieces. The attribute must be GPU-only: it owns
121
+ * no CPU array, and these writes are the only thing that fills it.
122
+ *
123
+ * @param {Object} renderer - WebGPURenderer
124
+ * @param {Object} attr - attribute from gpuOnlyStorageAttribute()
125
+ * @param {Array<TypedArray>} chunks - in order; total byte length must match the attribute
126
+ */
127
+ export function uploadStorageChunks( renderer, attr, chunks ) {
128
+
129
+ const backend = renderer.backend;
130
+ backend.createStorageAttribute( attr );
131
+
132
+ const buffer = backend.get( attr ).buffer;
133
+ let byteOffset = 0;
134
+
135
+ for ( const chunk of chunks ) {
136
+
137
+ backend.device.queue.writeBuffer( buffer, byteOffset, chunk, 0, chunk.length );
138
+ byteOffset += chunk.byteLength;
139
+
140
+ }
141
+
142
+ if ( byteOffset !== attr.gpuByteLength ) {
143
+
144
+ throw new RangeError( `uploadStorageChunks wrote ${byteOffset} bytes into a ${attr.gpuByteLength}-byte buffer` );
145
+
146
+ }
147
+
148
+ }
149
+
150
+ /**
151
+ * Re-upload one lane range of a chunked storage attribute, spanning chunks as needed.
152
+ *
153
+ * The partial-update path three.js offers (`addUpdateRange` + a version bump) cannot work here:
154
+ * a GPU-only attribute owns no CPU array and its `updateAttribute` is a no-op, so a dirty range
155
+ * would silently never reach the GPU.
156
+ *
157
+ * @param {Object} renderer - WebGPURenderer
158
+ * @param {Object} attr - attribute from gpuOnlyStorageAttribute()
159
+ * @param {Object} records - the ChunkedRecords backing it
160
+ * @param {number} startLane - first lane to write
161
+ * @param {number} laneCount - how many lanes
162
+ */
163
+ export function uploadStorageChunkRange( renderer, attr, records, startLane, laneCount ) {
164
+
165
+ const backend = renderer.backend;
166
+ backend.createStorageAttribute( attr );
167
+
168
+ const buffer = backend.get( attr ).buffer;
169
+ const lanesPerChunk = records.recordsPerChunk * records.lanesPerRecord;
170
+ const bytesPerLane = records.chunks[ 0 ].BYTES_PER_ELEMENT;
171
+ const end = startLane + laneCount;
172
+
173
+ let lane = startLane;
174
+ while ( lane < end ) {
175
+
176
+ const ci = Math.floor( lane / lanesPerChunk );
177
+ const chunk = records.chunks[ ci ];
178
+ const local = lane - ci * lanesPerChunk;
179
+ const n = Math.min( chunk.length - local, end - lane );
180
+ backend.device.queue.writeBuffer( buffer, lane * bytesPerLane, chunk, local, n );
181
+ lane += n;
182
+
183
+ }
184
+
185
+ }
186
+
187
+ const _origUpdateAttribute = WebGPUBackend.prototype.updateAttribute;
188
+
189
+ WebGPUBackend.prototype.updateAttribute = function ( attribute ) {
190
+
191
+ if ( this.attributeUtils._getBufferAttribute( attribute ).isGPUOnly ) return;
192
+ return _origUpdateAttribute.call( this, attribute );
193
+
194
+ };
195
+
72
196
  // ---------------------------------------------------------------------------
73
197
  // 2. Larger timestamp query pool (stats-gl GPU/compute timing)
74
198
  // ---------------------------------------------------------------------------
@@ -226,87 +350,28 @@ export function struct( members, name = null ) {
226
350
  }
227
351
 
228
352
  // ---------------------------------------------------------------------------
229
- // 4. Workgroup-scoped atomic arrays — var<workgroup> array<atomic<T>, N>
353
+ // 4. r186 ContextNode.setup regression — DELETE WHEN THE three DEP REACHES r187
230
354
  // ---------------------------------------------------------------------------
231
- // TSL's `workgroupArray(type, count)` emits `var<workgroup> name: array<T, N>`
232
- // (`WGSLNodeBuilder.getScopedArrays`, r185 ~line 1773) with a NON-atomic element
233
- // type, so `atomicAdd(arr.element(i), v)` fails WGSL validation. AtomicFunctionNode
234
- // is itself address-space-agnostic — it emits `atomicAdd(&name[i], v)`, valid for
235
- // BOTH storage and workgroup pointers — so the ONLY missing piece is the
236
- // declaration. This patch adds `workgroupAtomicArray()` (a `workgroupArray` tagged
237
- // atomic) and overrides `getScopedArrays` to wrap the element type in `atomic<…>`
238
- // for tagged arrays, enabling true on-chip workgroup-shared atomics (e.g. fast
239
- // per-workgroup histograms) instead of the slow global-storage-atomic fallback.
240
- // Access tagged arrays ONLY via atomic ops (atomicAdd/atomicLoad/atomicStore).
241
-
242
- import { workgroupArray } from 'three/tsl';
243
-
244
- const _WorkgroupInfoNode = workgroupArray( 'uint', 1 ).constructor;
245
-
246
- // Tag the node so the patched getScopedArrays emits an atomic element type. The
247
- // array's WGSL name is only known after generate() runs, so we mark the builder's
248
- // scopedArrays entry there (idempotent across analyze/generate passes).
249
- const _origWorkgroupGenerate = _WorkgroupInfoNode.prototype.generate;
250
-
251
- _WorkgroupInfoNode.prototype.generate = function ( builder ) {
355
+ // r186 (#34251) made `ContextNode.setup()` return the node it had just built.
356
+ // Returning it stores the child's output node in the context node's own
357
+ // properties, so `Node.build()` then re-builds it generically OUTSIDE the
358
+ // context. `Loop()` wraps its body in a context (`fnCall.context({ nodeLoop })`),
359
+ // so every loop-heavy kernel is evaluated against the wrong one — measured on our
360
+ // corpus as 63x energy on furnace-clearcoat, 5.8x on furnace-sheen and NaN in the
361
+ // SSS random walk, all while the WGSL compiled without a single error.
362
+ //
363
+ // Upstream reverted it in #34531 (merged 2026-09-11, on dev, ships in r187); this
364
+ // body is that revert verbatim, and is also what r185 shipped, so it stays correct
365
+ // if the dep is rolled back. Verified: with it, all 27 quality scenes pass and the
366
+ // numbers are identical to r185. Drop this section and the `ContextNode` import
367
+ // once the dep is r187+.
252
368
 
253
- const name = _origWorkgroupGenerate.call( this, builder );
254
- if ( this.isAtomicArray === true ) {
369
+ ContextNode.prototype.setup = function ( builder ) {
255
370
 
256
- const entry = builder.scopedArrays && builder.scopedArrays.get( name );
257
- if ( entry ) entry.isAtomic = true;
371
+ const previousContext = builder.addContext( this.value );
258
372
 
259
- }
373
+ this.node.build( builder );
260
374
 
261
- return name;
375
+ builder.setContext( previousContext );
262
376
 
263
377
  };
264
-
265
- /**
266
- * Like `workgroupArray(type, count)` but declares the workgroup buffer with an
267
- * atomic element type: `var<workgroup> name: array<atomic<type>, count>`.
268
- * Elements MUST be accessed only via atomic ops (atomicAdd/atomicLoad/atomicStore).
269
- *
270
- * @param {string} type - Element type (e.g. 'uint').
271
- * @param {number} count - Number of elements.
272
- * @returns {WorkgroupInfoNode} The tagged workgroup array node.
273
- */
274
- export function workgroupAtomicArray( type, count ) {
275
-
276
- const node = workgroupArray( type, count );
277
- node.isAtomicArray = true;
278
- return node;
279
-
280
- }
281
-
282
- let _scopedArraysPatched = false;
283
-
284
- // Override getScopedArrays on the builder's prototype (compute-stage WGSL
285
- // assembly) to emit `atomic<T>` element types for tagged workgroup arrays.
286
- // Installed once, lazily, off the first node builder created.
287
- function _installScopedArrayAtomicPatch( builder ) {
288
-
289
- if ( _scopedArraysPatched ) return;
290
- const proto = Object.getPrototypeOf( builder );
291
- if ( ! proto || typeof proto.getScopedArrays !== 'function' ) return;
292
-
293
- proto.getScopedArrays = function ( shaderStage ) {
294
-
295
- if ( shaderStage !== 'compute' ) return;
296
-
297
- const snippets = [];
298
- for ( const { name, scope, bufferType, bufferCount, isAtomic } of this.scopedArrays.values() ) {
299
-
300
- const type = this.getType( bufferType );
301
- const elementType = ( isAtomic === true ) ? `atomic< ${ type } >` : type;
302
- snippets.push( `var<${ scope }> ${ name }: array< ${ elementType }, ${ bufferCount } >;` );
303
-
304
- }
305
-
306
- return snippets.join( '\n' );
307
-
308
- };
309
-
310
- _scopedArraysPatched = true;
311
-
312
- }
package/src/index.js CHANGED
@@ -72,6 +72,9 @@ export { OverlayManager } from './managers/OverlayManager.js';
72
72
 
73
73
  // VRAM accounting
74
74
  export { VRAMTracker, bufferBytes, textureBytes } from './Processor/VRAMTracker.js';
75
+ export {
76
+ MemoryLedger, estimateSceneBytes, probeAddressSpace, SAFE_SCENE_BYTES, MAX_SCENE_BYTES,
77
+ } from './Processor/HostMemory.js';
75
78
 
76
79
  // Pipeline infrastructure (for advanced consumers building custom stages)
77
80
  export { RenderPipeline } from './Pipeline/RenderPipeline.js';
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Owns the Three.js AnimationMixer, advances clips each frame,
5
5
  * extracts deformed vertex positions via CPU skinning, and returns
6
- * them in the format expected by PathTracerApp.refitBVH().
6
+ * a per-mesh reader in the shape PathTracerApp.refitBVH() accepts.
7
7
  */
8
8
 
9
9
  import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
@@ -24,7 +24,7 @@ export class AnimationManager extends EventDispatcher {
24
24
  this._mixerRoot = null; // mixer target (GLTF model root for track resolution)
25
25
  this._meshes = null;
26
26
  this._meshTriRanges = null; // { start, count, uniqueVerts, indices }[]
27
- this._posBuffer = null; // Float32Array(triCount * 9) — reused each frame
27
+ this._meshPositions = null; // per-mesh Float32Array(triCount * 9), allocated on first use
28
28
  this._tempVec = new Vector3();
29
29
  this._skinnedCache = null; // per-mesh Float32Array for skinned vertex positions
30
30
  this._clipsCache = null;
@@ -111,14 +111,14 @@ export class AnimationManager extends EventDispatcher {
111
111
  const uniqueVerts = positions.count;
112
112
 
113
113
  this._meshTriRanges.push( { start: offset, count, uniqueVerts, indices } );
114
- // Pre-allocate per-mesh skinned position cache (3 floats per unique vertex)
115
- this._skinnedCache.push( new Float32Array( uniqueVerts * 3 ) );
116
114
  offset += count;
117
115
 
118
116
  }
119
117
 
120
- // Allocate reusable output buffer
121
- this._posBuffer = new Float32Array( offset * 9 );
118
+ // Scratch is per mesh and allocated on first use. One scene-wide output buffer would be
119
+ // 9 floats × every triangle — 1,030 MB at 30M, past what a renderer can allocate.
120
+ this._skinnedCache = [];
121
+ this._meshPositions = [];
122
122
 
123
123
  const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
124
124
  console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles` );
@@ -224,12 +224,12 @@ export class AnimationManager extends EventDispatcher {
224
224
  }
225
225
 
226
226
  /**
227
- * Seek to an absolute time and extract deformed vertex positions.
227
+ * Seek to an absolute time and prepare deformed vertex positions.
228
228
  * Does not require playback — works from any state (stopped, paused, playing).
229
229
  *
230
230
  * @param {number} time - Absolute time in seconds
231
231
  * @param {number} [clipIndex=0] - Clip to evaluate, or -1 for all active
232
- * @returns {Float32Array|null} Position buffer (9 floats/tri) or null if no mixer
232
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no mixer
233
233
  */
234
234
  seekTo( time, clipIndex = 0 ) {
235
235
 
@@ -261,8 +261,7 @@ export class AnimationManager extends EventDispatcher {
261
261
 
262
262
  }
263
263
 
264
- this._computePositions();
265
- return this._posBuffer;
264
+ return this._poseReader();
266
265
 
267
266
  }
268
267
 
@@ -277,10 +276,10 @@ export class AnimationManager extends EventDispatcher {
277
276
  }
278
277
 
279
278
  /**
280
- * Advance animation and extract deformed positions.
279
+ * Advance animation and prepare deformed positions.
281
280
  * Call once per frame from the animate loop.
282
281
  *
283
- * @returns {Float32Array|null} Position buffer (9 floats/tri, original mesh order) or null if not playing
282
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not playing
284
283
  */
285
284
  update() {
286
285
 
@@ -290,100 +289,111 @@ export class AnimationManager extends EventDispatcher {
290
289
  const delta = this.timer.getDelta();
291
290
  this.mixer.update( delta );
292
291
 
293
- this._computePositions();
292
+ return this._poseReader();
293
+
294
+ }
295
+
296
+ /** A reusable per-mesh buffer, grown only when that mesh is first skinned. @private */
297
+ _scratch( cache, index, length ) {
294
298
 
295
- return this._posBuffer;
299
+ const have = cache[ index ];
300
+ return have && have.length === length ? have : ( cache[ index ] = new Float32Array( length ) );
296
301
 
297
302
  }
298
303
 
299
304
  /**
300
- * Extract deformed vertex positions from all meshes.
301
- * Uses two-phase approach for indexed geometry:
302
- * Phase 1: Skin all unique vertices per mesh
303
- * Phase 2: Assemble triangles from index buffer
305
+ * Settle the pose, then hand back a reader that skins one mesh at a time. The refit pulls each
306
+ * mesh as it needs it, so only one mesh's worth of deformed positions exists at any moment.
304
307
  * @private
305
308
  */
306
- _computePositions() {
309
+ _poseReader() {
307
310
 
308
- const tempVec = this._tempVec;
309
- const output = this._posBuffer;
310
-
311
- // Update mixer root subtree — bones live outside mesh subtrees so
312
- // per-mesh updateMatrixWorld() misses them. Using mixerRoot (not full
313
- // scene) avoids recomputing matrices for unrelated static objects.
311
+ // Bones live outside mesh subtrees so per-mesh updateMatrixWorld() misses them. Doing it
312
+ // once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
314
313
  this._mixerRoot.updateMatrixWorld( true );
315
314
 
316
- for ( let m = 0; m < this._meshes.length; m ++ ) {
315
+ return m => this._computeMeshPositions( m );
316
+
317
+ }
317
318
 
318
- const mesh = this._meshes[ m ];
319
- const { start, count, uniqueVerts, indices } = this._meshTriRanges[ m ];
320
- const skinned = this._skinnedCache[ m ];
319
+ /**
320
+ * Skin one mesh into its own buffer. Two phases for indexed geometry: skin the unique
321
+ * vertices, then assemble triangles from the index buffer.
322
+ * @private
323
+ */
324
+ _computeMeshPositions( m ) {
321
325
 
322
- const worldMatrix = mesh.matrixWorld;
326
+ const tempVec = this._tempVec;
327
+ const mesh = this._meshes[ m ];
328
+ const { count, uniqueVerts, indices } = this._meshTriRanges[ m ];
329
+ const skinned = this._scratch( this._skinnedCache, m, uniqueVerts * 3 );
330
+ const output = this._scratch( this._meshPositions, m, count * 9 );
323
331
 
324
- // Phase 1: Compute world-space positions for all unique vertices
325
- for ( let v = 0; v < uniqueVerts; v ++ ) {
332
+ const worldMatrix = mesh.matrixWorld;
326
333
 
327
- // getVertexPosition handles morph targets + bone transforms (local space)
328
- mesh.getVertexPosition( v, tempVec );
329
- // Transform to world space (matches GeometryExtractor behavior)
330
- tempVec.applyMatrix4( worldMatrix );
334
+ // Phase 1: Compute world-space positions for all unique vertices
335
+ for ( let v = 0; v < uniqueVerts; v ++ ) {
331
336
 
332
- skinned[ v * 3 ] = tempVec.x;
333
- skinned[ v * 3 + 1 ] = tempVec.y;
334
- skinned[ v * 3 + 2 ] = tempVec.z;
337
+ // getVertexPosition handles morph targets + bone transforms (local space)
338
+ mesh.getVertexPosition( v, tempVec );
339
+ // Transform to world space (matches GeometryExtractor behavior)
340
+ tempVec.applyMatrix4( worldMatrix );
335
341
 
336
- }
342
+ skinned[ v * 3 ] = tempVec.x;
343
+ skinned[ v * 3 + 1 ] = tempVec.y;
344
+ skinned[ v * 3 + 2 ] = tempVec.z;
337
345
 
338
- // Phase 2: Assemble triangles
339
- if ( indices ) {
346
+ }
340
347
 
341
- for ( let t = 0; t < count; t ++ ) {
348
+ // Phase 2: Assemble triangles
349
+ if ( indices ) {
342
350
 
343
- const t3 = t * 3;
344
- const i0 = indices[ t3 ] * 3;
345
- const i1 = indices[ t3 + 1 ] * 3;
346
- const i2 = indices[ t3 + 2 ] * 3;
347
- const o = ( start + t ) * 9;
351
+ for ( let t = 0; t < count; t ++ ) {
348
352
 
349
- output[ o ] = skinned[ i0 ];
350
- output[ o + 1 ] = skinned[ i0 + 1 ];
351
- output[ o + 2 ] = skinned[ i0 + 2 ];
352
- output[ o + 3 ] = skinned[ i1 ];
353
- output[ o + 4 ] = skinned[ i1 + 1 ];
354
- output[ o + 5 ] = skinned[ i1 + 2 ];
355
- output[ o + 6 ] = skinned[ i2 ];
356
- output[ o + 7 ] = skinned[ i2 + 1 ];
357
- output[ o + 8 ] = skinned[ i2 + 2 ];
353
+ const t3 = t * 3;
354
+ const i0 = indices[ t3 ] * 3;
355
+ const i1 = indices[ t3 + 1 ] * 3;
356
+ const i2 = indices[ t3 + 2 ] * 3;
357
+ const o = t * 9;
358
358
 
359
- }
359
+ output[ o ] = skinned[ i0 ];
360
+ output[ o + 1 ] = skinned[ i0 + 1 ];
361
+ output[ o + 2 ] = skinned[ i0 + 2 ];
362
+ output[ o + 3 ] = skinned[ i1 ];
363
+ output[ o + 4 ] = skinned[ i1 + 1 ];
364
+ output[ o + 5 ] = skinned[ i1 + 2 ];
365
+ output[ o + 6 ] = skinned[ i2 ];
366
+ output[ o + 7 ] = skinned[ i2 + 1 ];
367
+ output[ o + 8 ] = skinned[ i2 + 2 ];
360
368
 
361
- } else {
369
+ }
362
370
 
363
- // Non-indexed: vertices are sequential triplets
364
- for ( let t = 0; t < count; t ++ ) {
371
+ } else {
365
372
 
366
- const v0 = ( t * 3 ) * 3;
367
- const v1 = ( t * 3 + 1 ) * 3;
368
- const v2 = ( t * 3 + 2 ) * 3;
369
- const o = ( start + t ) * 9;
373
+ // Non-indexed: vertices are sequential triplets
374
+ for ( let t = 0; t < count; t ++ ) {
370
375
 
371
- output[ o ] = skinned[ v0 ];
372
- output[ o + 1 ] = skinned[ v0 + 1 ];
373
- output[ o + 2 ] = skinned[ v0 + 2 ];
374
- output[ o + 3 ] = skinned[ v1 ];
375
- output[ o + 4 ] = skinned[ v1 + 1 ];
376
- output[ o + 5 ] = skinned[ v1 + 2 ];
377
- output[ o + 6 ] = skinned[ v2 ];
378
- output[ o + 7 ] = skinned[ v2 + 1 ];
379
- output[ o + 8 ] = skinned[ v2 + 2 ];
376
+ const v0 = ( t * 3 ) * 3;
377
+ const v1 = ( t * 3 + 1 ) * 3;
378
+ const v2 = ( t * 3 + 2 ) * 3;
379
+ const o = t * 9;
380
380
 
381
- }
381
+ output[ o ] = skinned[ v0 ];
382
+ output[ o + 1 ] = skinned[ v0 + 1 ];
383
+ output[ o + 2 ] = skinned[ v0 + 2 ];
384
+ output[ o + 3 ] = skinned[ v1 ];
385
+ output[ o + 4 ] = skinned[ v1 + 1 ];
386
+ output[ o + 5 ] = skinned[ v1 + 2 ];
387
+ output[ o + 6 ] = skinned[ v2 ];
388
+ output[ o + 7 ] = skinned[ v2 + 1 ];
389
+ output[ o + 8 ] = skinned[ v2 + 2 ];
382
390
 
383
391
  }
384
392
 
385
393
  }
386
394
 
395
+ return output;
396
+
387
397
  }
388
398
 
389
399
  /**
@@ -433,7 +443,7 @@ export class AnimationManager extends EventDispatcher {
433
443
  this._mixerRoot = null;
434
444
  this._meshes = null;
435
445
  this._meshTriRanges = null;
436
- this._posBuffer = null;
446
+ this._meshPositions = null;
437
447
  this._skinnedCache = null;
438
448
  this._clipsCache = null;
439
449
 
@@ -9,16 +9,17 @@
9
9
 
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
- import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T, normalizeAttenuationDistance } from '../EngineDefaults.js';
12
+ import {
13
+ MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T, normalizeAttenuationDistance,
14
+ TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, TRI_BLOCKER_SHIFT, shadowBlockerBits
15
+ } from '../EngineDefaults.js';
13
16
  import { createLogger, fmt } from '../utils/Logger.js';
14
17
 
15
18
  const log = createLogger( 'material' );
16
19
 
17
20
  const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
18
- // Per-triangle float offsets used by _patchTriangleSideForMaterial / _patchTriangleBlockerForMaterial.
19
- const TRI_MAT_IDX_OFFSET = T.UV_C_MAT_OFFSET + 2; // uvData2.z in shader
20
- const TRI_SIDE_OFFSET = T.NORMAL_C_OFFSET + 3; // normalCData.w in shader
21
- const TRI_BLOCKER_OFFSET = T.NORMAL_A_OFFSET + 3; // nA.w in shader (opaque-blocker fast path)
21
+ // Side and the opaque-blocker bit share the material lane, so a patch is a masked write.
22
+ const TRI_FLAGS_OFFSET = T.MATERIAL_FLAGS_OFFSET;
22
23
 
23
24
  // Material properties that affect the shadow-ray opaque-blocker flag.
24
25
  const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
@@ -722,9 +723,8 @@ export class MaterialDataManager {
722
723
  * @private
723
724
  */
724
725
  /**
725
- * Re-derive the shadow-ray opaque-blocker flag for a material from its
726
- * current buffer values and patch NORMAL_A.w on every matching triangle.
727
- * Kept in sync with the blocker definition in GeometryExtractor.
726
+ * Re-derive the two shadow-blocker bits for a material from its current buffer
727
+ * values and patch them on every matching triangle.
728
728
  * @private
729
729
  */
730
730
  _recomputeOpaqueBlockerForMaterial( materialIndex ) {
@@ -733,36 +733,42 @@ export class MaterialDataManager {
733
733
  if ( ! matBuf ) return;
734
734
 
735
735
  const matStride = materialIndex * M.FLOATS_PER_MATERIAL;
736
- const alphaMode = matBuf[ matStride + M.ALPHA_MODE ] | 0;
737
- const transparent = matBuf[ matStride + M.TRANSPARENT ] | 0;
738
- const transmission = matBuf[ matStride + M.TRANSMISSION ] || 0;
739
- const opacity = matBuf[ matStride + M.OPACITY ] ?? 1;
740
- const isOpaqueBlocker = ( alphaMode === 0 && transparent === 0 && transmission === 0 && opacity >= 1 ) ? 1.0 : 0.0;
736
+ const bits = shadowBlockerBits( {
737
+ alphaMode: matBuf[ matStride + M.ALPHA_MODE ],
738
+ transparent: matBuf[ matStride + M.TRANSPARENT ],
739
+ transmission: matBuf[ matStride + M.TRANSMISSION ],
740
+ opacity: matBuf[ matStride + M.OPACITY ],
741
+ } );
741
742
 
742
- this._patchTriangleFlagForMaterial( materialIndex, TRI_BLOCKER_OFFSET, isOpaqueBlocker );
743
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_BLOCKER_SHIFT, 2, bits );
743
744
 
744
745
  }
745
746
 
746
747
  /**
747
- * Generic helper: patch a single per-triangle float at `triOffset` for every
748
- * triangle whose materialIndex matches, then fire onTriangleDataChanged.
748
+ * Generic helper: rewrite `width` bits at `shift` in the flags lane of every triangle
749
+ * whose materialIndex matches, then fire onTriangleDataChanged.
749
750
  * @private
750
751
  */
751
- _patchTriangleFlagForMaterial( materialIndex, triOffset, value ) {
752
+ _patchTriangleFlagForMaterial( materialIndex, shift, width, value ) {
752
753
 
753
754
  const triInfo = this.callbacks.getTriangleData?.();
754
- const triData = triInfo?.array;
755
+ // Chunked past the ~2 GB array cap; a flat array is the single-chunk case.
756
+ const records = triInfo?.records;
757
+ const flat = records ? null : triInfo?.array;
755
758
  const triCount = triInfo?.count | 0;
756
- if ( ! triData || triCount === 0 ) return;
759
+ if ( ( ! flat && ! records ) || triCount === 0 ) return;
757
760
 
758
761
  const stride = T.FLOATS_PER_TRIANGLE;
762
+ const mask = ( ( ( 1 << width ) - 1 ) << shift ) >>> 0;
763
+ const bits = ( ( value << shift ) & mask ) >>> 0;
759
764
  let patched = 0;
760
765
  for ( let i = 0; i < triCount; i ++ ) {
761
766
 
762
- const base = i * stride;
763
- if ( triData[ base + TRI_MAT_IDX_OFFSET ] === materialIndex ) {
767
+ const triData = records ? records.chunkFor( i ) : flat;
768
+ const base = records ? records.baseOf( i ) : i * stride;
769
+ if ( ( triData[ base + TRI_FLAGS_OFFSET ] & TRI_MATERIAL_MASK ) === materialIndex ) {
764
770
 
765
- triData[ base + triOffset ] = value;
771
+ triData[ base + TRI_FLAGS_OFFSET ] = ( ( triData[ base + TRI_FLAGS_OFFSET ] & ~ mask ) | bits ) >>> 0;
766
772
  patched ++;
767
773
 
768
774
  }
@@ -779,7 +785,7 @@ export class MaterialDataManager {
779
785
 
780
786
  _patchTriangleSideForMaterial( materialIndex, sideValue ) {
781
787
 
782
- this._patchTriangleFlagForMaterial( materialIndex, TRI_SIDE_OFFSET, sideValue );
788
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_SIDE_SHIFT, 2, sideValue );
783
789
 
784
790
  }
785
791