rayzee 8.6.0 → 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.
@@ -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
- if ( this._batches && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
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 ) group.add( 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
- _meshFromGeometry( shape, geometry, sharedMaterial, ctm, name ) {
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 m = new Matrix4().fromArray( cam.cameraToWorld );
1026
- const e = m.elements;
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
- let eye = new Vector3( e[ 12 ], e[ 13 ], e[ 14 ] );
1030
- let dir = new Vector3( e[ 8 ], e[ 9 ], e[ 10 ] );
1031
- let up = new Vector3( e[ 4 ], e[ 5 ], e[ 6 ] );
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.lookAt( target );
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( cam.cameraToWorld ) > 0 ) !== this.convertHandedness ) camera.scale.x = - 1;
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
- * @returns {Promise<{group, camera, environment, warnings, entryPath}>}
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 parser = new PBRTParser( {
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
- } );
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
- const ir = await parser.parse( entryBytes, baseDir );
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 { ...result, entryPath, candidates, warnings: [ ...warnings, ...( result.warnings ?? [] ) ], parseMs, buildMs: performance.now() - buildStart };
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
- this.bvhStorageAttr.needsUpdate = true;
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
 
package/src/index.js CHANGED
@@ -99,11 +99,11 @@ export { VideoRenderManager } from './managers/VideoRenderManager.js';
99
99
  // Interaction
100
100
  export { InteractionManager } from './managers/InteractionManager.js';
101
101
 
102
- // DLSS neural passes. The app drives these through `denoisingManager` — what is exported here is
102
+ // Neural post passes. The app drives these through `denoisingManager` — what is exported here is
103
103
  // the offline entry point (`renderUpscaled`, used by the regression bench) plus the two contracts a
104
104
  // host needs to build UI against.
105
- export { renderUpscaled, SR_SCALE, SR_MAX_INPUT } from './dlss/DLSSSuperRes.js';
106
- export { DLSS_NR_DEFAULTS, DLSS_NR_RANGES, DLSS_NR_MAX_PIXELS } from './dlss/DLSSNeural.js';
105
+ export { renderUpscaled, SR_SCALE, SR_MAX_INPUT } from './neural/NeuralSuperRes.js';
106
+ export { RETOUCH_DEFAULTS, RETOUCH_RANGES, RETOUCH_MAX_PIXELS } from './neural/NeuralRetouch.js';
107
107
 
108
108
  // ── Colour management ───────────────────────────────────────────────────────────────────────────
109
109
  // `app.color` is the instance a host normally uses; these are for building UI against it, and for