rayzee 7.31.0 → 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 +6547 -4334
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +6 -0
- package/src/PathTracerApp.js +128 -20
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +357 -22
- 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 +173 -0
- package/src/Processor/PBRT/LoopSubdivision.js +240 -0
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +28 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +659 -34
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +306 -45
- 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 +1063 -366
- 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/CameraManager.js +11 -0
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
|
@@ -53,62 +53,279 @@ 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
|
+
}
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 ) {
|
|
135
|
+
|
|
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;
|
|
89
155
|
|
|
90
156
|
}
|
|
91
157
|
|
|
92
158
|
}
|
|
93
159
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
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;
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Basenames this entry pulls in via Include/Import, lowercased. */
|
|
209
|
+
function includedBasenames( bytes, out ) {
|
|
210
|
+
|
|
211
|
+
for ( const word of INCLUDE_WORDS ) {
|
|
212
|
+
|
|
213
|
+
for ( let at = findBytes( bytes, word ); at >= 0; at = findBytes( bytes, word, at + 1 ) ) {
|
|
214
|
+
|
|
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() );
|
|
224
|
+
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Rank every .pbrt in the archive that could be a top-level scene, best first.
|
|
233
|
+
*
|
|
234
|
+
* Name shape alone is not enough: a scene's `geometry.pbrt` fragment carries a shorter
|
|
235
|
+
* name than the real entry and would win on sort, loading every shape with an unresolved
|
|
236
|
+
* NamedMaterial and no camera or lights. Content decides first — a fragment has no
|
|
237
|
+
* `WorldBegin` and is named by someone else's Include — and the name only breaks ties.
|
|
238
|
+
*
|
|
239
|
+
* More than one survivor is a real archive shape, not an error: transparent-machines
|
|
240
|
+
* ships five independent frames side by side. Callers should say which one they took.
|
|
241
|
+
*/
|
|
242
|
+
export function listEntryPaths( entries ) {
|
|
96
243
|
|
|
97
244
|
const pbrts = Object.keys( entries ).filter( k => k.toLowerCase().endsWith( '.pbrt' ) );
|
|
98
|
-
if ( pbrts.length
|
|
245
|
+
if ( pbrts.length <= 1 ) return pbrts;
|
|
246
|
+
|
|
247
|
+
// Scanned over bytes: a fragment can be gigabytes, past what a string would hold.
|
|
248
|
+
const included = new Set();
|
|
249
|
+
const worlds = new Map();
|
|
250
|
+
for ( const k of pbrts ) {
|
|
99
251
|
|
|
100
|
-
|
|
101
|
-
|
|
252
|
+
const bytes = entries[ k ];
|
|
253
|
+
worlds.set( k, hasWorldBegin( bytes ) );
|
|
254
|
+
includedBasenames( bytes, included );
|
|
102
255
|
|
|
103
|
-
|
|
104
|
-
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return rankEntryPaths( pbrts, worlds, included );
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Shared ranking for the eager and lazy entry pickers. */
|
|
263
|
+
function rankEntryPaths( pbrts, worlds, included ) {
|
|
264
|
+
|
|
265
|
+
const narrow = ( candidates, keep ) => {
|
|
266
|
+
|
|
267
|
+
const kept = candidates.filter( keep );
|
|
268
|
+
return kept.length ? kept : candidates;
|
|
269
|
+
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
let pool = narrow( pbrts, k => worlds.get( k ) );
|
|
273
|
+
pool = narrow( pool, k => ! included.has( k.toLowerCase().split( '/' ).pop() ) );
|
|
274
|
+
pool = narrow( pool, k => /(^|\/)(scene|main)\.pbrt$/i.test( k ) );
|
|
275
|
+
|
|
276
|
+
// Shallowest, then shortest name, then the path itself — the last key keeps the
|
|
277
|
+
// order independent of however the zip happened to enumerate its entries.
|
|
278
|
+
return pool.sort( ( a, b ) => {
|
|
105
279
|
|
|
106
280
|
const da = a.split( '/' ).length, db = b.split( '/' ).length;
|
|
107
|
-
|
|
281
|
+
if ( da !== db ) return da - db;
|
|
282
|
+
if ( a.length !== b.length ) return a.length - b.length;
|
|
283
|
+
return a < b ? - 1 : a > b ? 1 : 0;
|
|
108
284
|
|
|
109
285
|
} );
|
|
110
286
|
|
|
111
|
-
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Best top-level .pbrt entry, or null when the archive has none. */
|
|
290
|
+
export function pickEntryPath( entries ) {
|
|
291
|
+
|
|
292
|
+
return listEntryPaths( entries )[ 0 ] || null;
|
|
293
|
+
|
|
294
|
+
}
|
|
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 );
|
|
112
329
|
|
|
113
330
|
}
|
|
114
331
|
|
|
@@ -124,56 +341,100 @@ export function pickEntryPath( entries ) {
|
|
|
124
341
|
*/
|
|
125
342
|
export async function loadPBRTScene( args ) {
|
|
126
343
|
|
|
127
|
-
const { vfs: rawEntries, plyParser, imageFromBytes, envFromBytes, convertHandedness } = args;
|
|
128
|
-
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 = [];
|
|
347
|
+
|
|
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' );
|
|
352
|
+
|
|
353
|
+
const requested = args.entryPath && candidates.includes( args.entryPath ) ? args.entryPath : null;
|
|
354
|
+
if ( args.entryPath && ! requested ) {
|
|
129
355
|
|
|
130
|
-
|
|
131
|
-
if ( ! entryPath ) throw new Error( 'PBRT loader: no .pbrt file found in archive' );
|
|
356
|
+
warnings.push( `PBRT entry "${args.entryPath}" is not a scene in this archive — auto-detecting instead` );
|
|
132
357
|
|
|
133
|
-
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const entryPath = requested || candidates[ 0 ];
|
|
361
|
+
const entryBytes = await vfs.readPath( entryPath );
|
|
134
362
|
if ( ! entryBytes ) throw new Error( `PBRT loader: entry "${entryPath}" not readable` );
|
|
135
363
|
|
|
136
364
|
const baseDir = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
|
|
137
365
|
|
|
138
|
-
// 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.
|
|
139
368
|
const parser = new PBRTParser( {
|
|
140
|
-
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
|
+
} );
|
|
141
375
|
|
|
142
|
-
|
|
143
|
-
|
|
376
|
+
const parseStart = performance.now();
|
|
377
|
+
const ir = await parser.parse( entryBytes, baseDir );
|
|
378
|
+
const parseMs = performance.now() - parseStart;
|
|
144
379
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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();
|
|
148
385
|
|
|
149
386
|
// Build scene graph
|
|
150
|
-
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
|
+
|
|
151
405
|
const builder = new PBRTSceneBuilder( {
|
|
152
406
|
convertHandedness,
|
|
407
|
+
maxTriangles: args.maxTriangles,
|
|
408
|
+
maxPlacements: args.maxPlacements,
|
|
409
|
+
mergeShapesAbove: args.mergeShapesAbove,
|
|
410
|
+
curveSteps: args.curveSteps,
|
|
411
|
+
curveSides: args.curveSides,
|
|
153
412
|
resolvePLY: async ( filename ) => {
|
|
154
413
|
|
|
155
|
-
const
|
|
156
|
-
if ( !
|
|
157
|
-
|
|
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 );
|
|
158
418
|
|
|
159
419
|
},
|
|
160
420
|
resolveImage: async ( filename ) => {
|
|
161
421
|
|
|
162
|
-
const bytes = vfs.
|
|
422
|
+
const bytes = await vfs.readPath( filename );
|
|
163
423
|
if ( ! bytes ) return null;
|
|
164
424
|
return imageFromBytes( bytes, filename );
|
|
165
425
|
|
|
166
426
|
},
|
|
167
427
|
resolveEnvironment: async ( filename ) => {
|
|
168
428
|
|
|
169
|
-
const bytes = vfs.
|
|
429
|
+
const bytes = await vfs.readPath( filename );
|
|
170
430
|
if ( ! bytes ) return null;
|
|
171
431
|
return ( envFromBytes || imageFromBytes )( bytes, filename );
|
|
172
432
|
|
|
173
433
|
}
|
|
174
434
|
} );
|
|
175
435
|
|
|
436
|
+
const buildStart = performance.now();
|
|
176
437
|
const result = await builder.build( ir );
|
|
177
|
-
return { ...result, entryPath };
|
|
438
|
+
return { ...result, entryPath, candidates, warnings: [ ...warnings, ...( result.warnings ?? [] ) ], parseMs, buildMs: performance.now() - buildStart };
|
|
178
439
|
|
|
179
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();
|