rayzee 11.2.0 → 11.3.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 (63) hide show
  1. package/README.md +62 -33
  2. package/dist/ArchiveImporter-D735xX-4.js +7860 -0
  3. package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-DFGbnmLb.js → NeuralRetouch-DiBH63kz.js} +1 -1
  6. package/dist/{NeuralSuperRes-BtXvyJQl.js → NeuralSuperRes-CZNQP13W.js} +1 -1
  7. package/dist/{PathTracer-BHXGJtPN.js → PathTracer-BXCCVrPK.js} +4371 -4612
  8. package/dist/{PhysicalSky-DN6u8UX4.js → PhysicalSky-DqmaS0lb.js} +2 -2
  9. package/dist/{RayzeeRenderer-Dw1x8mVL.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/addons/archives.es.js +1 -1
  12. package/dist/addons/bidirectional.es.js +1 -1
  13. package/dist/addons/physical-sky.es.js +1 -1
  14. package/dist/rayzee-core.es.js +7 -7
  15. package/dist/rayzee.es.js +472 -468
  16. package/dist/rayzee.umd.js +106 -105
  17. package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
  18. package/package.json +1 -1
  19. package/src/EngineDefaults.js +1 -1
  20. package/src/PathTracerApp.js +4 -3
  21. package/src/Processor/ArchiveImporter.js +173 -94
  22. package/src/Processor/ArchiveReader.js +20 -23
  23. package/src/Processor/AssetLoader.js +48 -1
  24. package/src/Processor/BVHRefitter.js +109 -13
  25. package/src/Processor/BufferLayout.js +115 -0
  26. package/src/Processor/ChunkedRecords.js +3 -2
  27. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  28. package/src/Processor/GeometryExtractor.js +189 -79
  29. package/src/Processor/HostMemory.js +39 -9
  30. package/src/Processor/InstanceTable.js +714 -42
  31. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  32. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  33. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  34. package/src/Processor/PackedRayBuffer.js +5 -2
  35. package/src/Processor/SceneProcessor.js +747 -141
  36. package/src/Processor/TLASBuilder.js +32 -17
  37. package/src/Processor/USD/USDLayer.js +1149 -0
  38. package/src/Processor/USD/USDScene.js +1655 -0
  39. package/src/Processor/USD/USDStage.js +682 -0
  40. package/src/Processor/USD/USDText.js +674 -0
  41. package/src/Processor/USD/index.js +138 -0
  42. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  43. package/src/RayzeeRenderer.js +168 -27
  44. package/src/Stages/NormalDepth.js +7 -12
  45. package/src/Stages/PathTracer.js +12 -17
  46. package/src/Stages/PathTracerStage.js +273 -96
  47. package/src/Storage/GeometrySpill.js +46 -14
  48. package/src/Storage/SceneGraphCodec.js +17 -3
  49. package/src/Storage/SpillStore.js +9 -0
  50. package/src/TSL/BVHTraversal.js +183 -7
  51. package/src/TSL/Common.js +57 -13
  52. package/src/TSL/ShadeKernel.js +3 -2
  53. package/src/TSL/TextureSampling.js +11 -8
  54. package/src/TSL/patches.js +84 -0
  55. package/src/integrators/BidirectionalIntegrator.js +20 -2
  56. package/src/managers/DenoisingManager.js +4 -4
  57. package/src/managers/InteractionManager.js +19 -6
  58. package/src/managers/MaterialDataManager.js +22 -5
  59. package/src/managers/UniformManager.js +3 -0
  60. package/dist/ArchiveImporter-eNlu0_6F.js +0 -5198
  61. package/dist/BidirectionalIntegrator-D1-6UySB.js +0 -946
  62. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  63. package/dist/sceneCachePolicy-DA7iGoL4.js +0 -485
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.2.0",
3
+ "version": "11.3.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -77,7 +77,7 @@ export const ENGINE_DEFAULTS = deepFreeze( {
77
77
 
78
78
  // Read when a model loads
79
79
  maxTextureSize: 4096, // longest edge of a material texture; clamped to the hardware ceiling
80
- areaLightIntensityScale: 1, // power of a glTF model's placeholder area lights (RectAreaLight extras)
80
+ areaLightIntensityScale: 0.1, // power of a glTF model's placeholder area lights (RectAreaLight extras)
81
81
 
82
82
  // Debug
83
83
  visMode: 0, // a debug view (TSL/Debugger.js); 0 renders normally
@@ -189,7 +189,8 @@ export class PathTracerApp extends RayzeeRenderer {
189
189
  canvas: this.canvas,
190
190
  assetLoader: this.assetLoader,
191
191
  pathTracer: null,
192
- floorPlane: this.assetLoader.floorPlane
192
+ floorPlane: this.assetLoader.floorPlane,
193
+ isGeometryOnDisk: () => this.geometryOnDisk,
193
194
  } );
194
195
 
195
196
  this.interactionManager.wireAppEvents( this );
@@ -678,7 +679,7 @@ export class PathTracerApp extends RayzeeRenderer {
678
679
 
679
680
  this._notePlacementsMoving( meshIndices );
680
681
  this._sdf.updateMeshTransforms( meshIndices );
681
- this.stages.pathTracer?.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
682
+ this.stages.pathTracer?.updateBufferRanges( [], this._sdf.takeMoveRanges() );
682
683
  if ( this._sdf.movesEmitters( meshIndices ) ) {
683
684
 
684
685
  this._emittersMoved = true;
@@ -717,7 +718,7 @@ export class PathTracerApp extends RayzeeRenderer {
717
718
 
718
719
  const run = table.placementRunOf( meshIndex );
719
720
  if ( ! run ) continue;
720
- for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], table.world, p * 16 );
721
+ for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], () => table.matrixWorldOf( p ) );
721
722
 
722
723
  }
723
724
 
@@ -10,7 +10,7 @@
10
10
  import { FloatType, EquirectangularReflectionMapping, Texture, SRGBColorSpace, RepeatWrapping, LoadingManager } from 'three';
11
11
  import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
12
12
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
13
- import { zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
13
+ import { strFromU8 } from 'three/addons/libs/fflate.module.js';
14
14
  import {
15
15
  detectArchiveKind, readTarGz, elementFilter, listArchiveElements, openTar, indexTarHeaders, openFolder } from './ArchiveReader.js';
16
16
  import { openZip, readZipDirectory } from './ZipReader.js';
@@ -23,16 +23,13 @@ import { worthStoring } from '../Storage/sceneCachePolicy.js';
23
23
  import { VERSION } from '../version.js';
24
24
  import { updateLoading } from './utils';
25
25
  import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './PBRT/index.js';
26
+ import { loadUSDScene, listUSDRootLayers, listUSDParts, USDFiles, USDStage, USD_LAYER } from './USD/index.js';
26
27
  import { pfmTexture } from './PBRT/PFM.js';
27
28
  import { extractSceneMetadata } from './SceneMetadata.js';
28
29
  import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
29
30
  import { ARCHIVE_FORMATS } from './archiveFormats.js';
30
31
  import { withHostWorker } from '../Platform.js';
31
32
 
32
- // Loose USD layers inside a ZIP compose into one scene; these pick out the
33
- // layers and the image assets they reference.
34
- const USD_LAYER_RE = /\.(usd|usda|usdc)$/i;
35
- const USD_IMAGE_RE = /\.(png|jpg|jpeg|avif)$/i;
36
33
  const MTL_TEXTURE_TIMEOUT_MS = 30000;
37
34
  const MAIN_MODEL_FILES = [ 'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb', 'main.gltf', 'main.glb', 'asset.gltf', 'asset.glb' ];
38
35
 
@@ -72,6 +69,9 @@ const bufferOf = async entry => {
72
69
  */
73
70
  export const ARCHIVE_ELEMENT_PROMPT_BYTES = 4_000_000_000;
74
71
 
72
+ /** The same question for a USD scene, in USD layer bytes: binary layers hold far more geometry a byte than pbrt text. */
73
+ export const USD_ELEMENT_PROMPT_BYTES = 1_000_000_000;
74
+
75
75
  export class ArchiveImporter {
76
76
 
77
77
  /** The archive formats it adds to the loader's. */
@@ -83,6 +83,7 @@ export class ArchiveImporter {
83
83
  this.loader = loader;
84
84
  this._pendingGraph = null;
85
85
  this.lastPBRTStats = null;
86
+ this.lastUSDStats = null;
86
87
 
87
88
  }
88
89
 
@@ -274,7 +275,7 @@ export class ArchiveImporter {
274
275
  );
275
276
 
276
277
  const archiveId = this.loader.storage ? identityKey( await folderIdentity( folder ) ) : null;
277
- return await this._loadSource( source, folder.name, { element, pbrtEntry, pbrt, archiveId, kind: 'folder' } );
278
+ return await this._loadSource( source, folder.name, { element, pbrtEntry, pbrt, archiveId, promptBytes, kind: 'folder' } );
278
279
 
279
280
  }
280
281
 
@@ -294,12 +295,12 @@ export class ArchiveImporter {
294
295
  async _loadSeekable( file, filename, { element, promptBytes, pbrtEntry, pbrt, index = null, zip = false, archiveId = null } ) {
295
296
 
296
297
  const source = await this._openSeekableArchive( file, filename, element, promptBytes, { index, zip } );
297
- return await this._loadSource( source, filename, { element, pbrtEntry, pbrt, archiveId } );
298
+ return await this._loadSource( source, filename, { element, pbrtEntry, pbrt, archiveId, promptBytes } );
298
299
 
299
300
  }
300
301
 
301
302
  /** Loads what an opened archive or folder holds. @private */
302
- async _loadSource( source, filename, { element, pbrtEntry, pbrt, archiveId, kind = 'archive' } ) {
303
+ async _loadSource( source, filename, { element, pbrtEntry, pbrt, archiveId, promptBytes, kind = 'archive' } ) {
303
304
 
304
305
  source.archiveId = archiveId;
305
306
  source.elements = ( Array.isArray( element ) ? element : [ element ] ).filter( Boolean );
@@ -311,7 +312,9 @@ export class ArchiveImporter {
311
312
 
312
313
  const entries = Object.create( null );
313
314
  for ( const e of source.listing ) if ( e.offset !== undefined ) entries[ e.path ] = source.entries[ e.path ] ?? await source.slice( e.path );
314
- const result = await this._loadNonPBRTArchive( entries, filename );
315
+ const result = this._mainModelPath( Object.keys( entries ) )?.usd
316
+ ? await this.loadUSDScene( entries, filename, { ...pbrt, element, promptBytes, listing: source.listing } )
317
+ : await this._loadNonPBRTArchive( entries, filename );
315
318
  this.loader._sourceKey = this.loader._keyed( kind, archiveId );
316
319
  return result;
317
320
 
@@ -628,39 +631,8 @@ export class ArchiveImporter {
628
631
  }
629
632
 
630
633
  const plyParser = ( buf ) => this.loader.loaderCache.ply.parse( buf );
631
-
632
- // Texture maps — decode by extension (pbrt uses .png/.jpg but also .exr/.hdr/.tga). Tagged
633
- // with their archive path so a stored scene can read them back instead of keeping pixels.
634
- const imageFromBytes = async ( bytes, fname ) => {
635
-
636
- const texture = await this._pbrtTextureFromBytes( bytes, fname );
637
- if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
638
- return texture;
639
-
640
- };
641
-
642
- // Infinite-light maps → HDR/EXR/LDR via the shared environment decoder.
643
- const envFromBytes = async ( bytes, fname ) => {
644
-
645
- const ext = fname.split( '.' ).pop().toLowerCase();
646
- const blob = new Blob( [ bytes ] );
647
- const url = ext === 'pfm' ? null : URL.createObjectURL( blob );
648
- try {
649
-
650
- const texture = url ? await this.loader.loadEnvironmentByExtension( url, ext, { loader: ext === 'exr' ? this._exrLoader() : null } ) : pfmTexture( bytes );
651
- texture.mapping = EquirectangularReflectionMapping;
652
- setEnvironmentSource( texture, `bytes:${await sampleHash( blob )}` );
653
- texture.userData[ ARCHIVE_PATH ] = fname;
654
- texture.userData[ ARCHIVE_LOADER ] = 'environment';
655
- return texture;
656
-
657
- } finally {
658
-
659
- if ( url ) URL.revokeObjectURL( url );
660
-
661
- }
662
-
663
- };
634
+ const imageFromBytes = ( bytes, fname ) => this._archiveImage( bytes, fname );
635
+ const envFromBytes = ( bytes, fname ) => this._archiveEnvironment( bytes, fname );
664
636
 
665
637
  const pbrtStart = performance.now();
666
638
  const shape = {
@@ -754,6 +726,150 @@ export class ArchiveImporter {
754
726
 
755
727
  }
756
728
 
729
+ /** A texture map from an archive, tagged with its path so a stored scene can read it back. @private */
730
+ async _archiveImage( bytes, fname ) {
731
+
732
+ const texture = await this._pbrtTextureFromBytes( bytes, fname );
733
+ if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
734
+ return texture;
735
+
736
+ }
737
+
738
+ async _archiveEnvironment( bytes, fname ) {
739
+
740
+ const ext = fname.split( '.' ).pop().toLowerCase();
741
+ const blob = new Blob( [ bytes ] );
742
+ const url = ext === 'pfm' ? null : URL.createObjectURL( blob );
743
+ try {
744
+
745
+ const texture = url ? await this.loader.loadEnvironmentByExtension( url, ext, { loader: ext === 'exr' ? this._exrLoader() : null } ) : pfmTexture( bytes );
746
+ texture.mapping = EquirectangularReflectionMapping;
747
+ setEnvironmentSource( texture, `bytes:${await sampleHash( blob )}` );
748
+ texture.userData[ ARCHIVE_PATH ] = fname;
749
+ texture.userData[ ARCHIVE_LOADER ] = 'environment';
750
+ return texture;
751
+
752
+ } finally {
753
+
754
+ if ( url ) URL.revokeObjectURL( url );
755
+
756
+ }
757
+
758
+ }
759
+
760
+ /**
761
+ * A USD scene from a folder's or archive's files (path → bytes or Blob), each layer read as composition reaches it.
762
+ * @param {object} [options] - `element`, `promptBytes`, `listing` (every file, loaded or not), and loadArchiveFromFile's budgets
763
+ */
764
+ async loadUSDScene( entries, filename, options = {} ) {
765
+
766
+ updateLoading( { isLoading: true, status: 'Reading USD scene...', progress: 5 } );
767
+ const paths = Object.keys( entries );
768
+ const omitted = ( options.listing ?? [] ).filter( e => ! ( e.path in entries ) ).map( e => e.path );
769
+ const files = new USDFiles( paths, async path => ( entries[ path ] ? bytesOf( entries[ path ] ) : null ), { omitted } );
770
+ const candidates = listUSDRootLayers( paths );
771
+ if ( candidates.length === 0 ) throw new Error( `No USD layer found in ${filename}` );
772
+ const entryPath = candidates[ 0 ];
773
+ if ( candidates.length > 1 ) {
774
+
775
+ console.warn( `${filename} holds ${candidates.length} top-level USD layers; loaded "${entryPath}". Others: ${candidates.slice( 1 ).join( ', ' )}` );
776
+ this.loader._issues?.record(
777
+ ISSUE_CODES.ASSET_AMBIGUOUS_ENTRY,
778
+ `${filename} holds ${candidates.length} top-level USD layers; loaded "${entryPath}"`,
779
+ { loaded: entryPath, alternatives: candidates.slice( 1 ) },
780
+ ISSUE_SEVERITY.WARNING
781
+ );
782
+
783
+ }
784
+
785
+ const chosen = ( Array.isArray( options.element ) ? options.element : [ options.element ] ).filter( Boolean );
786
+ if ( chosen.length === 0 ) await this._requireUSDPartChoice( filename, files, entryPath, options.listing ?? paths.map( path => ( { path, size: entries[ path ]?.size ?? entries[ path ]?.byteLength ?? 0 } ) ), options.promptBytes );
787
+
788
+ const start = performance.now();
789
+ const built = await loadUSDScene( {
790
+ files, entryPath,
791
+ resolveImage: async path => {
792
+
793
+ const bytes = await files.read( path );
794
+ return bytes ? this._archiveImage( bytes, path ) : null;
795
+
796
+ },
797
+ resolveEnvironment: async path => {
798
+
799
+ const bytes = await files.read( path );
800
+ return bytes ? this._archiveEnvironment( bytes, path ) : null;
801
+
802
+ },
803
+ maxTriangles: options.maxTriangles, maxPlacements: options.maxPlacements, mergeShapesAbove: options.mergeShapesAbove,
804
+ curveSteps: options.curveSteps, curveSides: options.curveSides, curveTolerance: options.curveTolerance,
805
+ } );
806
+
807
+ const { group, environment, warnings, triangleCount, placementCount, skippedForBudget, meshCount, parseMs, buildMs, fitNote } = built;
808
+ this.lastUSDStats = { parseMs, buildMs, loaderMs: performance.now() - start, triangleCount, placementCount, skippedForBudget, meshCount, fitNote };
809
+ if ( fitNote || skippedForBudget > 0 ) this.loader._issues?.record(
810
+ ISSUE_CODES.SCENE_MEMORY_BUDGET,
811
+ fitNote ?? `${skippedForBudget.toLocaleString()} placements past the scene budgets left out`,
812
+ { triangleCount, placementCount, skippedForBudget },
813
+ ISSUE_SEVERITY.WARNING
814
+ );
815
+ if ( warnings.length ) {
816
+
817
+ console.warn( `USD loader: ${warnings.length} warning(s) loading "${entryPath}"` );
818
+ warnings.forEach( w => console.warn( ' •', w ) );
819
+
820
+ }
821
+
822
+ if ( environment?.texture ) {
823
+
824
+ environment.texture.generateMipmaps = true;
825
+ this.loader.applyEnvironmentToScene( environment.texture );
826
+
827
+ }
828
+
829
+ group.name = entryPath.split( '/' ).pop();
830
+ this.loader.releaseTargetModel();
831
+ this.loader.targetModel = group;
832
+ this.loader.animations = [];
833
+ // Without a dome light the current environment stays: USD assets are usually lit by whatever views them.
834
+ this.loader.sceneMetadata = environment?.texture ? { environment: { rotation: environment.rotation, intensity: environment.intensity } } : null;
835
+
836
+ updateLoading( { isLoading: true, status: 'Processing USD geometry...', progress: 10 } );
837
+ await this.loader.onModelLoad( group );
838
+ this.loader.dispatchEvent( { type: 'load', model: group, filename: `${entryPath} (from ${filename})` } );
839
+ return group;
840
+
841
+ }
842
+
843
+ /** Asks which parts of a large USD scene to load. @private */
844
+ async _requireUSDPartChoice( filename, files, entryPath, listing, promptBytes = USD_ELEMENT_PROMPT_BYTES ) {
845
+
846
+ const layerBytes = listing.reduce( ( n, e ) => n + ( USD_LAYER.test( e.path ) ? e.size : 0 ), 0 );
847
+ if ( layerBytes < promptBytes ) return;
848
+
849
+ const stage = new USDStage( files );
850
+ await stage.open( entryPath );
851
+ const elements = await listUSDParts( stage, listing );
852
+ if ( elements.length < 2 ) return;
853
+
854
+ const totalBytes = listing.reduce( ( n, e ) => n + e.size, 0 );
855
+ const error = new Error(
856
+ `"${filename}" holds ${( layerBytes / 1e9 ).toFixed( 1 )} GB of USD across ${elements.length} parts. ` +
857
+ 'Choose which to load — loading all of them at once may exhaust memory.'
858
+ );
859
+ error.code = 'ARCHIVE_NEEDS_ELEMENT';
860
+ error.root = entryPath.includes( '/' ) ? entryPath.slice( 0, entryPath.lastIndexOf( '/' ) ) : '';
861
+ error.elements = elements;
862
+ error.totalBytes = totalBytes;
863
+ this.loader._issues?.record(
864
+ ISSUE_CODES.ASSET_ARCHIVE_TOO_LARGE,
865
+ `USD scene holds ${( layerBytes / 1e9 ).toFixed( 1 )} GB of layers across ${elements.length} parts; choose which to load`,
866
+ { root: error.root, elements: elements.map( e => e.prefix ), totalBytes },
867
+ ISSUE_SEVERITY.WARNING
868
+ );
869
+ throw error;
870
+
871
+ }
872
+
757
873
  /**
758
874
  * Decodes a pbrt texture map from raw bytes, picking a decoder by extension.
759
875
  * ImageBitmap can't handle EXR/HDR/TGA, which pbrt scenes use freely.
@@ -891,24 +1007,29 @@ export class ArchiveImporter {
891
1007
 
892
1008
  }
893
1009
 
894
- async findAndLoadModelFromZip( zip, filename = 'the ZIP archive' ) {
1010
+ /** The model an archive or folder loads as: a conventional main file, else the shallowest, glTF first. @private */
1011
+ _mainModelPath( paths ) {
895
1012
 
896
- const paths = Object.keys( zip );
897
1013
  const top = paths[ 0 ]?.split( '/' )[ 0 ];
898
1014
  const root = top !== undefined && paths.every( p => p.startsWith( top + '/' ) ) ? top + '/' : '';
899
1015
  const models = paths.filter( p => p.split( '.' ).pop().toLowerCase() === 'obj' || this.loader.getFileFormat( p )?.type === 'model' );
900
- if ( models.length === 0 ) throw new Error( `No supported model files found in ${filename}` );
901
-
902
- // Shallowest first, glTF before other formats at the same depth.
1016
+ if ( models.length === 0 ) return null;
903
1017
  const rank = p => p.split( '/' ).length * 2 + ( /\.(gltf|glb)$/i.test( p ) ? 0 : 1 );
904
- const main = MAIN_MODEL_FILES.map( name => root + name ).find( p => zip[ p ] );
1018
+ const have = new Set( paths );
1019
+ const main = MAIN_MODEL_FILES.map( name => root + name ).find( p => have.has( p ) );
905
1020
  const path = main ?? models.reduce( ( best, p ) => ( rank( p ) < rank( best ) ? p : best ) );
1021
+ return { path, models, main: !! main, usd: ! main && USD_LAYER.test( path ) };
1022
+
1023
+ }
906
1024
 
907
- // A loose layer is only one slice of a USD scene — hand the whole
908
- // archive over so its references and payloads can resolve.
909
- if ( ! main && USD_LAYER_RE.test( path ) ) return await this.loadUSDHierarchyFromZip( zip );
1025
+ async findAndLoadModelFromZip( zip, filename = 'the ZIP archive' ) {
1026
+
1027
+ const found = this._mainModelPath( Object.keys( zip ) );
1028
+ if ( ! found ) throw new Error( `No supported model files found in ${filename}` );
1029
+ if ( found.usd ) return await this.loadUSDScene( zip, filename );
910
1030
 
911
- const others = models.filter( p => p !== path && ! USD_LAYER_RE.test( p ) );
1031
+ const { path, models } = found;
1032
+ const others = models.filter( p => p !== path && ! USD_LAYER.test( p ) );
912
1033
  if ( others.length > 0 ) {
913
1034
 
914
1035
  console.warn( `${filename} holds ${others.length + 1} models; loaded "${path}". Others: ${others.join( ', ' )}` );
@@ -926,48 +1047,6 @@ export class ArchiveImporter {
926
1047
 
927
1048
  }
928
1049
 
929
- // USDLoader only builds a cross-layer asset map on its USDZ branch, so repack
930
- // the archive as USDZ — root layer first, per AOUSD core spec 16.4.1.2 — and
931
- // let the loader resolve the references/payloads itself.
932
- async loadUSDHierarchyFromZip( zip ) {
933
-
934
- const layers = Object.keys( zip ).filter( name => USD_LAYER_RE.test( name ) );
935
- if ( layers.length === 0 ) throw new Error( 'No USD layers found in the ZIP archive' );
936
-
937
- const root = ArchiveImporter._pickUSDRootLayer( layers );
938
- console.log( `Loading USD scene from ZIP: ${root} (${layers.length} layers)` );
939
-
940
- const packed = { [ root ]: [ await bytesOf( zip[ root ] ), { level: 0 } ] };
941
- for ( const name of Object.keys( zip ) ) {
942
-
943
- if ( name === root ) continue;
944
- if ( USD_LAYER_RE.test( name ) || USD_IMAGE_RE.test( name ) ) packed[ name ] = [ await bytesOf( zip[ name ] ), { level: 0 } ];
945
-
946
- }
947
-
948
- return await this.loader._loadModelFileByExtension( new File( [ zipSync( packed ) ], root ), root );
949
-
950
- }
951
-
952
- // Shallowest layer wins; among ties prefer the <dir>/<dir>.usd convention so a
953
- // set's entry point beats its sibling variants.
954
- static _pickUSDRootLayer( layers ) {
955
-
956
- const depth = name => name.split( '/' ).length;
957
- const minDepth = Math.min( ...layers.map( depth ) );
958
- const candidates = layers.filter( name => depth( name ) === minDepth );
959
-
960
- const conventional = candidates.find( name => {
961
-
962
- const parts = name.split( '/' );
963
- return parts.length > 1 && parts[ parts.length - 1 ].replace( USD_LAYER_RE, '' ) === parts[ parts.length - 2 ];
964
-
965
- } );
966
-
967
- return conventional || candidates[ 0 ];
968
-
969
- }
970
-
971
1050
  async loadModelFromZipEntry( fileContent, filePath, extension, zipContents, from = 'ZIP' ) {
972
1051
 
973
1052
  try {
@@ -656,44 +656,41 @@ export async function readTar( source, options = {} ) {
656
656
  /**
657
657
  * Retain-predicate for one or several subtrees of a scene archive.
658
658
  *
659
- * Keeps each chosen element plus everything above it — the top-level scene file, the shared
660
- * material library, and any `textures` folder hanging off an ancestor — so the entry .pbrt
661
- * still parses with its siblings absent. An Include pointing at a subtree that was left out
662
- * only warns, which is what makes a partial load work at all.
659
+ * Leaves out only the chosen elements' siblings (a `textures` folder excepted) and keeps everything else: the scene
660
+ * file, a material library beside it (pbrt) or in a directory of its own (USD), textures. An Include or reference
661
+ * pointing at a subtree that was left out only warns, which is what makes a partial load work at all.
663
662
  *
664
663
  * @param {string|string[]} prefixes - one element path, or several to load together
665
664
  */
666
665
  export function elementFilter( prefixes ) {
667
666
 
668
- const wanted = ( Array.isArray( prefixes ) ? prefixes : [ prefixes ] )
669
- .filter( Boolean )
670
- .map( prefix => {
667
+ const chosen = ( Array.isArray( prefixes ) ? prefixes : [ prefixes ] ).filter( Boolean ).map( normalizeTarPath );
668
+ if ( chosen.length === 0 ) return () => true;
671
669
 
672
- const p = normalizeTarPath( prefix );
673
- return { p, inside: p + '/' };
670
+ const siblings = new Map();
671
+ for ( const p of chosen ) {
674
672
 
675
- } );
673
+ const cut = p.lastIndexOf( '/' );
674
+ const parent = cut < 0 ? '' : p.slice( 0, cut + 1 );
675
+ if ( ! siblings.has( parent ) ) siblings.set( parent, new Set() );
676
+ siblings.get( parent ).add( p.slice( cut + 1 ) );
676
677
 
677
- if ( wanted.length === 0 ) return () => true;
678
+ }
678
679
 
679
680
  return path => {
680
681
 
681
- const cut = path.lastIndexOf( '/' );
682
- const dir = cut < 0 ? '' : path.slice( 0, cut );
683
- if ( dir === '' ) return true;
684
-
685
- const tex = dir.lastIndexOf( '/textures' );
686
- const texRoot = tex >= 0 && tex === dir.length - 9 ? dir.slice( 0, tex ) + '/' : null;
687
-
688
- for ( const { p, inside } of wanted ) {
682
+ for ( const [ parent, names ] of siblings ) {
689
683
 
690
- if ( path === p || path.startsWith( inside ) ) return true;
691
- if ( inside.startsWith( dir + '/' ) ) return true;
692
- if ( texRoot !== null && inside.startsWith( texRoot ) ) return true;
684
+ if ( ! path.startsWith( parent ) ) continue;
685
+ const rest = path.slice( parent.length );
686
+ const slash = rest.indexOf( '/' );
687
+ if ( slash < 0 ) continue;
688
+ const name = rest.slice( 0, slash );
689
+ if ( ! names.has( name ) && name !== 'textures' ) return false;
693
690
 
694
691
  }
695
692
 
696
- return false;
693
+ return true;
697
694
 
698
695
  };
699
696
 
@@ -18,6 +18,9 @@ import { decodersOnDemand } from './GLTFDecoders.js';
18
18
  import { extractSceneMetadata } from './SceneMetadata.js';
19
19
  import { ISSUE_CODES } from '../EngineIssues.js';
20
20
  import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
21
+ import { createLogger } from '../utils/Logger.js';
22
+
23
+ const log = createLogger( 'assets' );
21
24
 
22
25
  // Define supported file formats
23
26
  // What the core reads itself; every other format is registered (registerFormat) or comes with an add-on.
@@ -534,7 +537,9 @@ export class AssetLoader extends EventDispatcher {
534
537
  async loadModelFromFile( file, filename ) {
535
538
 
536
539
  const key = this.storage ? this._keyed( 'file', identityKey( await fileIdentity( file ) ) ) : null;
537
- const result = await this._loadModelFileByExtension( file, filename );
540
+ // three's USDLoader misreads crate files from 0.8.0 on; a .usdz package stays with it.
541
+ const usd = /\.(usd|usda|usdc)$/i.test( filename ) ? await this._archiveImporter() : null;
542
+ const result = usd ? await usd.loadUSDScene( { [ filename ]: file }, filename ) : await this._loadModelFileByExtension( file, filename );
538
543
  this._sourceKey = key;
539
544
  return result;
540
545
 
@@ -739,6 +744,7 @@ export class AssetLoader extends EventDispatcher {
739
744
 
740
745
  this._downloadComplete( "Processing Environment...", 62 );
741
746
 
747
+ if ( this.maxEnvironmentWidth ) limitEnvironmentWidth( texture, this.maxEnvironmentWidth );
742
748
  texture.mapping = EquirectangularReflectionMapping;
743
749
  texture.minFilter = LinearFilter;
744
750
  texture.magFilter = LinearFilter;
@@ -1375,3 +1381,44 @@ export class AssetLoader extends EventDispatcher {
1375
1381
 
1376
1382
  }
1377
1383
 
1384
+ /**
1385
+ * Shrinks a float environment wider than `maxWidth` by a whole factor, each texel the mean of the block it covers, so
1386
+ * the sky keeps its light. In place, then the buffer is cut to size: Moana's 14K sky is 1.6 GB as RGBA floats.
1387
+ */
1388
+ export function limitEnvironmentWidth( texture, maxWidth ) {
1389
+
1390
+ const { data, width, height } = texture.image ?? {};
1391
+ if ( ! ( data instanceof Float32Array ) || width <= maxWidth || data.length !== width * height * 4 ) return;
1392
+
1393
+ const f = Math.ceil( width / maxWidth );
1394
+ const w = Math.ceil( width / f ), h = Math.ceil( height / f );
1395
+ for ( let y = 0; y < h; y ++ ) {
1396
+
1397
+ const y1 = Math.min( height, ( y + 1 ) * f );
1398
+ for ( let x = 0; x < w; x ++ ) {
1399
+
1400
+ const x1 = Math.min( width, ( x + 1 ) * f );
1401
+ let r = 0, g = 0, b = 0, a = 0, n = 0;
1402
+ for ( let sy = y * f; sy < y1; sy ++ ) for ( let sx = x * f; sx < x1; sx ++ ) {
1403
+
1404
+ const i = ( sy * width + sx ) * 4;
1405
+ r += data[ i ]; g += data[ i + 1 ]; b += data[ i + 2 ]; a += data[ i + 3 ];
1406
+ n ++;
1407
+
1408
+ }
1409
+
1410
+ // Never past an unread texel: this block's first is at ( y·f·width + x·f )·4 ≥ the write.
1411
+ const o = ( y * w + x ) * 4;
1412
+ data[ o ] = r / n; data[ o + 1 ] = g / n; data[ o + 2 ] = b / n; data[ o + 3 ] = a / n;
1413
+
1414
+ }
1415
+
1416
+ }
1417
+
1418
+ const bytes = w * h * 16;
1419
+ const whole = data.byteOffset === 0 && data.byteLength === data.buffer.byteLength && data.buffer.transfer;
1420
+ texture.image = { data: whole ? new Float32Array( data.buffer.transfer( bytes ) ) : data.slice( 0, w * h * 4 ), width: w, height: h };
1421
+ texture.needsUpdate = true;
1422
+ log.info( `environment ${width}×${height} shrunk to ${w}×${h} (memory spill)` );
1423
+
1424
+ }