UNPKG

playcanvas

Version:

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

919 lines (918 loc) 29.9 kB
import { CoreExporter } from "./core-exporter.js"; import { math } from "../../core/math/math.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Vec3 } from "../../core/math/vec3.js"; import { Quat } from "../../core/math/quat.js"; import { Color } from "../../core/math/color.js"; import { BoundingBox } from "../../core/shape/bounding-box.js"; import { CULLFACE_NONE, INDEXFORMAT_UINT8, INDEXFORMAT_UINT16, INDEXFORMAT_UINT32, ADDRESS_CLAMP_TO_EDGE, ADDRESS_MIRRORED_REPEAT, ADDRESS_REPEAT, FILTER_LINEAR, FILTER_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, FILTER_NEAREST_MIPMAP_NEAREST, SEMANTIC_POSITION, SEMANTIC_NORMAL, SEMANTIC_TANGENT, SEMANTIC_COLOR, SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SEMANTIC_TEXCOORD0, SEMANTIC_TEXCOORD1, SEMANTIC_TEXCOORD2, SEMANTIC_TEXCOORD3, SEMANTIC_TEXCOORD4, SEMANTIC_TEXCOORD5, SEMANTIC_TEXCOORD6, SEMANTIC_TEXCOORD7, TYPE_INT8, TYPE_UINT8, TYPE_INT16, TYPE_UINT16, TYPE_INT32, TYPE_UINT32, TYPE_FLOAT32 } from "../../platform/graphics/constants.js"; import { IndexBuffer } from "../../platform/graphics/index-buffer.js"; import { VertexBuffer } from "../../platform/graphics/vertex-buffer.js"; import { StandardMaterial } from "../../scene/materials/standard-material.js"; import { BLEND_NONE, BLEND_NORMAL, PROJECTION_ORTHOGRAPHIC } from "../../scene/constants.js"; const ARRAY_BUFFER = 34962; const ELEMENT_ARRAY_BUFFER = 34963; const getIndexComponentType = (indexFormat) => { switch (indexFormat) { case INDEXFORMAT_UINT8: return 5121; case INDEXFORMAT_UINT16: return 5123; case INDEXFORMAT_UINT32: return 5125; } return 0; }; const getComponentType = (dataType) => { switch (dataType) { case TYPE_INT8: return 5120; case TYPE_UINT8: return 5121; case TYPE_INT16: return 5122; case TYPE_UINT16: return 5123; case TYPE_INT32: return 5124; case TYPE_UINT32: return 5125; case TYPE_FLOAT32: return 5126; } return 0; }; const getAccessorType = (componentCount) => { switch (componentCount) { case 1: return "SCALAR"; case 2: return "VEC2"; case 3: return "VEC3"; case 4: return "VEC4"; } return 0; }; const getSemantic = (engineSemantic) => { switch (engineSemantic) { case SEMANTIC_POSITION: return "POSITION"; case SEMANTIC_NORMAL: return "NORMAL"; case SEMANTIC_TANGENT: return "TANGENT"; case SEMANTIC_COLOR: return "COLOR_0"; case SEMANTIC_BLENDINDICES: return "JOINTS_0"; case SEMANTIC_BLENDWEIGHT: return "WEIGHTS_0"; case SEMANTIC_TEXCOORD0: return "TEXCOORD_0"; case SEMANTIC_TEXCOORD1: return "TEXCOORD_1"; case SEMANTIC_TEXCOORD2: return "TEXCOORD_2"; case SEMANTIC_TEXCOORD3: return "TEXCOORD_3"; case SEMANTIC_TEXCOORD4: return "TEXCOORD_4"; case SEMANTIC_TEXCOORD5: return "TEXCOORD_5"; case SEMANTIC_TEXCOORD6: return "TEXCOORD_6"; case SEMANTIC_TEXCOORD7: return "TEXCOORD_7"; } return ""; }; const getFilter = function(filter) { switch (filter) { case FILTER_NEAREST: return 9728; case FILTER_LINEAR: return 9729; case FILTER_NEAREST_MIPMAP_NEAREST: return 9984; case FILTER_LINEAR_MIPMAP_NEAREST: return 9985; case FILTER_NEAREST_MIPMAP_LINEAR: return 9986; case FILTER_LINEAR_MIPMAP_LINEAR: return 9987; } return 0; }; const getWrap = function(wrap) { switch (wrap) { case ADDRESS_CLAMP_TO_EDGE: return 33071; case ADDRESS_MIRRORED_REPEAT: return 33648; case ADDRESS_REPEAT: return 10497; } return 0; }; function isCanvasTransparent(canvas) { const context = canvas.getContext("2d"); const pixelData = context.getImageData(0, 0, canvas.width, canvas.height).data; for (let i = 3; i < pixelData.length; i += 4) { if (pixelData[i] < 255) { return true; } } return false; } const textureSemantics = [ "anisotropyMap", "aoMap", "clearCoatGlossMap", "clearCoatMap", "clearCoatNormalMap", "colorMap", "diffuseMap", "emissiveMap", "iridescenceMap", "iridescenceThicknessMap", "metalnessMap", "normalMap", "refractionMap", "sheenGlossMap", "sheenMap", "specularityFactorMap", "specularMap", "thicknessMap" ]; class GltfExporter extends CoreExporter { collectResources(root) { const resources = { buffers: [], cameras: [], entities: [], materials: [], skins: [], textures: [], // entry: { node, meshInstances} entityMeshInstances: [], // maps a buffer (vertex or index) to an array of bufferview indices bufferViewMap: /* @__PURE__ */ new Map(), compressableTexture: /* @__PURE__ */ new Set() }; const { materials, buffers, entityMeshInstances, textures } = resources; root.forEach((entity) => { resources.entities.push(entity); }); const collectMeshInstances = (meshInstances) => { meshInstances.forEach((meshInstance) => { const material = meshInstance.material; if (materials.indexOf(material) < 0) { resources.materials.push(material); textureSemantics.forEach((semantic) => { const texture = material[semantic]; if (texture && textures.indexOf(texture) < 0) { if (semantic !== "normalMap") { resources.compressableTexture.add(texture); } textures.push(texture); } }); } const node = meshInstance.node; let nodeMeshInstances = entityMeshInstances.find((e) => e.node === node); if (!nodeMeshInstances) { nodeMeshInstances = { node, meshInstances: [] }; entityMeshInstances.push(nodeMeshInstances); } nodeMeshInstances.meshInstances.push(meshInstance); const mesh = meshInstance.mesh; const vertexBuffer = mesh.vertexBuffer; if (buffers.indexOf(vertexBuffer) < 0) { buffers.unshift(vertexBuffer); } const indexBuffer = mesh.indexBuffer[0]; if (buffers.indexOf(indexBuffer) < 0) { buffers.push(indexBuffer); } if (mesh.skin && resources.skins.indexOf(mesh.skin) < 0) { resources.skins.push(mesh.skin); } }); }; resources.entities.forEach((entity) => { if (entity.camera) { resources.cameras.push(entity.camera); } if (entity.render && entity.render.enabled) { collectMeshInstances(entity.render.meshInstances); } if (entity.model && entity.model.enabled && entity.model.meshInstances) { collectMeshInstances(entity.model.meshInstances); } }); return resources; } writeBufferViews(resources, json) { json.bufferViews = []; for (const buffer of resources.buffers) { GltfExporter.writeBufferView(resources, json, buffer); } } static writeBufferView(resources, json, buffer) { json.buffers = json.buffers ?? []; json.buffers[0] = json.buffers[0] ?? { byteLength: 0 }; const bufferInfo = json.buffers[0]; bufferInfo.byteLength = math.roundUp(bufferInfo.byteLength, 4); const offset = bufferInfo.byteLength; const addBufferView = (target, byteLength, byteOffset, byteStride) => { const bufferView = { buffer: 0, byteLength, byteOffset }; if (target === ARRAY_BUFFER || target === ELEMENT_ARRAY_BUFFER) { bufferView.target = target; } if (byteStride !== void 0) { bufferView.byteStride = byteStride; } return json.bufferViews.push(bufferView) - 1; }; let arrayBuffer; if (buffer instanceof VertexBuffer) { arrayBuffer = buffer.lock(); const format = buffer.getFormat(); if (format.interleaved) { const bufferViewIndex = addBufferView(ARRAY_BUFFER, arrayBuffer.byteLength, offset, format.size); resources.bufferViewMap.set(buffer, [bufferViewIndex]); } else { const bufferViewIndices = []; for (const element of format.elements) { const bufferViewIndex = addBufferView( ARRAY_BUFFER, element.size * format.vertexCount, offset + element.offset, element.size ); bufferViewIndices.push(bufferViewIndex); } resources.bufferViewMap.set(buffer, bufferViewIndices); } } else if (buffer instanceof IndexBuffer) { arrayBuffer = buffer.lock(); const bufferViewIndex = addBufferView(ELEMENT_ARRAY_BUFFER, arrayBuffer.byteLength, offset); resources.bufferViewMap.set(buffer, [bufferViewIndex]); } else { arrayBuffer = buffer; const bufferViewIndex = addBufferView(void 0, arrayBuffer.byteLength, offset); resources.bufferViewMap.set(buffer, [bufferViewIndex]); } bufferInfo.byteLength += arrayBuffer.byteLength; } writeCameras(resources, json) { if (resources.cameras.length > 0) { json.cameras = resources.cameras.map((cam) => { const projection = cam.projection; const nearClip = cam.nearClip; const farClip = cam.farClip; const camera = {}; if (projection === PROJECTION_ORTHOGRAPHIC) { camera.type = "orthographic"; camera.orthographic = { xmag: 1, ymag: 1, znear: nearClip, zfar: farClip }; } else { const fov = cam.fov; camera.type = "perspective"; camera.perspective = { yfov: fov * math.DEG_TO_RAD, znear: nearClip, zfar: farClip }; } return camera; }); } } attachTexture(resources, material, destination, name, textureSemantic, json) { const texture = material[textureSemantic]; if (texture) { const textureIndex = resources.textures.indexOf(texture); if (textureIndex < 0) console.warn(`Texture ${texture.name} wasn't collected.`); destination[name] = { index: textureIndex }; const textureTexCoord = material[`${textureSemantic}Uv`] ?? 0; if (textureTexCoord !== 0) { destination[name].texCoord = textureTexCoord; } const scale = material[`${textureSemantic}Tiling`]; const offset = material[`${textureSemantic}Offset`]; const rotation = material[`${textureSemantic}Rotation`]; if (scale && !scale.equals(Vec2.ONE) || offset && !offset.equals(Vec2.ZERO) || rotation !== 0) { destination[name].extensions = { KHR_texture_transform: {} }; json.extensionsUsed = json.extensionsUsed ?? []; if (!json.extensionsUsed.includes("KHR_texture_transform")) { json.extensionsUsed.push("KHR_texture_transform"); } json.extensionsRequired = json.extensionsRequired ?? []; if (!json.extensionsRequired.includes("KHR_texture_transform")) { json.extensionsRequired.push("KHR_texture_transform"); } if (scale && !scale.equals(Vec2.ONE)) { destination[name].extensions.KHR_texture_transform.scale = [scale.x, scale.y]; } if (offset && !offset.equals(Vec2.ZERO)) { destination[name].extensions.KHR_texture_transform.offset = [offset.x, offset.y - 1 + scale.y]; } if (rotation !== 0) { destination[name].extensions.KHR_texture_transform.rotation = rotation * math.DEG_TO_RAD; } } } } addExtension(json, output, name, data = {}) { output.extensions = output.extensions || {}; output.extensions[name] = data; json.extensionsUsed = json.extensionsUsed ?? []; if (!json.extensionsUsed.includes(name)) { json.extensionsUsed.push(name); } } writeStandardMaterial(resources, mat, output, json) { const { diffuse, emissive, opacity, metalness, gloss, glossInvert } = mat; const pbr = output.pbrMetallicRoughness; const baseColor = mat.useLighting ? diffuse : emissive; if (!baseColor.equals(Color.WHITE) || opacity !== 1) { const { r, g, b } = baseColor.clone().linear(); pbr.baseColorFactor = [r, g, b, opacity]; } if (mat.useMetalness) { if (metalness !== 1) { pbr.metallicFactor = metalness; } } else { pbr.metallicFactor = 0; } const roughness = glossInvert ? gloss : 1 - gloss; if (roughness !== 1) { pbr.roughnessFactor = roughness; } this.attachTexture(resources, mat, pbr, "baseColorTexture", mat.useLighting ? "diffuseMap" : "emissiveMap", json); this.attachTexture(resources, mat, pbr, "metallicRoughnessTexture", "metalnessMap", json); if (mat.useLighting && !emissive.equals(Color.BLACK)) { const { r, g, b } = emissive.clone().linear(); output.emissiveFactor = [r, g, b]; } if (mat.enableGGXSpecular && (mat.anisotropyIntensity !== 0 || mat.anisotropyRotation !== 0 || mat.anisotropyMap)) { const anisotropyExt = {}; if (mat.anisotropyIntensity !== 0) { anisotropyExt.anisotropyStrength = mat.anisotropyIntensity; } if (mat.anisotropyRotation !== 0) { anisotropyExt.anisotropyRotation = mat.anisotropyRotation * math.DEG_TO_RAD; } this.attachTexture(resources, mat, anisotropyExt, "anisotropyTexture", "anisotropyMap", json); if (Object.keys(anisotropyExt).length > 0) { this.addExtension(json, output, "KHR_materials_anisotropy", anisotropyExt); } } if (mat.clearCoat > 0) { const clearcoatExt = { // Parser multiplies by 0.25, so we divide by 0.25 (multiply by 4) to reverse clearcoatFactor: Math.min(mat.clearCoat * 4, 1) }; if (mat.clearCoatGloss !== 0) { clearcoatExt.clearcoatRoughnessFactor = mat.clearCoatGloss; } this.attachTexture(resources, mat, clearcoatExt, "clearcoatTexture", "clearCoatMap", json); this.attachTexture(resources, mat, clearcoatExt, "clearcoatRoughnessTexture", "clearCoatGlossMap", json); if (mat.clearCoatNormalMap) { this.attachTexture(resources, mat, clearcoatExt, "clearcoatNormalTexture", "clearCoatNormalMap", json); if (mat.clearCoatBumpiness !== 1) { clearcoatExt.clearcoatNormalTexture.scale = mat.clearCoatBumpiness; } } this.addExtension(json, output, "KHR_materials_clearcoat", clearcoatExt); } if (mat.dispersion !== 0) { this.addExtension(json, output, "KHR_materials_dispersion", { dispersion: mat.dispersion }); } if (mat.useLighting && mat.emissiveIntensity !== 1) { this.addExtension(json, output, "KHR_materials_emissive_strength", { emissiveStrength: mat.emissiveIntensity }); } const defaultRefractionIndex = 1 / 1.5; if (mat.refractionIndex !== defaultRefractionIndex && mat.refractionIndex > 0) { this.addExtension(json, output, "KHR_materials_ior", { ior: 1 / mat.refractionIndex }); } if (mat.useIridescence) { const iridescenceExt = {}; if (mat.iridescence !== 0) { iridescenceExt.iridescenceFactor = mat.iridescence; } if (mat.iridescenceRefractionIndex !== 1.3) { iridescenceExt.iridescenceIor = mat.iridescenceRefractionIndex; } if (mat.iridescenceThicknessMin !== 100) { iridescenceExt.iridescenceThicknessMinimum = mat.iridescenceThicknessMin; } if (mat.iridescenceThicknessMax !== 400) { iridescenceExt.iridescenceThicknessMaximum = mat.iridescenceThicknessMax; } this.attachTexture(resources, mat, iridescenceExt, "iridescenceTexture", "iridescenceMap", json); this.attachTexture(resources, mat, iridescenceExt, "iridescenceThicknessTexture", "iridescenceThicknessMap", json); if (Object.keys(iridescenceExt).length > 0) { this.addExtension(json, output, "KHR_materials_iridescence", iridescenceExt); } } if (mat.useSheen) { const sheenExt = {}; if (!mat.sheen.equals(Color.BLACK)) { const { r, g, b } = mat.sheen.clone().linear(); sheenExt.sheenColorFactor = [r, g, b]; } if (mat.sheenGloss !== 0) { sheenExt.sheenRoughnessFactor = mat.sheenGloss; } this.attachTexture(resources, mat, sheenExt, "sheenColorTexture", "sheenMap", json); this.attachTexture(resources, mat, sheenExt, "sheenRoughnessTexture", "sheenGlossMap", json); if (Object.keys(sheenExt).length > 0) { this.addExtension(json, output, "KHR_materials_sheen", sheenExt); } } if (mat.useMetalnessSpecularColor || !mat.useMetalness) { const specularExt = {}; if (!mat.specular.equals(Color.WHITE)) { const { r, g, b } = mat.specular.clone().linear(); specularExt.specularColorFactor = [r, g, b]; } if (mat.specularityFactor !== 1) { specularExt.specularFactor = mat.specularityFactor; } this.attachTexture(resources, mat, specularExt, "specularColorTexture", "specularMap", json); this.attachTexture(resources, mat, specularExt, "specularTexture", "specularityFactorMap", json); if (Object.keys(specularExt).length > 0) { this.addExtension(json, output, "KHR_materials_specular", specularExt); } } if (mat.useDynamicRefraction && (mat.refraction !== 0 || mat.refractionMap)) { const transmissionExt = {}; if (mat.refraction !== 0) { transmissionExt.transmissionFactor = mat.refraction; } this.attachTexture(resources, mat, transmissionExt, "transmissionTexture", "refractionMap", json); if (Object.keys(transmissionExt).length > 0) { this.addExtension(json, output, "KHR_materials_transmission", transmissionExt); } } if (!mat.useLighting) { this.addExtension(json, output, "KHR_materials_unlit"); } if (mat.useDynamicRefraction && (mat.thickness !== 0 || mat.attenuationDistance !== 0 || !mat.attenuation.equals(Color.WHITE) || mat.thicknessMap)) { const volumeExt = {}; if (mat.thickness !== 0) { volumeExt.thicknessFactor = mat.thickness; } if (mat.attenuationDistance !== 0) { volumeExt.attenuationDistance = mat.attenuationDistance; } if (!mat.attenuation.equals(Color.WHITE)) { const { r, g, b } = mat.attenuation.clone().linear(); volumeExt.attenuationColor = [r, g, b]; } this.attachTexture(resources, mat, volumeExt, "thicknessTexture", "thicknessMap", json); if (Object.keys(volumeExt).length > 0) { this.addExtension(json, output, "KHR_materials_volume", volumeExt); } } } writeMaterials(resources, json) { if (resources.materials.length > 0) { json.materials = resources.materials.map((mat) => { const { name, blendType, cull, alphaTest } = mat; const material = { pbrMetallicRoughness: {} }; if (name && name.length > 0) { material.name = name; } if (mat instanceof StandardMaterial) { this.writeStandardMaterial(resources, mat, material, json); } if (blendType === BLEND_NORMAL) { material.alphaMode = "BLEND"; } else if (blendType === BLEND_NONE) { if (alphaTest !== 0) { material.alphaMode = "MASK"; material.alphaCutoff = alphaTest; } } if (cull === CULLFACE_NONE) { material.doubleSided = true; } this.attachTexture(resources, mat, material, "normalTexture", "normalMap", json); this.attachTexture(resources, mat, material, "occlusionTexture", "aoMap", json); this.attachTexture(resources, mat, material, "emissiveTexture", "emissiveMap", json); return material; }); } } writeNodes(resources, json) { if (resources.entities.length > 0) { json.nodes = resources.entities.map((entity) => { const name = entity.name; const t = entity.getLocalPosition(); const r = entity.getLocalRotation(); const s = entity.getLocalScale(); const node = {}; if (name && name.length > 0) { node.name = name; } if (!t.equals(Vec3.ZERO)) { node.translation = [t.x, t.y, t.z]; } if (!r.equals(Quat.IDENTITY)) { node.rotation = [r.x, r.y, r.z, r.w]; } if (!s.equals(Vec3.ONE)) { node.scale = [s.x, s.y, s.z]; } if (entity.camera && entity.camera.enabled) { node.camera = resources.cameras.indexOf(entity.camera); } const entityMeshInstance = resources.entityMeshInstances.find((e) => e.node === entity); if (entityMeshInstance) { node.mesh = resources.entityMeshInstances.indexOf(entityMeshInstance); const meshInstance = entityMeshInstance.meshInstances[0]; if (meshInstance && meshInstance.mesh.skin) { node.skin = resources.skins.indexOf(meshInstance.mesh.skin); } } if (entity.children.length > 0) { node.children = []; entity.children.forEach((child) => { node.children.push(resources.entities.indexOf(child)); }); } return node; }); } } writeMeshes(resources, json, options) { if (resources.entityMeshInstances.length > 0) { json.accessors = []; json.meshes = []; resources.entityMeshInstances.forEach((entityMeshInstances) => { const mesh = { primitives: [] }; const meshInstances = entityMeshInstances.meshInstances; meshInstances.forEach((meshInstance) => { const primitive = GltfExporter.createPrimitive(resources, json, meshInstance.mesh, options); primitive.material = resources.materials.indexOf(meshInstance.material); mesh.primitives.push(primitive); }); json.meshes.push(mesh); }); } } static createPrimitive(resources, json, mesh, options = {}) { const primitive = { attributes: {} }; const { vertexBuffer } = mesh; const { format } = vertexBuffer; const { interleaved, elements } = format; const numVertices = vertexBuffer.getNumVertices(); elements.forEach((element, elementIndex) => { const semantic = getSemantic(element.name); if (options.stripUnusedAttributes) { let isUsed = true; if (semantic.startsWith("TEXCOORD_")) { const texCoordIndex = parseInt(semantic.split("_")[1], 10); isUsed = resources.materials.some((material) => { return textureSemantics.some((texSemantic) => { const texture = material[texSemantic]; if (!texture) { return false; } const uv = material[`${texSemantic}Uv`] ?? 0; return uv === texCoordIndex; }); }); } if (semantic === "COLOR_0") { isUsed = resources.materials.some((material) => material.vertexColors); } if (semantic === "TANGENT") { isUsed = resources.materials.some((material) => material.normalMap); } if (semantic === "JOINTS_0" || semantic === "WEIGHTS_0") { isUsed = resources.entityMeshInstances.some((emi) => emi.meshInstances.some((mi) => mi.mesh.skin)); } if (!isUsed) { return; } } let bufferView = resources.bufferViewMap.get(vertexBuffer); if (!bufferView) { GltfExporter.writeBufferView(resources, json, vertexBuffer); resources.buffers.push(vertexBuffer); bufferView = resources.bufferViewMap.get(vertexBuffer); } const viewIndex = bufferView[interleaved ? 0 : elementIndex]; const accessor = { bufferView: viewIndex, byteOffset: interleaved ? element.offset : 0, componentType: getComponentType(element.dataType), type: getAccessorType(element.numComponents), count: numVertices }; const idx = json.accessors.push(accessor) - 1; primitive.attributes[semantic] = idx; if (element.name === SEMANTIC_POSITION) { const positions = []; mesh.getPositions(positions); const min = new Vec3(); const max = new Vec3(); BoundingBox.computeMinMax(positions, min, max); accessor.min = [min.x, min.y, min.z]; accessor.max = [max.x, max.y, max.z]; } }); const indexBuffer = mesh.indexBuffer[0]; if (indexBuffer) { let bufferView = resources.bufferViewMap.get(indexBuffer); if (!bufferView) { GltfExporter.writeBufferView(resources, json, indexBuffer); resources.buffers.push(indexBuffer); bufferView = resources.bufferViewMap.get(indexBuffer); } const viewIndex = bufferView[0]; const accessor = { bufferView: viewIndex, componentType: getIndexComponentType(indexBuffer.getFormat()), count: indexBuffer.getNumIndices(), type: "SCALAR" }; const idx = json.accessors.push(accessor) - 1; primitive.indices = idx; } return primitive; } writeSkins(resources, json) { if (resources.skins.length > 0) { json.skins = resources.skins.map((skin) => { const matrices = new Float32Array(skin.inverseBindPose.length * 16); for (let i = 0; i < skin.inverseBindPose.length; i++) { const ibm = skin.inverseBindPose[i]; matrices.set(ibm.data, i * 16); } const matrixBuffer = matrices.buffer; GltfExporter.writeBufferView(resources, json, matrixBuffer); resources.buffers.push(matrixBuffer); const bufferView = resources.bufferViewMap.get(matrixBuffer); const accessor = { bufferView: bufferView[0], componentType: getComponentType(TYPE_FLOAT32), count: skin.inverseBindPose.length, type: "MAT4" }; const accessorIndex = json.accessors.push(accessor) - 1; const joints = skin.boneNames.map((boneName) => { const node = resources.entities.find((entity) => entity.name === boneName); return resources.entities.indexOf(node); }); return { inverseBindMatrices: accessorIndex, joints }; }); } } convertTextures(srcTextures, options) { const textureOptions = { maxTextureSize: options.maxTextureSize }; const promises = []; srcTextures.forEach((srcTexture) => { const promise = this.textureToCanvas(srcTexture, textureOptions); promise.then((canvas) => { return new Promise((resolve) => resolve(canvas)); }); promises.push(promise); }); return promises; } writeTextures(resources, textureCanvases, json, options) { const textures = resources.textures; const promises = []; for (let i = 0; i < textureCanvases.length; i++) { const texture = textures[i]; const canvas = textureCanvases[i]; const isRGBA = isCanvasTransparent(canvas) || !resources.compressableTexture.has(texture); const mimeType = isRGBA ? "image/png" : "image/jpeg"; promises.push( this.getBlob(canvas, mimeType).then((blob) => { const reader = new FileReader(); reader.readAsArrayBuffer(blob); return new Promise((resolve) => { reader.onloadend = () => { resolve(reader); }; }); }).then((reader) => { const buffer = this.getPaddedArrayBuffer(reader.result); GltfExporter.writeBufferView(resources, json, buffer); resources.buffers.push(buffer); const bufferView = resources.bufferViewMap.get(buffer); json.images[i] = { mimeType, bufferView: bufferView[0] }; json.samplers[i] = { minFilter: getFilter(texture.minFilter), magFilter: getFilter(texture.magFilter), wrapS: getWrap(texture.addressU), wrapT: getWrap(texture.addressV) }; json.textures[i] = { sampler: i, source: i }; }) ); } return Promise.all(promises); } getBlob(canvas, mimeType) { if (canvas.toBlob !== void 0) { return new Promise((resolve) => { canvas.toBlob(resolve, mimeType); }); } let quality = 1; if (mimeType === "image/jpeg") { quality = 0.92; } return canvas.convertToBlob({ type: mimeType, quality }); } getPaddedArrayBuffer(arrayBuffer, paddingByte = 0) { const paddedLength = math.roundUp(arrayBuffer.byteLength, 4); if (paddedLength !== arrayBuffer.byteLength) { const array = new Uint8Array(paddedLength); array.set(new Uint8Array(arrayBuffer)); if (paddingByte !== 0) { for (let i = arrayBuffer.byteLength; i < paddedLength; i++) { array[i] = paddingByte; } } return array.buffer; } return arrayBuffer; } buildJson(resources, options) { const promises = this.convertTextures(resources.textures, options); return Promise.all(promises).then(async (textureCanvases) => { const json = { asset: { version: "2.0", generator: "PlayCanvas GltfExporter" }, scenes: [ { nodes: [ 0 ] } ], images: [], samplers: [], textures: [], scene: 0 }; this.writeBufferViews(resources, json); this.writeCameras(resources, json); this.writeMeshes(resources, json, options); this.writeMaterials(resources, json); this.writeNodes(resources, json, options); this.writeSkins(resources, json); await this.writeTextures(resources, textureCanvases, json, options); if (!json.images.length) delete json.images; if (!json.samplers.length) delete json.samplers; if (!json.textures.length) delete json.textures; return json; }); } build(entity, options = {}) { const resources = this.collectResources(entity); return this.buildJson(resources, options).then((json) => { const encoder = new TextEncoder(); const jsonData = encoder.encode(JSON.stringify(json)); const headerLength = 12; const jsonHeaderLength = 8; const jsonDataLength = jsonData.length; const jsonPaddingLength = 4 - (jsonDataLength & 3) & 3; const binaryHeaderLength = 8; const binaryDataLength = json.buffers.reduce( (total, buffer) => math.roundUp(total + buffer.byteLength, 4), 0 ); let totalLength = headerLength + jsonHeaderLength + jsonDataLength + jsonPaddingLength; if (binaryDataLength > 0) { totalLength += binaryHeaderLength + binaryDataLength; } const glbBuffer = new ArrayBuffer(totalLength); const glbView = new DataView(glbBuffer); glbView.setUint32(0, 1179937895, true); glbView.setUint32(4, 2, true); glbView.setUint32(8, totalLength, true); glbView.setUint32(12, jsonDataLength + jsonPaddingLength, true); glbView.setUint32(16, 1313821514, true); let offset = headerLength + jsonHeaderLength; new Uint8Array(glbBuffer, offset, jsonDataLength).set(jsonData); offset += jsonDataLength; for (let i = 0; i < jsonPaddingLength; i++) { glbView.setUint8(offset + i, 32); } offset += jsonPaddingLength; if (binaryDataLength > 0) { glbView.setUint32(offset, binaryDataLength, true); glbView.setUint32(offset + 4, 5130562, true); offset += binaryHeaderLength; resources.buffers.forEach((buffer) => { let src; const bufferViewId = resources.bufferViewMap.get(buffer)[0]; const bufferOffset = json.bufferViews[bufferViewId].byteOffset; if (buffer instanceof ArrayBuffer) { src = new Uint8Array(buffer); } else { const srcBuffer = buffer.lock(); if (srcBuffer instanceof ArrayBuffer) { src = new Uint8Array(srcBuffer); } else { src = new Uint8Array(srcBuffer.buffer, srcBuffer.byteOffset, srcBuffer.byteLength); } } const dst = new Uint8Array(glbBuffer, offset + bufferOffset, src.byteLength); dst.set(src); }); } return Promise.resolve(glbBuffer); }); } } export { GltfExporter };