rayzee 11.2.1 → 11.4.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.
Files changed (76) hide show
  1. package/README.md +91 -34
  2. package/dist/ArchiveImporter-Cgyx5AF7.js +7860 -0
  3. package/dist/BidirectionalIntegrator-BKF-cwH2.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
  6. package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-DV5TtURP.js} +11 -5
  7. package/dist/{PathTracer-D6nbknj5.js → PathTracer-D3MvEaZK.js} +4384 -4618
  8. package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-CY4jiz6a.js} +2 -2
  9. package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-D23m6NqG.js} +486 -391
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
  12. package/dist/addons/archives.es.js +1 -1
  13. package/dist/addons/bidirectional.es.js +1 -1
  14. package/dist/addons/physical-sky.es.js +1 -1
  15. package/dist/rayzee-core.es.js +7 -7
  16. package/dist/rayzee.es.js +1327 -838
  17. package/dist/rayzee.umd.js +146 -134
  18. package/dist/sceneCachePolicy-AKOEu2lf.js +1602 -0
  19. package/package.json +1 -1
  20. package/src/EngineDefaults.js +2 -2
  21. package/src/Passes/AIUpscaler.js +2 -0
  22. package/src/PathTracerApp.js +126 -19
  23. package/src/Pipeline/CompletionTracker.js +7 -3
  24. package/src/Processor/ArchiveImporter.js +173 -94
  25. package/src/Processor/ArchiveReader.js +20 -23
  26. package/src/Processor/AssetLoader.js +48 -1
  27. package/src/Processor/BVHRefitter.js +109 -13
  28. package/src/Processor/BufferLayout.js +115 -0
  29. package/src/Processor/ChunkedRecords.js +3 -2
  30. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  31. package/src/Processor/GeometryExtractor.js +189 -79
  32. package/src/Processor/HostMemory.js +39 -9
  33. package/src/Processor/InstanceTable.js +714 -42
  34. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  35. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  36. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  37. package/src/Processor/PackedRayBuffer.js +5 -2
  38. package/src/Processor/SceneProcessor.js +747 -141
  39. package/src/Processor/TLASBuilder.js +32 -17
  40. package/src/Processor/ToneMapCPU.js +16 -4
  41. package/src/Processor/ToneMapGPU.js +30 -14
  42. package/src/Processor/USD/USDLayer.js +1149 -0
  43. package/src/Processor/USD/USDScene.js +1655 -0
  44. package/src/Processor/USD/USDStage.js +682 -0
  45. package/src/Processor/USD/USDText.js +674 -0
  46. package/src/Processor/USD/index.js +138 -0
  47. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  48. package/src/RayzeeRenderer.js +221 -33
  49. package/src/RenderSettings.js +4 -2
  50. package/src/Stages/AutoExposure.js +484 -494
  51. package/src/Stages/Compositor.js +27 -6
  52. package/src/Stages/LocalExposure.js +546 -0
  53. package/src/Stages/NormalDepth.js +7 -12
  54. package/src/Stages/PathTracer.js +18 -21
  55. package/src/Stages/PathTracerStage.js +282 -103
  56. package/src/Storage/GeometrySpill.js +46 -14
  57. package/src/Storage/SceneGraphCodec.js +17 -3
  58. package/src/Storage/SpillStore.js +9 -0
  59. package/src/TSL/BVHTraversal.js +183 -7
  60. package/src/TSL/Common.js +57 -13
  61. package/src/TSL/ShadeKernel.js +3 -2
  62. package/src/TSL/TextureSampling.js +11 -8
  63. package/src/TSL/patches.js +84 -0
  64. package/src/index.js +1 -0
  65. package/src/integrators/BidirectionalIntegrator.js +20 -2
  66. package/src/managers/DenoisingManager.js +15 -19
  67. package/src/managers/InteractionManager.js +19 -6
  68. package/src/managers/MaterialDataManager.js +22 -5
  69. package/src/managers/UniformManager.js +3 -0
  70. package/src/managers/VideoRenderManager.js +30 -4
  71. package/src/neural/NeuralRetouch.js +1 -0
  72. package/src/neural/NeuralSuperRes.js +6 -1
  73. package/dist/ArchiveImporter-D_x863yu.js +0 -5198
  74. package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
  75. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  76. package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
@@ -94,6 +94,24 @@ function pointAt( P, basis, spans, u, w, out, o ) {
94
94
 
95
95
  }
96
96
 
97
+ /** Width at x in [0, 1] along a curve from a width per control point, weighted as the points are. */
98
+ function widthAlong( widths, basis, spans ) {
99
+
100
+ const w = [ 0, 0, 0, 0 ];
101
+ return x => {
102
+
103
+ const u = x * spans;
104
+ let span = Math.floor( u );
105
+ if ( span >= spans ) span = spans - 1;
106
+ if ( basis === 'bspline' ) bsplineWeights( u - span, w );
107
+ else bezierWeights( u - span, w );
108
+ const base = basis === 'bspline' ? span : span * 3;
109
+ return w[ 0 ] * widths[ base ] + w[ 1 ] * widths[ base + 1 ] + w[ 2 ] * widths[ base + 2 ] + w[ 3 ] * widths[ base + 3 ];
110
+
111
+ };
112
+
113
+ }
114
+
97
115
  /**
98
116
  * Sample a curve's centreline.
99
117
  * @returns {{points: Float64Array, count: number}} count points, 3 floats each
@@ -130,7 +148,7 @@ function segmentDistance( pa, a, pb, b, c ) {
130
148
  * half-width, or by the uniform strip's own worst error on this curve if that is larger.
131
149
  * @returns {Int32Array|null} kept sample indices, or null when every sample is kept
132
150
  */
133
- function adaptiveSamples( P, basis, line, tolerance, width0, width1 ) {
151
+ function adaptiveSamples( P, basis, line, tolerance, widthOf ) {
134
152
 
135
153
  const { points, count, spans } = line;
136
154
  if ( count < 3 ) return null;
@@ -143,7 +161,7 @@ function adaptiveSamples( P, basis, line, tolerance, width0, width1 ) {
143
161
  let worst = 0;
144
162
  for ( let k = 0; k < total; k ++ ) worst = Math.max( worst, segmentDistance( mids, k, points, k, k + 1 ) );
145
163
 
146
- const allowed = x => Math.max( worst, tolerance * 0.5 * ( width0 + ( width1 - width0 ) * ( x / total ) ) );
164
+ const allowed = x => Math.max( worst, tolerance * 0.5 * widthOf( x / total ) );
147
165
  const fits = ( i, j ) => {
148
166
 
149
167
  for ( let k = i + 1; k < j; k ++ ) if ( segmentDistance( points, k, points, i, j ) > allowed( k ) ) return false;
@@ -174,6 +192,8 @@ function adaptiveSamples( P, basis, line, tolerance, width0, width1 ) {
174
192
  * @param {string} [spec.basis='bezier'] - 'bezier' | 'bspline'
175
193
  * @param {number} [spec.width0=1]
176
194
  * @param {number} [spec.width1=1]
195
+ * @param {ArrayLike<number>} [spec.widths] - a width per control point, following the curve's basis like the points;
196
+ * overrides width0 / width1
177
197
  * @param {ArrayLike<number>} [spec.N] - ribbon normals (start, end), 3 floats each
178
198
  * @param {number} [spec.steps] - samples per spline span
179
199
  * @param {number} [spec.sides=1] - 1 ribbon, 2 crossed ribbons, >=3 closed tube
@@ -193,8 +213,9 @@ export function tessellateCurve( spec ) {
193
213
  const line = sampleCenterline( P, basis, steps );
194
214
  if ( ! line ) return null;
195
215
 
196
- const { points, count } = line;
197
- const kept = spec.tolerance > 0 ? adaptiveSamples( P, basis, line, spec.tolerance, width0, width1 ) : null;
216
+ const { points, count, spans } = line;
217
+ const widthOf = spec.widths ? widthAlong( spec.widths, basis, spans ) : x => width0 + ( width1 - width0 ) * x;
218
+ const kept = spec.tolerance > 0 ? adaptiveSamples( P, basis, line, spec.tolerance, widthOf ) : null;
198
219
  const rows = kept ? kept.length : count;
199
220
  const rings = sides >= 3 ? sides : sides * 2;
200
221
  const positions = new Float32Array( rows * rings * 3 );
@@ -256,8 +277,7 @@ export function tessellateCurve( spec ) {
256
277
  cross( tangent, side, up );
257
278
  normalize( up );
258
279
 
259
- const u = i / ( count - 1 || 1 );
260
- const radius = ( width0 + ( width1 - width0 ) * u ) * 0.5;
280
+ const radius = widthOf( i / ( count - 1 || 1 ) ) * 0.5;
261
281
  const cx = points[ i * 3 ], cy = points[ i * 3 + 1 ], cz = points[ i * 3 + 2 ];
262
282
 
263
283
  for ( let r = 0; r < rings; r ++ ) {
@@ -324,3 +344,201 @@ export function curveTriangleCount( controlPoints, basis, steps, sides ) {
324
344
  return segments * Math.max( 1, Math.round( sides ) ) * 2;
325
345
 
326
346
  }
347
+
348
+ const lerp3 = ( out, o, P, a, b, t ) => {
349
+
350
+ for ( let k = 0; k < 3; k ++ ) out[ o + k ] = P[ a * 3 + k ] + ( P[ b * 3 + k ] - P[ a * 3 + k ] ) * t;
351
+
352
+ };
353
+
354
+ /** Linear or Catmull-Rom spans as cubic Bézier ones (exact for both), with widths carried the same way. */
355
+ function bezierSpans( P, widths, segments, ends ) {
356
+
357
+ const out = new Float64Array( ( segments.length * 3 + 1 ) * 3 );
358
+ const w = widths ? new Float64Array( segments.length * 3 + 1 ) : null;
359
+ let o = 0;
360
+ for ( let s = 0; s < segments.length; s ++ ) {
361
+
362
+ const [ a, b, prev, next ] = segments[ s ];
363
+ if ( s === 0 ) {
364
+
365
+ for ( let k = 0; k < 3; k ++ ) out[ k ] = P[ a * 3 + k ];
366
+ if ( w ) w[ 0 ] = widths[ a ];
367
+
368
+ }
369
+
370
+ if ( ends === 'linear' ) {
371
+
372
+ lerp3( out, ( o + 1 ) * 3, P, a, b, 1 / 3 );
373
+ lerp3( out, ( o + 2 ) * 3, P, a, b, 2 / 3 );
374
+
375
+ } else {
376
+
377
+ for ( let k = 0; k < 3; k ++ ) {
378
+
379
+ out[ ( o + 1 ) * 3 + k ] = P[ a * 3 + k ] + ( P[ b * 3 + k ] - P[ prev * 3 + k ] ) / 6;
380
+ out[ ( o + 2 ) * 3 + k ] = P[ b * 3 + k ] - ( P[ next * 3 + k ] - P[ a * 3 + k ] ) / 6;
381
+
382
+ }
383
+
384
+ }
385
+
386
+ for ( let k = 0; k < 3; k ++ ) out[ ( o + 3 ) * 3 + k ] = P[ b * 3 + k ];
387
+ if ( w ) {
388
+
389
+ w[ o + 1 ] = widths[ a ] + ( widths[ b ] - widths[ a ] ) / 3;
390
+ w[ o + 2 ] = widths[ a ] + ( widths[ b ] - widths[ a ] ) * 2 / 3;
391
+ w[ o + 3 ] = widths[ b ];
392
+
393
+ }
394
+
395
+ o += 3;
396
+
397
+ }
398
+
399
+ return { P: out, widths: w, basis: 'bezier' };
400
+
401
+ }
402
+
403
+ /** One USD curve as the cubic B-spline or Bézier controls tessellateCurve takes, or null. */
404
+ function cubicControls( P, widths, type, basis, wrap ) {
405
+
406
+ const n = P.length / 3;
407
+ const periodic = wrap === 'periodic';
408
+ if ( type === 'linear' ) {
409
+
410
+ if ( n < 2 ) return null;
411
+ const segments = [];
412
+ for ( let i = 0; i + 1 < n; i ++ ) segments.push( [ i, i + 1 ] );
413
+ if ( periodic && n > 2 ) segments.push( [ n - 1, 0 ] );
414
+ return bezierSpans( P, widths, segments, 'linear' );
415
+
416
+ }
417
+
418
+ if ( basis === 'bspline' ) {
419
+
420
+ if ( wrap === 'pinned' ) {
421
+
422
+ const index = [ 0, 0, ...Array.from( { length: n }, ( _, i ) => i ), n - 1, n - 1 ];
423
+ return withIndex( P, widths, index, 'bspline' );
424
+
425
+ }
426
+
427
+ if ( periodic ) return withIndex( P, widths, [ ...Array.from( { length: n }, ( _, i ) => i ), 0, 1, 2 ], 'bspline' );
428
+ return n >= 4 ? { P, widths, basis: 'bspline' } : null;
429
+
430
+ }
431
+
432
+ if ( basis === 'bezier' ) {
433
+
434
+ if ( periodic ) return withIndex( P, widths, [ ...Array.from( { length: n }, ( _, i ) => i ), 0 ], 'bezier' );
435
+ return n >= 4 ? { P, widths, basis: 'bezier' } : null;
436
+
437
+ }
438
+
439
+ if ( basis === 'catmullRom' ) {
440
+
441
+ const segments = [];
442
+ const at = i => ( periodic ? ( i + n ) % n : Math.min( n - 1, Math.max( 0, i ) ) );
443
+ const first = periodic || wrap === 'pinned' ? 0 : 1;
444
+ const last = periodic ? n : wrap === 'pinned' ? n - 1 : n - 2;
445
+ for ( let i = first; i < last; i ++ ) segments.push( [ at( i ), at( i + 1 ), at( i - 1 ), at( i + 2 ) ] );
446
+ return segments.length ? bezierSpans( P, widths, segments, 'catmullRom' ) : null;
447
+
448
+ }
449
+
450
+ return null;
451
+
452
+ }
453
+
454
+ function withIndex( P, widths, index, basis ) {
455
+
456
+ const out = new Float64Array( index.length * 3 );
457
+ const w = widths ? new Float64Array( index.length ) : null;
458
+ index.forEach( ( v, i ) => {
459
+
460
+ for ( let k = 0; k < 3; k ++ ) out[ i * 3 + k ] = P[ v * 3 + k ];
461
+ if ( w ) w[ i ] = widths[ v ];
462
+
463
+ } );
464
+ return { P: out, widths: w, basis };
465
+
466
+ }
467
+
468
+ /**
469
+ * A USD BasisCurves prim's curves (one point array, a count per curve) as one strip mesh. Modes and types are USD's;
470
+ * the first normal of a curve orients its ribbon.
471
+ */
472
+ export function tessellateCurves( spec ) {
473
+
474
+ const { P, counts, widths = null, widthMode = 'constant', N = null, normalMode = 'vertex', type = 'cubic', basis = 'bspline', wrap = 'nonperiodic' } = spec;
475
+ const pieces = [];
476
+ let vertices = 0, indexCount = 0, skipped = 0, start = 0, varying = 0;
477
+
478
+ for ( let c = 0; c < counts.length; c ++ ) {
479
+
480
+ const n = counts[ c ];
481
+ const points = P.subarray ? P.subarray( start * 3, ( start + n ) * 3 ) : P.slice( start * 3, ( start + n ) * 3 );
482
+ let curveWidths = null, width = 1;
483
+ if ( widths?.length ) {
484
+
485
+ if ( widthMode === 'vertex' ) curveWidths = Float64Array.from( { length: n }, ( _, i ) => widths[ start + i ] );
486
+ else if ( widthMode === 'uniform' ) width = widths[ c ] ?? widths[ 0 ];
487
+ else if ( widthMode === 'varying' ) {
488
+
489
+ const spans = type === 'linear' ? n - 1 : basis === 'bezier' ? ( n - 1 ) / 3 : n - 3;
490
+ const k = Math.max( 1, Math.round( spans ) + 1 );
491
+ const values = Array.from( { length: k }, ( _, i ) => widths[ varying + i ] ?? widths[ widths.length - 1 ] );
492
+ varying += k;
493
+ curveWidths = Float64Array.from( { length: n }, ( _, i ) => values[ Math.min( k - 1, Math.round( i / Math.max( 1, n - 1 ) * ( k - 1 ) ) ) ] );
494
+
495
+ } else width = widths[ 0 ];
496
+
497
+ }
498
+
499
+ let seed = null;
500
+ if ( N?.length ) {
501
+
502
+ const at = normalMode === 'uniform' ? c : normalMode === 'constant' ? 0 : start;
503
+ seed = [ N[ at * 3 ], N[ at * 3 + 1 ], N[ at * 3 + 2 ] ];
504
+
505
+ }
506
+
507
+ const cubic = cubicControls( points, curveWidths, type, basis, wrap );
508
+ const built = cubic && tessellateCurve( {
509
+ P: cubic.P, basis: cubic.basis, widths: cubic.widths, width0: width, width1: width, N: seed,
510
+ steps: spec.steps, sides: spec.sides, tolerance: spec.tolerance,
511
+ } );
512
+
513
+ if ( built ) {
514
+
515
+ pieces.push( built );
516
+ vertices += built.positions.length / 3;
517
+ indexCount += built.indices.length;
518
+
519
+ } else {
520
+
521
+ skipped ++;
522
+
523
+ }
524
+
525
+ start += n;
526
+
527
+ }
528
+
529
+ if ( pieces.length === 0 ) return null;
530
+ const positions = new Float32Array( vertices * 3 );
531
+ const indices = new Uint32Array( indexCount );
532
+ let v = 0, i = 0;
533
+ for ( const piece of pieces ) {
534
+
535
+ positions.set( piece.positions, v * 3 );
536
+ for ( let k = 0; k < piece.indices.length; k ++ ) indices[ i + k ] = piece.indices[ k ] + v;
537
+ v += piece.positions.length / 3;
538
+ i += piece.indices.length;
539
+
540
+ }
541
+
542
+ return { positions, indices, skipped };
543
+
544
+ }
@@ -348,6 +348,9 @@ async function applyConductorColor( mat, params, ctx, prefix, applyAlbedo, setCo
348
348
  */
349
349
  export async function buildMaterial( def, ctx ) {
350
350
 
351
+ // Built already, by the USD importer.
352
+ if ( def?.material ) return def.material;
353
+
351
354
  const type = def?.type || 'diffuse';
352
355
  const params = def?.params || {};
353
356
  const mat = new MeshPhysicalMaterial( { side: DoubleSide, roughness: 1, metalness: 0 } );
@@ -17,7 +17,7 @@
17
17
  import { freeNow, resized } from './buffers.js';
18
18
  import {
19
19
  Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
20
- BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute,
20
+ BufferGeometry, BufferAttribute, Float32BufferAttribute, Uint32BufferAttribute,
21
21
  DataTexture, FloatType, RGBAFormat, LinearFilter, EquirectangularReflectionMapping,
22
22
  SRGBColorSpace, NoColorSpace, AnimationClip, VectorKeyframeTrack, QuaternionKeyframeTrack,
23
23
  NumberKeyframeTrack, BooleanKeyframeTrack, DirectionalLight, PointLight, SpotLight, FrontSide, DoubleSide,
@@ -27,8 +27,9 @@ import { buildMaterial, pBool, pFloat, pString, resolveSpectrum } from './PBRTMa
27
27
  import { makeLayer, layerMean, bake, mixOf, srgbToLinear, linearToSRGB, hasAlpha } from './PBRTTextureBake.js';
28
28
  import { loopSubdivide } from './LoopSubdivision.js';
29
29
  import { octahedralToEquirect } from './EqualAreaOctahedral.js';
30
- import { tessellateCurve } from './PBRTCurves.js';
30
+ import { tessellateCurve, tessellateCurves } from './PBRTCurves.js';
31
31
  import * as M from './PBRTMath.js';
32
+ import { packUnitAttribute } from '../GeometryExtractor.js';
32
33
 
33
34
  const LAMP_TYPES = new Set( [ 'distant', 'point', 'spot' ] );
34
35
 
@@ -107,7 +108,7 @@ const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
107
108
  const DEFAULT_CURVE_TOLERANCE = 0.05;
108
109
 
109
110
  /** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
110
- export const PBRT_BUILD_REVISION = 9;
111
+ export const PBRT_BUILD_REVISION = 10;
111
112
 
112
113
  function samePlacements( a, b ) {
113
114
 
@@ -445,7 +446,7 @@ function rasterize( width, height, colorAt ) {
445
446
  }
446
447
 
447
448
  // A clone that keeps the engine's own material properties, which three.js's copy() does not know.
448
- function cloneMaterial( material ) {
449
+ export function cloneMaterial( material ) {
449
450
 
450
451
  const out = material.clone();
451
452
  for ( const key of Object.keys( material ) ) if ( ! ( key in out ) ) out[ key ] = material[ key ];
@@ -617,14 +618,14 @@ export class PBRTSceneBuilder {
617
618
 
618
619
  const shape = ir.shapes[ i ];
619
620
  const [ geometry, surface ] = await Promise.all( [
620
- this._batches ? this._createGeometry( shape ) : this._buildGeometry( shape ),
621
+ this._batches && ! shape.shared ? this._createGeometry( shape ) : this._buildGeometry( shape ),
621
622
  this._getMaterial( shape )
622
623
  ] );
623
624
  if ( ! geometry ) continue;
624
625
  const sharedMaterial = shape.areaLight ? this._poweredMaterial( shape, geometry, surface ) : surface;
625
626
 
626
627
  // A moving shape needs a node of its own to move.
627
- if ( this._batches && ! shape.motion && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
628
+ if ( this._batches && ! shape.motion && ! shape.shared && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
628
629
 
629
630
  this._mergeShape( shape, geometry, sharedMaterial, group );
630
631
  ir.shapes[ i ] = null; // its parsed arrays are copied out; let them go
@@ -738,30 +739,45 @@ export class PBRTSceneBuilder {
738
739
 
739
740
  const scratch = new Float64Array( 16 );
740
741
  const matrix = new Matrix4();
742
+ // A template's shapes at the same relative transform share one matrix attribute, which the
743
+ // engine places as one object: one TLAS entry a copy, not one a shape.
744
+ const sharedMatrices = new Map();
741
745
 
742
746
  const place = ( list, geometry, material, rel, shapeType, materialType, tris ) => {
743
747
 
748
+ const relKey = rel ? Array.prototype.join.call( rel, ',' ) : '';
749
+ let byRel = sharedMatrices.get( list );
750
+ if ( ! byRel ) sharedMatrices.set( list, byRel = new Map() );
751
+ const shared = byRel.get( relKey );
752
+
744
753
  const affordable = Math.max( 0, this.maxPlacements - this.placementCount );
745
- const count = Math.min( list.count, affordable );
746
- if ( count < list.count ) this.skippedForBudget += list.count - count;
754
+ const count = shared ? shared.count : Math.min( list.count, affordable );
755
+ if ( ! shared && count < list.count ) this.skippedForBudget += list.count - count;
747
756
  if ( count === 0 ) return;
748
757
 
749
758
  const mesh = new InstancedMesh( geometry, material, count );
750
759
  mesh.name = `instance_${n ++}`;
751
760
  mesh.frustumCulled = false;
752
761
 
753
- for ( let i = 0; i < count; i ++ ) {
762
+ if ( shared ) mesh.instanceMatrix = shared.attribute;
763
+ else {
764
+
765
+ for ( let i = 0; i < count; i ++ ) {
766
+
767
+ if ( rel ) M.multiplyInto( scratch, list.matrices, i * 16, rel );
768
+ else for ( let e = 0; e < 16; e ++ ) scratch[ e ] = list.matrices[ i * 16 + e ];
769
+ mesh.setMatrixAt( i, matrix.fromArray( this.convertHandedness ? M.multiply( FLIP_Z, scratch ) : scratch ) );
770
+
771
+ }
754
772
 
755
- if ( rel ) M.multiplyInto( scratch, list.matrices, i * 16, rel );
756
- else for ( let e = 0; e < 16; e ++ ) scratch[ e ] = list.matrices[ i * 16 + e ];
757
- mesh.setMatrixAt( i, matrix.fromArray( this.convertHandedness ? M.multiply( FLIP_Z, scratch ) : scratch ) );
773
+ mesh.instanceMatrix.needsUpdate = true;
774
+ byRel.set( relKey, { attribute: mesh.instanceMatrix, count } );
775
+ this.placementCount += count;
758
776
 
759
777
  }
760
778
 
761
- mesh.instanceMatrix.needsUpdate = true;
762
779
  group.add( mesh );
763
780
 
764
- this.placementCount += count;
765
781
  this.reportedMeshes += count;
766
782
  if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
767
783
  mesh: `${mesh.name} ×${count}`,
@@ -800,7 +816,7 @@ export class PBRTSceneBuilder {
800
816
  // eight Pandanus trees in Moana hold 22,965 leaves each. A .ply may be shared with
801
817
  // other templates and stays whole.
802
818
  let inline = 0;
803
- for ( const shape of template ) if ( shape.type !== 'plymesh' ) inline ++;
819
+ for ( const shape of template ) if ( shape.type !== 'plymesh' && ! shape.shared ) inline ++;
804
820
  const batches = inline >= 2 ? new Map() : null;
805
821
 
806
822
  for ( const shape of template ) {
@@ -818,7 +834,7 @@ export class PBRTSceneBuilder {
818
834
  ] );
819
835
  if ( ! geometry ) continue;
820
836
 
821
- if ( batches && shape.type !== 'plymesh' && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
837
+ if ( batches && shape.type !== 'plymesh' && ! shape.shared && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
822
838
 
823
839
  this._geometryCache.delete( shape );
824
840
  const full = this._appendToBatch( batches, shape, geometry, sharedMaterial, shape.relativeCTM || shape.ctm );
@@ -1253,11 +1269,12 @@ export class PBRTSceneBuilder {
1253
1269
  /** A batch's triangles as one geometry; the batch's own arrays are released. @private */
1254
1270
  _batchGeometry( batch ) {
1255
1271
 
1272
+ // The batch's arrays cut to size, not copied: Float32BufferAttribute copies whatever it is given.
1256
1273
  const geometry = new BufferGeometry();
1257
- geometry.setAttribute( 'position', new Float32BufferAttribute( resized( batch.positions, batch.vertexCount * 3 ), 3 ) );
1258
- geometry.setAttribute( 'normal', new Float32BufferAttribute( resized( batch.normals, batch.vertexCount * 3 ), 3 ) );
1259
- if ( batch.uvs ) geometry.setAttribute( 'uv', new Float32BufferAttribute( resized( batch.uvs, batch.vertexCount * 2 ), 2 ) );
1260
- geometry.setIndex( new Uint32BufferAttribute( resized( batch.indices, batch.indexCount ), 1 ) );
1274
+ geometry.setAttribute( 'position', new BufferAttribute( resized( batch.positions, batch.vertexCount * 3 ), 3 ) );
1275
+ geometry.setAttribute( 'normal', new BufferAttribute( resized( batch.normals, batch.vertexCount * 3 ), 3 ) );
1276
+ if ( batch.uvs ) geometry.setAttribute( 'uv', new BufferAttribute( resized( batch.uvs, batch.vertexCount * 2 ), 2 ) );
1277
+ geometry.setIndex( new BufferAttribute( resized( batch.indices, batch.indexCount ), 1 ) );
1261
1278
  batch.positions = batch.normals = batch.uvs = batch.indices = null;
1262
1279
  return geometry;
1263
1280
 
@@ -1294,9 +1311,11 @@ export class PBRTSceneBuilder {
1294
1311
 
1295
1312
  // An ObjectInstance template is built once per placement, and the Moana palm debris
1296
1313
  // alone places one 208-triangle leaf 2.25 million times. three.js is happy to share
1297
- // one BufferGeometry across meshes; only the per-mesh matrix differs.
1298
- let pending = this._geometryCache.get( shape );
1299
- if ( ! pending ) this._geometryCache.set( shape, pending = this._createGeometry( shape ) );
1314
+ // one BufferGeometry across meshes; only the per-mesh matrix differs. Shapes of one
1315
+ // `geometryKey` (a USD mesh read again elsewhere) share it the same way.
1316
+ const key = shape.geometryKey ?? shape;
1317
+ let pending = this._geometryCache.get( key );
1318
+ if ( ! pending ) this._geometryCache.set( key, pending = this._createGeometry( shape ) );
1300
1319
  return pending;
1301
1320
 
1302
1321
  }
@@ -1361,6 +1380,7 @@ export class PBRTSceneBuilder {
1361
1380
  case 'bilinearmesh': return this._bilinearMesh( shape.params );
1362
1381
  case 'loopsubdiv': return this._loopSubdiv( shape.params );
1363
1382
  case 'curve': return this._curve( shape.params );
1383
+ case 'curves': return this._curves( shape.params );
1364
1384
  case 'plymesh': return this._plyMesh( shape.params );
1365
1385
  case 'sphere': return this._sphere( shape.params );
1366
1386
  case 'disk': return this._disk( shape.params );
@@ -1439,10 +1459,47 @@ export class PBRTSceneBuilder {
1439
1459
 
1440
1460
  }
1441
1461
 
1462
+ // The tessellation's own arrays, not copies; normals at 16 bits from the start, as extraction would store them.
1463
+ const geo = new BufferGeometry();
1464
+ geo.setAttribute( 'position', new BufferAttribute( built.positions, 3 ) );
1465
+ geo.setIndex( new BufferAttribute( built.indices, 1 ) );
1466
+ geo.computeVertexNormals();
1467
+ packUnitAttribute( geo, 'normal' );
1468
+ return geo;
1469
+
1470
+ }
1471
+
1472
+ /** Many curves on one point array, as USD writes them. */
1473
+ _curves( params ) {
1474
+
1475
+ const built = tessellateCurves( {
1476
+ P: params.P.value,
1477
+ counts: params.counts.value,
1478
+ widths: params.widths?.value ?? null,
1479
+ widthMode: pString( params, 'widthMode', 'constant' ),
1480
+ N: params.N?.value ?? null,
1481
+ normalMode: pString( params, 'normalMode', 'vertex' ),
1482
+ type: pString( params, 'type', 'cubic' ),
1483
+ basis: pString( params, 'basis', 'bspline' ),
1484
+ wrap: pString( params, 'wrap', 'nonperiodic' ),
1485
+ steps: this.curveSteps,
1486
+ sides: this.curveSides ?? 1,
1487
+ tolerance: this.curveTolerance,
1488
+ } );
1489
+
1490
+ if ( ! built ) {
1491
+
1492
+ this.warn( 'curves could not be tessellated' ); return null;
1493
+
1494
+ }
1495
+
1496
+ if ( built.skipped > 0 ) this.warn( `${built.skipped} curve(s) of an unsupported basis or too few points skipped` );
1497
+ // The tessellation's own arrays, not copies; normals at 16 bits from the start, as extraction would store them.
1442
1498
  const geo = new BufferGeometry();
1443
- geo.setAttribute( 'position', new Float32BufferAttribute( built.positions, 3 ) );
1444
- geo.setIndex( new Uint32BufferAttribute( built.indices, 1 ) );
1499
+ geo.setAttribute( 'position', new BufferAttribute( built.positions, 3 ) );
1500
+ geo.setIndex( new BufferAttribute( built.indices, 1 ) );
1445
1501
  geo.computeVertexNormals();
1502
+ packUnitAttribute( geo, 'normal' );
1446
1503
  return geo;
1447
1504
 
1448
1505
  }
@@ -57,6 +57,8 @@ export const LIGHT_VERTEX_STRIDE = 4;
57
57
  // SoA region stride, baked into the shader graph at build time; single instance, rebuilt on resize.
58
58
  let _cap = 0;
59
59
  let _lightVertexBase = 0;
60
+ // The G-buffer is the hit buffer's last region, one uvec4 a path: in a buffer of its own it took Shade's tenth binding.
61
+ let _gBufferBase = 0;
60
62
 
61
63
  const soa = ( id, slot ) => ( slot === 0 ? id : id.add( slot * _cap ) );
62
64
 
@@ -104,6 +106,7 @@ export class PackedRayBuffer {
104
106
  _cap = capacity;
105
107
  const hitSlots = lightVertices > 0 ? HIT_STRIDE_BIDIRECTIONAL : HIT_STRIDE;
106
108
  _lightVertexBase = capacity * hitSlots;
109
+ _gBufferBase = _lightVertexBase + lightVertices * LIGHT_VERTEX_STRIDE;
107
110
 
108
111
  // count=0 so StorageBufferNode.getHash() shares the buffer → RW and RO nodes bind the same GPU data.
109
112
  const rayCount = capacity * RAY_STRIDE;
@@ -114,7 +117,7 @@ export class PackedRayBuffer {
114
117
  ro: storage( rayAttr, 'vec4' ).toReadOnly(),
115
118
  };
116
119
 
117
- const hitCount = capacity * hitSlots + lightVertices * LIGHT_VERTEX_STRIDE;
120
+ const hitCount = _gBufferBase + capacity * GBUFFER_STRIDE;
118
121
  const hitAttr = gpuOnlyStorageAttribute( hitCount, 4, Uint32Array );
119
122
  this._attrs.hit = hitAttr;
120
123
  this.hitBuffer = {
@@ -178,7 +181,7 @@ export const readRayRadiance = ( buf, id ) =>
178
181
  // normal: raw unit vec3; depth: linear [0,1]; albedo: vec3 [0,1]. Packed values live in u32 lanes
179
182
  // verbatim (no f32 bitcast) so NaN-range bit patterns (snorm ±1 → 0x7FFF) survive store/load intact.
180
183
  // One uvec4 per pixel (stride 1); AoS base = pixelIndex * GBUFFER_STRIDE.
181
- const gbLane = ( pixelIndex ) => uint( pixelIndex ).mul( GBUFFER_STRIDE );
184
+ const gbLane = ( pixelIndex ) => uint( pixelIndex ).mul( GBUFFER_STRIDE ).add( _gBufferBase );
182
185
 
183
186
  // hitDist = null keeps the lane's current normHitDist.
184
187
  export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo, hitDist = null ) => {