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
|
@@ -53,54 +53,176 @@ function joinPath( dir, rel ) {
|
|
|
53
53
|
* Wraps the zip contents with tolerant, case-insensitive lookup that falls back
|
|
54
54
|
* to a basename match — pbrt scenes are inconsistent about path roots.
|
|
55
55
|
*/
|
|
56
|
-
class VirtualFS {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
export class VirtualFS {
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {Object<string,Uint8Array>} entries - materialised bytes, possibly empty
|
|
60
|
+
* @param {object} [source] - seekable archive: `{ listing, read(path) }`. When present the
|
|
61
|
+
* records it lists are read on demand and never held, so a multi-gigabyte archive is
|
|
62
|
+
* indexed rather than resident.
|
|
63
|
+
*/
|
|
64
|
+
constructor( entries, source = null ) {
|
|
65
|
+
|
|
66
|
+
// entries: { path: Uint8Array }. Held so a release can drop the caller's reference
|
|
67
|
+
// too — nulling only our own copy frees nothing while the archive object is alive,
|
|
68
|
+
// and that object is every byte of a 30 GB scene.
|
|
69
|
+
this.entries = entries;
|
|
70
|
+
this.source = source;
|
|
71
|
+
this.records = [];
|
|
72
|
+
this.byPath = new Map(); // normalized lowercase -> { key, norm, bytes }
|
|
73
|
+
this.byBase = new Map(); // basename lowercase -> [ { key, norm, bytes } ] (insertion order)
|
|
74
|
+
const add = ( key, bytes, lazy ) => {
|
|
64
75
|
|
|
65
76
|
const norm = normalizePath( key ).toLowerCase();
|
|
66
|
-
|
|
77
|
+
if ( this.byPath.has( norm ) ) return;
|
|
78
|
+
const rec = { key, norm, bytes, lazy };
|
|
79
|
+
this.records.push( rec );
|
|
67
80
|
this.byPath.set( norm, rec );
|
|
68
81
|
const base = norm.split( '/' ).pop();
|
|
69
82
|
const bucket = this.byBase.get( base );
|
|
70
83
|
if ( bucket ) bucket.push( rec );
|
|
71
84
|
else this.byBase.set( base, [ rec ] );
|
|
72
85
|
|
|
73
|
-
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for ( const key in entries ) add( key, entries[ key ], false );
|
|
89
|
+
for ( const e of source?.listing ?? [] ) if ( e.offset !== undefined ) add( e.path, null, true );
|
|
74
90
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
/** Bytes for a record: already held, or read from the source on demand. */
|
|
94
|
+
async read( rec ) {
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
if (
|
|
96
|
+
if ( ! rec ) return null;
|
|
97
|
+
if ( rec.bytes ) return rec.bytes;
|
|
98
|
+
if ( ! rec.lazy || ! this.source ) return null;
|
|
99
|
+
return this.source.read( rec.key );
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve and read in one step; `dir` is joined first, as Include does. */
|
|
104
|
+
async readPath( path, dir = null ) {
|
|
105
|
+
|
|
106
|
+
const rec = ( dir !== null && this.findRecord( joinPath( dir, path ) ) ) || this.findRecord( path );
|
|
107
|
+
return this.read( rec );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
release( rec ) {
|
|
112
|
+
|
|
113
|
+
rec.bytes = null;
|
|
114
|
+
delete this.entries[ rec.key ];
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
releasePath( path, dir = null ) {
|
|
119
|
+
|
|
120
|
+
const rec = ( dir !== null && this.findRecord( joinPath( dir, path ) ) ) || this.findRecord( path );
|
|
121
|
+
// Only a lazy record, which can be read again. Releasing a resident one would drop a
|
|
122
|
+
// fragment that a later Include still needs — isHibiscusYoung pulls one in six times.
|
|
123
|
+
if ( rec?.lazy ) this.release( rec );
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Drop every .pbrt entry's bytes. Safe only once parsing is complete. */
|
|
128
|
+
releaseScenes() {
|
|
129
|
+
|
|
130
|
+
for ( const rec of this.records ) if ( rec.norm.endsWith( '.pbrt' ) ) this.release( rec );
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
findRecord( path ) {
|
|
81
135
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
136
|
+
const norm = normalizePath( path ).toLowerCase();
|
|
137
|
+
if ( this.byPath.has( norm ) ) return this.byPath.get( norm );
|
|
138
|
+
|
|
139
|
+
// Resolve by basename (O(1)); among collisions only a path-suffix match will do.
|
|
140
|
+
// The blind first-entry fallback is safe only when the name is unique: every Moana
|
|
141
|
+
// element ships its own `objects.pbrt`, and `Include "isPalmRig/objects.pbrt"` joined
|
|
142
|
+
// against isPalmRig/ makes a doubled path that matches no suffix — so loading several
|
|
143
|
+
// elements together silently handed each one the FIRST element's instance templates,
|
|
144
|
+
// and every placement that referenced a real template was then dropped.
|
|
85
145
|
const bucket = this.byBase.get( norm.split( '/' ).pop() );
|
|
86
146
|
if ( ! bucket ) return null;
|
|
87
147
|
const suffixHit = bucket.find( rec => rec.norm.endsWith( '/' + norm ) );
|
|
88
|
-
return
|
|
148
|
+
return suffixHit || ( bucket.length === 1 ? bucket[ 0 ] : null );
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
find( path ) {
|
|
153
|
+
|
|
154
|
+
return this.findRecord( path )?.bytes ?? null;
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Byte offset of `needle` in `bytes` at or after `from`, or -1. */
|
|
161
|
+
function findBytes( bytes, needle, from = 0 ) {
|
|
162
|
+
|
|
163
|
+
const first = needle[ 0 ];
|
|
164
|
+
const last = bytes.length - needle.length;
|
|
165
|
+
outer: for ( let i = from; i <= last; i ++ ) {
|
|
166
|
+
|
|
167
|
+
if ( bytes[ i ] !== first ) continue;
|
|
168
|
+
for ( let j = 1; j < needle.length; j ++ ) if ( bytes[ i + j ] !== needle[ j ] ) continue outer;
|
|
169
|
+
return i;
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return - 1;
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ASCII = s => Uint8Array.from( s, c => c.charCodeAt( 0 ) );
|
|
178
|
+
const WORLD_BEGIN = ASCII( 'WorldBegin' );
|
|
179
|
+
const INCLUDE_WORDS = [ ASCII( 'Include' ), ASCII( 'Import' ) ];
|
|
180
|
+
|
|
181
|
+
function startsLine( bytes, at ) {
|
|
182
|
+
|
|
183
|
+
for ( let i = at - 1; i >= 0; i -- ) {
|
|
184
|
+
|
|
185
|
+
const b = bytes[ i ];
|
|
186
|
+
if ( b === 10 ) return true;
|
|
187
|
+
if ( b !== 32 && b !== 9 ) return false;
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** True when the entry declares a world block, i.e. it is a scene and not a fragment. */
|
|
196
|
+
function hasWorldBegin( bytes ) {
|
|
197
|
+
|
|
198
|
+
for ( let at = findBytes( bytes, WORLD_BEGIN ); at >= 0; at = findBytes( bytes, WORLD_BEGIN, at + 1 ) ) {
|
|
199
|
+
|
|
200
|
+
if ( startsLine( bytes, at ) ) return true;
|
|
89
201
|
|
|
90
202
|
}
|
|
91
203
|
|
|
204
|
+
return false;
|
|
205
|
+
|
|
92
206
|
}
|
|
93
207
|
|
|
94
|
-
/**
|
|
95
|
-
function
|
|
208
|
+
/** Basenames this entry pulls in via Include/Import, lowercased. */
|
|
209
|
+
function includedBasenames( bytes, out ) {
|
|
96
210
|
|
|
97
|
-
|
|
211
|
+
for ( const word of INCLUDE_WORDS ) {
|
|
98
212
|
|
|
99
|
-
|
|
213
|
+
for ( let at = findBytes( bytes, word ); at >= 0; at = findBytes( bytes, word, at + 1 ) ) {
|
|
100
214
|
|
|
101
|
-
|
|
215
|
+
if ( ! startsLine( bytes, at ) ) continue;
|
|
216
|
+
let i = at + word.length;
|
|
217
|
+
while ( i < bytes.length && ( bytes[ i ] === 32 || bytes[ i ] === 9 ) ) i ++;
|
|
218
|
+
if ( bytes[ i ] !== 34 ) continue;
|
|
219
|
+
const start = ++ i;
|
|
220
|
+
while ( i < bytes.length && bytes[ i ] !== 34 && bytes[ i ] !== 10 ) i ++;
|
|
221
|
+
if ( bytes[ i ] !== 34 ) continue;
|
|
222
|
+
const path = decoder.decode( bytes.subarray( start, i ) );
|
|
223
|
+
out.add( normalizePath( path ).toLowerCase().split( '/' ).pop() );
|
|
102
224
|
|
|
103
|
-
|
|
225
|
+
}
|
|
104
226
|
|
|
105
227
|
}
|
|
106
228
|
|
|
@@ -122,18 +244,23 @@ export function listEntryPaths( entries ) {
|
|
|
122
244
|
const pbrts = Object.keys( entries ).filter( k => k.toLowerCase().endsWith( '.pbrt' ) );
|
|
123
245
|
if ( pbrts.length <= 1 ) return pbrts;
|
|
124
246
|
|
|
125
|
-
|
|
126
|
-
|
|
247
|
+
// Scanned over bytes: a fragment can be gigabytes, past what a string would hold.
|
|
127
248
|
const included = new Set();
|
|
128
|
-
|
|
249
|
+
const worlds = new Map();
|
|
250
|
+
for ( const k of pbrts ) {
|
|
129
251
|
|
|
130
|
-
|
|
252
|
+
const bytes = entries[ k ];
|
|
253
|
+
worlds.set( k, hasWorldBegin( bytes ) );
|
|
254
|
+
includedBasenames( bytes, included );
|
|
131
255
|
|
|
132
|
-
|
|
256
|
+
}
|
|
133
257
|
|
|
134
|
-
|
|
258
|
+
return rankEntryPaths( pbrts, worlds, included );
|
|
135
259
|
|
|
136
|
-
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Shared ranking for the eager and lazy entry pickers. */
|
|
263
|
+
function rankEntryPaths( pbrts, worlds, included ) {
|
|
137
264
|
|
|
138
265
|
const narrow = ( candidates, keep ) => {
|
|
139
266
|
|
|
@@ -142,7 +269,7 @@ export function listEntryPaths( entries ) {
|
|
|
142
269
|
|
|
143
270
|
};
|
|
144
271
|
|
|
145
|
-
let pool = narrow( pbrts, k =>
|
|
272
|
+
let pool = narrow( pbrts, k => worlds.get( k ) );
|
|
146
273
|
pool = narrow( pool, k => ! included.has( k.toLowerCase().split( '/' ).pop() ) );
|
|
147
274
|
pool = narrow( pool, k => /(^|\/)(scene|main)\.pbrt$/i.test( k ) );
|
|
148
275
|
|
|
@@ -166,6 +293,42 @@ export function pickEntryPath( entries ) {
|
|
|
166
293
|
|
|
167
294
|
}
|
|
168
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Entry .pbrt for a VirtualFS, reading each candidate in turn and dropping it again so a
|
|
298
|
+
* lazily-indexed archive is never materialised just to pick a starting file.
|
|
299
|
+
*/
|
|
300
|
+
export async function pickEntryPathFrom( vfs ) {
|
|
301
|
+
|
|
302
|
+
return ( await listEntryPathsFrom( vfs ) )[ 0 ] || null;
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Candidate scene files for a VirtualFS, best first. Each candidate is read to test it and
|
|
308
|
+
* dropped again, so a lazily-indexed archive is never materialised just to rank its entries.
|
|
309
|
+
*/
|
|
310
|
+
export async function listEntryPathsFrom( vfs ) {
|
|
311
|
+
|
|
312
|
+
const pbrts = vfs.records.filter( r => r.norm.endsWith( '.pbrt' ) );
|
|
313
|
+
if ( pbrts.length === 0 ) return [];
|
|
314
|
+
if ( pbrts.length === 1 ) return [ pbrts[ 0 ].key ];
|
|
315
|
+
|
|
316
|
+
const included = new Set();
|
|
317
|
+
const worlds = new Map();
|
|
318
|
+
for ( const rec of pbrts ) {
|
|
319
|
+
|
|
320
|
+
const bytes = await vfs.read( rec );
|
|
321
|
+
if ( ! bytes ) continue;
|
|
322
|
+
worlds.set( rec.key, hasWorldBegin( bytes ) );
|
|
323
|
+
includedBasenames( bytes, included );
|
|
324
|
+
if ( rec.lazy ) vfs.release( rec ); // read again later if it is actually used
|
|
325
|
+
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return rankEntryPaths( pbrts.map( r => r.key ), worlds, included );
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
169
332
|
/**
|
|
170
333
|
* @param {object} args
|
|
171
334
|
* @param {Object<string,Uint8Array>} args.vfs - zip entries (path → bytes)
|
|
@@ -178,56 +341,100 @@ export function pickEntryPath( entries ) {
|
|
|
178
341
|
*/
|
|
179
342
|
export async function loadPBRTScene( args ) {
|
|
180
343
|
|
|
181
|
-
const { vfs: rawEntries, plyParser, imageFromBytes, envFromBytes, convertHandedness } = args;
|
|
182
|
-
const vfs = new VirtualFS( rawEntries );
|
|
344
|
+
const { vfs: rawEntries, source, plyParser, imageFromBytes, envFromBytes, convertHandedness } = args;
|
|
345
|
+
const vfs = new VirtualFS( rawEntries, source );
|
|
346
|
+
const warnings = [];
|
|
183
347
|
|
|
184
|
-
|
|
185
|
-
|
|
348
|
+
// Ranked once here so a caller does not have to read every .pbrt a second time to learn
|
|
349
|
+
// what else was in the archive.
|
|
350
|
+
const candidates = await listEntryPathsFrom( vfs );
|
|
351
|
+
if ( candidates.length === 0 ) throw new Error( 'PBRT loader: no .pbrt file found in archive' );
|
|
186
352
|
|
|
187
|
-
const
|
|
353
|
+
const requested = args.entryPath && candidates.includes( args.entryPath ) ? args.entryPath : null;
|
|
354
|
+
if ( args.entryPath && ! requested ) {
|
|
355
|
+
|
|
356
|
+
warnings.push( `PBRT entry "${args.entryPath}" is not a scene in this archive — auto-detecting instead` );
|
|
357
|
+
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const entryPath = requested || candidates[ 0 ];
|
|
361
|
+
const entryBytes = await vfs.readPath( entryPath );
|
|
188
362
|
if ( ! entryBytes ) throw new Error( `PBRT loader: entry "${entryPath}" not readable` );
|
|
189
363
|
|
|
190
364
|
const baseDir = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
|
|
191
365
|
|
|
192
|
-
// Parse (with Include resolution)
|
|
366
|
+
// Parse (with Include resolution). Bytes go straight to the lexer — a scene file can
|
|
367
|
+
// be larger than the longest string JavaScript will build.
|
|
193
368
|
const parser = new PBRTParser( {
|
|
194
|
-
resolveInclude: ( path, currentDir ) =>
|
|
369
|
+
resolveInclude: ( path, currentDir ) => vfs.readPath( path, currentDir ),
|
|
370
|
+
// Depth-first, so this keeps only the open include chain live rather than every
|
|
371
|
+
// scene file at once — the difference between 5.7 GB resident and a few MB.
|
|
372
|
+
releaseInclude: ( path, currentDir ) => vfs.releasePath( path, currentDir ),
|
|
373
|
+
maxPlacements: args.maxPlacements
|
|
374
|
+
} );
|
|
195
375
|
|
|
196
|
-
|
|
197
|
-
|
|
376
|
+
const parseStart = performance.now();
|
|
377
|
+
const ir = await parser.parse( entryBytes, baseDir );
|
|
378
|
+
const parseMs = performance.now() - parseStart;
|
|
198
379
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
380
|
+
// Scene text is dead once parsed, and it is the bulk of a big element — isIronwoodA1 is
|
|
381
|
+
// 6.9 GB of it. Freeing before the builder runs halves peak while geometry is allocated.
|
|
382
|
+
// Only after the whole parse: a file can be Included more than once (isHibiscusYoung
|
|
383
|
+
// pulls its xgBonsai scatter in six times, once per placement of the plant).
|
|
384
|
+
vfs.releaseScenes();
|
|
202
385
|
|
|
203
386
|
// Build scene graph
|
|
204
|
-
const sliceBuf = ( bytes ) =>
|
|
387
|
+
const sliceBuf = ( bytes ) => (
|
|
388
|
+
bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
|
|
389
|
+
? bytes.buffer
|
|
390
|
+
: bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength )
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Cached against the RESOLVED entry, not the spelling used, so a file is decoded once and
|
|
394
|
+
// its source bytes can go — isCoral's 8,596 .ply files are 2.3 GB of the load.
|
|
395
|
+
const plyCache = new Map();
|
|
396
|
+
const decodePly = async ( rec ) => {
|
|
397
|
+
|
|
398
|
+
const bytes = await vfs.read( rec );
|
|
399
|
+
if ( ! bytes ) return null;
|
|
400
|
+
vfs.release( rec );
|
|
401
|
+
return plyParser( sliceBuf( bytes ) );
|
|
402
|
+
|
|
403
|
+
};
|
|
404
|
+
|
|
205
405
|
const builder = new PBRTSceneBuilder( {
|
|
206
406
|
convertHandedness,
|
|
407
|
+
maxTriangles: args.maxTriangles,
|
|
408
|
+
maxPlacements: args.maxPlacements,
|
|
409
|
+
mergeShapesAbove: args.mergeShapesAbove,
|
|
410
|
+
curveSteps: args.curveSteps,
|
|
411
|
+
curveSides: args.curveSides,
|
|
207
412
|
resolvePLY: async ( filename ) => {
|
|
208
413
|
|
|
209
|
-
const
|
|
210
|
-
if ( !
|
|
211
|
-
|
|
414
|
+
const rec = vfs.findRecord( filename );
|
|
415
|
+
if ( ! rec ) return null;
|
|
416
|
+
if ( ! plyCache.has( rec.norm ) ) plyCache.set( rec.norm, await decodePly( rec ) );
|
|
417
|
+
return plyCache.get( rec.norm );
|
|
212
418
|
|
|
213
419
|
},
|
|
214
420
|
resolveImage: async ( filename ) => {
|
|
215
421
|
|
|
216
|
-
const bytes = vfs.
|
|
422
|
+
const bytes = await vfs.readPath( filename );
|
|
217
423
|
if ( ! bytes ) return null;
|
|
218
424
|
return imageFromBytes( bytes, filename );
|
|
219
425
|
|
|
220
426
|
},
|
|
221
427
|
resolveEnvironment: async ( filename ) => {
|
|
222
428
|
|
|
223
|
-
const bytes = vfs.
|
|
429
|
+
const bytes = await vfs.readPath( filename );
|
|
224
430
|
if ( ! bytes ) return null;
|
|
225
431
|
return ( envFromBytes || imageFromBytes )( bytes, filename );
|
|
226
432
|
|
|
227
433
|
}
|
|
228
434
|
} );
|
|
229
435
|
|
|
436
|
+
const buildStart = performance.now();
|
|
230
437
|
const result = await builder.build( ir );
|
|
231
|
-
return { ...result, entryPath };
|
|
438
|
+
return { ...result, entryPath, candidates, warnings: [ ...warnings, ...( result.warnings ?? [] ) ], parseMs, buildMs: performance.now() - buildStart };
|
|
232
439
|
|
|
233
440
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Packed buffer manager for wavefront path tracing — one storage buffer per data category.
|
|
3
3
|
* RAY/HIT are SoA-within-a-buffer (field `slot` of element `id` lives at `id + slot*_cap`).
|
|
4
|
+
*
|
|
5
|
+
* All three are GPU-only: every lane is written by a kernel before anything reads it and none
|
|
6
|
+
* is ever read back, so they carry no CPU array (see gpuOnlyStorageAttribute).
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import {
|
|
7
10
|
storage, uintBitsToFloat, floatBitsToUint, vec2, vec3, vec4, uvec4, uint, int, float, clamp,
|
|
8
11
|
packSnorm2x16, packUnorm2x16, unpackSnorm2x16, unpackUnorm2x16, select, floor, log2, exp2, max,
|
|
9
12
|
} from 'three/tsl';
|
|
10
|
-
import {
|
|
13
|
+
import { gpuOnlyStorageAttribute } from '../TSL/patches.js';
|
|
14
|
+
import { packNormalOct, unpackTriangleNormal } from '../TSL/Common.js';
|
|
11
15
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
12
16
|
|
|
13
17
|
const log = createLogger( 'gpu' );
|
|
@@ -32,9 +36,12 @@ export const RAY = {
|
|
|
32
36
|
SSS_SIGMA_S: 6, // vec4(sigmaS.xyz, g) — SSS scattering coeff + Henyey-Greenstein anisotropy (sigmaS==0 ⇒ glass)
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
// uvec4 so packed lanes keep their exact bits — an f32 lane may canonicalise a NaN pattern.
|
|
35
40
|
export const HIT = {
|
|
36
|
-
DIST_TRI_BARY: 0, //
|
|
37
|
-
|
|
41
|
+
DIST_TRI_BARY: 0, // uvec4(bits(distance), triIndex, bits(bary.u), bits(bary.v))
|
|
42
|
+
// The geometric normal rides as one oct16 word (it was interpolated from oct16 triangle
|
|
43
|
+
// normals to begin with), which leaves the instance leaf a full 32-bit lane of its own.
|
|
44
|
+
NORMAL_MAT: 1, // uvec4(octNormal, matIndex, instanceLeaf + 1, 0)
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
// SoA region stride, baked into the shader graph at build time; single instance, rebuilt on resize.
|
|
@@ -82,14 +89,14 @@ export class PackedRayBuffer {
|
|
|
82
89
|
|
|
83
90
|
// count=0 so StorageBufferNode.getHash() shares the buffer → RW and RO nodes bind the same GPU data.
|
|
84
91
|
const rayCount = capacity * RAY_STRIDE;
|
|
85
|
-
const rayAttr =
|
|
92
|
+
const rayAttr = gpuOnlyStorageAttribute( rayCount, 4 );
|
|
86
93
|
this._attrs.ray = rayAttr;
|
|
87
94
|
this.rayBuffer = {
|
|
88
95
|
rw: storage( rayAttr, 'vec4' ),
|
|
89
96
|
ro: storage( rayAttr, 'vec4' ).toReadOnly(),
|
|
90
97
|
};
|
|
91
98
|
|
|
92
|
-
const rngAttr =
|
|
99
|
+
const rngAttr = gpuOnlyStorageAttribute( capacity, 1, Uint32Array );
|
|
93
100
|
this._attrs.rng = rngAttr;
|
|
94
101
|
this.rngBuffer = {
|
|
95
102
|
rw: storage( rngAttr, 'uint' ),
|
|
@@ -97,11 +104,11 @@ export class PackedRayBuffer {
|
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
const hitCount = capacity * HIT_STRIDE;
|
|
100
|
-
const hitAttr =
|
|
107
|
+
const hitAttr = gpuOnlyStorageAttribute( hitCount, 4, Uint32Array );
|
|
101
108
|
this._attrs.hit = hitAttr;
|
|
102
109
|
this.hitBuffer = {
|
|
103
|
-
rw: storage( hitAttr, '
|
|
104
|
-
ro: storage( hitAttr, '
|
|
110
|
+
rw: storage( hitAttr, 'uvec4' ),
|
|
111
|
+
ro: storage( hitAttr, 'uvec4' ).toReadOnly(),
|
|
105
112
|
};
|
|
106
113
|
|
|
107
114
|
// Kept for the [gpu] startup summary in PathTracer._buildWavefrontKernels.
|
|
@@ -253,29 +260,33 @@ export const writeRayRadiance = ( buf, id, radiance ) =>
|
|
|
253
260
|
.assign( radiance );
|
|
254
261
|
|
|
255
262
|
export const readHitDistance = ( buf, id ) =>
|
|
256
|
-
buf.element( soa( id, HIT.DIST_TRI_BARY ) ).x;
|
|
263
|
+
uintBitsToFloat( buf.element( soa( id, HIT.DIST_TRI_BARY ) ).x );
|
|
257
264
|
|
|
258
265
|
export const readHitTriangleIndex = ( buf, id ) =>
|
|
259
|
-
|
|
266
|
+
buf.element( soa( id, HIT.DIST_TRI_BARY ) ).y;
|
|
260
267
|
|
|
261
268
|
export const readHitBarycentrics = ( buf, id ) =>
|
|
262
|
-
buf.element( soa( id, HIT.DIST_TRI_BARY ) ).zw;
|
|
269
|
+
uintBitsToFloat( buf.element( soa( id, HIT.DIST_TRI_BARY ) ).zw );
|
|
263
270
|
|
|
264
271
|
export const readHitNormal = ( buf, id ) =>
|
|
265
|
-
buf.element( soa( id, HIT.NORMAL_MAT ) ).
|
|
272
|
+
unpackTriangleNormal( buf.element( soa( id, HIT.NORMAL_MAT ) ).x );
|
|
266
273
|
|
|
267
274
|
export const readHitMaterialIndex = ( buf, id ) =>
|
|
268
|
-
uint(
|
|
275
|
+
uint( buf.element( soa( id, HIT.NORMAL_MAT ) ).y );
|
|
269
276
|
|
|
270
|
-
|
|
271
|
-
|
|
277
|
+
/**
|
|
278
|
+
* TLAS leaf that owns the hit, biased by one so 0 reads as "no instance" — leaf 0 is a real
|
|
279
|
+
* node when the scene has a single mesh.
|
|
280
|
+
*/
|
|
281
|
+
export const readHitInstanceLeaf = ( buf, id ) =>
|
|
282
|
+
int( buf.element( soa( id, HIT.NORMAL_MAT ) ).z ).sub( int( 1 ) );
|
|
272
283
|
|
|
273
|
-
export const writeHitPacked = ( buf, id, distance, triIndex, baryU, baryV, normal, matIndex,
|
|
284
|
+
export const writeHitPacked = ( buf, id, distance, triIndex, baryU, baryV, normal, matIndex, instanceLeaf ) => {
|
|
274
285
|
|
|
275
286
|
buf.element( soa( id, HIT.DIST_TRI_BARY ) )
|
|
276
|
-
.assign(
|
|
287
|
+
.assign( uvec4( floatBitsToUint( distance ), triIndex, floatBitsToUint( baryU ), floatBitsToUint( baryV ) ) );
|
|
277
288
|
buf.element( soa( id, HIT.NORMAL_MAT ) )
|
|
278
|
-
.assign(
|
|
289
|
+
.assign( uvec4( packNormalOct( normal ), matIndex, instanceLeaf, uint( 0 ) ) );
|
|
279
290
|
|
|
280
291
|
};
|
|
281
292
|
|
|
@@ -16,7 +16,7 @@ setBVHWorkerFactory( () => new BVHWorker() );
|
|
|
16
16
|
|
|
17
17
|
const log = createLogger( 'bvh' );
|
|
18
18
|
|
|
19
|
-
const FPT =
|
|
19
|
+
const FPT = 20; // FLOATS_PER_TRIANGLE — the record is uint lanes, not floats
|
|
20
20
|
const PARALLEL_THRESHOLD = 50000;
|
|
21
21
|
const MAX_PARALLEL_WORKERS = 8;
|
|
22
22
|
|
|
@@ -27,11 +27,11 @@ const MAX_PARALLEL_WORKERS = 8;
|
|
|
27
27
|
* Phase 2: Subtree workers build independent subtrees in parallel.
|
|
28
28
|
* Phase 3: Coordinator assembles final BVH and reorders triangles.
|
|
29
29
|
*
|
|
30
|
-
* @param {
|
|
30
|
+
* @param {Uint32Array} triangles - Triangle records (20 lanes each)
|
|
31
31
|
* @param {number} depth - Maximum BVH depth
|
|
32
32
|
* @param {Function|null} progressCallback - Optional progress callback (0-100)
|
|
33
33
|
* @param {Object} config - Builder config (maxLeafSize, numBins, treelet settings, etc.)
|
|
34
|
-
* @returns {Promise<{bvhData: Float32Array, bvhRoot: true, reorderedTriangles:
|
|
34
|
+
* @returns {Promise<{bvhData: Float32Array, bvhRoot: true, reorderedTriangles: Uint32Array, splitStats: Object}>}
|
|
35
35
|
*/
|
|
36
36
|
export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
37
37
|
|
|
@@ -47,7 +47,8 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
|
47
47
|
|
|
48
48
|
// Allocate SharedArrayBuffers
|
|
49
49
|
const sharedTriangleData = new SharedArrayBuffer( triangles.byteLength );
|
|
50
|
-
new
|
|
50
|
+
new Uint32Array( sharedTriangleData ).set( triangles );
|
|
51
|
+
triangles = null; // shared copy is the only one needed; the fallback rebuilds from it
|
|
51
52
|
|
|
52
53
|
const sharedCentroids = new SharedArrayBuffer( triangleCount * 3 * 4 );
|
|
53
54
|
const sharedBMin = new SharedArrayBuffer( triangleCount * 3 * 4 );
|
|
@@ -97,8 +98,8 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
|
97
98
|
cleanup();
|
|
98
99
|
// Copy from SharedArrayBuffer to regular ArrayBuffer for transfer
|
|
99
100
|
const restoredBuffer = new ArrayBuffer( sharedTriangleData.byteLength );
|
|
100
|
-
new
|
|
101
|
-
const restoredTriangles = new
|
|
101
|
+
new Uint32Array( restoredBuffer ).set( new Uint32Array( sharedTriangleData ) );
|
|
102
|
+
const restoredTriangles = new Uint32Array( restoredBuffer );
|
|
102
103
|
resolve( buildSingleWorker( restoredTriangles, depth, progressCallback, config ) );
|
|
103
104
|
|
|
104
105
|
};
|
|
@@ -149,7 +150,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
|
149
150
|
|
|
150
151
|
settled = true;
|
|
151
152
|
cleanup();
|
|
152
|
-
const reorderedTriangles = new
|
|
153
|
+
const reorderedTriangles = new Uint32Array( sharedReorderBuffer );
|
|
153
154
|
resolve( { bvhData: msg.bvhData, bvhRoot: true, reorderedTriangles, originalToBvh: msg.originalToBvh || null, splitStats: phase1Stats || {} } );
|
|
154
155
|
return;
|
|
155
156
|
|
|
@@ -439,7 +440,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
|
|
|
439
440
|
|
|
440
441
|
worker.terminate();
|
|
441
442
|
const reorderedTriangles = sharedReorderBuffer
|
|
442
|
-
? new
|
|
443
|
+
? new Uint32Array( sharedReorderBuffer )
|
|
443
444
|
: transferredTriangles;
|
|
444
445
|
|
|
445
446
|
resolve( { bvhData, bvhRoot: true, reorderedTriangles, originalToBvh: originalToBvh || null, splitStats: treeletStats || {} } );
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { storage } from 'three/tsl';
|
|
6
6
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
7
|
+
import { gpuOnlyStorageAttribute } from '../TSL/patches.js';
|
|
7
8
|
import { freeStorageAttribute } from './PackedRayBuffer.js';
|
|
8
9
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
9
10
|
|
|
@@ -94,8 +95,9 @@ export class QueueManager {
|
|
|
94
95
|
);
|
|
95
96
|
this.bounceCounts = storage( this._bounceCountsAttr, 'uint' );
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
const
|
|
98
|
+
// Index lists, not data: written wholesale by compaction before any read, never read back.
|
|
99
|
+
const attrA = gpuOnlyStorageAttribute( capacity, 1, Uint32Array );
|
|
100
|
+
const attrB = gpuOnlyStorageAttribute( capacity, 1, Uint32Array );
|
|
99
101
|
this._attrA = attrA;
|
|
100
102
|
this._attrB = attrB;
|
|
101
103
|
|
|
@@ -112,7 +114,7 @@ export class QueueManager {
|
|
|
112
114
|
|
|
113
115
|
// Material-sort output: a material-reordered permutation of the active index
|
|
114
116
|
// list, written by the global material sort and read by Shade in place of activeIndices.
|
|
115
|
-
const sortAttr =
|
|
117
|
+
const sortAttr = gpuOnlyStorageAttribute( capacity, 1, Uint32Array );
|
|
116
118
|
this._sortAttr = sortAttr;
|
|
117
119
|
this.sortedIndices = storage( sortAttr, 'uint' );
|
|
118
120
|
this.sortedIndicesRO = storage( sortAttr, 'uint' ).toReadOnly();
|