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.
- package/README.md +62 -33
- package/dist/ArchiveImporter-D735xX-4.js +7860 -0
- package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
- package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
- package/dist/{NeuralRetouch-DFGbnmLb.js → NeuralRetouch-DiBH63kz.js} +1 -1
- package/dist/{NeuralSuperRes-BtXvyJQl.js → NeuralSuperRes-CZNQP13W.js} +1 -1
- package/dist/{PathTracer-BHXGJtPN.js → PathTracer-BXCCVrPK.js} +4371 -4612
- package/dist/{PhysicalSky-DN6u8UX4.js → PhysicalSky-DqmaS0lb.js} +2 -2
- package/dist/{RayzeeRenderer-Dw1x8mVL.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
- package/dist/SolarLimb-bZE6y63S.js +762 -0
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +7 -7
- package/dist/rayzee.es.js +472 -468
- package/dist/rayzee.umd.js +106 -105
- package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
- package/package.json +1 -1
- package/src/EngineDefaults.js +1 -1
- package/src/PathTracerApp.js +4 -3
- package/src/Processor/ArchiveImporter.js +173 -94
- package/src/Processor/ArchiveReader.js +20 -23
- package/src/Processor/AssetLoader.js +48 -1
- package/src/Processor/BVHRefitter.js +109 -13
- package/src/Processor/BufferLayout.js +115 -0
- package/src/Processor/ChunkedRecords.js +3 -2
- package/src/Processor/EmissiveTriangleBuilder.js +6 -6
- package/src/Processor/GeometryExtractor.js +189 -79
- package/src/Processor/HostMemory.js +39 -9
- package/src/Processor/InstanceTable.js +714 -42
- package/src/Processor/PBRT/PBRTCurves.js +224 -6
- package/src/Processor/PBRT/PBRTMaterials.js +3 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
- package/src/Processor/PackedRayBuffer.js +5 -2
- package/src/Processor/SceneProcessor.js +747 -141
- package/src/Processor/TLASBuilder.js +32 -17
- package/src/Processor/USD/USDLayer.js +1149 -0
- package/src/Processor/USD/USDScene.js +1655 -0
- package/src/Processor/USD/USDStage.js +682 -0
- package/src/Processor/USD/USDText.js +674 -0
- package/src/Processor/USD/index.js +138 -0
- package/src/Processor/Workers/BVHRefitWorker.js +3 -3
- package/src/RayzeeRenderer.js +168 -27
- package/src/Stages/NormalDepth.js +7 -12
- package/src/Stages/PathTracer.js +12 -17
- package/src/Stages/PathTracerStage.js +273 -96
- package/src/Storage/GeometrySpill.js +46 -14
- package/src/Storage/SceneGraphCodec.js +17 -3
- package/src/Storage/SpillStore.js +9 -0
- package/src/TSL/BVHTraversal.js +183 -7
- package/src/TSL/Common.js +57 -13
- package/src/TSL/ShadeKernel.js +3 -2
- package/src/TSL/TextureSampling.js +11 -8
- package/src/TSL/patches.js +84 -0
- package/src/integrators/BidirectionalIntegrator.js +20 -2
- package/src/managers/DenoisingManager.js +4 -4
- package/src/managers/InteractionManager.js +19 -6
- package/src/managers/MaterialDataManager.js +22 -5
- package/src/managers/UniformManager.js +3 -0
- package/dist/ArchiveImporter-eNlu0_6F.js +0 -5198
- package/dist/BidirectionalIntegrator-D1-6UySB.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-DA7iGoL4.js +0 -485
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -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
|
package/src/PathTracerApp.js
CHANGED
|
@@ -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( [],
|
|
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.
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 )
|
|
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
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
|
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
|
-
*
|
|
660
|
-
* material library
|
|
661
|
-
*
|
|
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
|
|
669
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
-
|
|
678
|
+
}
|
|
678
679
|
|
|
679
680
|
return path => {
|
|
680
681
|
|
|
681
|
-
const
|
|
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 (
|
|
691
|
-
|
|
692
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|