UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

362 lines (355 loc) 12.1 kB
import { CoreExporter } from "./core-exporter.js"; import { zipSync, strToU8 } from "../../../modules/fflate/esm/browser.js"; import { Color } from "../../core/math/color.js"; import { SEMANTIC_POSITION, SEMANTIC_NORMAL, SEMANTIC_TEXCOORD0, SEMANTIC_TEXCOORD1 } from "../../platform/graphics/constants.js"; const ROOT_FILE_NAME = "root"; const header = `#usda 1.0 ( customLayerData = { string creator = "PlayCanvas UsdzExporter" } metersPerUnit = 1 upAxis = "Y" ) `; const materialListTemplate = (materials) => ` def "Materials" { ${materials.join("\n")} } `; const meshTemplate = (faceVertexCounts, indices, normals, positions, uv0, uv1) => ` def "Mesh" { def Mesh "Mesh" { int[] faceVertexCounts = [${faceVertexCounts}] int[] faceVertexIndices = [${indices}] normal3f[] normals = [${normals}] ( interpolation = "vertex" ) point3f[] points = [${positions}] texCoord2f[] primvars:st = [${uv0}] ( interpolation = "vertex" ) texCoord2f[] primvars:st1 = [${uv1}] ( interpolation = "vertex" ) uniform token subdivisionScheme = "none" } } `; const meshInstanceTemplate = (nodeName, meshRefPath, worldMatrix, materialRefPath) => ( /* usd */ ` def Xform "${nodeName}" ( prepend references = ${meshRefPath} ) { matrix4d xformOp:transform = ${worldMatrix} uniform token[] xformOpOrder = ["xformOp:transform"] rel material:binding = ${materialRefPath} } ` ); const materialValueTemplate = (type, name, value) => ` ${type} inputs:${name} = ${value}`; class UsdzExporter extends CoreExporter { meshMap; materialMap; materials; textureMap; nodeNames; files; init() { this.meshMap = /* @__PURE__ */ new Map(); this.textureMap = /* @__PURE__ */ new Map(); this.materialMap = /* @__PURE__ */ new Map(); this.materials = []; this.files = {}; this.nodeNames = /* @__PURE__ */ new Set(); } done() { this.meshMap = null; this.textureMap = null; this.materialMap = null; this.materials = null; this.files = null; this.nodeNames = null; } build(entity, options = {}) { this.init(); this.addFile(null, ROOT_FILE_NAME); const allMeshInstances = []; if (entity) { const renders = entity.findComponents("render"); renders.forEach((render) => { allMeshInstances.push(...render.meshInstances); }); } let rootContent = ""; allMeshInstances.forEach((meshInstance) => { rootContent += this.buildMeshInstance(meshInstance); }); rootContent += materialListTemplate(this.materials); this.addFile(null, ROOT_FILE_NAME, "", rootContent); const textureOptions = { maxTextureSize: options.maxTextureSize }; const textureArray = Array.from(this.textureMap.keys()); const promises = []; for (let i = 0; i < textureArray.length; i++) { const isRGBA = true; const mimeType = isRGBA ? "image/png" : "image/jpeg"; const texture = textureArray[i]; const texturePromise = this.textureToCanvas(texture, textureOptions).then((canvas) => { if (canvas) { return new Promise((resolve) => canvas.toBlob(resolve, mimeType, 1)).then( (blob) => blob.arrayBuffer() ); } console.warn(`Export of texture ${texture.name} is not currently supported.`); return new Promise((resolve) => resolve(null)); }); promises.push(texturePromise); } const finalData = Promise.all(promises).then((values) => { values.forEach((textureArrayBuffer, index) => { const texture = textureArray[index]; const ids = this.getTextureFileIds(texture); this.files[ids.fileName] = new Uint8Array(textureArrayBuffer); }); this.alignFiles(); const arraybuffer = zipSync(this.files, { level: 0 }); this.done(); return arraybuffer; }); return finalData; } alignFiles() { let offset = 0; for (const filename in this.files) { const file = this.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); this.files[filename] = [file, { extra: { 12345: padding } }]; } offset = file.length; } } getFileIds(category, name, ref, extension = "usda") { const fileName = `${category ? `${category}/` : ""}${name}.${extension}`; const refName = `@./${fileName}@</${ref}>`; return { name, fileName, refName }; } getTextureFileIds(texture) { return this.getFileIds("texture", `Texture_${texture.id}`, "Texture", "png"); } addFile(category, uniqueId, refName = "", content = "") { let contentU8 = null; if (content) { content = `${header} ${content}`; contentU8 = strToU8(content); } const ids = this.getFileIds(category, uniqueId, refName); this.files[ids.fileName] = contentU8; return ids.refName; } getMaterialRef(material) { let materialRef = this.materialMap.get(material); if (!materialRef) { materialRef = this.buildMaterial(material); this.materialMap.set(material, materialRef); } return materialRef; } getMeshRef(mesh) { let meshRef = this.meshMap.get(mesh); if (!meshRef) { meshRef = this.buildMesh(mesh); this.meshMap.set(mesh, meshRef); } return meshRef; } buildArray2(array) { const components = []; const count = array.length; for (let i = 0; i < count; i += 2) { components.push(`(${array[i]}, ${1 - array[i + 1]})`); } return components.join(", "); } buildArray3(array) { const components = []; const count = array.length; for (let i = 0; i < count; i += 3) { components.push(`(${array[i]}, ${array[i + 1]}, ${array[i + 2]})`); } return components.join(", "); } buildMat4(mat) { const data = mat.data; const vectors = []; for (let i = 0; i < 16; i += 4) { vectors.push(`(${data[i]}, ${data[i + 1]}, ${data[i + 2]}, ${data[i + 3]})`); } return `( ${vectors.join(", ")} )`; } // format: https://graphics.pixar.com/usd/release/spec_usdpreviewsurface.html buildMaterial(material) { const materialName = `Material_${material.id}`; const materialPath = `/Materials/${materialName}`; const materialPropertyPath = (property) => `<${materialPath}${property}>`; const buildTexture = (texture, textureIds, mapType, uvChannel, tiling, offset, rotation, tintColor) => { return ` def Shader "Transform2d_${mapType}" ( sdrMetadata = { string role = "math" } ) { uniform token info:id = "UsdTransform2d" float2 inputs:in.connect = ${materialPropertyPath(`/uvReader_${uvChannel}.outputs:result`)} float inputs:rotation = ${rotation} float2 inputs:scale = (${tiling.x}, ${tiling.y}) float2 inputs:translation = (${offset.x}, ${offset.y}) float2 outputs:result } def Shader "Texture_${texture.id}_${mapType}" { uniform token info:id = "UsdUVTexture" asset inputs:file = @${textureIds.fileName}@ float2 inputs:st.connect = ${materialPropertyPath(`/Transform2d_${mapType}.outputs:result`)} token inputs:wrapS = "repeat" token inputs:wrapT = "repeat" float4 inputs:scale = (${tintColor.r}, ${tintColor.g}, ${tintColor.b}, ${tintColor.a}) float outputs:r float outputs:g float outputs:b float3 outputs:rgb float outputs:a } `; }; const inputs = []; const samplers = []; const addTexture = (textureSlot, uniform, propType, propName, valueName, handleOpacity = false, tintTexture = false) => { const texture = material[textureSlot]; if (texture) { const textureIds = this.getTextureFileIds(texture); this.textureMap.set(texture, textureIds.refName); const channel = material[`${textureSlot}Channel`] || "rgb"; const textureValue = materialPropertyPath(`/${textureIds.name}_${valueName}.outputs:${channel}`); inputs.push(materialValueTemplate(propType, `${propName}.connect`, textureValue)); if (handleOpacity) { if (material.alphaTest > 0) { } } const tiling = material[`${textureSlot}Tiling`]; const offset = material[`${textureSlot}Offset`]; const rotation = material[`${textureSlot}Rotation`]; const uvChannel = material[`${textureSlot}Uv`] === 1 ? "st1" : "st"; const tintColor = tintTexture && uniform ? uniform : Color.WHITE; samplers.push(buildTexture(texture, textureIds, valueName, uvChannel, tiling, offset, rotation, tintColor)); } else if (uniform) { const value = propType === "float" ? `${uniform}` : `(${uniform.r}, ${uniform.g}, ${uniform.b})`; inputs.push(materialValueTemplate(propType, propName, value)); } }; addTexture("diffuseMap", material.diffuse, "color3f", "diffuseColor", "diffuse", false, true); if (material.transparent || material.alphaTest > 0) { addTexture("opacityMap", material.opacity, "float", "opacity", "opacity", true); } addTexture("normalMap", null, "normal3f", "normal", "normal"); addTexture("emissiveMap", material.emissive, "color3f", "emissiveColor", "emissive", false, true); addTexture("aoMap", null, "float", "occlusion", "occlusion"); const metalness = material.useMetalness ? material.metalness : 0; addTexture("metalnessMap", metalness, "float", "metallic", "metallic"); const roughness = material.glossInvert ? material.gloss : 1 - material.gloss; addTexture("glossMap", roughness, "float", "roughness", "roughness"); const materialObject = ` def Material "${materialName}" { def Shader "PreviewSurface" { uniform token info:id = "UsdPreviewSurface" ${inputs.join("\n")} int inputs:useSpecularWorkflow = 0 token outputs:surface } token outputs:surface.connect = ${materialPropertyPath("/PreviewSurface.outputs:surface")} def Shader "uvReader_st" { uniform token info:id = "UsdPrimvarReader_float2" token inputs:varname = "st" float2 inputs:fallback = (0.0, 0.0) float2 outputs:result } def Shader "uvReader_st1" { uniform token info:id = "UsdPrimvarReader_float2" token inputs:varname = "st1" float2 inputs:fallback = (0.0, 0.0) float2 outputs:result } ${samplers.join("\n")} } `; this.materials.push(materialObject); return materialPropertyPath(""); } buildMesh(mesh) { let positions = []; const indices = []; let normals = []; let uv0 = []; let uv1 = []; mesh.getVertexStream(SEMANTIC_POSITION, positions); mesh.getVertexStream(SEMANTIC_NORMAL, normals); mesh.getVertexStream(SEMANTIC_TEXCOORD0, uv0); mesh.getVertexStream(SEMANTIC_TEXCOORD1, uv1); mesh.getIndices(indices); const indicesCount = indices.length || positions.length; const faceVertexCounts = Array(indicesCount / 3).fill(3).join(", "); if (!indices.length) { for (let i = 0; i < indicesCount; i++) { indices[i] = i; } } const numVerts = positions.length / 3; normals = normals.length ? normals : Array(numVerts * 3).fill(0); uv0 = uv0.length ? uv0 : Array(numVerts * 2).fill(0); uv1 = uv1.length ? uv1 : Array(numVerts * 2).fill(0); positions = this.buildArray3(positions); normals = this.buildArray3(normals); uv0 = this.buildArray2(uv0); uv1 = this.buildArray2(uv1); const meshObject = meshTemplate(faceVertexCounts, indices, normals, positions, uv0, uv1); const refPath = this.addFile("mesh", `Mesh_${mesh.id}`, "Mesh", meshObject); return refPath; } buildMeshInstance(meshInstance) { const meshRefPath = this.getMeshRef(meshInstance.mesh); const materialRefPath = this.getMaterialRef(meshInstance.material); const worldMatrix = this.buildMat4(meshInstance.node.getWorldTransform()); const name = meshInstance.node.name.replace(/[^a-z0-9]/gi, "_"); let nodeName = name; while (this.nodeNames.has(nodeName)) { nodeName = `${name}_${Math.random().toString(36).slice(2, 7)}`; } this.nodeNames.add(nodeName); return meshInstanceTemplate(nodeName, meshRefPath, worldMatrix, materialRefPath); } } export { UsdzExporter };