From f4f07b2b4a5b5bb0fb0be723b19dc0641ba7f8b7 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 20 Feb 2026 15:05:37 +0900 Subject: [PATCH 1/6] Editor: fallback viewport camera to default camera --- editor/js/Editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/js/Editor.js b/editor/js/Editor.js index cfe7dbd01178df..98873d47289b2e 100644 --- a/editor/js/Editor.js +++ b/editor/js/Editor.js @@ -553,7 +553,7 @@ Editor.prototype = { setViewportCamera: function ( uuid ) { - this.viewportCamera = this.cameras[ uuid ]; + this.viewportCamera = this.cameras[ uuid ] || this.camera; this.signals.viewportCameraChanged.dispatch(); }, From 022c006347d355c4a93e449e90b07a8ce30a537e Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 20 Feb 2026 03:09:31 -0300 Subject: [PATCH 2/6] WebGPURenderer: Fix binding update after dispose a `StorageTexture` (#33028) --- src/renderers/common/Sampler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderers/common/Sampler.js b/src/renderers/common/Sampler.js index d7ae005b360c45..fbeba4972a9829 100644 --- a/src/renderers/common/Sampler.js +++ b/src/renderers/common/Sampler.js @@ -35,7 +35,7 @@ class Sampler extends Binding { this._onTextureDispose = () => { this.generation = null; - this.version = 0; + this.version = - 1; }; @@ -47,7 +47,7 @@ class Sampler extends Binding { * * @type {number} */ - this.version = texture ? texture.version : 0; + this.version = texture ? texture.version : - 1; /** * The binding's generation which is an additional version @@ -95,7 +95,7 @@ class Sampler extends Binding { this._texture = value; this.generation = null; - this.version = 0; + this.version = - 1; if ( this._texture ) { @@ -150,7 +150,7 @@ class Sampler extends Binding { clonedSampler._onTextureDispose = () => { clonedSampler.generation = null; - clonedSampler.version = 0; + clonedSampler.version = - 1; }; From d76b5d0031473956a64cd24d248db99985d2f21d Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 20 Feb 2026 03:12:57 -0300 Subject: [PATCH 3/6] WGSLNodeBuilder: Introduce `generateStorageTextureLoad` (#33029) --- src/nodes/accessors/StorageTextureNode.js | 22 ++++++ src/renderers/webgpu/nodes/WGSLNodeBuilder.js | 57 +++++++++------ .../accessors/StorageTextureNode.tests.js | 39 ----------- .../webgpu/nodes/WGSLNodeBuilder.tests.js | 69 ------------------- test/unit/three.source.unit.js | 6 -- 5 files changed, 58 insertions(+), 135 deletions(-) delete mode 100644 test/unit/src/nodes/accessors/StorageTextureNode.tests.js delete mode 100644 test/unit/src/renderers/webgpu/nodes/WGSLNodeBuilder.tests.js diff --git a/src/nodes/accessors/StorageTextureNode.js b/src/nodes/accessors/StorageTextureNode.js index e249c03c1924b7..62c6ac5809222b 100644 --- a/src/nodes/accessors/StorageTextureNode.js +++ b/src/nodes/accessors/StorageTextureNode.js @@ -162,6 +162,28 @@ class StorageTextureNode extends TextureNode { } + /** + * Generates the snippet for the storage texture. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {string} textureProperty - The texture property. + * @param {string} uvSnippet - The uv snippet. + * @param {?string} levelSnippet - The level snippet. + * @param {?string} biasSnippet - The bias snippet. + * @param {?string} depthSnippet - The depth snippet. + * @param {?string} compareSnippet - The compare snippet. + * @param {?Array} gradSnippet - The grad snippet. + * @param {?string} offsetSnippet - The offset snippet. + * @return {string} The generated code snippet. + */ + generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, depthSnippet, compareSnippet, gradSnippet, offsetSnippet ) { + + const texture = this.value; + + return builder.generateStorageTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ); + + } + /** * Convenience method for configuring a read/write node access. * diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index e1abfaa83945f7..12d41fe4b02837 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -582,7 +582,7 @@ class WGSLNodeBuilder extends NodeBuilder { } /** - * Generates the WGSL snippet that reads a single texel from a texture without sampling or filtering. + * Generates the WGSL snippet that reads a single texel from a storage texture. * * @param {Texture} texture - The texture. * @param {string} textureProperty - The name of the texture uniform in the shader. @@ -592,11 +592,7 @@ class WGSLNodeBuilder extends NodeBuilder { * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. * @return {string} The WGSL snippet. */ - generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { - - const isStorageTexture = texture.isStorageTexture === true; - - if ( levelSnippet === null && ! isStorageTexture ) levelSnippet = '0u'; + generateStorageTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { if ( offsetSnippet ) { @@ -608,33 +604,52 @@ class WGSLNodeBuilder extends NodeBuilder { if ( depthSnippet ) { - // Storage textures don't take a level parameter in WGSL - if ( isStorageTexture ) { + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet } )`; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet } )`; + } else { - } else { + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet } )`; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`; + } - } + return snippet; - } else { + } + + /** + * Generates the WGSL snippet that reads a single texel from a texture without sampling or filtering. + * + * @param {Texture} texture - The texture. + * @param {string} textureProperty - The name of the texture uniform in the shader. + * @param {string} uvIndexSnippet - A WGSL snippet that represents texture coordinates used for sampling. + * @param {?string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture. + * @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample. + * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @return {string} The WGSL snippet. + */ + generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) { - // Storage textures don't take a level parameter in WGSL - if ( isStorageTexture ) { + if ( levelSnippet === null ) levelSnippet = '0u'; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet } )`; + if ( offsetSnippet ) { - } else { + uvIndexSnippet = `${ uvIndexSnippet } + ${ offsetSnippet }`; - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`; + } - if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { + let snippet; - snippet += '.x'; + if ( depthSnippet ) { - } + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`; + + } else { + + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`; + + if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) { + + snippet += '.x'; } diff --git a/test/unit/src/nodes/accessors/StorageTextureNode.tests.js b/test/unit/src/nodes/accessors/StorageTextureNode.tests.js deleted file mode 100644 index f3291c7bc7a3d8..00000000000000 --- a/test/unit/src/nodes/accessors/StorageTextureNode.tests.js +++ /dev/null @@ -1,39 +0,0 @@ -import { storageTexture } from '../../../../../src/nodes/accessors/StorageTextureNode.js'; -import { NodeAccess } from '../../../../../src/nodes/core/constants.js'; -import StorageTexture from '../../../../../src/renderers/common/StorageTexture.js'; - -export default QUnit.module( 'Nodes', () => { - - QUnit.module( 'Accessors', () => { - - QUnit.module( 'StorageTextureNode', () => { - - QUnit.test( 'clone preserves access property', ( assert ) => { - - const texture = new StorageTexture( 512, 512 ); - const node = storageTexture( texture ).setAccess( NodeAccess.READ_ONLY ); - - assert.strictEqual( node.access, NodeAccess.READ_ONLY, 'original has READ_ONLY access' ); - - const cloned = node.clone(); - - assert.strictEqual( cloned.access, NodeAccess.READ_ONLY, 'cloned node preserves READ_ONLY access' ); - - } ); - - QUnit.test( 'clone preserves READ_WRITE access', ( assert ) => { - - const texture = new StorageTexture( 512, 512 ); - const node = storageTexture( texture ).setAccess( NodeAccess.READ_WRITE ); - - const cloned = node.clone(); - - assert.strictEqual( cloned.access, NodeAccess.READ_WRITE, 'cloned node preserves READ_WRITE access' ); - - } ); - - } ); - - } ); - -} ); diff --git a/test/unit/src/renderers/webgpu/nodes/WGSLNodeBuilder.tests.js b/test/unit/src/renderers/webgpu/nodes/WGSLNodeBuilder.tests.js deleted file mode 100644 index e71ba70551d65a..00000000000000 --- a/test/unit/src/renderers/webgpu/nodes/WGSLNodeBuilder.tests.js +++ /dev/null @@ -1,69 +0,0 @@ -import WGSLNodeBuilder from '../../../../../../src/renderers/webgpu/nodes/WGSLNodeBuilder.js'; - -export default QUnit.module( 'Renderers', () => { - - QUnit.module( 'WebGPU', () => { - - QUnit.module( 'Nodes', () => { - - QUnit.module( 'WGSLNodeBuilder', () => { - - // generateTextureLoad is essentially a pure function (texture info -> WGSL string) - // The only 'this' access is renderer.backend.compatibilityMode for a depth texture edge case - // We test the real method with minimal context to verify WGSL output - - QUnit.test( 'generateTextureLoad omits level for storage textures', ( assert ) => { - - const context = { - renderer: { backend: { compatibilityMode: false } } - }; - - const storageTexture = { isStorageTexture: true }; - - const snippet = WGSLNodeBuilder.prototype.generateTextureLoad.call( - context, - storageTexture, - 'testTexture', - 'uvec2(0, 0)', - null, // levelSnippet - null, // depthSnippet - null // offsetSnippet - ); - - // Storage textures should NOT have level parameter (WGSL spec) - assert.notOk( snippet.includes( 'u32(' ), 'storage texture load should not include level parameter' ); - assert.strictEqual( snippet, 'textureLoad( testTexture, uvec2(0, 0) )', 'correct WGSL for storage texture' ); - - } ); - - QUnit.test( 'generateTextureLoad includes level for regular textures', ( assert ) => { - - const context = { - renderer: { backend: { compatibilityMode: false } } - }; - - const regularTexture = { isStorageTexture: false }; - - const snippet = WGSLNodeBuilder.prototype.generateTextureLoad.call( - context, - regularTexture, - 'testTexture', - 'uvec2(0, 0)', - null, // levelSnippet - should default to '0u' - null, - null - ); - - // Regular textures SHOULD have level parameter - assert.ok( snippet.includes( 'u32( 0u )' ), 'regular texture load should include default level parameter' ); - assert.strictEqual( snippet, 'textureLoad( testTexture, uvec2(0, 0), u32( 0u ) )' ); - - } ); - - } ); - - } ); - - } ); - -} ); diff --git a/test/unit/three.source.unit.js b/test/unit/three.source.unit.js index 168016d347f44e..37ea95329d7bd2 100644 --- a/test/unit/three.source.unit.js +++ b/test/unit/three.source.unit.js @@ -265,12 +265,6 @@ import './src/renderers/webgl/WebGLTextures.tests.js'; import './src/renderers/webgl/WebGLUniforms.tests.js'; import './src/renderers/webgl/WebGLUtils.tests.js'; -//src/renderers/webgpu/nodes -import './src/renderers/webgpu/nodes/WGSLNodeBuilder.tests.js'; - -//src/nodes/accessors -import './src/nodes/accessors/StorageTextureNode.tests.js'; - //src/scenes import './src/scenes/Fog.tests.js'; From 608f75712d745b76d02a13441b4b428fbdd6eafa Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 20 Feb 2026 15:12:00 +0900 Subject: [PATCH 4/6] USDLoader: Fix skin binding fallback when geomBindTransform is missing --- examples/jsm/loaders/usd/USDComposer.js | 76 +------------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/examples/jsm/loaders/usd/USDComposer.js b/examples/jsm/loaders/usd/USDComposer.js index 4a20de859bae82..fcfb21430cc4a4 100644 --- a/examples/jsm/loaders/usd/USDComposer.js +++ b/examples/jsm/loaders/usd/USDComposer.js @@ -3559,7 +3559,9 @@ class USDComposer { } - // Use geomBindTransform if available, otherwise compute from mesh/skeleton alignment + // Use geomBindTransform if available, otherwise fall back to identity. + // Estimating bind transforms from vertex/joint samples is not robust and can + // produce severe skinning distortion for valid assets. let bindMatrix = new Matrix4(); if ( geomBindTransform && geomBindTransform.length === 16 ) { @@ -3573,11 +3575,6 @@ class USDComposer { m[ 3 ], m[ 7 ], m[ 11 ], m[ 15 ] ); - } else { - - // Compute geomBindTransform by comparing mesh vertices with skeleton bind positions - bindMatrix = this._computeGeomBindTransform( mesh, skeleton ); - } mesh.bind( skeleton, bindMatrix ); @@ -3586,73 +3583,6 @@ class USDComposer { } - _computeGeomBindTransform( mesh, skeleton ) { - - const bindMatrix = new Matrix4(); - const geometry = mesh.geometry; - const position = geometry.attributes.position; - const skinIndex = geometry.attributes.skinIndex; - - if ( ! position || ! skinIndex || position.count === 0 ) { - - return bindMatrix; - - } - - // Sample vertices and their influencing joints to compute average scale - const boneInverses = skeleton.boneInverses; - const sampleCount = Math.min( 50, position.count ); - let sumRatioX = 0, sumRatioY = 0, sumRatioZ = 0; - let validSamples = 0; - - for ( let i = 0; i < sampleCount; i ++ ) { - - const vi = Math.floor( i * position.count / sampleCount ); - const vx = position.getX( vi ); - const vy = position.getY( vi ); - const vz = position.getZ( vi ); - - // Get primary joint for this vertex - const jointIdx = skinIndex.getX( vi ); - if ( jointIdx >= boneInverses.length ) continue; - - // Get joint bind position from inverse bind matrix - const inverseBindMatrix = boneInverses[ jointIdx ]; - const bindTransform = inverseBindMatrix.clone().invert(); - const jx = bindTransform.elements[ 12 ]; - const jy = bindTransform.elements[ 13 ]; - const jz = bindTransform.elements[ 14 ]; - - // Compute ratio if both values are non-zero - if ( Math.abs( vx ) > 0.001 && Math.abs( jx ) > 0.001 ) { - - sumRatioX += jx / vx; - sumRatioY += jy / vy; - sumRatioZ += jz / vz; - validSamples ++; - - } - - } - - if ( validSamples > 0 ) { - - // Use average scale to create geomBindTransform - const avgScale = ( sumRatioX + sumRatioY + sumRatioZ ) / ( validSamples * 3 ); - - // Only apply if scale is significantly different from 1 - if ( Math.abs( avgScale - 1 ) > 0.1 ) { - - bindMatrix.makeScale( avgScale, avgScale, avgScale ); - - } - - } - - return bindMatrix; - - } - _buildAnimations() { const animations = []; From ee01f32583d15adf56c828c82fa63dabb9eec1b9 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 20 Feb 2026 15:24:49 +0900 Subject: [PATCH 5/6] USDLoader: Align USDZ root-layer handling with core spec --- examples/jsm/loaders/USDLoader.js | 45 +++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/examples/jsm/loaders/USDLoader.js b/examples/jsm/loaders/USDLoader.js index 4a8db4481f6c48..c0c287e674da6e 100644 --- a/examples/jsm/loaders/USDLoader.js +++ b/examples/jsm/loaders/USDLoader.js @@ -91,17 +91,28 @@ class USDLoader extends Loader { const usda = new USDAParser(); const usdc = new USDCParser(); + function getLowercaseExtension( filename ) { + + const lastDot = filename.lastIndexOf( '.' ); + if ( lastDot < 0 ) return ''; + + const lastSlash = filename.lastIndexOf( '/' ); + if ( lastSlash > lastDot ) return ''; + + return filename.slice( lastDot + 1 ).toLowerCase(); + + } + function parseAssets( zip ) { const data = {}; - const loader = new FileLoader(); - loader.setResponseType( 'arraybuffer' ); for ( const filename in zip ) { - if ( filename.endsWith( 'png' ) || filename.endsWith( 'jpg' ) || filename.endsWith( 'jpeg' ) ) { + const ext = getLowercaseExtension( filename ); + if ( ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'avif' ) { - const type = filename.endsWith( 'png' ) ? 'image/png' : 'image/jpeg'; + const type = ext === 'png' ? 'image/png' : ext === 'avif' ? 'image/avif' : 'image/jpeg'; const blob = new Blob( [ zip[ filename ] ], { type } ); data[ filename ] = URL.createObjectURL( blob ); @@ -111,7 +122,8 @@ class USDLoader extends Loader { for ( const filename in zip ) { - if ( filename.endsWith( 'usd' ) || filename.endsWith( 'usda' ) || filename.endsWith( 'usdc' ) ) { + const ext = getLowercaseExtension( filename ); + if ( ext === 'usd' || ext === 'usda' || ext === 'usdc' ) { if ( isCrateFile( zip[ filename ] ) ) { @@ -159,24 +171,25 @@ class USDLoader extends Loader { function findUSD( zip ) { - if ( zip.length < 1 ) return { file: undefined, basePath: '' }; + const fileNames = Object.keys( zip ); + if ( fileNames.length < 1 ) return { file: undefined, basePath: '' }; - const firstFileName = Object.keys( zip )[ 0 ]; + const firstFileName = fileNames[ 0 ]; + const ext = getLowercaseExtension( firstFileName ); let isCrate = false; const lastSlash = firstFileName.lastIndexOf( '/' ); const basePath = lastSlash >= 0 ? firstFileName.slice( 0, lastSlash ) : ''; - // As per the USD specification, the first entry in the zip archive is used as the main file ("UsdStage"). + // Per AOUSD core spec v1.0.1 section 16.4.1.2, the first ZIP entry is the root layer. // ASCII files can end in either .usda or .usd. - // See https://openusd.org/release/spec_usdz.html#layout - if ( firstFileName.endsWith( 'usda' ) ) return { file: zip[ firstFileName ], basePath }; + if ( ext === 'usda' ) return { file: zip[ firstFileName ], basePath }; - if ( firstFileName.endsWith( 'usdc' ) ) { + if ( ext === 'usdc' ) { isCrate = true; - } else if ( firstFileName.endsWith( 'usd' ) ) { + } else if ( ext === 'usd' ) { // If this is not a crate file, we assume it is a plain USDA file. if ( ! isCrateFile( zip[ firstFileName ] ) ) { @@ -230,11 +243,15 @@ class USDLoader extends Loader { if ( bytes[ 0 ] === 0x50 && bytes[ 1 ] === 0x4B ) { const zip = unzipSync( bytes ); - const assets = parseAssets( zip ); - const { file, basePath } = findUSD( zip ); + if ( ! file ) { + + throw new Error( 'USDLoader: Invalid USDZ package. The first ZIP entry must be a USD layer (.usd/.usda/.usdc).' ); + + } + const composer = new USDComposer( scope.manager ); let data; From 80587e802b6104083c56f70d9d4275620b4d7915 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Fri, 20 Feb 2026 15:37:37 +0900 Subject: [PATCH 6/6] USDLoader: simplify asset parsing and avoid redundant work --- examples/jsm/loaders/USDLoader.js | 81 +++++++++++++++---------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/examples/jsm/loaders/USDLoader.js b/examples/jsm/loaders/USDLoader.js index c0c287e674da6e..15673638f75681 100644 --- a/examples/jsm/loaders/USDLoader.js +++ b/examples/jsm/loaders/USDLoader.js @@ -90,6 +90,21 @@ class USDLoader extends Loader { const usda = new USDAParser(); const usdc = new USDCParser(); + const textDecoder = new TextDecoder(); + + function toArrayBuffer( data ) { + + if ( data instanceof ArrayBuffer ) return data; + + if ( data.byteOffset === 0 && data.byteLength === data.buffer.byteLength ) { + + return data.buffer; + + } + + return data.buffer.slice( data.byteOffset, data.byteOffset + data.byteLength ); + + } function getLowercaseExtension( filename ) { @@ -109,39 +124,26 @@ class USDLoader extends Loader { for ( const filename in zip ) { + const fileBytes = zip[ filename ]; const ext = getLowercaseExtension( filename ); + if ( ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'avif' ) { - const type = ext === 'png' ? 'image/png' : ext === 'avif' ? 'image/avif' : 'image/jpeg'; - const blob = new Blob( [ zip[ filename ] ], { type } ); - data[ filename ] = URL.createObjectURL( blob ); + // Keep raw image bytes and create object URLs lazily in USDComposer. + data[ filename ] = fileBytes; + continue; } - } + if ( ext !== 'usd' && ext !== 'usda' && ext !== 'usdc' ) continue; - for ( const filename in zip ) { - - const ext = getLowercaseExtension( filename ); - if ( ext === 'usd' || ext === 'usda' || ext === 'usdc' ) { - - if ( isCrateFile( zip[ filename ] ) ) { - - // Store parsed data (specsByPath) for on-demand composition - const parsedData = usdc.parseData( zip[ filename ].buffer ); - data[ filename ] = parsedData; - // Store raw buffer for re-parsing with variant selections - data[ filename + ':buffer' ] = zip[ filename ].buffer; + if ( isCrateFile( fileBytes ) ) { - } else { + data[ filename ] = usdc.parseData( toArrayBuffer( fileBytes ) ); - const text = new TextDecoder().decode( zip[ filename ] ); - // Store parsed data (specsByPath) for on-demand composition - data[ filename ] = usda.parseData( text ); - // Store raw text for re-parsing with variant selections - data[ filename + ':text' ] = text; + } else { - } + data[ filename ] = usda.parseData( textDecoder.decode( fileBytes ) ); } @@ -154,10 +156,9 @@ class USDLoader extends Loader { function isCrateFile( buffer ) { const crateHeader = new Uint8Array( [ 0x50, 0x58, 0x52, 0x2D, 0x55, 0x53, 0x44, 0x43 ] ); // PXR-USDC + const view = buffer instanceof Uint8Array ? buffer : new Uint8Array( buffer ); - if ( buffer.byteLength < crateHeader.length ) return false; - - const view = new Uint8Array( buffer, 0, crateHeader.length ); + if ( view.byteLength < crateHeader.length ) return false; for ( let i = 0; i < crateHeader.length; i ++ ) { @@ -172,7 +173,7 @@ class USDLoader extends Loader { function findUSD( zip ) { const fileNames = Object.keys( zip ); - if ( fileNames.length < 1 ) return { file: undefined, basePath: '' }; + if ( fileNames.length < 1 ) return { file: undefined, filename: '', basePath: '' }; const firstFileName = fileNames[ 0 ]; const ext = getLowercaseExtension( firstFileName ); @@ -183,7 +184,7 @@ class USDLoader extends Loader { // Per AOUSD core spec v1.0.1 section 16.4.1.2, the first ZIP entry is the root layer. // ASCII files can end in either .usda or .usd. - if ( ext === 'usda' ) return { file: zip[ firstFileName ], basePath }; + if ( ext === 'usda' ) return { file: zip[ firstFileName ], filename: firstFileName, basePath }; if ( ext === 'usdc' ) { @@ -194,7 +195,7 @@ class USDLoader extends Loader { // If this is not a crate file, we assume it is a plain USDA file. if ( ! isCrateFile( zip[ firstFileName ] ) ) { - return { file: zip[ firstFileName ], basePath }; + return { file: zip[ firstFileName ], filename: firstFileName, basePath }; } else { @@ -206,11 +207,11 @@ class USDLoader extends Loader { if ( isCrate ) { - return { file: zip[ firstFileName ], basePath }; + return { file: zip[ firstFileName ], filename: firstFileName, basePath }; } - return { file: undefined, basePath: '' }; + return { file: undefined, filename: '', basePath: '' }; } @@ -231,7 +232,7 @@ class USDLoader extends Loader { if ( isCrateFile( buffer ) ) { const composer = new USDComposer( scope.manager ); - const data = usdc.parseData( buffer ); + const data = usdc.parseData( toArrayBuffer( buffer ) ); return composer.compose( data, {} ); } @@ -244,7 +245,7 @@ class USDLoader extends Loader { const zip = unzipSync( bytes ); const assets = parseAssets( zip ); - const { file, basePath } = findUSD( zip ); + const { file, filename, basePath } = findUSD( zip ); if ( ! file ) { @@ -253,16 +254,10 @@ class USDLoader extends Loader { } const composer = new USDComposer( scope.manager ); - let data; - - if ( isCrateFile( file ) ) { - - data = usdc.parseData( file.buffer ); - - } else { + const data = assets[ filename ]; + if ( ! data ) { - const text = new TextDecoder().decode( file ); - data = usda.parseData( text ); + throw new Error( 'USDLoader: Failed to parse root layer "' + filename + '".' ); } @@ -273,7 +268,7 @@ class USDLoader extends Loader { // USDA (standalone, as ArrayBuffer) const composer = new USDComposer( scope.manager ); - const text = new TextDecoder().decode( bytes ); + const text = textDecoder.decode( bytes ); const data = usda.parseData( text ); return composer.compose( data, {} );