diff --git a/examples/jsm/loaders/usd/USDAParser.js b/examples/jsm/loaders/usd/USDAParser.js index a95037f1fbca24..ca1f787a49ea1c 100644 --- a/examples/jsm/loaders/usd/USDAParser.js +++ b/examples/jsm/loaders/usd/USDAParser.js @@ -454,6 +454,12 @@ class USDAParser { } + if ( header.metersPerUnit !== undefined ) { + + rootFields.metersPerUnit = parseFloat( header.metersPerUnit ); + + } + } specsByPath[ '/' ] = { specType: SpecType.Prim, fields: rootFields }; diff --git a/examples/jsm/loaders/usd/USDComposer.js b/examples/jsm/loaders/usd/USDComposer.js index 7862e1fed054bf..759c4ea0f1dc7a 100644 --- a/examples/jsm/loaders/usd/USDComposer.js +++ b/examples/jsm/loaders/usd/USDComposer.js @@ -1,8 +1,12 @@ import { AnimationClip, + BoxGeometry, BufferAttribute, BufferGeometry, + CapsuleGeometry, ClampToEdgeWrapping, + ConeGeometry, + CylinderGeometry, Euler, Group, Matrix4, @@ -20,6 +24,7 @@ import { SkinnedMesh, Skeleton, Bone, + SphereGeometry, SRGBColorSpace, Texture, Vector2, @@ -110,6 +115,15 @@ class USDComposer { // Build animations group.animations = this._buildAnimations(); + // Handle metersPerUnit scaling + const metersPerUnit = rootFields.metersPerUnit; + + if ( metersPerUnit !== undefined && metersPerUnit !== 1 ) { + + group.scale.setScalar( metersPerUnit ); + + } + // Handle Z-up to Y-up conversion if ( rootSpec && rootSpec.fields && rootSpec.fields.upAxis === 'Z' ) { @@ -438,18 +452,37 @@ class USDComposer { } - // Build shader index (shaders are children of materials) + // Build shader index (shaders are children or descendants of materials) if ( typeName === 'Shader' && lastSlash > 0 ) { - const materialPath = path.slice( 0, lastSlash ); + // Walk up ancestors to find the nearest Material prim. + // Shaders may be direct children of a Material, or nested + // inside a NodeGraph (common with MaterialX materials). - if ( ! this.shadersByMaterialPath.has( materialPath ) ) { + let ancestorPath = path.slice( 0, lastSlash ); - this.shadersByMaterialPath.set( materialPath, [] ); + while ( ancestorPath.length > 0 ) { - } + const ancestorSpec = this.specsByPath[ ancestorPath ]; + + if ( ancestorSpec && ancestorSpec.specType === SpecType.Prim && ancestorSpec.fields.typeName === 'Material' ) { + + if ( ! this.shadersByMaterialPath.has( ancestorPath ) ) { + + this.shadersByMaterialPath.set( ancestorPath, [] ); + + } + + this.shadersByMaterialPath.get( ancestorPath ).push( path ); + break; + + } - this.shadersByMaterialPath.get( materialPath ).push( path ); + const slash = ancestorPath.lastIndexOf( '/' ); + if ( slash <= 0 ) break; + ancestorPath = ancestorPath.slice( 0, slash ); + + } } @@ -681,6 +714,16 @@ class USDComposer { parent.add( obj ); this._buildHierarchy( obj, path ); + } else if ( typeName === 'Cube' || typeName === 'Sphere' || typeName === 'Cylinder' || typeName === 'Cone' || typeName === 'Capsule' ) { + + const obj = this._buildGeomPrimitive( path, spec, typeName ); + if ( obj ) { + + parent.add( obj ); + this._buildHierarchy( obj, path ); + + } + } else if ( typeName === 'Material' || typeName === 'Shader' || typeName === 'GeomSubset' ) { // Skip materials/shaders/subsets, they're referenced by meshes @@ -1073,6 +1116,86 @@ class USDComposer { } + /** + * Build a mesh from a USD geometric primitive (Cube, Sphere, Cylinder, Cone, Capsule). + */ + _buildGeomPrimitive( path, spec, typeName ) { + + const attrs = this._getAttributes( path ); + const name = path.split( '/' ).pop(); + + let geometry; + + switch ( typeName ) { + + case 'Cube': { + + const size = attrs[ 'size' ] || 2; + geometry = new BoxGeometry( size, size, size ); + break; + + } + + case 'Sphere': { + + const radius = attrs[ 'radius' ] || 1; + geometry = new SphereGeometry( radius, 32, 16 ); + break; + + } + + case 'Cylinder': { + + const height = attrs[ 'height' ] || 2; + const radius = attrs[ 'radius' ] || 1; + geometry = new CylinderGeometry( radius, radius, height, 32 ); + break; + + } + + case 'Cone': { + + const height = attrs[ 'height' ] || 2; + const radius = attrs[ 'radius' ] || 1; + geometry = new ConeGeometry( radius, height, 32 ); + break; + + } + + case 'Capsule': { + + const height = attrs[ 'height' ] || 1; + const radius = attrs[ 'radius' ] || 0.5; + geometry = new CapsuleGeometry( radius, height, 16, 32 ); + break; + + } + + } + + // USD defaults axis to "Z", Three.js uses Y + const axis = attrs[ 'axis' ] || 'Z'; + + if ( axis === 'X' ) { + + geometry.rotateZ( - Math.PI / 2 ); + + } else if ( axis === 'Z' ) { + + geometry.rotateX( Math.PI / 2 ); + + } + + const material = this._buildMaterial( path, spec.fields ); + const mesh = new Mesh( geometry, material ); + mesh.name = name; + + this.applyTransform( mesh, spec.fields, attrs ); + + return mesh; + + } + /** * Build a mesh from a Mesh spec. */ @@ -1451,7 +1574,12 @@ class USDComposer { } else { - geometry.computeVertexNormals(); + // Compute vertex normals from the original indexed topology where + // vertices are shared, then expand them like positions. + const vertexNormals = this._computeVertexNormals( points, indices ); + geometry.setAttribute( 'normal', new BufferAttribute( new Float32Array( + this._expandAttribute( vertexNormals, indices, 3 ) + ), 3 ) ); } @@ -1727,12 +1855,18 @@ class USDComposer { ? this._applyTriangulationPattern( Array.from( { length: numFaceVertices }, ( _, i ) => i ), triPattern ) : null ); + // When no normals are provided, compute vertex normals from + // the indexed topology so that shared vertices produce averaged normals. + const vertexNormals = ( ! normals && origIndices.length > 0 ) + ? this._computeVertexNormals( points, origIndices ) + : null; + // Build reordered vertex data const vertexCount = triangleCount * 3; const positions = new Float32Array( vertexCount * 3 ); const uvData = uvs ? new Float32Array( vertexCount * 2 ) : null; const uv1Data = uvs2 ? new Float32Array( vertexCount * 2 ) : null; - const normalData = normals ? new Float32Array( vertexCount * 3 ) : null; + const normalData = ( normals || vertexNormals ) ? new Float32Array( vertexCount * 3 ) : null; const skinIndexData = jointIndices ? new Uint16Array( vertexCount * 4 ) : null; const skinWeightData = jointWeights ? new Float32Array( vertexCount * 4 ) : null; @@ -1784,21 +1918,27 @@ class USDComposer { } - if ( normalData && normals ) { + if ( normalData ) { - if ( origNormalIndices ) { + if ( normals && origNormalIndices ) { const normalIdx = origNormalIndices[ origIdx ]; normalData[ newIdx * 3 ] = normals[ normalIdx * 3 ]; normalData[ newIdx * 3 + 1 ] = normals[ normalIdx * 3 + 1 ]; normalData[ newIdx * 3 + 2 ] = normals[ normalIdx * 3 + 2 ]; - } else if ( normals.length === points.length ) { + } else if ( normals && normals.length === points.length ) { normalData[ newIdx * 3 ] = normals[ pointIdx * 3 ]; normalData[ newIdx * 3 + 1 ] = normals[ pointIdx * 3 + 1 ]; normalData[ newIdx * 3 + 2 ] = normals[ pointIdx * 3 + 2 ]; + } else if ( vertexNormals ) { + + normalData[ newIdx * 3 ] = vertexNormals[ pointIdx * 3 ]; + normalData[ newIdx * 3 + 1 ] = vertexNormals[ pointIdx * 3 + 1 ]; + normalData[ newIdx * 3 + 2 ] = vertexNormals[ pointIdx * 3 + 2 ]; + } } @@ -1841,15 +1981,7 @@ class USDComposer { } - if ( normalData ) { - - geometry.setAttribute( 'normal', new BufferAttribute( normalData, 3 ) ); - - } else { - - geometry.computeVertexNormals(); - - } + geometry.setAttribute( 'normal', new BufferAttribute( normalData, 3 ) ); if ( skinIndexData ) { @@ -2352,6 +2484,57 @@ class USDComposer { } + /** + * Compute per-vertex normals from indexed triangle data. + * Accumulates area-weighted face normals at each shared vertex and normalizes. + */ + _computeVertexNormals( points, indices ) { + + const numVertices = points.length / 3; + const normals = new Float32Array( numVertices * 3 ); + + for ( let i = 0; i < indices.length; i += 3 ) { + + const a = indices[ i ]; + const b = indices[ i + 1 ]; + const c = indices[ i + 2 ]; + + const ax = points[ a * 3 ], ay = points[ a * 3 + 1 ], az = points[ a * 3 + 2 ]; + const bx = points[ b * 3 ], by = points[ b * 3 + 1 ], bz = points[ b * 3 + 2 ]; + const cx = points[ c * 3 ], cy = points[ c * 3 + 1 ], cz = points[ c * 3 + 2 ]; + + const e1x = bx - ax, e1y = by - ay, e1z = bz - az; + const e2x = cx - ax, e2y = cy - ay, e2z = cz - az; + + const nx = e1y * e2z - e1z * e2y; + const ny = e1z * e2x - e1x * e2z; + const nz = e1x * e2y - e1y * e2x; + + normals[ a * 3 ] += nx; normals[ a * 3 + 1 ] += ny; normals[ a * 3 + 2 ] += nz; + normals[ b * 3 ] += nx; normals[ b * 3 + 1 ] += ny; normals[ b * 3 + 2 ] += nz; + normals[ c * 3 ] += nx; normals[ c * 3 + 1 ] += ny; normals[ c * 3 + 2 ] += nz; + + } + + for ( let i = 0; i < numVertices; i ++ ) { + + const x = normals[ i * 3 ], y = normals[ i * 3 + 1 ], z = normals[ i * 3 + 2 ]; + const len = Math.sqrt( x * x + y * y + z * z ); + + if ( len > 0 ) { + + normals[ i * 3 ] /= len; + normals[ i * 3 + 1 ] /= len; + normals[ i * 3 + 2 ] /= len; + + } + + } + + return normals; + + } + /** * Get the material path for a mesh, checking various binding sources. */ @@ -2550,7 +2733,7 @@ class USDComposer { const shaderAttrs = this._getAttributes( path ); const infoId = shaderAttrs[ 'info:id' ] || spec.fields[ 'info:id' ]; - if ( infoId === 'UsdPreviewSurface' ) { + if ( infoId === 'UsdPreviewSurface' || infoId === 'ND_UsdPreviewSurface_surfaceshader' ) { this._applyPreviewSurface( material, path ); diff --git a/examples/screenshots/webgl_loader_imagebitmap.jpg b/examples/screenshots/webgl_loader_imagebitmap.jpg index 7ebfbf8ead504d..659a6cc3d631ad 100644 Binary files a/examples/screenshots/webgl_loader_imagebitmap.jpg and b/examples/screenshots/webgl_loader_imagebitmap.jpg differ diff --git a/examples/webgl_loader_imagebitmap.html b/examples/webgl_loader_imagebitmap.html index 9cd7493a6025b9..394a39b3f05695 100644 --- a/examples/webgl_loader_imagebitmap.html +++ b/examples/webgl_loader_imagebitmap.html @@ -32,6 +32,7 @@ function addImageBitmap() { new THREE.ImageBitmapLoader() + .setOptions( { imageOrientation: 'flipY' } ) .load( 'textures/planets/earth_atmos_2048.jpg?' + performance.now(), function ( imageBitmap ) { const texture = new THREE.CanvasTexture( imageBitmap ); diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js index 5dca90f30f6de1..5bee14f90deafc 100644 --- a/src/objects/BatchedMesh.js +++ b/src/objects/BatchedMesh.js @@ -1133,7 +1133,23 @@ class BatchedMesh extends Mesh { getColorAt( instanceId, color ) { this.validateInstanceId( instanceId ); - return color.fromArray( this._colorsTexture.image.data, instanceId * 4 ); + if ( this._colorsTexture === null ) { + + if ( color.isVector4 ) { + + return color.set( 1, 1, 1, 1 ); + + } else { + + return color.setRGB( 1, 1, 1 ); + + } + + } else { + + return color.fromArray( this._colorsTexture.image.data, instanceId * 4 ); + + } } diff --git a/src/objects/InstancedMesh.js b/src/objects/InstancedMesh.js index fcad3777288d98..0947edc574a5d3 100644 --- a/src/objects/InstancedMesh.js +++ b/src/objects/InstancedMesh.js @@ -217,7 +217,15 @@ class InstancedMesh extends Mesh { */ getColorAt( index, color ) { - return color.fromArray( this.instanceColor.array, index * 3 ); + if ( this.instanceColor === null ) { + + return color.setRGB( 1, 1, 1 ); + + } else { + + return color.fromArray( this.instanceColor.array, index * 3 ); + + } } diff --git a/src/renderers/webgl/WebGLTextures.js b/src/renderers/webgl/WebGLTextures.js index 08ba467679a24c..85c2bcad042999 100644 --- a/src/renderers/webgl/WebGLTextures.js +++ b/src/renderers/webgl/WebGLTextures.js @@ -874,14 +874,21 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils, state.activeTexture( _gl.TEXTURE0 + slot ); - const workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace ); - const texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace ); - const unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL; + const isImageBitmap = ( typeof ImageBitmap !== 'undefined' && texture.image instanceof ImageBitmap ); + + if ( isImageBitmap === false ) { + + const workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace ); + const texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace ); + const unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? _gl.NONE : _gl.BROWSER_DEFAULT_WEBGL; + + _gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY ); + _gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha ); + _gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion ); + + } - _gl.pixelStorei( _gl.UNPACK_FLIP_Y_WEBGL, texture.flipY ); - _gl.pixelStorei( _gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha ); _gl.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment ); - _gl.pixelStorei( _gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion ); let image = resizeImage( texture.image, false, capabilities.maxTextureSize ); image = verifyColorSpace( texture, image ); diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 9a23d4250fc2c3..b52aedc9983704 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -538,7 +538,7 @@ class WebGPUTextureUtils { } else if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isData3DTexture ) { - if ( texture.layerUpdates.size > 0 ) { + if ( texture.layerUpdates && texture.layerUpdates.size > 0 ) { for ( const layerIndex of texture.layerUpdates ) {