rayzee 7.31.1 → 8.0.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 +6366 -4361
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +120 -19
- 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 +577 -92
- 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/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/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +128 -3
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping } from "three";
|
|
2
|
-
import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
|
|
2
|
+
import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
|
|
3
3
|
import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
|
|
4
4
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
5
5
|
import { linearToSRGB } from './ToneMapCPU.js';
|
|
@@ -1148,12 +1148,15 @@ export class TextureCreator {
|
|
|
1148
1148
|
|
|
1149
1149
|
flattenBVH( bvhRoot );
|
|
1150
1150
|
|
|
1151
|
-
// Layout: 4 vec4 per node (16 floats)
|
|
1151
|
+
// Layout: 4 vec4 per node (16 floats). Bounds are floats; every index field is a u32
|
|
1152
|
+
// bit pattern (see EngineDefaults.BVH_LEAF_MARKERS).
|
|
1152
1153
|
// Inner: [leftMin.xyz, leftChild] [leftMax.xyz, rightChild] [rightMin.xyz, 0] [rightMax.xyz, 0]
|
|
1153
|
-
// Leaf: [triOffset, triCount, 0,
|
|
1154
|
+
// Leaf: [triOffset, triCount, 0, TRIANGLE_LEAF] [0,0,0,0] [0,0,0,0] [0,0,0,0]
|
|
1154
1155
|
const floatsPerNode = TEXTURE_CONSTANTS.VEC4_PER_BVH_NODE * TEXTURE_CONSTANTS.FLOATS_PER_VEC4;
|
|
1156
|
+
assertBVHIndexFits( nodes.length, 'BVH node count' );
|
|
1155
1157
|
const size = nodes.length * floatsPerNode;
|
|
1156
1158
|
const data = new Float32Array( size );
|
|
1159
|
+
const idx = bvhIndexView( data ); // index fields are u32 bit patterns
|
|
1157
1160
|
|
|
1158
1161
|
for ( let i = 0; i < nodes.length; i ++ ) {
|
|
1159
1162
|
|
|
@@ -1171,12 +1174,12 @@ export class TextureCreator {
|
|
|
1171
1174
|
data[ stride ] = left.boundsMin.x;
|
|
1172
1175
|
data[ stride + 1 ] = left.boundsMin.y;
|
|
1173
1176
|
data[ stride + 2 ] = left.boundsMin.z;
|
|
1174
|
-
|
|
1177
|
+
idx[ stride + 3 ] = leftIdx;
|
|
1175
1178
|
|
|
1176
1179
|
data[ stride + 4 ] = left.boundsMax.x;
|
|
1177
1180
|
data[ stride + 5 ] = left.boundsMax.y;
|
|
1178
1181
|
data[ stride + 6 ] = left.boundsMax.z;
|
|
1179
|
-
|
|
1182
|
+
idx[ stride + 7 ] = rightIdx;
|
|
1180
1183
|
|
|
1181
1184
|
data[ stride + 8 ] = right.boundsMin.x;
|
|
1182
1185
|
data[ stride + 9 ] = right.boundsMin.y;
|
|
@@ -1188,10 +1191,9 @@ export class TextureCreator {
|
|
|
1188
1191
|
|
|
1189
1192
|
} else {
|
|
1190
1193
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
data[ stride + 3 ] = - 1; // Leaf marker
|
|
1194
|
+
idx[ stride ] = node.triangleOffset;
|
|
1195
|
+
idx[ stride + 1 ] = node.triangleCount;
|
|
1196
|
+
idx[ stride + 3 ] = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
|
|
1195
1197
|
|
|
1196
1198
|
}
|
|
1197
1199
|
|
|
@@ -32,10 +32,10 @@ function texelBytes( tex ) {
|
|
|
32
32
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Exact byte size of a
|
|
35
|
+
/** Exact GPU byte size of a buffer attribute. GPU-only ones carry no backing array. */
|
|
36
36
|
export function bufferBytes( attr ) {
|
|
37
37
|
|
|
38
|
-
return attr?.array?.byteLength
|
|
38
|
+
return attr?.gpuByteLength ?? attr?.array?.byteLength ?? 0;
|
|
39
39
|
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -141,7 +141,16 @@ export class VRAMTracker {
|
|
|
141
141
|
// synthetic { bytes } (e.g. attributeArray-backed histograms)
|
|
142
142
|
if ( typeof r.bytes === 'number' && ! r.isTexture && ! r.isRenderTarget ) return r.bytes;
|
|
143
143
|
|
|
144
|
-
// buffer attribute — dedupe by backing array
|
|
144
|
+
// buffer attribute — dedupe by backing array, or by the attribute itself when it is
|
|
145
|
+
// GPU-only and has none (rw/ro nodes share one buffer either way)
|
|
146
|
+
if ( r.gpuByteLength != null ) {
|
|
147
|
+
|
|
148
|
+
if ( seen.has( r ) ) return 0;
|
|
149
|
+
seen.add( r );
|
|
150
|
+
return r.gpuByteLength;
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
if ( r.array && r.array.byteLength != null ) {
|
|
146
155
|
|
|
147
156
|
if ( seen.has( r.array ) ) return 0;
|
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* BVHRefitWorker — Off-main-thread BVH refit using SharedArrayBuffer.
|
|
3
3
|
*
|
|
4
4
|
* Protocol:
|
|
5
|
-
* 'init' → receives
|
|
6
|
-
* 'refit' →
|
|
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
|
|
7
|
+
*
|
|
8
|
+
* Positions never reach here: the main thread scatters each mesh into the shared triangle
|
|
9
|
+
* records as it reads it, so no buffer the size of the scene exists on either side.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { BVHRefitter } from '../BVHRefitter.js';
|
|
13
|
+
import { ChunkedRecords } from '../ChunkedRecords.js';
|
|
10
14
|
|
|
11
|
-
const FLOATS_PER_NODE = 16;
|
|
12
15
|
const refitter = new BVHRefitter();
|
|
13
16
|
|
|
14
17
|
// Cached shared memory views (set once on 'init', reused every frame)
|
|
15
18
|
let bvhData = null;
|
|
16
19
|
let triData = null;
|
|
17
|
-
let posData = null;
|
|
18
|
-
let bvhToOriginal = null;
|
|
19
20
|
let nodeCount = 0;
|
|
20
21
|
|
|
21
22
|
self.onmessage = function ( e ) {
|
|
@@ -24,11 +25,16 @@ self.onmessage = function ( e ) {
|
|
|
24
25
|
|
|
25
26
|
if ( type === 'init' ) {
|
|
26
27
|
|
|
27
|
-
bvhData =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
bvhData = ChunkedRecords.adopt(
|
|
29
|
+
e.data.sharedBvhBufs.map( buf => new Float32Array( buf ) ),
|
|
30
|
+
e.data.bvhRecordCount, 16, e.data.bvhRecordsPerChunk
|
|
31
|
+
);
|
|
32
|
+
// Triangles arrive as one shared buffer per chunk; a single-chunk scene is the usual case.
|
|
33
|
+
triData = ChunkedRecords.adopt(
|
|
34
|
+
e.data.sharedTriBufs.map( buf => new Uint32Array( buf ) ),
|
|
35
|
+
e.data.triRecordCount, e.data.triLanesPerRecord, e.data.triRecordsPerChunk
|
|
36
|
+
);
|
|
37
|
+
nodeCount = bvhData.recordCount;
|
|
32
38
|
return;
|
|
33
39
|
|
|
34
40
|
}
|
|
@@ -39,7 +45,6 @@ self.onmessage = function ( e ) {
|
|
|
39
45
|
|
|
40
46
|
const startTime = performance.now();
|
|
41
47
|
|
|
42
|
-
refitter.updateTrianglePositions( triData, posData, bvhToOriginal );
|
|
43
48
|
refitter.refit( bvhData, triData, nodeCount );
|
|
44
49
|
|
|
45
50
|
self.postMessage( {
|
|
@@ -3,7 +3,7 @@ import { createLogger, fmt, applyWorkerLogLevel } from '../../utils/Logger.js';
|
|
|
3
3
|
|
|
4
4
|
const log = createLogger( 'bvh' );
|
|
5
5
|
|
|
6
|
-
const FPT =
|
|
6
|
+
const FPT = 20; // FLOATS_PER_TRIANGLE — the record is uint lanes, not floats
|
|
7
7
|
|
|
8
8
|
// --- Message dispatcher ---
|
|
9
9
|
|
|
@@ -60,7 +60,7 @@ function handlePhase1( data ) {
|
|
|
60
60
|
} : null;
|
|
61
61
|
|
|
62
62
|
// Attach shared buffer views
|
|
63
|
-
builder.triangles = new
|
|
63
|
+
builder.triangles = new Uint32Array( sharedTriangleData );
|
|
64
64
|
builder.centroids = new Float32Array( sharedCentroids );
|
|
65
65
|
builder.bMin = new Float32Array( sharedBMin );
|
|
66
66
|
builder.bMax = new Float32Array( sharedBMax );
|
|
@@ -148,8 +148,8 @@ function handleAssemble( data ) {
|
|
|
148
148
|
|
|
149
149
|
// Reorder triangles using final indices from SharedArrayBuffer
|
|
150
150
|
const indices = new Uint32Array( sharedIndices );
|
|
151
|
-
const src = new
|
|
152
|
-
const dst = new
|
|
151
|
+
const src = new Uint32Array( sharedTriangleData );
|
|
152
|
+
const dst = new Uint32Array( sharedReorderBuffer );
|
|
153
153
|
|
|
154
154
|
for ( let i = 0; i < triangleCount; i ++ ) {
|
|
155
155
|
|
|
@@ -219,11 +219,11 @@ function handleFullBuild( data ) {
|
|
|
219
219
|
} : null;
|
|
220
220
|
|
|
221
221
|
const inputTriangles = triangleByteOffset !== undefined
|
|
222
|
-
? new
|
|
223
|
-
: new
|
|
222
|
+
? new Uint32Array( triangleData, triangleByteOffset, triangleByteLength / 4 )
|
|
223
|
+
: new Uint32Array( triangleData );
|
|
224
224
|
|
|
225
225
|
const reorderTarget = sharedReorderBuffer
|
|
226
|
-
? new
|
|
226
|
+
? new Uint32Array( sharedReorderBuffer )
|
|
227
227
|
: null;
|
|
228
228
|
|
|
229
229
|
const bvhRoot = builder.buildSync( inputTriangles, depth, progressCallback, reorderTarget );
|
|
@@ -243,21 +243,21 @@ function handleFullBuild( data ) {
|
|
|
243
243
|
self.postMessage( {
|
|
244
244
|
bvhData,
|
|
245
245
|
originalToBvh,
|
|
246
|
-
triangleCount: inputTriangles.length /
|
|
246
|
+
triangleCount: inputTriangles.length / FPT,
|
|
247
247
|
treeletStats: builder.splitStats
|
|
248
248
|
}, transferables );
|
|
249
249
|
|
|
250
250
|
} else {
|
|
251
251
|
|
|
252
|
-
const
|
|
253
|
-
const triangleCount =
|
|
252
|
+
const reordered = builder.reorderedTriangleData;
|
|
253
|
+
const triangleCount = reordered.byteLength / ( FPT * 4 );
|
|
254
254
|
|
|
255
|
-
const transferables = [ bvhData.buffer,
|
|
255
|
+
const transferables = [ bvhData.buffer, reordered.buffer ];
|
|
256
256
|
if ( originalToBvh ) transferables.push( originalToBvh.buffer );
|
|
257
257
|
|
|
258
258
|
self.postMessage( {
|
|
259
259
|
bvhData,
|
|
260
|
-
triangles:
|
|
260
|
+
triangles: reordered,
|
|
261
261
|
originalToBvh,
|
|
262
262
|
triangleCount,
|
|
263
263
|
treeletStats: builder.splitStats
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the TLAS off the main thread.
|
|
3
|
+
*
|
|
4
|
+
* Only the SAH tree runs here — it is the O(n log n) half, and at 6M placements it pinned the
|
|
5
|
+
* main thread for 25 s. The leaf payloads (BLAS pointer, visibility, world-to-object matrix)
|
|
6
|
+
* are filled in by the caller afterwards: that pass is O(n) and cheap, and keeping it out of
|
|
7
|
+
* the message avoids shipping a 16-float matrix per instance across the boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TLASBuilder } from '../TLASBuilder.js';
|
|
11
|
+
import { createLogger, fmt, applyWorkerLogLevel } from '../../utils/Logger.js';
|
|
12
|
+
|
|
13
|
+
const log = createLogger( 'bvh' );
|
|
14
|
+
|
|
15
|
+
self.onmessage = function ( e ) {
|
|
16
|
+
|
|
17
|
+
const { aabbs, count, logLevel } = e.data;
|
|
18
|
+
applyWorkerLogLevel( logLevel );
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
// A fresh builder per message sizes its buffer exactly, so the result's ArrayBuffer can
|
|
24
|
+
// be transferred whole rather than copied out of a reused, oversized one.
|
|
25
|
+
const { data, nodeCount } = new TLASBuilder().buildStructure( new Float64Array( aabbs ), count );
|
|
26
|
+
log.debug( `TLAS ${fmt.n( nodeCount )} nodes in ${fmt.ms( performance.now() - start )}` );
|
|
27
|
+
|
|
28
|
+
self.postMessage( { tlasData: data, nodeCount }, [ data.buffer ] );
|
|
29
|
+
|
|
30
|
+
} catch ( error ) {
|
|
31
|
+
|
|
32
|
+
log.error( 'TLAS build failed:', error );
|
|
33
|
+
self.postMessage( { error: error.message } );
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
};
|
|
@@ -167,8 +167,9 @@ export class NormalDepth extends RenderStage {
|
|
|
167
167
|
|
|
168
168
|
if ( pt.triangleStorageAttr && ! this._triStorageNode ) {
|
|
169
169
|
|
|
170
|
+
// uvec4, matching PathTracerStage: packed lanes must keep their exact bit pattern.
|
|
170
171
|
this._triStorageNode = storage(
|
|
171
|
-
pt.triangleStorageAttr, '
|
|
172
|
+
pt.triangleStorageAttr, 'uvec4', pt.triangleStorageAttr.count
|
|
172
173
|
).toReadOnly();
|
|
173
174
|
|
|
174
175
|
}
|
|
@@ -270,7 +271,10 @@ export class NormalDepth extends RenderStage {
|
|
|
270
271
|
const uvTangent = vec4( 0.0 ).toVar();
|
|
271
272
|
If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
|
|
272
273
|
|
|
273
|
-
uvTangent.assign( triangleUVTangent(
|
|
274
|
+
uvTangent.assign( triangleUVTangent(
|
|
275
|
+
triStorage, hit.triangleIndex, hit.normal, material.normalTransform,
|
|
276
|
+
bvhStorage, hit.instanceLeaf
|
|
277
|
+
) );
|
|
274
278
|
|
|
275
279
|
} );
|
|
276
280
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { uniform, texture, storage } from 'three/tsl';
|
|
8
|
-
import {
|
|
8
|
+
import { gpuOnlyStorageAttribute } from '../TSL/patches.js';
|
|
9
9
|
import { PathTracerStage } from './PathTracerStage.js';
|
|
10
10
|
import { PackedRayBuffer, GBUFFER_STRIDE, RAY_STRIDE, HIT_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
11
11
|
import { QueueManager, COUNTER, ENERGY_SCALE } from '../Processor/QueueManager.js';
|
|
@@ -993,7 +993,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
993
993
|
// canonicalize; u32 stores the bits verbatim.
|
|
994
994
|
const gBufferVec4s = B * GBUFFER_STRIDE;
|
|
995
995
|
freeStorageAttribute( this.renderer, this._gBufferAttr );
|
|
996
|
-
this._gBufferAttr =
|
|
996
|
+
this._gBufferAttr = gpuOnlyStorageAttribute( gBufferVec4s, 4, Uint32Array );
|
|
997
997
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
998
998
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
999
999
|
|
|
@@ -1009,15 +1009,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
1009
1009
|
|
|
1010
1010
|
// Tier-1 convergence: per-pixel running mean of luminance² (Welford second moment), read+written by FinalWrite.
|
|
1011
1011
|
freeStorageAttribute( this.renderer, this._m2Attr );
|
|
1012
|
-
this._m2Attr =
|
|
1012
|
+
this._m2Attr = gpuOnlyStorageAttribute( maxPixels, 1 );
|
|
1013
1013
|
|
|
1014
1014
|
// Tier-2: per-pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
|
|
1015
1015
|
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
1016
|
-
this._streakAttr =
|
|
1016
|
+
this._streakAttr = gpuOnlyStorageAttribute( maxPixels, 1, Uint32Array );
|
|
1017
1017
|
|
|
1018
1018
|
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
1019
1019
|
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
1020
|
-
this._frozenMaskAttr =
|
|
1020
|
+
this._frozenMaskAttr = gpuOnlyStorageAttribute( maxPixels, 1, Uint32Array );
|
|
1021
1021
|
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { storage } from 'three/tsl';
|
|
2
|
+
import { gpuOnlyStorageAttribute, uploadStorageChunkRange, uploadStorageChunks } from '../TSL/patches.js';
|
|
2
3
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
3
4
|
import {
|
|
4
5
|
NearestFilter, Vector2, Matrix4,
|
|
@@ -109,17 +110,21 @@ export class PathTracerStage extends RenderStage {
|
|
|
109
110
|
// Initialize material data manager
|
|
110
111
|
this.materialData = new MaterialDataManager( this.sdfs );
|
|
111
112
|
this.materialData.callbacks.onReset = () => this.reset();
|
|
112
|
-
// Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The
|
|
113
|
-
//
|
|
114
|
-
// which isn't populated on the PathTracerApp build
|
|
115
|
-
//
|
|
113
|
+
// Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The authoritative
|
|
114
|
+
// CPU copy is `_triangleRecords` when the scene is chunked and triangleStorageAttr.array
|
|
115
|
+
// otherwise (not sdfs.triangleData, which isn't populated on the PathTracerApp build
|
|
116
|
+
// path). The patch mutates it in place, then the GPU copy is refreshed.
|
|
116
117
|
this.materialData.callbacks.getTriangleData = () => ( {
|
|
117
118
|
array: this.triangleStorageAttr?.array,
|
|
119
|
+
records: this._triangleRecords,
|
|
118
120
|
count: this.triangleCount,
|
|
119
121
|
} );
|
|
120
122
|
this.materialData.callbacks.onTriangleDataChanged = () => {
|
|
121
123
|
|
|
122
|
-
if ( this.triangleStorageAttr )
|
|
124
|
+
if ( ! this.triangleStorageAttr ) return;
|
|
125
|
+
// A chunked attribute owns no CPU array for three.js to re-upload.
|
|
126
|
+
if ( this._triangleRecords ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
|
|
127
|
+
else this.triangleStorageAttr.needsUpdate = true;
|
|
123
128
|
|
|
124
129
|
};
|
|
125
130
|
|
|
@@ -171,11 +176,13 @@ export class PathTracerStage extends RenderStage {
|
|
|
171
176
|
|
|
172
177
|
// Triangle data (storage buffer for WebGPU)
|
|
173
178
|
this.triangleStorageAttr = null;
|
|
179
|
+
this._triangleRecords = null;
|
|
174
180
|
this.triangleStorageNode = null;
|
|
175
181
|
this.triangleCount = 0;
|
|
176
182
|
|
|
177
183
|
// BVH data (storage buffer for WebGPU)
|
|
178
184
|
this.bvhStorageAttr = null;
|
|
185
|
+
this._bvhRecords = null;
|
|
179
186
|
this.bvhStorageNode = null;
|
|
180
187
|
this.bvhNodeCount = 0;
|
|
181
188
|
|
|
@@ -704,19 +711,28 @@ export class PathTracerStage extends RenderStage {
|
|
|
704
711
|
* On first call, creates the storage buffer and node.
|
|
705
712
|
* On subsequent calls, creates a new attribute with the correct size
|
|
706
713
|
* and updates the storage node's value to preserve shader graph references.
|
|
707
|
-
* @param {
|
|
714
|
+
* @param {Uint32Array} triangleData - Packed triangle records (uvec4 lanes)
|
|
708
715
|
* @param {number} triangleCount - Number of triangles
|
|
709
716
|
*/
|
|
710
717
|
setTriangleData( triangleData, triangleCount ) {
|
|
711
718
|
|
|
712
719
|
if ( ! triangleData ) return;
|
|
713
720
|
|
|
714
|
-
|
|
721
|
+
// Past the ~2 GB array cap the records arrive as several chunks; they still become one
|
|
722
|
+
// GPU buffer, written at their running byte offsets, so no binding or shader changes.
|
|
723
|
+
const chunked = triangleData.chunks && triangleData.chunks.length > 1 ? triangleData : null;
|
|
724
|
+
const flat = chunked ? null : ( triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData );
|
|
725
|
+
const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
|
|
726
|
+
const vec4Count = lanes / 4;
|
|
727
|
+
|
|
728
|
+
const makeAttr = () => chunked
|
|
729
|
+
? gpuOnlyStorageAttribute( vec4Count, 4, Uint32Array )
|
|
730
|
+
: new StorageInstancedBufferAttribute( flat, 4 );
|
|
715
731
|
|
|
716
732
|
if ( this.triangleStorageNode ) {
|
|
717
733
|
|
|
718
734
|
// Create new attribute with correct size (old one is GC'd, backend WeakMap cleans up GPU buffer)
|
|
719
|
-
this.triangleStorageAttr =
|
|
735
|
+
this.triangleStorageAttr = makeAttr();
|
|
720
736
|
|
|
721
737
|
// Update storage node references (preserves compiled shader graph)
|
|
722
738
|
this.triangleStorageNode.value = this.triangleStorageAttr;
|
|
@@ -725,11 +741,14 @@ export class PathTracerStage extends RenderStage {
|
|
|
725
741
|
} else {
|
|
726
742
|
|
|
727
743
|
// First time: create storage buffer and node
|
|
728
|
-
this.triangleStorageAttr =
|
|
729
|
-
this.triangleStorageNode = storage( this.triangleStorageAttr, '
|
|
744
|
+
this.triangleStorageAttr = makeAttr();
|
|
745
|
+
this.triangleStorageNode = storage( this.triangleStorageAttr, 'uvec4', vec4Count ).toReadOnly();
|
|
730
746
|
|
|
731
747
|
}
|
|
732
748
|
|
|
749
|
+
this._triangleRecords = chunked;
|
|
750
|
+
if ( chunked ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, chunked.chunks );
|
|
751
|
+
|
|
733
752
|
this.triangleCount = triangleCount;
|
|
734
753
|
|
|
735
754
|
log.debug( `${fmt.n( this.triangleCount )} triangles (storage buffer)` );
|
|
@@ -744,21 +763,33 @@ export class PathTracerStage extends RenderStage {
|
|
|
744
763
|
|
|
745
764
|
if ( ! bvhImageData ) return;
|
|
746
765
|
|
|
747
|
-
|
|
766
|
+
// Same story as the triangles: past the ~2 GB array cap the nodes arrive as several
|
|
767
|
+
// chunks and still become one GPU buffer.
|
|
768
|
+
const chunked = bvhImageData.chunks && bvhImageData.chunks.length > 1 ? bvhImageData : null;
|
|
769
|
+
const flat = chunked ? null : ( bvhImageData.chunks ? bvhImageData.chunks[ 0 ] : bvhImageData );
|
|
770
|
+
const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
|
|
771
|
+
const vec4Count = lanes / 4;
|
|
772
|
+
|
|
773
|
+
const makeAttr = () => chunked
|
|
774
|
+
? gpuOnlyStorageAttribute( vec4Count, 4, Float32Array )
|
|
775
|
+
: new StorageInstancedBufferAttribute( flat, 4 );
|
|
748
776
|
|
|
749
777
|
if ( this.bvhStorageNode ) {
|
|
750
778
|
|
|
751
|
-
this.bvhStorageAttr =
|
|
779
|
+
this.bvhStorageAttr = makeAttr();
|
|
752
780
|
this.bvhStorageNode.value = this.bvhStorageAttr;
|
|
753
781
|
this.bvhStorageNode.bufferCount = vec4Count;
|
|
754
782
|
|
|
755
783
|
} else {
|
|
756
784
|
|
|
757
|
-
this.bvhStorageAttr =
|
|
785
|
+
this.bvhStorageAttr = makeAttr();
|
|
758
786
|
this.bvhStorageNode = storage( this.bvhStorageAttr, 'vec4', vec4Count ).toReadOnly();
|
|
759
787
|
|
|
760
788
|
}
|
|
761
789
|
|
|
790
|
+
this._bvhRecords = chunked;
|
|
791
|
+
if ( chunked ) uploadStorageChunks( this.renderer, this.bvhStorageAttr, chunked.chunks );
|
|
792
|
+
|
|
762
793
|
this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
|
|
763
794
|
log.debug( `${fmt.n( this.bvhNodeCount )} BVH nodes (storage buffer)` );
|
|
764
795
|
|
|
@@ -784,13 +815,31 @@ export class PathTracerStage extends RenderStage {
|
|
|
784
815
|
|
|
785
816
|
if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
|
|
786
817
|
|
|
787
|
-
|
|
818
|
+
this._patchVisibilityFromMeshes( meshes );
|
|
819
|
+
this._flushBVHEdits();
|
|
788
820
|
|
|
789
|
-
|
|
821
|
+
}
|
|
790
822
|
|
|
791
|
-
|
|
823
|
+
/**
|
|
824
|
+
* Resolve each placement's visibility from the object it came from. Entries are per
|
|
825
|
+
* instance, so an InstancedMesh covers a whole run of them with one authored flag.
|
|
826
|
+
* @private
|
|
827
|
+
*/
|
|
828
|
+
_patchVisibilityFromMeshes( meshes ) {
|
|
792
829
|
|
|
793
|
-
|
|
830
|
+
const table = this._instanceTable;
|
|
831
|
+
const cache = new Map();
|
|
832
|
+
|
|
833
|
+
for ( let i = 0; i < table.count; i ++ ) {
|
|
834
|
+
|
|
835
|
+
if ( ! table.isSet[ i ] ) continue;
|
|
836
|
+
|
|
837
|
+
const src = table.sourceMesh[ i ];
|
|
838
|
+
let visible = cache.get( src );
|
|
839
|
+
if ( visible === undefined ) cache.set( src, visible = this._isWorldVisible( meshes[ src ] ) );
|
|
840
|
+
this._patchTLASLeafVisibility( i, visible );
|
|
841
|
+
|
|
842
|
+
}
|
|
794
843
|
|
|
795
844
|
}
|
|
796
845
|
|
|
@@ -802,7 +851,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
802
851
|
updateMeshVisibility( meshIndex, visible ) {
|
|
803
852
|
|
|
804
853
|
if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
|
|
805
|
-
|
|
854
|
+
this._flushBVHEdits();
|
|
806
855
|
|
|
807
856
|
}
|
|
808
857
|
|
|
@@ -814,13 +863,8 @@ export class PathTracerStage extends RenderStage {
|
|
|
814
863
|
|
|
815
864
|
if ( ! this._meshRefs || ! this._instanceTable ) return;
|
|
816
865
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
this._patchTLASLeafVisibility( i, this._isWorldVisible( this._meshRefs[ i ] ) );
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
|
|
866
|
+
this._patchVisibilityFromMeshes( this._meshRefs );
|
|
867
|
+
this._flushBVHEdits();
|
|
824
868
|
|
|
825
869
|
}
|
|
826
870
|
|
|
@@ -831,15 +875,51 @@ export class PathTracerStage extends RenderStage {
|
|
|
831
875
|
*/
|
|
832
876
|
_patchTLASLeafVisibility( meshIndex, visible ) {
|
|
833
877
|
|
|
834
|
-
const
|
|
835
|
-
if ( !
|
|
878
|
+
const table = this._instanceTable;
|
|
879
|
+
if ( ! table || ! table.isSet?.[ meshIndex ] || ! this.bvhStorageAttr ) return false;
|
|
836
880
|
|
|
837
|
-
|
|
838
|
-
|
|
881
|
+
const leaf = table.tlasLeafIndex[ meshIndex ];
|
|
882
|
+
if ( leaf < 0 ) return false;
|
|
883
|
+
|
|
884
|
+
table.visible[ meshIndex ] = visible ? 1 : 0;
|
|
885
|
+
|
|
886
|
+
// A chunked attribute owns no CPU array; write into the chunk that holds this leaf.
|
|
887
|
+
if ( this._bvhRecords ) this._bvhRecords.chunkFor( leaf )[ this._bvhRecords.baseOf( leaf ) + 2 ] = visible ? 1.0 : 0.0;
|
|
888
|
+
else this.bvhStorageAttr.array[ leaf * 16 + 2 ] = visible ? 1.0 : 0.0;
|
|
889
|
+
|
|
890
|
+
this._dirtyBVHLeaves ??= new Set();
|
|
891
|
+
this._dirtyBVHLeaves.add( leaf );
|
|
839
892
|
return true;
|
|
840
893
|
|
|
841
894
|
}
|
|
842
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Push BVH edits to the GPU. A chunked attribute cannot go through three.js's dirty flag,
|
|
898
|
+
* so the touched leaves are written straight into the buffer.
|
|
899
|
+
* @private
|
|
900
|
+
*/
|
|
901
|
+
_flushBVHEdits() {
|
|
902
|
+
|
|
903
|
+
if ( ! this.bvhStorageAttr ) return;
|
|
904
|
+
|
|
905
|
+
if ( ! this._bvhRecords ) {
|
|
906
|
+
|
|
907
|
+
this.bvhStorageAttr.needsUpdate = true;
|
|
908
|
+
this._dirtyBVHLeaves?.clear();
|
|
909
|
+
return;
|
|
910
|
+
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
for ( const leaf of this._dirtyBVHLeaves ?? [] ) {
|
|
914
|
+
|
|
915
|
+
uploadStorageChunkRange( this.renderer, this.bvhStorageAttr, this._bvhRecords, leaf * 16, 16 );
|
|
916
|
+
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
this._dirtyBVHLeaves?.clear();
|
|
920
|
+
|
|
921
|
+
}
|
|
922
|
+
|
|
843
923
|
/**
|
|
844
924
|
* Collect mesh references from scene, ordered by meshIndex (assigned during extraction).
|
|
845
925
|
* @param {Object3D} scene
|
|
@@ -903,14 +983,28 @@ export class PathTracerStage extends RenderStage {
|
|
|
903
983
|
/** Update triangle positions in the existing GPU buffer (full). */
|
|
904
984
|
updateTriangleData( triangleData ) {
|
|
905
985
|
|
|
906
|
-
|
|
986
|
+
if ( this._triangleRecords ) {
|
|
987
|
+
|
|
988
|
+
uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
|
|
989
|
+
return;
|
|
990
|
+
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
this._updateStorageBuffer( this.triangleStorageAttr, triangleData?.chunks ? triangleData.chunks[ 0 ] : triangleData );
|
|
907
994
|
|
|
908
995
|
}
|
|
909
996
|
|
|
910
997
|
/** Update BVH node data in the existing GPU buffer (full). */
|
|
911
998
|
updateBVHData( bvhData ) {
|
|
912
999
|
|
|
913
|
-
|
|
1000
|
+
if ( this._bvhRecords ) {
|
|
1001
|
+
|
|
1002
|
+
uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
|
|
1003
|
+
return;
|
|
1004
|
+
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
this._updateStorageBuffer( this.bvhStorageAttr, bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData );
|
|
914
1008
|
|
|
915
1009
|
}
|
|
916
1010
|
|
|
@@ -925,29 +1019,55 @@ export class PathTracerStage extends RenderStage {
|
|
|
925
1019
|
|
|
926
1020
|
if ( this.triangleStorageAttr && triRanges.length > 0 ) {
|
|
927
1021
|
|
|
928
|
-
this.
|
|
1022
|
+
if ( this._triangleRecords ) {
|
|
929
1023
|
|
|
930
|
-
|
|
1024
|
+
// Chunked: three.js cannot re-upload an attribute with no CPU array, so write
|
|
1025
|
+
// the dirty lanes straight into the GPU buffer.
|
|
1026
|
+
for ( const r of triRanges ) {
|
|
931
1027
|
|
|
932
|
-
|
|
1028
|
+
uploadStorageChunkRange( this.renderer, this.triangleStorageAttr, this._triangleRecords, r.offset, r.count );
|
|
933
1029
|
|
|
934
|
-
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
} else {
|
|
1033
|
+
|
|
1034
|
+
this.triangleStorageAttr.clearUpdateRanges();
|
|
935
1035
|
|
|
936
|
-
|
|
1036
|
+
for ( const r of triRanges ) {
|
|
1037
|
+
|
|
1038
|
+
this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
|
|
1039
|
+
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
this.triangleStorageAttr.version ++;
|
|
1043
|
+
|
|
1044
|
+
}
|
|
937
1045
|
|
|
938
1046
|
}
|
|
939
1047
|
|
|
940
1048
|
if ( this.bvhStorageAttr && bvhRanges.length > 0 ) {
|
|
941
1049
|
|
|
942
|
-
this.
|
|
1050
|
+
if ( this._bvhRecords ) {
|
|
943
1051
|
|
|
944
|
-
|
|
1052
|
+
for ( const r of bvhRanges ) {
|
|
945
1053
|
|
|
946
|
-
|
|
1054
|
+
uploadStorageChunkRange( this.renderer, this.bvhStorageAttr, this._bvhRecords, r.offset, r.count );
|
|
947
1055
|
|
|
948
|
-
|
|
1056
|
+
}
|
|
949
1057
|
|
|
950
|
-
|
|
1058
|
+
} else {
|
|
1059
|
+
|
|
1060
|
+
this.bvhStorageAttr.clearUpdateRanges();
|
|
1061
|
+
|
|
1062
|
+
for ( const r of bvhRanges ) {
|
|
1063
|
+
|
|
1064
|
+
this.bvhStorageAttr.addUpdateRange( r.offset, r.count );
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
this.bvhStorageAttr.version ++;
|
|
1069
|
+
|
|
1070
|
+
}
|
|
951
1071
|
|
|
952
1072
|
}
|
|
953
1073
|
|
|
@@ -1449,8 +1569,10 @@ export class PathTracerStage extends RenderStage {
|
|
|
1449
1569
|
|
|
1450
1570
|
// Clear data references
|
|
1451
1571
|
this.triangleStorageAttr = null;
|
|
1572
|
+
this._triangleRecords = null;
|
|
1452
1573
|
this.triangleStorageNode = null;
|
|
1453
1574
|
this.bvhStorageAttr = null;
|
|
1575
|
+
this._bvhRecords = null;
|
|
1454
1576
|
this.bvhStorageNode = null;
|
|
1455
1577
|
this.placeholderTexture = null;
|
|
1456
1578
|
|