rayzee 7.23.3 → 7.25.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.
@@ -9,7 +9,7 @@ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
9
9
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
10
10
  import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
11
11
  import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
12
- import { unzipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
12
+ import { unzipSync, zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
13
13
  import { disposeObjectFromMemory, updateLoading } from './utils';
14
14
  import { BuildTimer } from './BuildTimer.js';
15
15
  import { getAssetConfig } from '../AssetConfig.js';
@@ -21,13 +21,20 @@ const SUPPORTED_FORMATS = {
21
21
  'fbx': { type: 'model', name: 'FBX' }, 'obj': { type: 'model', name: 'OBJ' },
22
22
  'stl': { type: 'model', name: 'STL' }, 'ply': { type: 'model', name: 'PLY (Polygon File Format)' },
23
23
  'dae': { type: 'model', name: 'Collada' }, '3mf': { type: 'model', name: '3D Manufacturing Format' },
24
- 'usdz': { type: 'model', name: 'Universal Scene Description' },
24
+ 'usd': { type: 'model', name: 'USD (Universal Scene Description)' },
25
+ 'usda': { type: 'model', name: 'USDA (USD ASCII)' },
26
+ 'usdc': { type: 'model', name: 'USDC (USD Crate)' },
27
+ 'usdz': { type: 'model', name: 'USDZ (USD Archive)' },
25
28
  'hdr': { type: 'environment', name: 'HDR (High Dynamic Range)' }, 'exr': { type: 'environment', name: 'EXR (OpenEXR)' },
26
29
  'png': { type: 'image', name: 'PNG' }, 'jpg': { type: 'image', name: 'JPEG' },
27
30
  'jpeg': { type: 'image', name: 'JPEG' }, 'webp': { type: 'image', name: 'WebP' },
28
31
  'zip': { type: 'archive', name: 'ZIP Archive' }
29
32
  };
30
33
 
34
+ // Loose USD layers inside a ZIP compose into one scene; these pick out the
35
+ // layers and the image assets they reference.
36
+ const USD_LAYER_RE = /\.(usd|usda|usdc)$/i;
37
+ const USD_IMAGE_RE = /\.(png|jpg|jpeg|avif)$/i;
31
38
  /**
32
39
  * AssetLoader class - handles loading of 3D models, environment maps, and archives
33
40
  */
@@ -234,7 +241,10 @@ export class AssetLoader extends EventDispatcher {
234
241
  case 'ply': return await this.loadPLYFromArrayBuffer( arrayBuffer, filename );
235
242
  case 'dae': return await this.loadColladaFromFile( file, filename );
236
243
  case '3mf': return await this.load3MFFromArrayBuffer( arrayBuffer, filename );
237
- case 'usdz': return await this.loadUSDZFromArrayBuffer( arrayBuffer, filename );
244
+ case 'usd':
245
+ case 'usda':
246
+ case 'usdc':
247
+ case 'usdz': return await this.loadUSDFromArrayBuffer( arrayBuffer, filename );
238
248
  default: throw new Error( `Support for ${extension} files is not yet implemented` );
239
249
 
240
250
  }
@@ -647,6 +657,10 @@ export class AssetLoader extends EventDispatcher {
647
657
  const extension = path.split( '.' ).pop().toLowerCase();
648
658
  if ( SUPPORTED_FORMATS[ extension ] && SUPPORTED_FORMATS[ extension ].type === 'model' ) {
649
659
 
660
+ // A loose layer is only one slice of a USD scene — hand the whole
661
+ // archive over so its references and payloads can resolve.
662
+ if ( USD_LAYER_RE.test( path ) ) return await this.loadUSDHierarchyFromZip( zip );
663
+
650
664
  console.log( `Loading model file from ZIP: ${path}` );
651
665
  return await this.loadModelFromZipEntry( zip[ path ], path, extension, zip );
652
666
 
@@ -658,6 +672,48 @@ export class AssetLoader extends EventDispatcher {
658
672
 
659
673
  }
660
674
 
675
+ // USDLoader only builds a cross-layer asset map on its USDZ branch, so repack
676
+ // the archive as USDZ — root layer first, per AOUSD core spec 16.4.1.2 — and
677
+ // let the loader resolve the references/payloads itself.
678
+ async loadUSDHierarchyFromZip( zip ) {
679
+
680
+ const layers = Object.keys( zip ).filter( name => USD_LAYER_RE.test( name ) );
681
+ if ( layers.length === 0 ) throw new Error( 'No USD layers found in the ZIP archive' );
682
+
683
+ const root = AssetLoader._pickUSDRootLayer( layers );
684
+ console.log( `Loading USD scene from ZIP: ${root} (${layers.length} layers)` );
685
+
686
+ const packed = { [ root ]: [ zip[ root ], { level: 0 } ] };
687
+ for ( const name of Object.keys( zip ) ) {
688
+
689
+ if ( name === root ) continue;
690
+ if ( USD_LAYER_RE.test( name ) || USD_IMAGE_RE.test( name ) ) packed[ name ] = [ zip[ name ], { level: 0 } ];
691
+
692
+ }
693
+
694
+ return await this.loadUSDFromArrayBuffer( zipSync( packed ), root );
695
+
696
+ }
697
+
698
+ // Shallowest layer wins; among ties prefer the <dir>/<dir>.usd convention so a
699
+ // set's entry point beats its sibling variants.
700
+ static _pickUSDRootLayer( layers ) {
701
+
702
+ const depth = name => name.split( '/' ).length;
703
+ const minDepth = Math.min( ...layers.map( depth ) );
704
+ const candidates = layers.filter( name => depth( name ) === minDepth );
705
+
706
+ const conventional = candidates.find( name => {
707
+
708
+ const parts = name.split( '/' );
709
+ return parts.length > 1 && parts[ parts.length - 1 ].replace( USD_LAYER_RE, '' ) === parts[ parts.length - 2 ];
710
+
711
+ } );
712
+
713
+ return conventional || candidates[ 0 ];
714
+
715
+ }
716
+
661
717
  async loadModelFromZipEntry( fileContent, filePath, extension, zipContents ) {
662
718
 
663
719
  try {
@@ -698,8 +754,11 @@ export class AssetLoader extends EventDispatcher {
698
754
  case '3mf':
699
755
  result = await this.load3MFFromArrayBuffer( fileContent.buffer, filePath );
700
756
  break;
757
+ case 'usd':
758
+ case 'usda':
759
+ case 'usdc':
701
760
  case 'usdz':
702
- result = await this.loadUSDZFromArrayBuffer( fileContent.buffer, filePath );
761
+ result = await this.loadUSDFromArrayBuffer( fileContent, filePath );
703
762
  break;
704
763
  default:
705
764
  throw new Error( `Support for ${extension} files is not yet implemented` );
@@ -1396,21 +1455,28 @@ export class AssetLoader extends EventDispatcher {
1396
1455
 
1397
1456
  }
1398
1457
 
1399
- async loadUSDZFromArrayBuffer( arrayBuffer, filename = 'model.usdz' ) {
1458
+ async loadUSDFromArrayBuffer( data, filename = 'model.usd' ) {
1400
1459
 
1401
1460
  try {
1402
1461
 
1403
- updateLoading( { isLoading: true, status: "Processing USDZ Data...", progress: 5 } );
1462
+ updateLoading( { isLoading: true, status: "Processing USD Data...", progress: 5 } );
1404
1463
  await new Promise( r => setTimeout( r, 0 ) );
1405
1464
 
1406
- if ( ! this.loaderCache.usdz ) {
1465
+ if ( ! this.loaderCache.usd ) {
1407
1466
 
1408
- const { USDZLoader } = await import( 'three/examples/jsm/loaders/USDZLoader.js' );
1409
- this.loaderCache.usdz = new USDZLoader();
1467
+ const { USDLoader } = await import( 'three/examples/jsm/loaders/USDLoader.js' );
1468
+ this.loaderCache.usd = new USDLoader();
1410
1469
 
1411
1470
  }
1412
1471
 
1413
- const object = this.loaderCache.usdz.parse( arrayBuffer );
1472
+ // parse() returns the group synchronously but resolves textures async;
1473
+ // setupPathTracing snapshots materials, so maps must land before it runs.
1474
+ const object = await new Promise( ( resolve, reject ) => {
1475
+
1476
+ this.loaderCache.usd.parse( data, '', resolve, reject );
1477
+
1478
+ } );
1479
+
1414
1480
  object.name = filename;
1415
1481
 
1416
1482
  this.releaseTargetModel();
@@ -1424,7 +1490,7 @@ export class AssetLoader extends EventDispatcher {
1424
1490
 
1425
1491
  } catch ( error ) {
1426
1492
 
1427
- console.error( 'Error loading USDZ:', error );
1493
+ console.error( 'Error loading USD:', error );
1428
1494
  this.dispatchEvent( { type: 'error', message: error.message, filename } );
1429
1495
  throw error;
1430
1496
 
@@ -140,15 +140,26 @@ function neutralToneMap( r, g, b, exposure, out ) {
140
140
 
141
141
  }
142
142
 
143
+ /**
144
+ * Three.js clamps the fragment output with `.max( 0 )` *before* tone mapping
145
+ * (NodeMaterial.js, "force unsigned floats"), so the GPU curves never see a negative
146
+ * channel. Compositor's saturation grade (default 1.2) drives complementary channels
147
+ * below zero on a third of a typical frame, and AgX/Neutral mix those negatives across
148
+ * channels instead of clipping them — measured 7-10 levels of shadow error against the
149
+ * viewport until the readback clamps the same way.
150
+ */
151
+ const clampNegative = fn => ( r, g, b, exposure, out ) =>
152
+ fn( r > 0 ? r : 0, g > 0 ? g : 0, b > 0 ? b : 0, exposure, out );
153
+
143
154
  /** Look-up table mapping Three.js ToneMapping constants to CPU functions. */
144
155
  export const TONE_MAP_FNS = new Map( [
145
- [ NoToneMapping, noToneMap ],
146
- [ LinearToneMapping, linearToneMap ],
147
- [ ReinhardToneMapping, reinhardToneMap ],
148
- [ CineonToneMapping, cineonToneMap ],
149
- [ ACESFilmicToneMapping, acesFilmicToneMap ],
150
- [ AgXToneMapping, agxToneMap ],
151
- [ NeutralToneMapping, neutralToneMap ]
156
+ [ NoToneMapping, clampNegative( noToneMap ) ],
157
+ [ LinearToneMapping, clampNegative( linearToneMap ) ],
158
+ [ ReinhardToneMapping, clampNegative( reinhardToneMap ) ],
159
+ [ CineonToneMapping, clampNegative( cineonToneMap ) ],
160
+ [ ACESFilmicToneMapping, clampNegative( acesFilmicToneMap ) ],
161
+ [ AgXToneMapping, clampNegative( agxToneMap ) ],
162
+ [ NeutralToneMapping, clampNegative( neutralToneMap ) ]
152
163
  ] );
153
164
 
154
165
  /** sRGB gamma (1/2.2) — fast pow approximation. Prefer `linearToSRGB` when matching Three.js's output. */
@@ -165,6 +176,25 @@ export function linearToSRGB( c ) {
165
176
 
166
177
  }
167
178
 
179
+ /**
180
+ * Exposure as the WebGPU output pass applies it.
181
+ *
182
+ * Three.js applies `toneMappingExposure` *inside* ToneMappingNode's tone-mapping branch, and
183
+ * that branch returns the colour untouched for NoToneMapping (ToneMappingNode.js: `if
184
+ * ( toneMapping === NoToneMapping ) return colorNode`). So exposure is a no-op on screen there,
185
+ * and a CPU readback that applies it anyway paints brighter than the viewport it replaces
186
+ * (measured +33.6 % at exposure 2).
187
+ *
188
+ * @param {number} exposure - renderer.toneMappingExposure
189
+ * @param {number} toneMapping - Three.js ToneMapping constant
190
+ * @returns {number}
191
+ */
192
+ export function effectiveExposure( exposure, toneMapping ) {
193
+
194
+ return toneMapping === NoToneMapping ? 1.0 : exposure;
195
+
196
+ }
197
+
168
198
  /** Rec.709 luminance coefficients (same as Display / Common.js). */
169
199
  const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
170
200
 
@@ -224,6 +224,7 @@ export class RenderSettings extends EventDispatcher {
224
224
  handleRenderLimitMode: ( value ) => {
225
225
 
226
226
  stages.pathTracer?.setRenderLimitMode?.( value );
227
+ reconcileCompletion?.();
227
228
 
228
229
  },
229
230
 
@@ -1185,25 +1185,15 @@ export class PathTracerStage extends RenderStage {
1185
1185
  }
1186
1186
 
1187
1187
  /**
1188
- * Update completion threshold based on render mode
1188
+ * Update completion threshold based on render mode. The ceiling holds in every limit mode —
1189
+ * uncapping it under a time budget lets a generous deadline silently slow every render.
1189
1190
  */
1190
1191
  updateCompletionThreshold() {
1191
1192
 
1192
- const renderMode = this.renderMode.value;
1193
- const maxFrames = this.maxSamples.value;
1194
-
1195
- if ( this.renderLimitMode === 'time' ) {
1196
-
1197
- this.completionThreshold = Infinity;
1198
-
1199
- } else {
1200
-
1201
- this.completionThreshold = updateCompletionThreshold(
1202
- renderMode,
1203
- maxFrames
1204
- );
1205
-
1206
- }
1193
+ this.completionThreshold = updateCompletionThreshold(
1194
+ this.renderMode.value,
1195
+ this.maxSamples.value
1196
+ );
1207
1197
 
1208
1198
  }
1209
1199
 
@@ -138,6 +138,13 @@ export class DenoisingManager extends EventDispatcher {
138
138
  adapterInfo: null
139
139
  } ),
140
140
 
141
+ refreshInput: () => {
142
+
143
+ const ctx = this.pipeline?.context;
144
+ if ( this._stages.compositor && ctx ) this._stages.compositor.render( ctx );
145
+
146
+ },
147
+
141
148
  getGPUTextures: () => {
142
149
 
143
150
  if ( ! pt?.storageTextures?.readTarget ) return null;
@@ -190,6 +197,13 @@ export class DenoisingManager extends EventDispatcher {
190
197
 
191
198
  },
192
199
 
200
+ refreshInput: () => {
201
+
202
+ const ctx = this.pipeline?.context;
203
+ if ( this._stages.compositor && ctx ) this._stages.compositor.render( ctx );
204
+
205
+ },
206
+
193
207
  getGPUTextures: () => {
194
208
 
195
209
  if ( ! pt?.storageTextures?.readTarget ) return null;
@@ -428,13 +442,6 @@ export class DenoisingManager extends EventDispatcher {
428
442
  // Remove any stale completion-chain listener from a previous render cycle
429
443
  this._cleanupCompletionListener();
430
444
 
431
- // Show post-process canvas if any post-process is enabled
432
- if ( ( this.denoiser?.enabled || this.upscaler?.enabled ) && this.denoiserCanvas ) {
433
-
434
- this.denoiserCanvas.style.display = 'block';
435
-
436
- }
437
-
438
445
  // Chain: denoise first (if enabled), then upscale (if enabled)
439
446
  const startUpscaler = () => {
440
447
 
@@ -458,13 +465,6 @@ export class DenoisingManager extends EventDispatcher {
458
465
 
459
466
  } else {
460
467
 
461
- // Re-render compositor stage so WebGPU canvas has valid content
462
- if ( this.upscaler?.enabled && this._stages.compositor && context ) {
463
-
464
- this._stages.compositor.render( context );
465
-
466
- }
467
-
468
468
  startUpscaler();
469
469
 
470
470
  }
@@ -638,8 +638,10 @@ export class DenoisingManager extends EventDispatcher {
638
638
  setOIDNQuality( quality ) {
639
639
 
640
640
  this.denoiser?.updateQuality( quality );
641
- // Clean-aux normal follows the model: balanced/high (clean-aux models) → accumulate; fast → keep bump.
642
- this._stages.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && quality !== 'fast' );
641
+ // Clean-aux normal follows the model, not the tier name — see OIDNDenoiser.QUALITY_MODELS.
642
+ this._stages.pathTracer?.setCleanAuxNormal?.(
643
+ !! this.denoiser?.enabled && !! this.denoiser?.expectsCleanAux( quality )
644
+ );
643
645
  this._onPostProcessRefresh?.();
644
646
 
645
647
  }