UNPKG

three

Version:

JavaScript 3D library

1,490 lines (989 loc) 35.5 kB
import { NoColorSpace, DoubleSide, Color, PropertyBinding, } from 'three'; import { strToU8, 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.replace( /\n/g, '\n' + pad + '\t' ); 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. * * ```js * const exporter = new USDZExporter(); * const arraybuffer = await exporter.parseAsync( scene ); * ``` * * @three_import import { USDZExporter } from 'three/addons/exporters/USDZExporter.js'; */ class USDZExporter { /** * Constructs a new USDZ exporter. */ constructor() { /** * A reference to a texture utils module. * * @type {?(WebGLTextureUtils|WebGPUTextureUtils)} * @default null */ this.textureUtils = null; } /** * Sets the texture utils for this exporter. Only relevant when compressed textures have to be exported. * * Depending on whether you use {@link WebGLRenderer} or {@link WebGPURenderer}, you must inject the * corresponding texture utils {@link WebGLTextureUtils} or {@link WebGPUTextureUtils}. * * @param {WebGLTextureUtils|WebGPUTextureUtils} utils - The texture utils. */ setTextureUtils( utils ) { this.textureUtils = utils; } /** * Parse the given 3D object and generates the USDZ output. * * @param {Object3D} scene - The 3D object to export. * @param {USDZExporter~OnDone} onDone - A callback function that is executed when the export has finished. * @param {USDZExporter~OnError} onError - A callback function that is executed when an error happens. * @param {USDZExporter~Options} options - The export options. */ parse( scene, onDone, onError, options ) { this.parseAsync( scene, options ).then( onDone ).catch( onError ); } /** * Async version of {@link USDZExporter#parse}. * * @async * @param {Object3D} scene - The 3D object to export. * @param {USDZExporter~Options} options - The export options. * @return {Promise<ArrayBuffer>} A Promise that resolved with the exported USDZ data. */ async parseAsync( scene, options = {} ) { options = Object.assign( { ar: { anchoring: { type: 'plane' }, planeAnchoring: { alignment: 'horizontal' }, }, includeAnchoringProperties: true, onlyVisible: true, quickLookCompatible: false, maxTextureSize: 1024, animations: [], animationFrameRate: 60, }, options ); const usedNames = new Set(); const files = {}; const modelFileName = 'model.usda'; // model file should be first in USDZ archive so we init it here files[ modelFileName ] = null; const animationTracks = buildAnimationTracks( scene, options.animations ); options.animationTracks = animationTracks; 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 ); let output; const materials = {}; const textures = {}; if ( scene.isScene ) { buildHierarchy( scene, sceneNode, materials, usedNames, files, options ); } else { buildNode( scene, sceneNode, materials, usedNames, files, options ); } const materialsNode = buildMaterials( materials, textures, options.quickLookCompatible ); const timeRange = animationTracks.size > 0 ? { fps: options.animationFrameRate, endTimeCode: getMaxClipDuration( options.animations ) * options.animationFrameRate } : null; output = buildHeader( timeRange ) + '\n' + root.toString() + '\n\n' + materialsNode.toString(); files[ modelFileName ] = strToU8( output ); output = null; for ( const id in textures ) { let texture = textures[ id ]; if ( texture.isCompressedTexture === true ) { if ( this.textureUtils === null ) { throw new Error( 'THREE.USDZExporter: setTextureUtils() must be called to process compressed textures.' ); } else { texture = await this.textureUtils.decompress( texture ); } } const canvas = imageToCanvas( texture.image, texture.flipY, options.maxTextureSize ); const mimeType = ( texture.userData.mimeType === 'image/jpeg' ) ? 'image/jpeg' : 'image/png'; const blob = await new Promise( ( resolve ) => canvas.toBlob( resolve, mimeType ) ); files[ `textures/Texture_${id}.${getTextureExtension( texture )}` ] = new Uint8Array( await blob.arrayBuffer() ); } // 64 byte alignment // https://github.com/101arrowz/fflate/issues/39#issuecomment-777263109 let offset = 0; for ( const filename in files ) { const file = files[ filename ]; const headerSize = 34 + filename.length; offset += headerSize; const offsetMod64 = offset & 63; if ( offsetMod64 !== 4 ) { const padLength = 64 - offsetMod64; const padding = new Uint8Array( padLength ); files[ filename ] = [ file, { extra: { 12345: padding } } ]; } offset = file.length; } return zipSync( files, { level: 0 } ); } } function getName( object, namesSet ) { let name = object.name; name = name.replace( /[^A-Za-z0-9_]/g, '' ); if ( /^[0-9]/.test( name ) ) { name = '_' + name; } if ( name === '' ) { if ( object.isCamera ) { name = 'Camera'; } else { name = 'Object'; } } if ( namesSet.has( name ) ) { name = name + '_' + object.id; } namesSet.add( name ); return name; } function getTextureExtension( texture ) { return texture.userData.mimeType === 'image/jpeg' ? 'jpg' : 'png'; } 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 ) ) { const scale = maxTextureSize / Math.max( image.width, image.height ); const canvas = document.createElement( 'canvas' ); canvas.width = image.width * Math.min( 1, scale ); canvas.height = image.height * Math.min( 1, scale ); const context = canvas.getContext( '2d' ); // TODO: We should be able to do this in the UsdTransform2d? if ( flipY === true ) { context.translate( 0, canvas.height ); context.scale( 1, - 1 ); } context.drawImage( image, 0, 0, canvas.width, canvas.height ); return canvas; } else { throw new Error( 'THREE.USDZExporter: No valid image data found. Unable to process texture.' ); } } // const PRECISION = 7; function buildHeader( timeRange = null ) { const timeMetadata = timeRange ? ` startTimeCode = 0 endTimeCode = ${timeRange.endTimeCode} timeCodesPerSecond = ${timeRange.fps} framesPerSecond = ${timeRange.fps}` : ''; return `#usda 1.0 ( customLayerData = { string creator = "Three.js USDZExporter" } defaultPrim = "Root" metersPerUnit = 1 upAxis = "Y"${timeMetadata} ) `; } function buildAnimationTracks( scene, clips ) { // Map<Object3D, { position?: KeyframeTrack, quaternion?: KeyframeTrack, scale?: KeyframeTrack }> const tracksByObject = new Map(); for ( let c = 0; c < clips.length; c ++ ) { const clip = clips[ c ]; for ( let t = 0; t < clip.tracks.length; t ++ ) { const track = clip.tracks[ t ]; const binding = PropertyBinding.parseTrackName( track.name ); const target = PropertyBinding.findNode( scene, binding.nodeName ); if ( target === null || target === undefined ) continue; const property = binding.propertyName; if ( property !== 'position' && property !== 'quaternion' && property !== 'scale' ) continue; let entry = tracksByObject.get( target ); if ( entry === undefined ) { entry = {}; tracksByObject.set( target, entry ); } entry[ property ] = track; } } return tracksByObject; } function getMaxClipDuration( clips ) { let max = 0; for ( let i = 0; i < clips.length; i ++ ) { if ( clips[ i ].duration > max ) max = clips[ i ].duration; } return max; } function buildVector3TimeSamples( opName, opType, track, fps ) { const times = track.times; const values = track.values; const samples = []; for ( let i = 0; i < times.length; i ++ ) { const o = i * 3; samples.push( `${( times[ i ] * fps ).toPrecision( PRECISION )}: (${values[ o ].toPrecision( PRECISION )}, ${values[ o + 1 ].toPrecision( PRECISION )}, ${values[ o + 2 ].toPrecision( PRECISION )})` ); } return `${opType} ${opName}.timeSamples = {\n\t${samples.join( ',\n\t' )},\n}`; } function buildQuaternionTimeSamples( track, fps ) { const times = track.times; const values = track.values; const samples = []; // three.js quaternion order: (x, y, z, w); USD quatf order: (w, x, y, z) for ( let i = 0; i < times.length; i ++ ) { const o = i * 4; samples.push( `${( times[ i ] * fps ).toPrecision( PRECISION )}: (${values[ o + 3 ].toPrecision( PRECISION )}, ${values[ o ].toPrecision( PRECISION )}, ${values[ o + 1 ].toPrecision( PRECISION )}, ${values[ o + 2 ].toPrecision( PRECISION )})` ); } return `quatf xformOp:orient.timeSamples = {\n\t${samples.join( ',\n\t' )},\n}`; } // Xform function buildHierarchy( object, parentNode, materials, usedNames, files, options ) { for ( let i = 0, l = object.children.length; i < l; i ++ ) { buildNode( object.children[ i ], parentNode, materials, usedNames, files, options ); } } function buildNode( object, parentNode, materials, usedNames, files, options ) { if ( object.visible === false && options.onlyVisible === true ) return; let childNode; if ( object.isMesh ) { const geometry = object.geometry; const isMultiMaterial = Array.isArray( object.material ); const meshMaterials = isMultiMaterial ? object.material : [ object.material ]; for ( let i = 0; i < meshMaterials.length; i ++ ) { const material = meshMaterials[ i ]; if ( ! material.isMeshStandardMaterial ) { console.warn( 'THREE.USDZExporter: Use MeshStandardMaterial for best results.' ); } if ( ! ( material.uuid in materials ) ) { materials[ material.uuid ] = material; } } const resolvedMaterials = meshMaterials.map( ( m ) => materials[ m.uuid ] ); if ( isMultiMaterial === false ) { const geometryFileName = `geometries/Geometry_${geometry.id}.usda`; if ( ! ( geometryFileName in files ) ) { const meshObject = buildMeshObject( geometry ); files[ geometryFileName ] = strToU8( buildHeader() + '\n' + meshObject.toString() ); } } childNode = buildMesh( object, geometry, resolvedMaterials, usedNames, options ); } else if ( object.isCamera ) { childNode = buildCamera( object, usedNames, options ); } else { childNode = buildXform( object, usedNames, options ); } parentNode.addChild( childNode ); buildHierarchy( object, childNode, materials, usedNames, files, options ); } function addTransformProperties( node, object, options ) { const animTracks = options.animationTracks.get( object ); const hasPivot = object.pivot !== null; if ( ! hasPivot && animTracks === undefined ) { const transform = buildMatrix( object.matrix ); node.addProperty( `matrix4d xformOp:transform = ${transform}` ); node.addProperty( 'uniform token[] xformOpOrder = ["xformOp:transform"]' ); return; } // Per-op layout: animated channels use timeSamples, others stay static. // Pivot ops (when present) are always static. const fps = options.animationFrameRate; const p = object.position; const q = object.quaternion; const s = object.scale; if ( animTracks !== undefined && animTracks.position !== undefined ) { node.addProperty( buildVector3TimeSamples( 'xformOp:translate', 'float3', animTracks.position, fps ) ); } else { node.addProperty( `float3 xformOp:translate = (${p.x.toPrecision( PRECISION )}, ${p.y.toPrecision( PRECISION )}, ${p.z.toPrecision( PRECISION )})` ); } if ( hasPivot ) { const piv = object.pivot; node.addProperty( `float3 xformOp:translate:pivot = (${piv.x.toPrecision( PRECISION )}, ${piv.y.toPrecision( PRECISION )}, ${piv.z.toPrecision( PRECISION )})` ); } if ( animTracks !== undefined && animTracks.quaternion !== undefined ) { node.addProperty( buildQuaternionTimeSamples( animTracks.quaternion, fps ) ); } else { node.addProperty( `quatf xformOp:orient = (${q.w.toPrecision( PRECISION )}, ${q.x.toPrecision( PRECISION )}, ${q.y.toPrecision( PRECISION )}, ${q.z.toPrecision( PRECISION )})` ); } if ( animTracks !== undefined && animTracks.scale !== undefined ) { node.addProperty( buildVector3TimeSamples( 'xformOp:scale', 'float3', animTracks.scale, fps ) ); } else { node.addProperty( `float3 xformOp:scale = (${s.x.toPrecision( PRECISION )}, ${s.y.toPrecision( PRECISION )}, ${s.z.toPrecision( PRECISION )})` ); } if ( hasPivot ) { node.addProperty( 'uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:translate:pivot", "xformOp:orient", "xformOp:scale", "!invert!xformOp:translate:pivot"]' ); } else { node.addProperty( 'uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]' ); } } function buildXform( object, usedNames, options ) { const name = getName( object, usedNames ); if ( object.matrix.determinant() < 0 ) { console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', object ); } const node = new USDNode( name, 'Xform' ); addTransformProperties( node, object, options ); return node; } function buildMesh( object, geometry, materials, usedNames, options ) { const node = buildXform( object, usedNames, options ); if ( materials.length === 1 ) { node.addMetadata( 'prepend references', `@./geometries/Geometry_${geometry.id}.usda@</Geometry>` ); node.addMetadata( 'prepend apiSchemas', '["MaterialBindingAPI"]' ); node.addProperty( `rel material:binding = </Materials/Material_${materials[ 0 ].id}>` ); } else { node.addChild( buildMeshNode( geometry, materials ) ); } return node; } function buildMatrix( matrix ) { const array = matrix.elements; 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 ] })`; } // Mesh function buildMeshObject( geometry ) { const node = new USDNode( 'Geometry' ); const meshNode = buildMeshNode( geometry ); node.addChild( meshNode ); return node; } function buildMeshNode( geometry, materials = null ) { const name = 'Geometry'; const attributes = geometry.attributes; const count = attributes.position.count; 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"' ); if ( materials !== null ) { const groups = geometry.groups; const totalFaces = ( geometry.index !== null ? geometry.index.count : attributes.position.count ) / 3; for ( let i = 0; i < groups.length; i ++ ) { const group = groups[ i ]; const material = materials[ group.materialIndex ]; if ( material === undefined ) continue; const startFace = Math.floor( group.start / 3 ); const endFace = Math.min( startFace + Math.floor( group.count / 3 ), totalFaces ); const indices = []; for ( let j = startFace; j < endFace; j ++ ) indices.push( j ); const subsetNode = new USDNode( `subset_${i}`, 'GeomSubset' ); subsetNode.addMetadata( 'prepend apiSchemas', '["MaterialBindingAPI"]' ); subsetNode.addProperty( 'uniform token elementType = "face"' ); subsetNode.addProperty( 'uniform token familyName = "materialBind"' ); subsetNode.addProperty( `int[] indices = [${indices.join( ', ' )}]` ); subsetNode.addProperty( `rel material:binding = </Materials/Material_${material.id}>` ); node.addChild( subsetNode ); } } return node; } function buildMeshVertexCount( geometry ) { const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count; return Array( count / 3 ) .fill( 3 ) .join( ', ' ); } function buildMeshVertexIndices( geometry ) { const index = geometry.index; const array = []; if ( index !== null ) { for ( let i = 0; i < index.count; i ++ ) { array.push( index.getX( i ) ); } } else { const length = geometry.attributes.position.count; for ( let i = 0; i < length; i ++ ) { array.push( i ); } } return array.join( ', ' ); } function buildVector3Array( attribute, count ) { if ( attribute === undefined ) { console.warn( 'USDZExporter: Normals missing.' ); return Array( count ).fill( '(0, 0, 0)' ).join( ', ' ); } const array = []; for ( let i = 0; i < attribute.count; i ++ ) { const x = attribute.getX( i ); const y = attribute.getY( i ); const z = attribute.getZ( i ); array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )}, ${z.toPrecision( PRECISION )})` ); } return array.join( ', ' ); } function buildVector2Array( attribute ) { const array = []; for ( let i = 0; i < attribute.count; i ++ ) { const x = attribute.getX( i ); const y = attribute.getY( i ); array.push( `(${x.toPrecision( PRECISION )}, ${1 - y.toPrecision( PRECISION )})` ); } return array.join( ', ' ); } // Materials function buildMaterials( materials, textures, quickLookCompatible = false ) { const materialsNode = new USDNode( 'Materials' ); for ( const uuid in materials ) { const material = materials[ uuid ]; materialsNode.addChild( buildMaterial( material, textures, quickLookCompatible ) ); } return materialsNode; } function buildMaterial( material, textures, quickLookCompatible = false ) { // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html const materialNode = new USDNode( `Material_${material.id}`, 'Material' ); function buildTextureNodes( texture, mapType, color ) { const id = texture.source.id + '_' + texture.flipY; textures[ id ] = texture; const uv = texture.channel > 0 ? 'st' + texture.channel : 'st'; const WRAPPINGS = { 1000: 'repeat', // RepeatWrapping 1001: 'clamp', // ClampToEdgeWrapping 1002: 'mirror', // MirroredRepeatWrapping }; const repeat = texture.repeat.clone(); const offset = texture.offset.clone(); const rotation = texture.rotation; // rotation is around the wrong point. after rotation we need to shift offset again so that we're rotating around the right spot const xRotationOffset = Math.sin( rotation ); const yRotationOffset = Math.cos( rotation ); // texture coordinates start in the opposite corner, need to correct offset.y = 1 - offset.y - repeat.y; // turns out QuickLook is buggy and interprets texture repeat inverted/applies operations in a different order. // Apple Feedback: FB10036297 and FB11442287 if ( quickLookCompatible ) { // This is NOT correct yet in QuickLook, but comes close for a range of models. // It becomes more incorrect the bigger the offset is offset.x = offset.x / repeat.x; offset.y = offset.y / repeat.y; offset.x += xRotationOffset / repeat.x; offset.y += yRotationOffset - 1; } else { // results match glTF results exactly. verified correct in usdview. offset.x += xRotationOffset * repeat.x; offset.y += ( 1 - yRotationOffset ) * repeat.y; } 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( `string inputs:varname = "${uv}"` ); primvarReaderNode.addProperty( 'float2 outputs:result' ); const transform2dNode = new USDNode( `Transform2d_${mapType}`, 'Shader' ); transform2dNode.addProperty( 'uniform token info:id = "UsdTransform2d"' ); transform2dNode.addProperty( `float2 inputs:in.connect = </Materials/Material_${material.id}/PrimvarReader_${mapType}.outputs:result>` ); 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}.${getTextureExtension( texture )}@` ); textureNode.addProperty( `float2 inputs:st.connect = </Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>` ); if ( color !== undefined ) { const alpha = ( mapType === 'diffuse' ) ? material.opacity : 1; textureNode.addProperty( `float4 inputs:scale = ${buildColor4( color, alpha )}` ); } if ( mapType === 'normal' ) { // Similar to GLTFExporter, only the x component is used so the y-negation that // GLTFLoader applies to tangent-less glTF assets is not baked into the export. const scale = material.normalScale.x; textureNode.addProperty( `float4 inputs:scale = (${ 2 * scale }, ${ 2 * scale }, 2, 1)` ); textureNode.addProperty( `float4 inputs:bias = (${ - scale }, ${ - scale }, -1, 0)` ); } 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' ); } return [ primvarReaderNode, transform2dNode, textureNode ]; } if ( material.side === DoubleSide ) { 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 ) { previewSurfaceNode.addProperty( `color3f inputs:diffuseColor.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>` ); if ( material.transparent ) { previewSurfaceNode.addProperty( `float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` ); } else if ( material.alphaTest > 0.0 ) { previewSurfaceNode.addProperty( `float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` ); previewSurfaceNode.addProperty( `float inputs:opacityThreshold = ${material.alphaTest}` ); } const textureNodes = buildTextureNodes( material.map, 'diffuse', material.color ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `color3f inputs:diffuseColor = ${buildColor( material.color )}` ); } if ( material.emissive ) { const emissiveIntensity = material.emissiveIntensity ?? 1; if ( material.emissiveMap ) { previewSurfaceNode.addProperty( `color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` ); const emissiveColor = new Color( material.emissive.r * emissiveIntensity, material.emissive.g * emissiveIntensity, material.emissive.b * emissiveIntensity ); const textureNodes = buildTextureNodes( material.emissiveMap, 'emissive', emissiveColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else if ( material.emissive.getHex() > 0 ) { previewSurfaceNode.addProperty( `color3f inputs:emissiveColor = ${buildColor( material.emissive )}` ); } } if ( material.normalMap ) { previewSurfaceNode.addProperty( `normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` ); const textureNodes = buildTextureNodes( material.normalMap, 'normal' ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } if ( material.aoMap ) { previewSurfaceNode.addProperty( `float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` ); const aoMapIntensity = material.aoMapIntensity ?? 1; const aoColor = new Color( aoMapIntensity, aoMapIntensity, aoMapIntensity ); const textureNodes = buildTextureNodes( material.aoMap, 'occlusion', aoColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } if ( material.roughnessMap ) { previewSurfaceNode.addProperty( `float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` ); const roughnessColor = new Color( material.roughness, material.roughness, material.roughness ); const textureNodes = buildTextureNodes( material.roughnessMap, 'roughness', roughnessColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `float inputs:roughness = ${material.roughness ?? 1}` ); } if ( material.metalnessMap ) { previewSurfaceNode.addProperty( `float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` ); const metalnessColor = new Color( material.metalness, material.metalness, material.metalness ); const textureNodes = buildTextureNodes( material.metalnessMap, 'metallic', metalnessColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `float inputs:metallic = ${material.metalness ?? 0}` ); } if ( material.alphaMap ) { previewSurfaceNode.addProperty( `float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` ); previewSurfaceNode.addProperty( 'float inputs:opacityThreshold = 0.0001' ); const textureNodes = buildTextureNodes( material.alphaMap, 'opacity' ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `float inputs:opacity = ${material.opacity}` ); } if ( material.isMeshPhysicalMaterial ) { if ( material.clearcoatMap !== null ) { previewSurfaceNode.addProperty( `float inputs:clearcoat.connect = </Materials/Material_${material.id}/Texture_${material.clearcoatMap.id}_clearcoat.outputs:r>` ); const clearcoatColor = new Color( material.clearcoat, material.clearcoat, material.clearcoat ); const textureNodes = buildTextureNodes( material.clearcoatMap, 'clearcoat', clearcoatColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `float inputs:clearcoat = ${material.clearcoat}` ); } if ( material.clearcoatRoughnessMap !== null ) { previewSurfaceNode.addProperty( `float inputs:clearcoatRoughness.connect = </Materials/Material_${material.id}/Texture_${material.clearcoatRoughnessMap.id}_clearcoatRoughness.outputs:g>` ); const clearcoatRoughnessColor = new Color( material.clearcoatRoughness, material.clearcoatRoughness, material.clearcoatRoughness ); const textureNodes = buildTextureNodes( material.clearcoatRoughnessMap, 'clearcoatRoughness', clearcoatRoughnessColor ); textureNodes.forEach( ( node ) => materialNode.addChild( node ) ); } else { previewSurfaceNode.addProperty( `float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` ); } previewSurfaceNode.addProperty( `float inputs:ior = ${material.ior}` ); } previewSurfaceNode.addProperty( 'int inputs:useSpecularWorkflow = 0' ); previewSurfaceNode.addProperty( 'token outputs:surface' ); materialNode.addChild( previewSurfaceNode ); materialNode.addProperty( `token outputs:surface.connect = </Materials/Material_${material.id}/PreviewSurface.outputs:surface>` ); return materialNode; } function buildColor( color ) { return `(${color.r}, ${color.g}, ${color.b})`; } function buildColor4( color, alpha = 1 ) { return `(${color.r}, ${color.g}, ${color.b}, ${alpha})`; } function buildVector2( vector ) { return `(${vector.x}, ${vector.y})`; } function buildCamera( camera, usedNames, options ) { const name = getName( camera, usedNames ); if ( camera.matrix.determinant() < 0 ) { console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', camera ); } const node = new USDNode( name, 'Camera' ); addTransformProperties( node, camera, options ); 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 ) { horizontalAperture = ( ( Math.abs( camera.left ) + Math.abs( camera.right ) ) * 10 ).toPrecision( PRECISION ); } 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 { 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; } /** * Export options of `USDZExporter`. * * @typedef {Object} USDZExporter~Options * @property {number} [maxTextureSize=1024] - The maximum texture size that is going to be exported. * @property {boolean} [includeAnchoringProperties=true] - Whether to include anchoring properties or not. * @property {boolean} [onlyVisible=true] - Export only visible 3D objects. * @property {Object} [ar] - If `includeAnchoringProperties` is set to `true`, the anchoring type and alignment * can be configured via `ar.anchoring.type` and `ar.planeAnchoring.alignment`. * @property {boolean} [quickLookCompatible=false] - Whether to make the exported USDZ compatible to QuickLook * which means the asset is modified to accommodate the bugs FB10036297 and FB11442287 (Apple Feedback). * @property {Array<AnimationClip>} [animations=[]] - Animation clips to bake into `xformOp` time samples on the * targeted objects. Only `position`, `quaternion`, and `scale` tracks are exported. * @property {number} [animationFrameRate=60] - Time codes per second used when writing animation samples. **/ /** * onDone callback of `USDZExporter`. * * @callback USDZExporter~OnDone * @param {ArrayBuffer} result - The generated USDZ. */ /** * onError callback of `USDZExporter`. * * @callback USDZExporter~OnError * @param {Error} error - The error object. */ export { USDZExporter };