rayzee 8.6.1 → 8.7.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/dist/rayzee.es.js +3507 -2955
- package/dist/rayzee.umd.js +16 -16
- package/package.json +1 -1
- package/src/PathTracerApp.js +97 -2
- package/src/Processor/AssetLoader.js +14 -8
- package/src/Processor/EmissiveTriangleBuilder.js +70 -0
- package/src/Processor/PBRT/PBRTAnimation.js +646 -0
- package/src/Processor/PBRT/PBRTParser.js +179 -52
- package/src/Processor/PBRT/PBRTSceneBuilder.js +181 -25
- package/src/Processor/PBRT/index.js +73 -14
- package/src/Processor/SceneProcessor.js +22 -0
- package/src/Stages/PathTracerStage.js +16 -1
- package/src/managers/AnimationManager.js +202 -8
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convert a parsed pbrt IR into a THREE scene graph the engine can ingest.
|
|
3
3
|
*
|
|
4
|
-
* Output: { group, camera, environment, warnings }
|
|
4
|
+
* Output: { group, camera, environment, animations, warnings }
|
|
5
5
|
* - group: THREE.Group of meshes (fed to PathTracerApp.loadObject3D)
|
|
6
6
|
* - camera: PerspectiveCamera matching the pbrt Camera/LookAt, parented
|
|
7
7
|
* into the group so AssetLoader.extractCamerasFromModel finds it
|
|
8
8
|
* - environment: { texture } | null — set by the caller as scene.environment
|
|
9
|
+
* - animations: [ AnimationClip ] when the IR carries motion (see PBRTAnimation.js), else []
|
|
9
10
|
*
|
|
10
11
|
* Handedness: pbrt scenes import correctly as-is. A `diag(1,1,-1)` mirror is
|
|
11
12
|
* available behind `convertHandedness` (default OFF) — three's `lookAt` builds
|
|
@@ -14,10 +15,11 @@
|
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import {
|
|
17
|
-
Group, Mesh, InstancedMesh, PerspectiveCamera, Matrix4, Vector3,
|
|
18
|
+
Group, Mesh, InstancedMesh, PerspectiveCamera, Matrix4, Vector3, Quaternion,
|
|
18
19
|
BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
|
|
19
20
|
DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
|
|
20
|
-
SRGBColorSpace
|
|
21
|
+
SRGBColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
|
|
22
|
+
NumberKeyframeTrack, BooleanKeyframeTrack
|
|
21
23
|
} from 'three';
|
|
22
24
|
import { buildMaterial, pFloat, pString, resolveSpectrum } from './PBRTMaterials.js';
|
|
23
25
|
import { loopSubdivide } from './LoopSubdivision.js';
|
|
@@ -200,6 +202,7 @@ export class PBRTSceneBuilder {
|
|
|
200
202
|
this._mergedBatches = 0;
|
|
201
203
|
this.mergedShapes = 0;
|
|
202
204
|
this.droppedNoTemplate = 0;
|
|
205
|
+
this._animated = []; // { node, motion }
|
|
203
206
|
|
|
204
207
|
// Shapes (direct + instanced)
|
|
205
208
|
for ( let i = 0; i < ir.shapes.length; i ++ ) {
|
|
@@ -218,7 +221,8 @@ export class PBRTSceneBuilder {
|
|
|
218
221
|
] );
|
|
219
222
|
if ( ! geometry ) continue;
|
|
220
223
|
|
|
221
|
-
|
|
224
|
+
// A moving shape needs a node of its own to move.
|
|
225
|
+
if ( this._batches && ! shape.motion && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
|
|
222
226
|
|
|
223
227
|
this._mergeShape( shape, geometry, sharedMaterial, group );
|
|
224
228
|
ir.shapes[ i ] = null; // its parsed arrays are copied out; let them go
|
|
@@ -227,7 +231,9 @@ export class PBRTSceneBuilder {
|
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.ctm, `shape_${i}` );
|
|
230
|
-
if ( mesh )
|
|
234
|
+
if ( ! mesh ) continue;
|
|
235
|
+
group.add( mesh );
|
|
236
|
+
if ( shape.motion ) this._animate( mesh, shape.motion );
|
|
231
237
|
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -245,6 +251,7 @@ export class PBRTSceneBuilder {
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
await this._buildInstances( ir, group );
|
|
254
|
+
await this._buildAnimatedPlacements( ir, group );
|
|
248
255
|
|
|
249
256
|
if ( this.skippedForBudget > 0 ) this.warn(
|
|
250
257
|
`stopped at ${this.triangleCount.toLocaleString()} stored triangles ` +
|
|
@@ -261,6 +268,8 @@ export class PBRTSceneBuilder {
|
|
|
261
268
|
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
const animations = ir.animation ? this._buildClip( ir, camera ) : [];
|
|
272
|
+
|
|
264
273
|
// Infinite light → environment
|
|
265
274
|
const environment = await this._buildEnvironment( ir.lights );
|
|
266
275
|
|
|
@@ -271,7 +280,7 @@ export class PBRTSceneBuilder {
|
|
|
271
280
|
);
|
|
272
281
|
|
|
273
282
|
return {
|
|
274
|
-
group, camera, environment,
|
|
283
|
+
group, camera, environment, animations,
|
|
275
284
|
report: this.report,
|
|
276
285
|
meshCount: this.reportedMeshes,
|
|
277
286
|
triangleCount: this.triangleCount,
|
|
@@ -395,6 +404,148 @@ export class PBRTSceneBuilder {
|
|
|
395
404
|
|
|
396
405
|
}
|
|
397
406
|
|
|
407
|
+
/** Moving placements, one Group each: an InstancedMesh cannot move a single instance. */
|
|
408
|
+
async _buildAnimatedPlacements( ir, group ) {
|
|
409
|
+
|
|
410
|
+
const placements = ir.animatedInstances ?? [];
|
|
411
|
+
for ( let k = 0; k < placements.length; k ++ ) {
|
|
412
|
+
|
|
413
|
+
const { name, motion } = placements[ k ];
|
|
414
|
+
const template = ir.objects.get( name );
|
|
415
|
+
if ( ! template ) {
|
|
416
|
+
|
|
417
|
+
this.droppedNoTemplate ++;
|
|
418
|
+
continue;
|
|
419
|
+
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if ( this._overBudget() ) {
|
|
423
|
+
|
|
424
|
+
this.skippedForBudget += placements.length - k;
|
|
425
|
+
break;
|
|
426
|
+
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const node = new Group();
|
|
430
|
+
node.name = `placement_${k}`;
|
|
431
|
+
const start = Array.from( motion.matrices.subarray( 0, 16 ) );
|
|
432
|
+
new Matrix4().fromArray( this.convertHandedness ? M.multiply( FLIP_Z, start ) : start ).decompose( node.position, node.quaternion, node.scale );
|
|
433
|
+
|
|
434
|
+
for ( let s = 0; s < template.length; s ++ ) {
|
|
435
|
+
|
|
436
|
+
const shape = template[ s ];
|
|
437
|
+
const [ geometry, sharedMaterial ] = await Promise.all( [ this._buildGeometry( shape ), this._getMaterial( shape ) ] );
|
|
438
|
+
if ( ! geometry ) continue;
|
|
439
|
+
const mesh = this._meshFromGeometry( shape, geometry, sharedMaterial, shape.relativeCTM || shape.ctm, `${node.name}_${s}`, true );
|
|
440
|
+
if ( mesh ) node.add( mesh );
|
|
441
|
+
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if ( node.children.length === 0 ) continue;
|
|
445
|
+
group.add( node );
|
|
446
|
+
this._animate( node, motion );
|
|
447
|
+
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Remember a node's keyframes and put it in its first-frame visibility. @private */
|
|
453
|
+
_animate( node, motion ) {
|
|
454
|
+
|
|
455
|
+
if ( motion.visible ) node.visible = motion.visible[ 0 ] === 1;
|
|
456
|
+
this._animated.push( { node, motion } );
|
|
457
|
+
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** One clip for the scene, with tracks only for what changes. @private */
|
|
461
|
+
_buildClip( ir, camera ) {
|
|
462
|
+
|
|
463
|
+
const tracks = [];
|
|
464
|
+
|
|
465
|
+
for ( const { node, motion } of this._animated ) {
|
|
466
|
+
|
|
467
|
+
this._transformTracks( tracks, node.name, motion, ( m, o ) => {
|
|
468
|
+
|
|
469
|
+
const key = Array.from( m.subarray( o, o + 16 ) );
|
|
470
|
+
return this.convertHandedness ? M.multiply( FLIP_Z, key ) : key;
|
|
471
|
+
|
|
472
|
+
} );
|
|
473
|
+
|
|
474
|
+
if ( motion.visible ) {
|
|
475
|
+
|
|
476
|
+
// Nearest frame wins: switching on the key itself lost to float32 rounding of the times.
|
|
477
|
+
const t = motion.times;
|
|
478
|
+
const times = t.map( ( time, k ) => k === 0 ? 0 : ( t[ k - 1 ] + time ) / 2 );
|
|
479
|
+
tracks.push( new BooleanKeyframeTrack( `${node.name}.visible`, times, Array.from( motion.visible, v => v === 1 ) ) );
|
|
480
|
+
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const camMotion = ir.camera?.motion;
|
|
486
|
+
if ( camera && camMotion ) {
|
|
487
|
+
|
|
488
|
+
const pose = new PerspectiveCamera();
|
|
489
|
+
this._transformTracks( tracks, camera.name, camMotion, ( m, o ) => {
|
|
490
|
+
|
|
491
|
+
this._poseCamera( pose, m.subarray( o, o + 16 ) );
|
|
492
|
+
pose.updateMatrix();
|
|
493
|
+
return pose.matrix.elements;
|
|
494
|
+
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
if ( camMotion.fov ) {
|
|
498
|
+
|
|
499
|
+
const fovs = Array.from( camMotion.fov, f => this._verticalFov( { fov: { type: 'float', value: [ f ] } }, camera.aspect ) );
|
|
500
|
+
tracks.push( new NumberKeyframeTrack( `${camera.name}.fov`, camMotion.times.slice(), fovs ) );
|
|
501
|
+
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// optimize() compacts in place, so no two tracks may share a times array.
|
|
507
|
+
for ( const track of tracks ) track.optimize();
|
|
508
|
+
if ( tracks.length === 0 ) return [];
|
|
509
|
+
return [ new AnimationClip( ir.animation.name, ir.animation.duration, tracks ) ];
|
|
510
|
+
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Decompose each key and emit tracks for the components that vary. @private */
|
|
514
|
+
_transformTracks( tracks, name, motion, matrixAt ) {
|
|
515
|
+
|
|
516
|
+
const n = motion.times.length;
|
|
517
|
+
const pos = new Float32Array( n * 3 ), rot = new Float32Array( n * 4 ), scl = new Float32Array( n * 3 );
|
|
518
|
+
const m = new Matrix4(), p = new Vector3(), q = new Quaternion(), sc = new Vector3();
|
|
519
|
+
|
|
520
|
+
for ( let k = 0; k < n; k ++ ) {
|
|
521
|
+
|
|
522
|
+
m.fromArray( matrixAt( motion.matrices, k * 16 ) ).decompose( p, q, sc );
|
|
523
|
+
// Same hemisphere as the previous key, so the blend takes the short way.
|
|
524
|
+
if ( k > 0 && q.x * rot[ k * 4 - 4 ] + q.y * rot[ k * 4 - 3 ] + q.z * rot[ k * 4 - 2 ] + q.w * rot[ k * 4 - 1 ] < 0 ) {
|
|
525
|
+
|
|
526
|
+
q.set( - q.x, - q.y, - q.z, - q.w );
|
|
527
|
+
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
p.toArray( pos, k * 3 );
|
|
531
|
+
q.toArray( rot, k * 4 );
|
|
532
|
+
sc.toArray( scl, k * 3 );
|
|
533
|
+
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const varies = ( values, stride ) => {
|
|
537
|
+
|
|
538
|
+
for ( let i = stride; i < values.length; i ++ ) if ( values[ i ] !== values[ i % stride ] ) return true;
|
|
539
|
+
return false;
|
|
540
|
+
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if ( varies( pos, 3 ) ) tracks.push( new VectorKeyframeTrack( `${name}.position`, motion.times.slice(), pos ) );
|
|
544
|
+
if ( varies( rot, 4 ) ) tracks.push( new QuaternionKeyframeTrack( `${name}.quaternion`, motion.times.slice(), rot ) );
|
|
545
|
+
if ( varies( scl, 3 ) ) tracks.push( new VectorKeyframeTrack( `${name}.scale`, motion.times.slice(), scl ) );
|
|
546
|
+
|
|
547
|
+
}
|
|
548
|
+
|
|
398
549
|
async _buildShapeMesh( shape, ctm, name ) {
|
|
399
550
|
|
|
400
551
|
// Geometry parse and material/texture resolution are independent — overlap them.
|
|
@@ -407,7 +558,8 @@ export class PBRTSceneBuilder {
|
|
|
407
558
|
|
|
408
559
|
}
|
|
409
560
|
|
|
410
|
-
|
|
561
|
+
/** @param {boolean} [local=false] - `ctm` is parent-relative; the parent carries the handedness mirror */
|
|
562
|
+
_meshFromGeometry( shape, geometry, sharedMaterial, ctm, name, local = false ) {
|
|
411
563
|
|
|
412
564
|
const hasUV = !! geometry.getAttribute( 'uv' );
|
|
413
565
|
const material = this._materialForGeometry( shape, geometry, sharedMaterial, name );
|
|
@@ -420,7 +572,7 @@ export class PBRTSceneBuilder {
|
|
|
420
572
|
// from position/quaternion/scale. A directly-set matrix gets overwritten
|
|
421
573
|
// with identity there — silently dropping every per-shape Transform.
|
|
422
574
|
// decompose() round-trips the handedness mirror (det<0) via a negative scale axis.
|
|
423
|
-
const world = this.convertHandedness ? M.multiply( FLIP_Z, ctm ) : ctm;
|
|
575
|
+
const world = this.convertHandedness && ! local ? M.multiply( FLIP_Z, ctm ) : ctm;
|
|
424
576
|
new Matrix4().fromArray( world ).decompose( mesh.position, mesh.quaternion, mesh.scale );
|
|
425
577
|
mesh.updateMatrix();
|
|
426
578
|
|
|
@@ -1022,13 +1174,26 @@ export class PBRTSceneBuilder {
|
|
|
1022
1174
|
|
|
1023
1175
|
_buildCamera( cam, film ) {
|
|
1024
1176
|
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1177
|
+
const aspect = film && film.yresolution ? film.xresolution / film.yresolution : 16 / 9;
|
|
1178
|
+
const fov = this._verticalFov( cam.params, aspect );
|
|
1179
|
+
|
|
1180
|
+
const camera = new PerspectiveCamera( fov, aspect, 0.01, 10000 );
|
|
1181
|
+
camera.name = 'PBRT Camera';
|
|
1182
|
+
this._poseCamera( camera, cam.cameraToWorld );
|
|
1183
|
+
camera.updateMatrixWorld( true );
|
|
1184
|
+
return camera;
|
|
1185
|
+
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Place a camera at a pbrt camera-to-world transform. @private */
|
|
1189
|
+
_poseCamera( camera, cameraToWorld ) {
|
|
1190
|
+
|
|
1191
|
+
const e = cameraToWorld;
|
|
1027
1192
|
|
|
1028
1193
|
// Columns of cameraToWorld: right(0), up(1), dir(2), eye(3).
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1194
|
+
const eye = new Vector3( e[ 12 ], e[ 13 ], e[ 14 ] );
|
|
1195
|
+
const dir = new Vector3( e[ 8 ], e[ 9 ], e[ 10 ] );
|
|
1196
|
+
const up = new Vector3( e[ 4 ], e[ 5 ], e[ 6 ] );
|
|
1032
1197
|
|
|
1033
1198
|
if ( this.convertHandedness ) {
|
|
1034
1199
|
|
|
@@ -1036,16 +1201,10 @@ export class PBRTSceneBuilder {
|
|
|
1036
1201
|
|
|
1037
1202
|
}
|
|
1038
1203
|
|
|
1039
|
-
const target = eye.clone().add( dir );
|
|
1040
|
-
|
|
1041
|
-
const aspect = film && film.yresolution ? film.xresolution / film.yresolution : 16 / 9;
|
|
1042
|
-
const fov = this._verticalFov( cam.params, aspect );
|
|
1043
|
-
|
|
1044
|
-
const camera = new PerspectiveCamera( fov, aspect, 0.01, 10000 );
|
|
1045
|
-
camera.name = 'PBRT Camera';
|
|
1046
1204
|
camera.up.copy( up.normalize() );
|
|
1047
1205
|
camera.position.copy( eye );
|
|
1048
|
-
camera.
|
|
1206
|
+
camera.scale.set( 1, 1, 1 );
|
|
1207
|
+
camera.lookAt( eye.clone().add( dir ) );
|
|
1049
1208
|
|
|
1050
1209
|
// pbrt's camera space is left-handed: image-right is cameraToWorld's first column,
|
|
1051
1210
|
// = cross(up, dir). three's lookAt() builds cross(up, -dir) — the opposite — so a
|
|
@@ -1054,10 +1213,7 @@ export class PBRTSceneBuilder {
|
|
|
1054
1213
|
// lookAt() happens to agree and no correction is wanted. Hence: mirror exactly when
|
|
1055
1214
|
// the scene does NOT. Verified against the reference images for contemporary-bathroom
|
|
1056
1215
|
// (has the Scale) and killeroos/killeroo-simple (does not).
|
|
1057
|
-
if ( ( M.determinant3(
|
|
1058
|
-
|
|
1059
|
-
camera.updateMatrixWorld( true );
|
|
1060
|
-
return camera;
|
|
1216
|
+
if ( ( M.determinant3( cameraToWorld ) > 0 ) !== this.convertHandedness ) camera.scale.x = - 1;
|
|
1061
1217
|
|
|
1062
1218
|
}
|
|
1063
1219
|
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import { PBRTParser } from './PBRTParser.js';
|
|
18
18
|
import { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
|
|
19
|
+
import { findFrameSequence, FrameSequenceMerger, motionFromShutter, SEQUENCE_FPS } from './PBRTAnimation.js';
|
|
19
20
|
|
|
20
21
|
export { PBRTParser } from './PBRTParser.js';
|
|
21
22
|
export { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
|
|
22
23
|
export { tokenize } from './PBRTTokenizer.js';
|
|
24
|
+
export { findFrameSequence, SEQUENCE_FPS } from './PBRTAnimation.js';
|
|
25
|
+
|
|
26
|
+
// Each frame is a full parse; past this many shapes + placements in the first, only it loads.
|
|
27
|
+
const MAX_SEQUENCE_PRIMITIVES = 2_000_000;
|
|
23
28
|
|
|
24
29
|
const decoder = new TextDecoder();
|
|
25
30
|
|
|
@@ -337,7 +342,9 @@ export async function listEntryPathsFrom( vfs ) {
|
|
|
337
342
|
* @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} args.imageFromBytes
|
|
338
343
|
* @param {(bytes:Uint8Array, filename:string)=>Promise<import('three').Texture>} [args.envFromBytes]
|
|
339
344
|
* @param {boolean} [args.convertHandedness=true]
|
|
340
|
-
* @
|
|
345
|
+
* @param {boolean} [args.animation=true] - load a frame sequence (`frame25.pbrt`, `frame35.pbrt`, …)
|
|
346
|
+
* as one animated scene instead of its first frame alone. Ignored when `entryPath` names a file.
|
|
347
|
+
* @returns {Promise<{group, camera, environment, animations, warnings, entryPath, frames}>}
|
|
341
348
|
*/
|
|
342
349
|
export async function loadPBRTScene( args ) {
|
|
343
350
|
|
|
@@ -358,23 +365,57 @@ export async function loadPBRTScene( args ) {
|
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
const entryPath = requested || candidates[ 0 ];
|
|
361
|
-
const entryBytes = await vfs.readPath( entryPath );
|
|
362
|
-
if ( ! entryBytes ) throw new Error( `PBRT loader: entry "${entryPath}" not readable` );
|
|
363
|
-
|
|
364
|
-
const baseDir = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
|
|
365
368
|
|
|
366
369
|
// Parse (with Include resolution). Bytes go straight to the lexer — a scene file can
|
|
367
370
|
// be larger than the longest string JavaScript will build.
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
const parseScene = async ( path ) => {
|
|
372
|
+
|
|
373
|
+
const bytes = await vfs.readPath( path );
|
|
374
|
+
if ( ! bytes ) throw new Error( `PBRT loader: entry "${path}" not readable` );
|
|
375
|
+
|
|
376
|
+
const parser = new PBRTParser( {
|
|
377
|
+
resolveInclude: ( include, currentDir ) => vfs.readPath( include, currentDir ),
|
|
378
|
+
// Depth-first, so this keeps only the open include chain live rather than every
|
|
379
|
+
// scene file at once — the difference between 5.7 GB resident and a few MB.
|
|
380
|
+
releaseInclude: ( include, currentDir ) => vfs.releasePath( include, currentDir ),
|
|
381
|
+
maxPlacements: args.maxPlacements
|
|
382
|
+
} );
|
|
383
|
+
|
|
384
|
+
return parser.parse( bytes, path.includes( '/' ) ? path.slice( 0, path.lastIndexOf( '/' ) ) : '' );
|
|
385
|
+
|
|
386
|
+
};
|
|
375
387
|
|
|
376
388
|
const parseStart = performance.now();
|
|
377
|
-
|
|
389
|
+
let sequence = ! requested && args.animation !== false ? findFrameSequence( candidates ) : null;
|
|
390
|
+
let ir;
|
|
391
|
+
|
|
392
|
+
if ( sequence ) {
|
|
393
|
+
|
|
394
|
+
const first = await parseScene( sequence[ 0 ].path );
|
|
395
|
+
if ( first.shapes.length + first.instanceCount > MAX_SEQUENCE_PRIMITIVES ) {
|
|
396
|
+
|
|
397
|
+
warnings.push( `${sequence.length} animation frames found, but the scene is too large to parse once per frame — loaded "${sequence[ 0 ].path}" only` );
|
|
398
|
+
sequence = null;
|
|
399
|
+
ir = motionFromShutter( first );
|
|
400
|
+
|
|
401
|
+
} else {
|
|
402
|
+
|
|
403
|
+
const merger = new FrameSequenceMerger(
|
|
404
|
+
sequence.map( f => ( f.number - sequence[ 0 ].number ) / SEQUENCE_FPS ),
|
|
405
|
+
sequenceName( sequence )
|
|
406
|
+
);
|
|
407
|
+
merger.addFrame( first );
|
|
408
|
+
for ( let i = 1; i < sequence.length; i ++ ) merger.addFrame( await parseScene( sequence[ i ].path ) );
|
|
409
|
+
ir = merger.finish();
|
|
410
|
+
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
} else {
|
|
414
|
+
|
|
415
|
+
ir = motionFromShutter( await parseScene( entryPath ) );
|
|
416
|
+
|
|
417
|
+
}
|
|
418
|
+
|
|
378
419
|
const parseMs = performance.now() - parseStart;
|
|
379
420
|
|
|
380
421
|
// Scene text is dead once parsed, and it is the bulk of a big element — isIronwoodA1 is
|
|
@@ -435,6 +476,24 @@ export async function loadPBRTScene( args ) {
|
|
|
435
476
|
|
|
436
477
|
const buildStart = performance.now();
|
|
437
478
|
const result = await builder.build( ir );
|
|
438
|
-
return {
|
|
479
|
+
return {
|
|
480
|
+
...result,
|
|
481
|
+
entryPath: sequence ? sequence[ 0 ].path : entryPath,
|
|
482
|
+
frames: sequence ? sequence.map( f => f.path ) : null,
|
|
483
|
+
candidates,
|
|
484
|
+
warnings: [ ...warnings, ...( result.warnings ?? [] ) ],
|
|
485
|
+
parseMs,
|
|
486
|
+
buildMs: performance.now() - buildStart
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** "zero-day · frames 25–380": the folder, or failing that the shared file-name prefix. */
|
|
492
|
+
function sequenceName( sequence ) {
|
|
493
|
+
|
|
494
|
+
const path = sequence[ 0 ].path;
|
|
495
|
+
const parts = path.split( '/' );
|
|
496
|
+
const label = parts.length > 1 ? parts[ parts.length - 2 ] : parts[ 0 ].replace( /\d+\.pbrt$/i, '' ) || 'frames';
|
|
497
|
+
return `${label} · frames ${sequence[ 0 ].number}–${sequence[ sequence.length - 1 ].number}`;
|
|
439
498
|
|
|
440
499
|
}
|
|
@@ -2281,6 +2281,28 @@ export class SceneProcessor {
|
|
|
2281
2281
|
|
|
2282
2282
|
}
|
|
2283
2283
|
|
|
2284
|
+
/** Whether any of these meshes emits light. */
|
|
2285
|
+
movesEmitters( meshIndices ) {
|
|
2286
|
+
|
|
2287
|
+
const emitters = this.emissiveTriangleBuilder?.emissiveMeshes;
|
|
2288
|
+
return !! emitters?.size && meshIndices.some( m => emitters.has( m ) );
|
|
2289
|
+
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Re-measure emitters at their current placements and rebuild the light hierarchy.
|
|
2294
|
+
* @returns {object|null} GPU upload payload, or null when the scene has no emitters
|
|
2295
|
+
*/
|
|
2296
|
+
refreshEmissiveTransforms() {
|
|
2297
|
+
|
|
2298
|
+
const builder = this.emissiveTriangleBuilder;
|
|
2299
|
+
if ( ! builder?.emissiveTriangles.length || ! this.instanceTable || ! this.triangles ) return null;
|
|
2300
|
+
|
|
2301
|
+
builder.refreshTransforms( this.triangles, this.instanceTable );
|
|
2302
|
+
return this._collectEmissivePayload();
|
|
2303
|
+
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2284
2306
|
/**
|
|
2285
2307
|
* Rebuild the Light BVH + sorted emissive data + bit-trail map (over the visible
|
|
2286
2308
|
* subset) so the stochastic descent and the bounce-hit MIS re-walk stay consistent,
|
|
@@ -922,7 +922,22 @@ export class PathTracerStage extends RenderStage {
|
|
|
922
922
|
|
|
923
923
|
if ( ! this._bvhRecords ) {
|
|
924
924
|
|
|
925
|
-
|
|
925
|
+
// Leaves sit in the TLAS at the front; needsUpdate would re-upload every BLAS too.
|
|
926
|
+
let lo = Infinity, hi = - 1;
|
|
927
|
+
for ( const leaf of this._dirtyBVHLeaves ?? [] ) {
|
|
928
|
+
|
|
929
|
+
if ( leaf < lo ) lo = leaf;
|
|
930
|
+
if ( leaf > hi ) hi = leaf;
|
|
931
|
+
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if ( hi >= 0 ) {
|
|
935
|
+
|
|
936
|
+
this.bvhStorageAttr.addUpdateRange( lo * 16, ( hi - lo + 1 ) * 16 );
|
|
937
|
+
this.bvhStorageAttr.version ++;
|
|
938
|
+
|
|
939
|
+
}
|
|
940
|
+
|
|
926
941
|
this._dirtyBVHLeaves?.clear();
|
|
927
942
|
return;
|
|
928
943
|
|