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.
- package/README.md +91 -34
- package/dist/ArchiveImporter-Cgyx5AF7.js +7860 -0
- package/dist/BidirectionalIntegrator-BKF-cwH2.js +954 -0
- package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
- package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
- package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-DV5TtURP.js} +11 -5
- package/dist/{PathTracer-D6nbknj5.js → PathTracer-D3MvEaZK.js} +4384 -4618
- package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-CY4jiz6a.js} +2 -2
- package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-D23m6NqG.js} +486 -391
- package/dist/SolarLimb-bZE6y63S.js +762 -0
- package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
- 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 +1327 -838
- package/dist/rayzee.umd.js +146 -134
- package/dist/sceneCachePolicy-AKOEu2lf.js +1602 -0
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -2
- package/src/Passes/AIUpscaler.js +2 -0
- package/src/PathTracerApp.js +126 -19
- package/src/Pipeline/CompletionTracker.js +7 -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/ToneMapCPU.js +16 -4
- package/src/Processor/ToneMapGPU.js +30 -14
- 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 +221 -33
- package/src/RenderSettings.js +4 -2
- package/src/Stages/AutoExposure.js +484 -494
- package/src/Stages/Compositor.js +27 -6
- package/src/Stages/LocalExposure.js +546 -0
- package/src/Stages/NormalDepth.js +7 -12
- package/src/Stages/PathTracer.js +18 -21
- package/src/Stages/PathTracerStage.js +282 -103
- 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/index.js +1 -0
- package/src/integrators/BidirectionalIntegrator.js +20 -2
- package/src/managers/DenoisingManager.js +15 -19
- package/src/managers/InteractionManager.js +19 -6
- package/src/managers/MaterialDataManager.js +22 -5
- package/src/managers/UniformManager.js +3 -0
- package/src/managers/VideoRenderManager.js +30 -4
- package/src/neural/NeuralRetouch.js +1 -0
- package/src/neural/NeuralSuperRes.js +6 -1
- package/dist/ArchiveImporter-D_x863yu.js +0 -5198
- package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -24,8 +24,8 @@ export const ENGINE_DEFAULTS = deepFreeze( {
|
|
|
24
24
|
|
|
25
25
|
// Sampling and convergence
|
|
26
26
|
maxSamples: 60,
|
|
27
|
-
renderLimitMode: 'frames',
|
|
28
|
-
renderTimeLimit: 30,
|
|
27
|
+
renderLimitMode: 'frames', // 'frames' | 'time' (ceiling or deadline, whichever first) | 'timeOnly' (deadline lifts the ceiling)
|
|
28
|
+
renderTimeLimit: 30, // seconds; read in 'time' and 'timeOnly'
|
|
29
29
|
renderMode: 0, // 0 preview, 1 production; configureForMode() sets it
|
|
30
30
|
useAdaptiveSampling: true,
|
|
31
31
|
noiseThreshold: 0.1, // a pixel under this √-luminance noise has converged; the interactive tier's value
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -100,6 +100,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
100
100
|
this.getExposure = options.getExposure || ( () => 1.0 );
|
|
101
101
|
this.getToneMapping = options.getToneMapping || ( () => ACESFilmicToneMapping );
|
|
102
102
|
this.getSaturation = options.getSaturation || ( () => 1.0 );
|
|
103
|
+
this.getDisplayGain = options.getDisplayGain || ( () => null );
|
|
103
104
|
|
|
104
105
|
// Configuration
|
|
105
106
|
this.enabled = false;
|
|
@@ -497,6 +498,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
497
498
|
exposure: this.getExposure(),
|
|
498
499
|
toneMapping: this.getToneMapping() ?? ACESFilmicToneMapping,
|
|
499
500
|
saturation: this.getSaturation(),
|
|
501
|
+
gain: this.getDisplayGain(),
|
|
500
502
|
} );
|
|
501
503
|
this._toneMapper.release();
|
|
502
504
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Variance } from './Stages/Variance.js';
|
|
|
8
8
|
import { BilateralFilter } from './Stages/BilateralFilter.js';
|
|
9
9
|
import { EdgeFilter } from './Stages/EdgeFilter.js';
|
|
10
10
|
import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
|
|
11
|
+
import { LocalExposure, LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
|
|
11
12
|
import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
|
|
12
13
|
import { createLogger } from './utils/Logger.js';
|
|
13
14
|
import { EngineEvents } from './EngineEvents.js';
|
|
@@ -156,10 +157,11 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
156
157
|
stages.bilateralFilter = new BilateralFilter( renderer, { enabled: false } );
|
|
157
158
|
stages.edgeFilter = new EdgeFilter( renderer, { enabled: false } );
|
|
158
159
|
stages.autoExposure = new AutoExposure( renderer, { enabled: AUTO_EXPOSURE_DEFAULTS.autoExposure } );
|
|
160
|
+
stages.localExposure = new LocalExposure( renderer, { enabled: LOCAL_EXPOSURE_DEFAULTS.localExposure } );
|
|
159
161
|
|
|
160
162
|
return [
|
|
161
163
|
stages.normalDepth, stages.motionVector, stages.nrd, stages.asvgf,
|
|
162
|
-
stages.variance, stages.bilateralFilter, stages.edgeFilter, stages.autoExposure,
|
|
164
|
+
stages.variance, stages.bilateralFilter, stages.edgeFilter, stages.autoExposure, stages.localExposure,
|
|
163
165
|
];
|
|
164
166
|
|
|
165
167
|
}
|
|
@@ -189,7 +191,8 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
189
191
|
canvas: this.canvas,
|
|
190
192
|
assetLoader: this.assetLoader,
|
|
191
193
|
pathTracer: null,
|
|
192
|
-
floorPlane: this.assetLoader.floorPlane
|
|
194
|
+
floorPlane: this.assetLoader.floorPlane,
|
|
195
|
+
isGeometryOnDisk: () => this.geometryOnDisk,
|
|
193
196
|
} );
|
|
194
197
|
|
|
195
198
|
this.interactionManager.wireAppEvents( this );
|
|
@@ -324,10 +327,11 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
324
327
|
const core = super._settingsBindings();
|
|
325
328
|
return {
|
|
326
329
|
...core,
|
|
327
|
-
//
|
|
330
|
+
// the manual exposure, and the compensation on top of auto exposure while that is on
|
|
328
331
|
applyExposure: ( value ) => {
|
|
329
332
|
|
|
330
|
-
if (
|
|
333
|
+
if ( this.stages.autoExposure ) this.stages.autoExposure.setCompensation( value );
|
|
334
|
+
else core.applyExposure( value );
|
|
331
335
|
|
|
332
336
|
},
|
|
333
337
|
onCameraProjection: ( value ) => {
|
|
@@ -350,6 +354,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
350
354
|
|
|
351
355
|
this.timeline.update();
|
|
352
356
|
this.cameraManager.updateControls();
|
|
357
|
+
this._updateExposure();
|
|
353
358
|
|
|
354
359
|
this._applyPendingRenderScale();
|
|
355
360
|
|
|
@@ -405,6 +410,8 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
405
410
|
// Render completion → denoise/upscale chain
|
|
406
411
|
_renderCompleted() {
|
|
407
412
|
|
|
413
|
+
// _finishImage() landed the exposure just before, with the loop already asleep: draw it.
|
|
414
|
+
if ( this.stages.autoExposure?.enabled ) this._refreshFinished();
|
|
408
415
|
this.denoisingManager.onRenderComplete( {
|
|
409
416
|
isStillComplete: () => this.completion.renderCompleteDispatched,
|
|
410
417
|
context: this.pipeline?.context,
|
|
@@ -412,6 +419,65 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
412
419
|
|
|
413
420
|
}
|
|
414
421
|
|
|
422
|
+
_settling() {
|
|
423
|
+
|
|
424
|
+
return !! this.stages.autoExposure?.settling;
|
|
425
|
+
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_displayGain() {
|
|
429
|
+
|
|
430
|
+
const le = this.stages.localExposure;
|
|
431
|
+
// A readback outside the loop (headless) can come before the loop would have built it from this image.
|
|
432
|
+
if ( le?.wantsBuild ) le.build( this.pipeline.context );
|
|
433
|
+
return le?.toneGain() ?? null;
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Local exposure (Unreal Engine 5's): scales the contrast of large bright and dark areas around middle grey
|
|
439
|
+
* while keeping detail, on the canvas and every tone-mapped readback alike.
|
|
440
|
+
* @param {boolean} enabled
|
|
441
|
+
*/
|
|
442
|
+
setLocalExposure( enabled ) {
|
|
443
|
+
|
|
444
|
+
const le = this.stages.localExposure;
|
|
445
|
+
if ( ! le ) return;
|
|
446
|
+
le.setEnabled( enabled );
|
|
447
|
+
this.stages.compositor.setDisplayGain( enabled ? ( rgb, uv ) => le.gainNode( rgb, uv ) : null );
|
|
448
|
+
this.refreshFrame();
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @param {{highlightContrast?: number, shadowContrast?: number, detailStrength?: number}} params - 1 changes nothing */
|
|
453
|
+
setLocalExposureParams( params ) {
|
|
454
|
+
|
|
455
|
+
this.stages.localExposure?.updateParameters( params );
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// A finished render is exposed for its own image: one reading of it, landed on.
|
|
460
|
+
_finishImage() {
|
|
461
|
+
|
|
462
|
+
const ae = this.stages.autoExposure;
|
|
463
|
+
if ( ! ae?.enabled ) return null;
|
|
464
|
+
return ae.meter( this.pipeline.context ).then( () => ae.advance( Infinity ) );
|
|
465
|
+
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The core's, plus the display-only exposure an sRGB readback was shaped by, which no setting records. */
|
|
469
|
+
getProvenance() {
|
|
470
|
+
|
|
471
|
+
const provenance = super.getProvenance();
|
|
472
|
+
const ae = this.stages.autoExposure, le = this.stages.localExposure;
|
|
473
|
+
provenance.exposure = {
|
|
474
|
+
auto: ae?.enabled ? { exposure: ae.getExposure(), strength: ae.strength, metering: ae.metering, keyValue: ae.keyValue } : null,
|
|
475
|
+
local: le?.enabled ? { highlightContrast: le.highlightContrast, shadowContrast: le.shadowContrast, detailStrength: le.detailStrength } : null,
|
|
476
|
+
};
|
|
477
|
+
return provenance;
|
|
478
|
+
|
|
479
|
+
}
|
|
480
|
+
|
|
415
481
|
_renderHelperOverlay() {
|
|
416
482
|
|
|
417
483
|
super._renderHelperOverlay();
|
|
@@ -549,7 +615,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
549
615
|
if ( enabled ) {
|
|
550
616
|
|
|
551
617
|
this.cameraManager?.setAutoFocusMode( 'manual' );
|
|
552
|
-
|
|
618
|
+
this.stages.autoExposure?.setEnabled( false );
|
|
553
619
|
// Cadence denoising is wall-clock driven, so which frame it lands on is not reproducible.
|
|
554
620
|
this.denoisingManager?.setContinuousDenoise( false );
|
|
555
621
|
return;
|
|
@@ -557,11 +623,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
557
623
|
}
|
|
558
624
|
|
|
559
625
|
if ( snapshot.autoFocusMode !== undefined ) this.cameraManager?.setAutoFocusMode( snapshot.autoFocusMode );
|
|
560
|
-
if ( this.stages.autoExposure
|
|
561
|
-
|
|
562
|
-
this.stages.autoExposure.enabled = snapshot.autoExposure;
|
|
563
|
-
|
|
564
|
-
}
|
|
626
|
+
if ( snapshot.autoExposure !== undefined ) this.stages.autoExposure?.setEnabled( snapshot.autoExposure );
|
|
565
627
|
|
|
566
628
|
if ( snapshot.continuousDenoise !== undefined ) {
|
|
567
629
|
|
|
@@ -678,7 +740,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
678
740
|
|
|
679
741
|
this._notePlacementsMoving( meshIndices );
|
|
680
742
|
this._sdf.updateMeshTransforms( meshIndices );
|
|
681
|
-
this.stages.pathTracer?.updateBufferRanges( [],
|
|
743
|
+
this.stages.pathTracer?.updateBufferRanges( [], this._sdf.takeMoveRanges() );
|
|
682
744
|
if ( this._sdf.movesEmitters( meshIndices ) ) {
|
|
683
745
|
|
|
684
746
|
this._emittersMoved = true;
|
|
@@ -717,7 +779,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
717
779
|
|
|
718
780
|
const run = table.placementRunOf( meshIndex );
|
|
719
781
|
if ( ! run ) continue;
|
|
720
|
-
for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], table.
|
|
782
|
+
for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], () => table.matrixWorldOf( p ) );
|
|
721
783
|
|
|
722
784
|
}
|
|
723
785
|
|
|
@@ -838,6 +900,9 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
838
900
|
|
|
839
901
|
this.timeline.stop();
|
|
840
902
|
this.cameraManager.controls.enabled = ! isProduction;
|
|
903
|
+
// A final render keeps the exposure it starts with through its first, noisiest samples (20 bounces: a 0.4-stop
|
|
904
|
+
// dip and back within 0.2 s at 2–8 samples), then lands as any still image does.
|
|
905
|
+
if ( this.stages.autoExposure ) this.stages.autoExposure.holdSamples = isProduction ? 8 : 0;
|
|
841
906
|
|
|
842
907
|
// Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
|
|
843
908
|
// `prev === value`, so a uniform written behind the map leaves it stale and the next set() silently no-ops.
|
|
@@ -1052,6 +1117,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
1052
1117
|
pipeline: this.pipeline,
|
|
1053
1118
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
1054
1119
|
getSaturation: () => this.settings.get( 'saturation' ) ?? 1.0,
|
|
1120
|
+
getDisplayGain: () => this._displayGain(),
|
|
1055
1121
|
issues: this._issues,
|
|
1056
1122
|
} );
|
|
1057
1123
|
|
|
@@ -1071,20 +1137,61 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
1071
1137
|
|
|
1072
1138
|
_setupAutoExposureListener() {
|
|
1073
1139
|
|
|
1074
|
-
|
|
1140
|
+
const ae = this.stages.autoExposure;
|
|
1141
|
+
if ( ! ae ) return;
|
|
1075
1142
|
|
|
1076
|
-
this.
|
|
1143
|
+
ae.setCompensation( this.settings.get( 'exposure' ) ?? 1 );
|
|
1144
|
+
if ( this.stages.localExposure ) this.stages.localExposure.onChange = () => this._refreshFinished();
|
|
1145
|
+
ae.onChange = () => this._refreshFinished();
|
|
1077
1146
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1147
|
+
// Another camera is a cut: meter it afresh.
|
|
1148
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.CAMERA_SWITCHED, () => ae.resetHistory() );
|
|
1149
|
+
|
|
1150
|
+
ae.on( 'autoexposure:updated', ( { exposure, autoExposure, targetExposure, luminance } ) => {
|
|
1151
|
+
|
|
1152
|
+
this.dispatchEvent( { type: EngineEvents.AUTO_EXPOSURE_UPDATED, exposure, autoExposure, targetExposure, luminance } );
|
|
1083
1153
|
|
|
1084
1154
|
} );
|
|
1085
1155
|
|
|
1086
1156
|
}
|
|
1087
1157
|
|
|
1158
|
+
// Only a finished render's idle loop is woken: renderFrames() and a video export drive frames themselves.
|
|
1159
|
+
_refreshFinished() {
|
|
1160
|
+
|
|
1161
|
+
if ( ! this.completion.renderCompleteDispatched ) return;
|
|
1162
|
+
this._needsDisplayRefresh = true;
|
|
1163
|
+
this.wake();
|
|
1164
|
+
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Every loop frame, a finished render included: builds local exposure and meters a finished image when asked, adapts.
|
|
1168
|
+
_updateExposure() {
|
|
1169
|
+
|
|
1170
|
+
const le = this.stages.localExposure;
|
|
1171
|
+
if ( le?.wantsBuild && this.completion.renderCompleteDispatched ) le.build( this.pipeline.context );
|
|
1172
|
+
|
|
1173
|
+
const ae = this.stages.autoExposure;
|
|
1174
|
+
if ( ! ae?.enabled ) return;
|
|
1175
|
+
|
|
1176
|
+
this._noteExposureView();
|
|
1177
|
+
const af = this.cameraManager.afScreenPoint;
|
|
1178
|
+
if ( ae.metering === 'spot' && ( ae.meteringPoint.x !== af.x || ae.meteringPoint.y !== af.y ) ) ae.updateParameters( { meteringPoint: af } );
|
|
1179
|
+
if ( ae.wantsMetering && this.completion.renderCompleteDispatched ) ae.meter( this.pipeline.context );
|
|
1180
|
+
if ( ae.update() ) this._needsDisplayRefresh = true;
|
|
1181
|
+
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Whatever moved the camera (controls, walk, timeline, a video export), auto exposure learns of it here.
|
|
1185
|
+
_noteExposureView() {
|
|
1186
|
+
|
|
1187
|
+
const { matrixWorld, projectionMatrix } = this.camera;
|
|
1188
|
+
const last = this._exposureView;
|
|
1189
|
+
if ( last?.view.equals( matrixWorld ) && last.projection.equals( projectionMatrix ) ) return;
|
|
1190
|
+
this._exposureView = { view: matrixWorld.clone(), projection: projectionMatrix.clone() };
|
|
1191
|
+
if ( last ) this.stages.autoExposure?.noteViewChanged();
|
|
1192
|
+
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1088
1195
|
async _setupOverlayManager() {
|
|
1089
1196
|
|
|
1090
1197
|
this.overlayManager = new OverlayManager( this.cameraManager.camera );
|
|
@@ -33,20 +33,24 @@ export class CompletionTracker {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Checks whether the wall-clock render budget has expired. An additional stop condition,
|
|
36
|
-
* never a replacement —
|
|
36
|
+
* never a replacement — convergence still binds when it comes first, and in 'time' mode the
|
|
37
|
+
* sample ceiling too ('timeOnly' lifts it: PathTracerStage.updateCompletionThreshold).
|
|
37
38
|
*
|
|
38
39
|
* Floored at one sample: frameCount does not advance while the stage compiles kernels or
|
|
39
40
|
* while the camera is in interaction mode, and retiring in that window denoises an empty
|
|
40
41
|
* buffer and captures a blank canvas.
|
|
41
42
|
*
|
|
42
43
|
* @param {Object} pathTracer - The PathTracer stage
|
|
43
|
-
* @param {string} renderLimitMode - 'time' or '
|
|
44
|
+
* @param {string} renderLimitMode - 'frames', 'time' or 'timeOnly'
|
|
44
45
|
* @param {number} renderTimeLimit - Time limit in seconds
|
|
45
46
|
* @returns {boolean}
|
|
46
47
|
*/
|
|
47
48
|
isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) {
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// Decided afresh on every check: a raised limit or a switch to 'frames' reopens a render the deadline retired.
|
|
51
|
+
this.budgetOverrun = false;
|
|
52
|
+
|
|
53
|
+
if ( ( renderLimitMode !== 'time' && renderLimitMode !== 'timeOnly' ) || ! ( renderTimeLimit > 0 ) ) return false;
|
|
50
54
|
if ( this.timeElapsed < renderTimeLimit ) return false;
|
|
51
55
|
|
|
52
56
|
if ( ! ( pathTracer?.frameCount > 0 ) ) {
|
|
@@ -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 {
|