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.
- package/README.md +164 -0
- package/dist/rayzee.es.js +6728 -4735
- package/dist/rayzee.umd.js +121 -73
- package/package.json +2 -2
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +151 -25
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +265 -23
- package/src/Processor/BVHBuilder.js +48 -32
- package/src/Processor/BVHRefitter.js +244 -99
- package/src/Processor/ChunkedRecords.js +302 -0
- package/src/Processor/EmissiveTriangleBuilder.js +48 -21
- package/src/Processor/GeometryExtractor.js +580 -93
- package/src/Processor/HostMemory.js +222 -0
- package/src/Processor/InstanceTable.js +548 -97
- package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +19 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +257 -50
- package/src/Processor/PackedRayBuffer.js +29 -18
- package/src/Processor/ParallelBVHBuilder.js +9 -8
- package/src/Processor/QueueManager.js +5 -3
- package/src/Processor/SceneProcessor.js +1046 -365
- package/src/Processor/TLASBuilder.js +546 -201
- package/src/Processor/TextureCreator.js +11 -9
- package/src/Processor/VRAMTracker.js +12 -3
- package/src/Processor/Workers/BVHRefitWorker.js +16 -11
- package/src/Processor/Workers/BVHWorker.js +12 -12
- package/src/Processor/Workers/TLASWorker.js +37 -0
- package/src/Processor/utils.js +8 -20
- package/src/Stages/NormalDepth.js +6 -2
- package/src/Stages/PathTracer.js +5 -5
- package/src/Stages/PathTracerStage.js +163 -41
- package/src/TSL/BVHTraversal.js +208 -90
- package/src/TSL/Common.js +135 -2
- package/src/TSL/Displacement.js +21 -11
- package/src/TSL/EmissiveSampling.js +49 -25
- package/src/TSL/ExtendKernel.js +3 -2
- package/src/TSL/LightBVHSampling.js +7 -4
- package/src/TSL/LightsDirect.js +30 -29
- package/src/TSL/ShadeKernel.js +8 -6
- package/src/TSL/SortGlobalKernels.js +3 -4
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +149 -84
- package/src/index.js +3 -0
- package/src/managers/AnimationManager.js +86 -76
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
package/src/TSL/patches.js
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
353
|
+
// 4. r186 ContextNode.setup regression — DELETE WHEN THE three DEP REACHES r187
|
|
230
354
|
// ---------------------------------------------------------------------------
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
254
|
-
if ( this.isAtomicArray === true ) {
|
|
369
|
+
ContextNode.prototype.setup = function ( builder ) {
|
|
255
370
|
|
|
256
|
-
|
|
257
|
-
if ( entry ) entry.isAtomic = true;
|
|
371
|
+
const previousContext = builder.addContext( this.value );
|
|
258
372
|
|
|
259
|
-
|
|
373
|
+
this.node.build( builder );
|
|
260
374
|
|
|
261
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
121
|
-
|
|
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
|
|
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}
|
|
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.
|
|
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
|
|
279
|
+
* Advance animation and prepare deformed positions.
|
|
281
280
|
* Call once per frame from the animate loop.
|
|
282
281
|
*
|
|
283
|
-
* @returns {Float32Array|null}
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
301
|
-
*
|
|
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
|
-
|
|
309
|
+
_poseReader() {
|
|
307
310
|
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
315
|
+
return m => this._computeMeshPositions( m );
|
|
316
|
+
|
|
317
|
+
}
|
|
317
318
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
for ( let v = 0; v < uniqueVerts; v ++ ) {
|
|
332
|
+
const worldMatrix = mesh.matrixWorld;
|
|
326
333
|
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
if ( indices ) {
|
|
346
|
+
}
|
|
340
347
|
|
|
341
|
-
|
|
348
|
+
// Phase 2: Assemble triangles
|
|
349
|
+
if ( indices ) {
|
|
342
350
|
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
}
|
|
369
|
+
}
|
|
362
370
|
|
|
363
|
-
|
|
364
|
-
for ( let t = 0; t < count; t ++ ) {
|
|
371
|
+
} else {
|
|
365
372
|
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
//
|
|
19
|
-
const
|
|
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-
|
|
726
|
-
*
|
|
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
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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,
|
|
743
|
+
this._patchTriangleFlagForMaterial( materialIndex, TRI_BLOCKER_SHIFT, 2, bits );
|
|
743
744
|
|
|
744
745
|
}
|
|
745
746
|
|
|
746
747
|
/**
|
|
747
|
-
* Generic helper:
|
|
748
|
-
*
|
|
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,
|
|
752
|
+
_patchTriangleFlagForMaterial( materialIndex, shift, width, value ) {
|
|
752
753
|
|
|
753
754
|
const triInfo = this.callbacks.getTriangleData?.();
|
|
754
|
-
|
|
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 ( !
|
|
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
|
|
763
|
-
|
|
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 +
|
|
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,
|
|
788
|
+
this._patchTriangleFlagForMaterial( materialIndex, TRI_SIDE_SHIFT, 2, sideValue );
|
|
783
789
|
|
|
784
790
|
}
|
|
785
791
|
|