diff --git a/examples/jsm/exporters/USDZExporter.js b/examples/jsm/exporters/USDZExporter.js index ef5a1fcbe339f4..5a7e058ce0cfd2 100644 --- a/examples/jsm/exporters/USDZExporter.js +++ b/examples/jsm/exporters/USDZExporter.js @@ -9,6 +9,121 @@ import { zipSync, } from '../libs/fflate.module.js'; +class USDNode { + + constructor( name, type = '', metadata = [], properties = [] ) { + + this.name = name; + this.type = type; + this.metadata = metadata; + this.properties = properties; + this.children = []; + + } + + addMetadata( key, value ) { + + this.metadata.push( { key, value } ); + + } + + addProperty( property, metadata = [] ) { + + this.properties.push( { property, metadata } ); + + } + + addChild( child ) { + + this.children.push( child ); + + } + + toString( indent = 0 ) { + + const pad = '\t'.repeat( indent ); + + const formattedMetadata = this.metadata.map( ( item ) => { + + const key = item.key; + const value = item.value; + + if ( Array.isArray( value ) ) { + + const lines = []; + lines.push( `${key} = {` ); + value.forEach( ( line ) => { + + lines.push( `${pad}\t\t${line}` ); + + } ); + lines.push( `${pad}\t}` ); + return lines.join( '\n' ); + + } else { + + return `${key} = ${value}`; + + } + + } ); + + const meta = formattedMetadata.length + ? ` (\n${formattedMetadata + .map( ( l ) => `${pad}\t${l}` ) + .join( '\n' )}\n${pad})` + : ''; + + const properties = this.properties.map( ( l ) => { + + const property = l.property; + const metadata = l.metadata.length + ? ` (\n${l.metadata.map( ( m ) => `${pad}\t\t${m}` ).join( '\n' )}\n${pad}\t)` + : ''; + return `${pad}\t${property}${metadata}`; + + } ); + const children = this.children.map( ( c ) => c.toString( indent + 1 ) ); + + const bodyLines = []; + + if ( properties.length > 0 ) { + + bodyLines.push( ...properties ); + + } + + if ( children.length > 0 ) { + + if ( properties.length > 0 ) { + + bodyLines.push( '' ); + + } + + for ( let i = 0; i < children.length; i ++ ) { + + bodyLines.push( children[ i ] ); + if ( i < children.length - 1 ) { + + bodyLines.push( '' ); + + } + + } + + } + + const bodyContent = bodyLines.join( '\n' ); + + const type = this.type ? this.type + ' ' : ''; + + return `${pad}def ${type}"${this.name}"${meta}\n${pad}{\n${bodyContent}\n${pad}}`; + + } + +} + /** * An exporter for USDZ. * @@ -74,15 +189,18 @@ class USDZExporter { */ async parseAsync( scene, options = {} ) { - options = Object.assign( { - ar: { - anchoring: { type: 'plane' }, - planeAnchoring: { alignment: 'horizontal' } + options = Object.assign( + { + ar: { + anchoring: { type: 'plane' }, + planeAnchoring: { alignment: 'horizontal' }, + }, + includeAnchoringProperties: true, + quickLookCompatible: false, + maxTextureSize: 1024, }, - includeAnchoringProperties: true, - quickLookCompatible: false, - maxTextureSize: 1024, - }, options ); + options + ); const files = {}; const modelFileName = 'model.usda'; @@ -90,9 +208,32 @@ class USDZExporter { // model file should be first in USDZ archive so we init it here files[ modelFileName ] = null; - let output = buildHeader(); + const root = new USDNode( 'Root', 'Xform' ); + const scenesNode = new USDNode( 'Scenes', 'Scope' ); + scenesNode.addMetadata( 'kind', '"sceneLibrary"' ); + root.addChild( scenesNode ); + + const sceneName = 'Scene'; + const sceneNode = new USDNode( sceneName, 'Xform' ); + sceneNode.addMetadata( 'customData', [ + 'bool preliminary_collidesWithEnvironment = 0', + `string sceneName = "${sceneName}"`, + ] ); + sceneNode.addMetadata( 'sceneName', `"${sceneName}"` ); + if ( options.includeAnchoringProperties ) { + + sceneNode.addProperty( + `token preliminary:anchoring:type = "${options.ar.anchoring.type}"` + ); + sceneNode.addProperty( + `token preliminary:planeAnchoring:alignment = "${options.ar.planeAnchoring.alignment}"` + ); + + } + + scenesNode.addChild( sceneNode ); - output += buildSceneStart( options ); + let output; const materials = {}; const textures = {}; @@ -106,12 +247,15 @@ class USDZExporter { if ( material.isMeshStandardMaterial ) { - const geometryFileName = 'geometries/Geometry_' + geometry.id + '.usda'; + const geometryFileName = + 'geometries/Geometry_' + geometry.id + '.usda'; if ( ! ( geometryFileName in files ) ) { const meshObject = buildMeshObject( geometry ); - files[ geometryFileName ] = buildUSDFileAsString( meshObject ); + files[ geometryFileName ] = strToU8( + buildHeader() + '\n' + meshObject.toString() + ); } @@ -121,26 +265,40 @@ class USDZExporter { } - output += buildXform( object, geometry, materials[ material.uuid ] ); + const node = buildXform( object, geometry, materials[ material.uuid ] ); + sceneNode.addChild( node ); } else { - console.warn( 'THREE.USDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', object ); + console.warn( + 'THREE.USDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', + object + ); } } else if ( object.isCamera ) { - output += buildCamera( object ); + const cameraNode = buildCamera( object ); + + sceneNode.addChild( cameraNode ); } } ); + const materialsNode = buildMaterials( + materials, + textures, + options.quickLookCompatible + ); - output += buildSceneEnd(); - - output += buildMaterials( materials, textures, options.quickLookCompatible ); + output = + buildHeader() + + '\n' + + root.toString() + + '\n\n' + + materialsNode.toString(); files[ modelFileName ] = strToU8( output ); output = null; @@ -153,7 +311,9 @@ class USDZExporter { if ( this.textureUtils === null ) { - throw new Error( 'THREE.USDZExporter: setTextureUtils() must be called to process compressed textures.' ); + throw new Error( + 'THREE.USDZExporter: setTextureUtils() must be called to process compressed textures.' + ); } else { @@ -163,10 +323,18 @@ class USDZExporter { } - const canvas = imageToCanvas( texture.image, texture.flipY, options.maxTextureSize ); - const blob = await new Promise( resolve => canvas.toBlob( resolve, 'image/png', 1 ) ); + const canvas = imageToCanvas( + texture.image, + texture.flipY, + options.maxTextureSize + ); + const blob = await new Promise( ( resolve ) => + canvas.toBlob( resolve, 'image/png', 1 ) + ); - files[ `textures/Texture_${ id }.png` ] = new Uint8Array( await blob.arrayBuffer() ); + files[ `textures/Texture_${id}.png` ] = new Uint8Array( + await blob.arrayBuffer() + ); } @@ -205,10 +373,15 @@ class USDZExporter { function imageToCanvas( image, flipY, maxTextureSize ) { - if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) || - ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) || - ( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) || - ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) { + if ( + ( typeof HTMLImageElement !== 'undefined' && + image instanceof HTMLImageElement ) || + ( typeof HTMLCanvasElement !== 'undefined' && + image instanceof HTMLCanvasElement ) || + ( typeof OffscreenCanvas !== 'undefined' && + image instanceof OffscreenCanvas ) || + ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) + ) { const scale = maxTextureSize / Math.max( image.width, image.height ); @@ -233,7 +406,9 @@ function imageToCanvas( image, flipY, maxTextureSize ) { } else { - throw new Error( 'THREE.USDZExporter: No valid image data found. Unable to process texture.' ); + throw new Error( + 'THREE.USDZExporter: No valid image data found. Unable to process texture.' + ); } @@ -254,54 +429,10 @@ function buildHeader() { metersPerUnit = 1 upAxis = "Y" ) - `; } -function buildSceneStart( options ) { - - const alignment = options.includeAnchoringProperties === true ? ` - token preliminary:anchoring:type = "${options.ar.anchoring.type}" - token preliminary:planeAnchoring:alignment = "${options.ar.planeAnchoring.alignment}" - ` : ''; - return `def Xform "Root" -{ - def Scope "Scenes" ( - kind = "sceneLibrary" - ) - { - def Xform "Scene" ( - customData = { - bool preliminary_collidesWithEnvironment = 0 - string sceneName = "Scene" - } - sceneName = "Scene" - ) - {${alignment} -`; - -} - -function buildSceneEnd() { - - return ` - } - } -} - -`; - -} - -function buildUSDFileAsString( dataToInsert ) { - - let output = buildHeader(); - output += dataToInsert; - return strToU8( output ); - -} - // Xform function buildXform( object, geometry, material ) { @@ -311,22 +442,29 @@ function buildXform( object, geometry, material ) { if ( object.matrixWorld.determinant() < 0 ) { - console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', object ); + console.warn( + 'THREE.USDZExporter: USDZ does not support negative scales', + object + ); } - return ` def Xform "${ name }" ( - prepend references = @./geometries/Geometry_${ geometry.id }.usda@ - prepend apiSchemas = ["MaterialBindingAPI"] - ) - { - matrix4d xformOp:transform = ${ transform } - uniform token[] xformOpOrder = ["xformOp:transform"] + const node = new USDNode( name, 'Xform' ); - rel material:binding = - } + node.addMetadata( + 'prepend references', + `@./geometries/Geometry_${geometry.id}.usda@` + ); + node.addMetadata( 'prepend apiSchemas', '["MaterialBindingAPI"]' ); -`; + node.addProperty( `matrix4d xformOp:transform = ${transform}` ); + node.addProperty( 'uniform token[] xformOpOrder = ["xformOp:transform"]' ); + + node.addProperty( + `rel material:binding = ` + ); + + return node; } @@ -334,13 +472,18 @@ function buildMatrix( matrix ) { const array = matrix.elements; - return `( ${ buildMatrixRow( array, 0 ) }, ${ buildMatrixRow( array, 4 ) }, ${ buildMatrixRow( array, 8 ) }, ${ buildMatrixRow( array, 12 ) } )`; + return `( ${buildMatrixRow( array, 0 )}, ${buildMatrixRow( + array, + 4 + )}, ${buildMatrixRow( array, 8 )}, ${buildMatrixRow( array, 12 )} )`; } function buildMatrixRow( array, offset ) { - return `(${ array[ offset + 0 ] }, ${ array[ offset + 1 ] }, ${ array[ offset + 2 ] }, ${ array[ offset + 3 ] })`; + return `(${array[ offset + 0 ]}, ${array[ offset + 1 ]}, ${array[ offset + 2 ]}, ${ + array[ offset + 3 ] + })`; } @@ -348,43 +491,81 @@ function buildMatrixRow( array, offset ) { function buildMeshObject( geometry ) { - const mesh = buildMesh( geometry ); - return ` -def "Geometry" -{ -${mesh} -} -`; + const node = new USDNode( 'Geometry' ); + + const meshNode = buildMeshNode( geometry ); + node.addChild( meshNode ); + + return node; } -function buildMesh( geometry ) { +function buildMeshNode( geometry ) { const name = 'Geometry'; const attributes = geometry.attributes; const count = attributes.position.count; - return ` - def Mesh "${ name }" - { - int[] faceVertexCounts = [${ buildMeshVertexCount( geometry ) }] - int[] faceVertexIndices = [${ buildMeshVertexIndices( geometry ) }] - normal3f[] normals = [${ buildVector3Array( attributes.normal, count )}] ( - interpolation = "vertex" - ) - point3f[] points = [${ buildVector3Array( attributes.position, count )}] -${ buildPrimvars( attributes ) } - uniform token subdivisionScheme = "none" + const node = new USDNode( name, 'Mesh' ); + + node.addProperty( + `int[] faceVertexCounts = [${buildMeshVertexCount( geometry )}]` + ); + node.addProperty( + `int[] faceVertexIndices = [${buildMeshVertexIndices( geometry )}]` + ); + node.addProperty( + `normal3f[] normals = [${buildVector3Array( attributes.normal, count )}]`, + [ 'interpolation = "vertex"' ] + ); + node.addProperty( + `point3f[] points = [${buildVector3Array( attributes.position, count )}]` + ); + + for ( let i = 0; i < 4; i ++ ) { + + const id = i > 0 ? i : ''; + const attribute = attributes[ 'uv' + id ]; + if ( attribute !== undefined ) { + + node.addProperty( + `texCoord2f[] primvars:st${id} = [${buildVector2Array( attribute )}]`, + [ 'interpolation = "vertex"' ] + ); + + } + } -`; + + const colorAttribute = attributes.color; + if ( colorAttribute !== undefined ) { + + node.addProperty( + `color3f[] primvars:displayColor = [${buildVector3Array( + colorAttribute, + count + )}]`, + [ 'interpolation = "vertex"' ] + ); + + } + + node.addProperty( 'uniform token subdivisionScheme = "none"' ); + + return node; } function buildMeshVertexCount( geometry ) { - const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count; + const count = + geometry.index !== null + ? geometry.index.count + : geometry.attributes.position.count; - return Array( count / 3 ).fill( 3 ).join( ', ' ); + return Array( count / 3 ) + .fill( 3 ) + .join( ', ' ); } @@ -434,7 +615,11 @@ function buildVector3Array( attribute, count ) { const y = attribute.getY( i ); const z = attribute.getZ( i ); - array.push( `(${ x.toPrecision( PRECISION ) }, ${ y.toPrecision( PRECISION ) }, ${ z.toPrecision( PRECISION ) })` ); + array.push( + `(${x.toPrecision( PRECISION )}, ${y.toPrecision( + PRECISION + )}, ${z.toPrecision( PRECISION )})` + ); } @@ -451,7 +636,9 @@ function buildVector2Array( attribute ) { const x = attribute.getX( i ); const y = attribute.getY( i ); - array.push( `(${ x.toPrecision( PRECISION ) }, ${ 1 - y.toPrecision( PRECISION ) })` ); + array.push( + `(${x.toPrecision( PRECISION )}, ${1 - y.toPrecision( PRECISION )})` + ); } @@ -459,65 +646,23 @@ function buildVector2Array( attribute ) { } -function buildPrimvars( attributes ) { - - let string = ''; - - for ( let i = 0; i < 4; i ++ ) { - - const id = ( i > 0 ? i : '' ); - const attribute = attributes[ 'uv' + id ]; - - if ( attribute !== undefined ) { - - string += ` - texCoord2f[] primvars:st${ id } = [${ buildVector2Array( attribute )}] ( - interpolation = "vertex" - )`; - - } - - } - - // vertex colors - - const colorAttribute = attributes.color; - - if ( colorAttribute !== undefined ) { - - const count = colorAttribute.count; - - string += ` - color3f[] primvars:displayColor = [${buildVector3Array( colorAttribute, count )}] ( - interpolation = "vertex" - )`; - - } - - return string; - -} - // Materials function buildMaterials( materials, textures, quickLookCompatible = false ) { - const array = []; + const materialsNode = new USDNode( 'Materials' ); for ( const uuid in materials ) { const material = materials[ uuid ]; - array.push( buildMaterial( material, textures, quickLookCompatible ) ); + materialsNode.addChild( + buildMaterial( material, textures, quickLookCompatible ) + ); } - return `def "Materials" -{ -${ array.join( '' ) } -} - -`; + return materialsNode; } @@ -525,11 +670,9 @@ function buildMaterial( material, textures, quickLookCompatible = false ) { // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html - const pad = ' '; - const inputs = []; - const samplers = []; + const materialNode = new USDNode( `Material_${material.id}`, 'Material' ); - function buildTexture( texture, mapType, color ) { + function buildTextureNodes( texture, mapType, color ) { const id = texture.source.id + '_' + texture.flipY; @@ -540,7 +683,7 @@ function buildMaterial( material, textures, quickLookCompatible = false ) { const WRAPPINGS = { 1000: 'repeat', // RepeatWrapping 1001: 'clamp', // ClampToEdgeWrapping - 1002: 'mirror' // MirroredRepeatWrapping + 1002: 'mirror', // MirroredRepeatWrapping }; const repeat = texture.repeat.clone(); @@ -575,135 +718,251 @@ function buildMaterial( material, textures, quickLookCompatible = false ) { } - return ` - def Shader "PrimvarReader_${ mapType }" - { - uniform token info:id = "UsdPrimvarReader_float2" - float2 inputs:fallback = (0.0, 0.0) - token inputs:varname = "${ uv }" - float2 outputs:result + const primvarReaderNode = new USDNode( `PrimvarReader_${mapType}`, 'Shader' ); + primvarReaderNode.addProperty( + 'uniform token info:id = "UsdPrimvarReader_float2"' + ); + primvarReaderNode.addProperty( 'float2 inputs:fallback = (0.0, 0.0)' ); + primvarReaderNode.addProperty( `token inputs:varname = "${uv}"` ); + primvarReaderNode.addProperty( 'float2 outputs:result' ); + + const transform2dNode = new USDNode( `Transform2d_${mapType}`, 'Shader' ); + transform2dNode.addProperty( 'uniform token info:id = "UsdTransform2d"' ); + transform2dNode.addProperty( + `token inputs:in.connect = ` + ); + transform2dNode.addProperty( + `float inputs:rotation = ${( rotation * ( 180 / Math.PI ) ).toFixed( + PRECISION + )}` + ); + transform2dNode.addProperty( + `float2 inputs:scale = ${buildVector2( repeat )}` + ); + transform2dNode.addProperty( + `float2 inputs:translation = ${buildVector2( offset )}` + ); + transform2dNode.addProperty( 'float2 outputs:result' ); + + const textureNode = new USDNode( + `Texture_${texture.id}_${mapType}`, + 'Shader' + ); + textureNode.addProperty( 'uniform token info:id = "UsdUVTexture"' ); + textureNode.addProperty( `asset inputs:file = @textures/Texture_${id}.png@` ); + textureNode.addProperty( + `float2 inputs:st.connect = ` + ); + + if ( color !== undefined ) { + + textureNode.addProperty( `float4 inputs:scale = ${buildColor4( color )}` ); + } - def Shader "Transform2d_${ mapType }" - { - uniform token info:id = "UsdTransform2d" - token inputs:in.connect = - float inputs:rotation = ${ ( rotation * ( 180 / Math.PI ) ).toFixed( PRECISION ) } - float2 inputs:scale = ${ buildVector2( repeat ) } - float2 inputs:translation = ${ buildVector2( offset ) } - float2 outputs:result + textureNode.addProperty( + `token inputs:sourceColorSpace = "${ + texture.colorSpace === NoColorSpace ? 'raw' : 'sRGB' + }"` + ); + textureNode.addProperty( + `token inputs:wrapS = "${WRAPPINGS[ texture.wrapS ]}"` + ); + textureNode.addProperty( + `token inputs:wrapT = "${WRAPPINGS[ texture.wrapT ]}"` + ); + textureNode.addProperty( 'float outputs:r' ); + textureNode.addProperty( 'float outputs:g' ); + textureNode.addProperty( 'float outputs:b' ); + textureNode.addProperty( 'float3 outputs:rgb' ); + + if ( material.transparent || material.alphaTest > 0.0 ) { + + textureNode.addProperty( 'float outputs:a' ); + } - def Shader "Texture_${ texture.id }_${ mapType }" - { - uniform token info:id = "UsdUVTexture" - asset inputs:file = @textures/Texture_${ id }.png@ - float2 inputs:st.connect = - ${ color !== undefined ? 'float4 inputs:scale = ' + buildColor4( color ) : '' } - token inputs:sourceColorSpace = "${ texture.colorSpace === NoColorSpace ? 'raw' : 'sRGB' }" - token inputs:wrapS = "${ WRAPPINGS[ texture.wrapS ] }" - token inputs:wrapT = "${ WRAPPINGS[ texture.wrapT ] }" - float outputs:r - float outputs:g - float outputs:b - float3 outputs:rgb - ${ material.transparent || material.alphaTest > 0.0 ? 'float outputs:a' : '' } - }`; + return [ primvarReaderNode, transform2dNode, textureNode ]; } - if ( material.side === DoubleSide ) { - console.warn( 'THREE.USDZExporter: USDZ does not support double sided materials', material ); + console.warn( + 'THREE.USDZExporter: USDZ does not support double sided materials', + material + ); } + const previewSurfaceNode = new USDNode( 'PreviewSurface', 'Shader' ); + previewSurfaceNode.addProperty( 'uniform token info:id = "UsdPreviewSurface"' ); + if ( material.map !== null ) { - inputs.push( `${ pad }color3f inputs:diffuseColor.connect = ` ); + previewSurfaceNode.addProperty( + `color3f inputs:diffuseColor.connect = ` + ); if ( material.transparent ) { - inputs.push( `${ pad }float inputs:opacity.connect = ` ); + previewSurfaceNode.addProperty( + `float inputs:opacity.connect = ` + ); } else if ( material.alphaTest > 0.0 ) { - inputs.push( `${ pad }float inputs:opacity.connect = ` ); - inputs.push( `${ pad }float inputs:opacityThreshold = ${material.alphaTest}` ); + previewSurfaceNode.addProperty( + `float inputs:opacity.connect = ` + ); + previewSurfaceNode.addProperty( + `float inputs:opacityThreshold = ${material.alphaTest}` + ); } - samplers.push( buildTexture( material.map, 'diffuse', material.color ) ); + const textureNodes = buildTextureNodes( + material.map, + 'diffuse', + material.color + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${ pad }color3f inputs:diffuseColor = ${ buildColor( material.color ) }` ); + previewSurfaceNode.addProperty( + `color3f inputs:diffuseColor = ${buildColor( material.color )}` + ); } + // Handle emissive if ( material.emissiveMap !== null ) { - inputs.push( `${ pad }color3f inputs:emissiveColor.connect = ` ); - - samplers.push( buildTexture( material.emissiveMap, 'emissive', new Color( material.emissive.r * material.emissiveIntensity, material.emissive.g * material.emissiveIntensity, material.emissive.b * material.emissiveIntensity ) ) ); + previewSurfaceNode.addProperty( + `color3f inputs:emissiveColor.connect = ` + ); + + const emissiveColor = new Color( + material.emissive.r * material.emissiveIntensity, + material.emissive.g * material.emissiveIntensity, + material.emissive.b * material.emissiveIntensity + ); + const textureNodes = buildTextureNodes( + material.emissiveMap, + 'emissive', + emissiveColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else if ( material.emissive.getHex() > 0 ) { - inputs.push( `${ pad }color3f inputs:emissiveColor = ${ buildColor( material.emissive ) }` ); + previewSurfaceNode.addProperty( + `color3f inputs:emissiveColor = ${buildColor( material.emissive )}` + ); } + // Handle normal if ( material.normalMap !== null ) { - inputs.push( `${ pad }normal3f inputs:normal.connect = ` ); + previewSurfaceNode.addProperty( + `normal3f inputs:normal.connect = ` + ); - samplers.push( buildTexture( material.normalMap, 'normal' ) ); + const textureNodes = buildTextureNodes( material.normalMap, 'normal' ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } + // Handle ambient occlusion if ( material.aoMap !== null ) { - inputs.push( `${ pad }float inputs:occlusion.connect = ` ); - - samplers.push( buildTexture( material.aoMap, 'occlusion', new Color( material.aoMapIntensity, material.aoMapIntensity, material.aoMapIntensity ) ) ); + previewSurfaceNode.addProperty( + `float inputs:occlusion.connect = ` + ); + + const aoColor = new Color( + material.aoMapIntensity, + material.aoMapIntensity, + material.aoMapIntensity + ); + const textureNodes = buildTextureNodes( + material.aoMap, + 'occlusion', + aoColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } if ( material.roughnessMap !== null ) { - inputs.push( `${ pad }float inputs:roughness.connect = ` ); - - samplers.push( buildTexture( material.roughnessMap, 'roughness', new Color( material.roughness, material.roughness, material.roughness ) ) ); + previewSurfaceNode.addProperty( + `float inputs:roughness.connect = ` + ); + + const roughnessColor = new Color( + material.roughness, + material.roughness, + material.roughness + ); + const textureNodes = buildTextureNodes( + material.roughnessMap, + 'roughness', + roughnessColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${ pad }float inputs:roughness = ${ material.roughness }` ); + previewSurfaceNode.addProperty( + `float inputs:roughness = ${material.roughness}` + ); } if ( material.metalnessMap !== null ) { - inputs.push( `${ pad }float inputs:metallic.connect = ` ); - - samplers.push( buildTexture( material.metalnessMap, 'metallic', new Color( material.metalness, material.metalness, material.metalness ) ) ); + previewSurfaceNode.addProperty( + `float inputs:metallic.connect = ` + ); + + const metalnessColor = new Color( + material.metalness, + material.metalness, + material.metalness + ); + const textureNodes = buildTextureNodes( + material.metalnessMap, + 'metallic', + metalnessColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${ pad }float inputs:metallic = ${ material.metalness }` ); + previewSurfaceNode.addProperty( + `float inputs:metallic = ${material.metalness}` + ); } if ( material.alphaMap !== null ) { - inputs.push( `${pad}float inputs:opacity.connect = ` ); - inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` ); + previewSurfaceNode.addProperty( + `float inputs:opacity.connect = ` + ); + previewSurfaceNode.addProperty( 'float inputs:opacityThreshold = 0.0001' ); - samplers.push( buildTexture( material.alphaMap, 'opacity' ) ); + const textureNodes = buildTextureNodes( material.alphaMap, 'opacity' ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${pad}float inputs:opacity = ${material.opacity}` ); + previewSurfaceNode.addProperty( + `float inputs:opacity = ${material.opacity}` + ); } @@ -711,69 +970,91 @@ function buildMaterial( material, textures, quickLookCompatible = false ) { if ( material.clearcoatMap !== null ) { - inputs.push( `${pad}float inputs:clearcoat.connect = ` ); - samplers.push( buildTexture( material.clearcoatMap, 'clearcoat', new Color( material.clearcoat, material.clearcoat, material.clearcoat ) ) ); + previewSurfaceNode.addProperty( + `float inputs:clearcoat.connect = ` + ); + + const clearcoatColor = new Color( + material.clearcoat, + material.clearcoat, + material.clearcoat + ); + const textureNodes = buildTextureNodes( + material.clearcoatMap, + 'clearcoat', + clearcoatColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${pad}float inputs:clearcoat = ${material.clearcoat}` ); + previewSurfaceNode.addProperty( + `float inputs:clearcoat = ${material.clearcoat}` + ); } if ( material.clearcoatRoughnessMap !== null ) { - inputs.push( `${pad}float inputs:clearcoatRoughness.connect = ` ); - samplers.push( buildTexture( material.clearcoatRoughnessMap, 'clearcoatRoughness', new Color( material.clearcoatRoughness, material.clearcoatRoughness, material.clearcoatRoughness ) ) ); + previewSurfaceNode.addProperty( + `float inputs:clearcoatRoughness.connect = ` + ); + + const clearcoatRoughnessColor = new Color( + material.clearcoatRoughness, + material.clearcoatRoughness, + material.clearcoatRoughness + ); + const textureNodes = buildTextureNodes( + material.clearcoatRoughnessMap, + 'clearcoatRoughness', + clearcoatRoughnessColor + ); + textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { - inputs.push( `${pad}float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` ); + previewSurfaceNode.addProperty( + `float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` + ); } - inputs.push( `${ pad }float inputs:ior = ${ material.ior }` ); + previewSurfaceNode.addProperty( `float inputs:ior = ${material.ior}` ); } - return ` - def Material "Material_${ material.id }" - { - def Shader "PreviewSurface" - { - uniform token info:id = "UsdPreviewSurface" -${ inputs.join( '\n' ) } - int inputs:useSpecularWorkflow = 0 - token outputs:surface - } + previewSurfaceNode.addProperty( 'int inputs:useSpecularWorkflow = 0' ); + previewSurfaceNode.addProperty( 'token outputs:surface' ); - token outputs:surface.connect = + materialNode.addChild( previewSurfaceNode ); -${ samplers.join( '\n' ) } + materialNode.addProperty( + `token outputs:surface.connect = ` + ); - } -`; + return materialNode; } function buildColor( color ) { - return `(${ color.r }, ${ color.g }, ${ color.b })`; + return `(${color.r}, ${color.g}, ${color.b})`; } function buildColor4( color ) { - return `(${ color.r }, ${ color.g }, ${ color.b }, 1.0)`; + return `(${color.r}, ${color.g}, ${color.b}, 1.0)`; } function buildVector2( vector ) { - return `(${ vector.x }, ${ vector.y })`; + return `(${vector.x}, ${vector.y})`; } - function buildCamera( camera ) { const name = camera.name ? camera.name : 'Camera_' + camera.id; @@ -782,44 +1063,71 @@ function buildCamera( camera ) { if ( camera.matrixWorld.determinant() < 0 ) { - console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', camera ); + console.warn( + 'THREE.USDZExporter: USDZ does not support negative scales', + camera + ); } + const node = new USDNode( name, 'Camera' ); + node.addProperty( `matrix4d xformOp:transform = ${transform}` ); + node.addProperty( 'uniform token[] xformOpOrder = ["xformOp:transform"]' ); + + const projection = camera.isOrthographicCamera + ? 'orthographic' + : 'perspective'; + node.addProperty( `token projection = "${projection}"` ); + + const clippingRange = `(${camera.near.toPrecision( + PRECISION + )}, ${camera.far.toPrecision( PRECISION )})`; + node.addProperty( `float2 clippingRange = ${clippingRange}` ); + + let horizontalAperture; if ( camera.isOrthographicCamera ) { - return `def Camera "${name}" - { - matrix4d xformOp:transform = ${ transform } - uniform token[] xformOpOrder = ["xformOp:transform"] + horizontalAperture = ( + ( Math.abs( camera.left ) + Math.abs( camera.right ) ) * + 10 + ).toPrecision( PRECISION ); - float2 clippingRange = (${ camera.near.toPrecision( PRECISION ) }, ${ camera.far.toPrecision( PRECISION ) }) - float horizontalAperture = ${ ( ( Math.abs( camera.left ) + Math.abs( camera.right ) ) * 10 ).toPrecision( PRECISION ) } - float verticalAperture = ${ ( ( Math.abs( camera.top ) + Math.abs( camera.bottom ) ) * 10 ).toPrecision( PRECISION ) } - token projection = "orthographic" - } + } else { + + horizontalAperture = camera.getFilmWidth().toPrecision( PRECISION ); + + } - `; + node.addProperty( `float horizontalAperture = ${horizontalAperture}` ); + + let verticalAperture; + if ( camera.isOrthographicCamera ) { + + verticalAperture = ( + ( Math.abs( camera.top ) + Math.abs( camera.bottom ) ) * + 10 + ).toPrecision( PRECISION ); } else { - return `def Camera "${name}" - { - matrix4d xformOp:transform = ${ transform } - uniform token[] xformOpOrder = ["xformOp:transform"] - - float2 clippingRange = (${ camera.near.toPrecision( PRECISION ) }, ${ camera.far.toPrecision( PRECISION ) }) - float focalLength = ${ camera.getFocalLength().toPrecision( PRECISION ) } - float focusDistance = ${ camera.focus.toPrecision( PRECISION ) } - float horizontalAperture = ${ camera.getFilmWidth().toPrecision( PRECISION ) } - token projection = "perspective" - float verticalAperture = ${ camera.getFilmHeight().toPrecision( PRECISION ) } - } + verticalAperture = camera.getFilmHeight().toPrecision( PRECISION ); + + } + + node.addProperty( `float verticalAperture = ${verticalAperture}` ); + + if ( camera.isPerspectiveCamera ) { - `; + const focalLength = camera.getFocalLength().toPrecision( PRECISION ); + node.addProperty( `float focalLength = ${focalLength}` ); + + const focusDistance = camera.focus.toPrecision( PRECISION ); + node.addProperty( `float focusDistance = ${focusDistance}` ); } + return node; + } /**