UNPKG

@xeokit/xeokit-convert

Version:

JavaScript utilities to create .XKT files

662 lines (593 loc) 25 kB
import {utils} from "../XKTModel/lib/utils.js"; import {math} from "../lib/math.js"; const atob2 = (typeof atob !== 'undefined') ? atob : a => Buffer.from(a, 'base64').toString('binary'); const WEBGL_COMPONENT_TYPES = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array }; const WEBGL_TYPE_SIZES = { 'SCALAR': 1, 'VEC2': 2, 'VEC3': 3, 'VEC4': 4, 'MAT2': 4, 'MAT3': 9, 'MAT4': 16 }; /** * @desc Parses glTF JSON into an {@link XKTModel}, without ````.glb```` and textures. * * * Lightweight JSON-based glTF parser which ignores textures * * For texture and ````.glb```` support, see {@link parseGLTFIntoXKTModel} * * ## Usage * * In the example below we'll create an {@link XKTModel}, then load a glTF model into it. * * ````javascript * utils.loadJSON("./models/gltf/duplex/scene.gltf", async (data) => { * * const xktModel = new XKTModel(); * * parseGLTFJSONIntoXKTModel({ * data, * xktModel, * log: (msg) => { console.log(msg); } * }).then(()=>{ * xktModel.finalize(); * }, * (msg) => { * console.error(msg); * }); * }); * ```` * * @param {Object} params Parsing parameters. * @param {Object} params.data The glTF JSON. * @param {Object} [params.metaModelData] Metamodel JSON. If this is provided, then parsing is able to ensure that the XKTObjects it creates will fit the metadata properly. * @param {XKTModel} params.xktModel XKTModel to parse into. * @param {Boolean} [params.includeNormals=false] Whether to parse normals. When false, the parser will ignore the glTF * geometry normals, and the glTF data will rely on the xeokit ````Viewer```` to automatically generate them. This has * the limitation that the normals will be face-aligned, and therefore the ````Viewer```` will only be able to render * a flat-shaded representation of the glTF. * @param {Boolean} [params.reuseGeometries=true] When true, the parser will enable geometry reuse within the XKTModel. When false, * will automatically "expand" all reused geometries into duplicate copies. This has the drawback of increasing the XKT * file size (~10-30% for typical models), but can make the model more responsive in the xeokit Viewer, especially if the model * has excessive geometry reuse. An example of excessive geometry reuse would be if we have 4000 geometries that are * shared amongst 2000 objects, ie. a large number of geometries with a low amount of reuse, which can present a * pathological performance case for xeokit's underlying graphics APIs (WebGL, WebGPU etc). * @param {function} [params.getAttachment] Callback through which to fetch attachments, if the glTF has them. * @param {Object} [params.stats] Collects statistics. * @param {function} [params.log] Logging callback. * @returns {Promise} */ function parseGLTFJSONIntoXKTModel({ data, xktModel, metaModelData, includeNormals, reuseGeometries, getAttachment, stats = {}, log }) { if (log) { log("Using parser: parseGLTFJSONIntoXKTModel"); } return new Promise(function (resolve, reject) { if (!data) { reject("Argument expected: data"); return; } if (!xktModel) { reject("Argument expected: xktModel"); return; } stats.sourceFormat = "glTF"; stats.schemaVersion = "2.0"; stats.title = ""; stats.author = ""; stats.created = ""; stats.numTriangles = 0; stats.numVertices = 0; stats.numNormals = 0; stats.numObjects = 0; stats.numGeometries = 0; const ctx = { gltf: data, metaModelCorrections: metaModelData ? getMetaModelCorrections(metaModelData) : null, getAttachment: getAttachment || (() => { throw new Error('You must define getAttachment() method to convert glTF with external resources') }), log: (log || function (msg) { }), xktModel, includeNormals, createXKTGeometryIds: {}, nextMeshId: 0, reuseGeometries: (reuseGeometries !== false), stats }; ctx.log(`Parsing normals: ${ctx.includeNormals ? "enabled" : "disabled"}`); parseBuffers(ctx).then(() => { parseBufferViews(ctx); freeBuffers(ctx); parseMaterials(ctx); parseDefaultScene(ctx); resolve(); }, (errMsg) => { reject(errMsg); }); }); } function getMetaModelCorrections(metaModelData) { const eachRootStats = {}; const eachChildRoot = {}; const metaObjects = metaModelData.metaObjects || []; const metaObjectsMap = {}; for (let i = 0, len = metaObjects.length; i < len; i++) { const metaObject = metaObjects[i]; metaObjectsMap[metaObject.id] = metaObject; } for (let i = 0, len = metaObjects.length; i < len; i++) { const metaObject = metaObjects[i]; if (metaObject.parent !== undefined && metaObject.parent !== null) { const metaObjectParent = metaObjectsMap[metaObject.parent]; if (metaObject.type === metaObjectParent.type) { let rootMetaObject = metaObjectParent; while (rootMetaObject.parent && metaObjectsMap[rootMetaObject.parent].type === rootMetaObject.type) { rootMetaObject = metaObjectsMap[rootMetaObject.parent]; } const rootStats = eachRootStats[rootMetaObject.id] || (eachRootStats[rootMetaObject.id] = { numChildren: 0, countChildren: 0 }); rootStats.numChildren++; eachChildRoot[metaObject.id] = rootMetaObject; } else { } } } const metaModelCorrections = { metaObjectsMap, eachRootStats, eachChildRoot }; return metaModelCorrections; } function parseBuffers(ctx) { // Parses geometry buffers into temporary "_buffer" Unit8Array properties on the glTF "buffer" elements const buffers = ctx.gltf.buffers; if (buffers) { return Promise.all(buffers.map(buffer => parseBuffer(ctx, buffer))); } else { return new Promise(function (resolve, reject) { resolve(); }); } } function parseBuffer(ctx, bufferInfo) { return new Promise(function (resolve, reject) { // Allow a shortcut where the glTF buffer is "enrichened" with direct // access to the data-arrayBuffer, w/out needing to either: // - read the file indicated by the ".uri" component of the buffer // - base64-decode the encoded data in the ".uri" component if (bufferInfo._arrayBuffer) { bufferInfo._buffer = bufferInfo._arrayBuffer; resolve(bufferInfo); return; } // Otherwise, proceed with "standard-glTF" .uri component. const uri = bufferInfo.uri; if (!uri) { reject('gltf/handleBuffer missing uri in ' + JSON.stringify(bufferInfo)); return; } parseArrayBuffer(ctx, uri).then((arrayBuffer) => { bufferInfo._buffer = arrayBuffer; resolve(arrayBuffer); }, (errMsg) => { reject(errMsg); }) }); } function parseArrayBuffer(ctx, uri) { return new Promise(function (resolve, reject) { const dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/; // Check for data: URI const dataUriRegexResult = uri.match(dataUriRegex); if (dataUriRegexResult) { // Safari can't handle data URIs through XMLHttpRequest const isBase64 = !!dataUriRegexResult[2]; let data = dataUriRegexResult[3]; data = decodeURIComponent(data); if (isBase64) { data = atob2(data); } const buffer = new ArrayBuffer(data.length); const view = new Uint8Array(buffer); for (let i = 0; i < data.length; i++) { view[i] = data.charCodeAt(i); } resolve(buffer); } else { // Uri is a path to a file ctx.getAttachment(uri).then( (arrayBuffer) => { resolve(arrayBuffer); }, (errMsg) => { reject(errMsg); }); } }); } function parseBufferViews(ctx) { // Parses our temporary "_buffer" properties into "_buffer" properties on glTF "bufferView" elements const bufferViewsInfo = ctx.gltf.bufferViews; if (bufferViewsInfo) { for (let i = 0, len = bufferViewsInfo.length; i < len; i++) { parseBufferView(ctx, bufferViewsInfo[i]); } } } function parseBufferView(ctx, bufferViewInfo) { const buffer = ctx.gltf.buffers[bufferViewInfo.buffer]; bufferViewInfo._typedArray = null; const byteLength = bufferViewInfo.byteLength || 0; const byteOffset = bufferViewInfo.byteOffset || 0; bufferViewInfo._buffer = buffer._buffer.slice(byteOffset, byteOffset + byteLength); } function freeBuffers(ctx) { // Deletes the "_buffer" properties from the glTF "buffer" elements, to save memory const buffers = ctx.gltf.buffers; if (buffers) { for (let i = 0, len = buffers.length; i < len; i++) { buffers[i]._buffer = null; } } } function parseMaterials(ctx) { const materialsInfo = ctx.gltf.materials; if (materialsInfo) { for (let i = 0, len = materialsInfo.length; i < len; i++) { const materialInfo = materialsInfo[i]; const material = parseMaterial(ctx, materialInfo); materialInfo._materialData = material; } } } function parseMaterial(ctx, materialInfo) { // Attempts to extract an RGBA color for a glTF material const material = { color: new Float32Array([1, 1, 1]), opacity: 1.0, metallic: 0, roughness: 1 }; const extensions = materialInfo.extensions; if (extensions) { const specularPBR = extensions["KHR_materials_pbrSpecularGlossiness"]; if (specularPBR) { const diffuseFactor = specularPBR.diffuseFactor; if (diffuseFactor !== null && diffuseFactor !== undefined) { material.color[0] = diffuseFactor[0]; material.color[1] = diffuseFactor[1]; material.color[2] = diffuseFactor[2]; } } const common = extensions["KHR_materials_common"]; if (common) { const technique = common.technique; const values = common.values || {}; const blinn = technique === "BLINN"; const phong = technique === "PHONG"; const lambert = technique === "LAMBERT"; const diffuse = values.diffuse; if (diffuse && (blinn || phong || lambert)) { if (!utils.isString(diffuse)) { material.color[0] = diffuse[0]; material.color[1] = diffuse[1]; material.color[2] = diffuse[2]; } } const transparency = values.transparency; if (transparency !== null && transparency !== undefined) { material.opacity = transparency; } const transparent = values.transparent; if (transparent !== null && transparent !== undefined) { material.opacity = transparent; } } } const metallicPBR = materialInfo.pbrMetallicRoughness; if (metallicPBR) { const baseColorFactor = metallicPBR.baseColorFactor; if (baseColorFactor) { material.color[0] = baseColorFactor[0]; material.color[1] = baseColorFactor[1]; material.color[2] = baseColorFactor[2]; material.opacity = baseColorFactor[3]; } const metallicFactor = metallicPBR.metallicFactor; if (metallicFactor !== null && metallicFactor !== undefined) { material.metallic = metallicFactor; } const roughnessFactor = metallicPBR.roughnessFactor; if (roughnessFactor !== null && roughnessFactor !== undefined) { material.roughness = roughnessFactor; } } return material; } function parseDefaultScene(ctx) { const scene = ctx.gltf.scene || 0; const defaultSceneInfo = ctx.gltf.scenes[scene]; if (!defaultSceneInfo) { throw new Error("glTF has no default scene"); } parseScene(ctx, defaultSceneInfo); } function parseScene(ctx, sceneInfo) { const nodes = sceneInfo.nodes; if (!nodes) { return; } for (let i = 0, len = nodes.length; i < len; i++) { const glTFNode = ctx.gltf.nodes[nodes[i]]; if (glTFNode) { parseNode(ctx, glTFNode, 0, null); } } } let deferredMeshIds = []; function parseNode(ctx, glTFNode, depth, matrix) { const gltf = ctx.gltf; const xktModel = ctx.xktModel; let localMatrix; if (glTFNode.matrix) { localMatrix = glTFNode.matrix; if (matrix) { matrix = math.mulMat4(matrix, localMatrix, math.mat4()); } else { matrix = localMatrix; } } if (glTFNode.translation) { localMatrix = math.translationMat4v(glTFNode.translation); if (matrix) { matrix = math.mulMat4(matrix, localMatrix, localMatrix); } else { matrix = localMatrix; } } if (glTFNode.rotation) { localMatrix = math.quaternionToMat4(glTFNode.rotation); if (matrix) { matrix = math.mulMat4(matrix, localMatrix, localMatrix); } else { matrix = localMatrix; } } if (glTFNode.scale) { localMatrix = math.scalingMat4v(glTFNode.scale); if (matrix) { matrix = math.mulMat4(matrix, localMatrix, localMatrix); } else { matrix = localMatrix; } } const gltfMeshId = glTFNode.mesh; if (gltfMeshId !== undefined) { const meshInfo = gltf.meshes[gltfMeshId]; if (meshInfo) { const numPrimitivesInMesh = meshInfo.primitives.length; if (numPrimitivesInMesh > 0) { for (let i = 0; i < numPrimitivesInMesh; i++) { const primitiveInfo = meshInfo.primitives[i]; const geometryHash = createPrimitiveGeometryHash(primitiveInfo); let xktGeometryId = ctx.createXKTGeometryIds[geometryHash]; if ((!ctx.reuseGeometries) || !xktGeometryId) { xktGeometryId = "geometry-" + ctx.nextMeshId++ const geometryArrays = {}; parsePrimitiveGeometry(ctx, primitiveInfo, geometryArrays); const colors = geometryArrays.colors; let colorsCompressed; if (geometryArrays.colors) { colorsCompressed = []; for (let j = 0, lenj = colors.length; j < lenj; j += 4) { colorsCompressed.push(colors[j + 0]); colorsCompressed.push(colors[j + 1]); colorsCompressed.push(colors[j + 2]); colorsCompressed.push(255); } } xktModel.createGeometry({ geometryId: xktGeometryId, primitiveType: geometryArrays.primitive, positions: geometryArrays.positions, normals: ctx.includeNormals ? geometryArrays.normals : null, colorsCompressed: colorsCompressed, indices: geometryArrays.indices }); ctx.stats.numGeometries++; ctx.stats.numVertices += geometryArrays.positions ? geometryArrays.positions.length / 3 : 0; ctx.stats.numNormals += (ctx.includeNormals && geometryArrays.normals) ? geometryArrays.normals.length / 3 : 0; ctx.stats.numTriangles += geometryArrays.indices ? geometryArrays.indices.length / 3 : 0; ctx.createXKTGeometryIds[geometryHash] = xktGeometryId; } else { // Geometry reused } const materialIndex = primitiveInfo.material; const materialInfo = (materialIndex !== null && materialIndex !== undefined) ? gltf.materials[materialIndex] : null; const color = materialInfo ? materialInfo._materialData.color : new Float32Array([1.0, 1.0, 1.0, 1.0]); const opacity = materialInfo ? materialInfo._materialData.opacity : 1.0; const metallic = materialInfo ? materialInfo._materialData.metallic : 0.0; const roughness = materialInfo ? materialInfo._materialData.roughness : 1.0; const xktMeshId = "mesh-" + ctx.nextMeshId++; xktModel.createMesh({ meshId: xktMeshId, geometryId: xktGeometryId, matrix: matrix ? matrix.slice() : math.identityMat4(), color: color, opacity: opacity, metallic: metallic, roughness: roughness }); deferredMeshIds.push(xktMeshId); } } } } if (glTFNode.children) { const children = glTFNode.children; for (let i = 0, len = children.length; i < len; i++) { const childNodeIdx = children[i]; const childGLTFNode = gltf.nodes[childNodeIdx]; if (!childGLTFNode) { console.warn('Node not found: ' + i); continue; } parseNode(ctx, childGLTFNode, depth + 1, matrix); } } // Post-order visit scene node const nodeName = glTFNode.name; if (((nodeName !== undefined && nodeName !== null) || depth === 0) && deferredMeshIds.length > 0) { if (nodeName === undefined || nodeName === null) { ctx.log(`[parseGLTFJSONIntoXKTModel] Warning: 'name' properties not found on glTF scene nodes - will randomly-generate object IDs in XKT`); } let xktEntityId = nodeName; // Fall back on generated ID when `name` not found on glTF scene node(s) if (xktEntityId === undefined || xktEntityId === null) { if (xktModel.entities[xktEntityId]) { ctx.error("Two or more glTF nodes found with same 'name' attribute: '" + nodeName + "'"); } while (!xktEntityId || xktModel.entities[xktEntityId]) { xktEntityId = "entity-" + ctx.nextId++; } } if (ctx.metaModelCorrections) { // Merging meshes into XKTObjects that map to metaobjects const rootMetaObject = ctx.metaModelCorrections.eachChildRoot[xktEntityId]; if (rootMetaObject) { const rootMetaObjectStats = ctx.metaModelCorrections.eachRootStats[rootMetaObject.id]; rootMetaObjectStats.countChildren++; if (rootMetaObjectStats.countChildren >= rootMetaObjectStats.numChildren) { xktModel.createEntity({ entityId: rootMetaObject.id, meshIds: deferredMeshIds }); ctx.stats.numObjects++; deferredMeshIds = []; } } else { const metaObject = ctx.metaModelCorrections.metaObjectsMap[xktEntityId]; if (metaObject) { xktModel.createEntity({ entityId: xktEntityId, meshIds: deferredMeshIds }); ctx.stats.numObjects++; deferredMeshIds = []; } } } else { // Create an XKTObject from the meshes at each named glTF node, don't care about metaobjects xktModel.createEntity({ entityId: xktEntityId, meshIds: deferredMeshIds }); ctx.stats.numObjects++; deferredMeshIds = []; } } } function createPrimitiveGeometryHash(primitiveInfo) { const attributes = primitiveInfo.attributes; if (!attributes) { return "empty"; } const mode = primitiveInfo.mode; const material = primitiveInfo.material; const indices = primitiveInfo.indices; const positions = primitiveInfo.attributes.POSITION; const normals = primitiveInfo.attributes.NORMAL; const colors = primitiveInfo.attributes.COLOR_0; const uv = primitiveInfo.attributes.TEXCOORD_0; return [ mode, // material, (indices !== null && indices !== undefined) ? indices : "-", (positions !== null && positions !== undefined) ? positions : "-", (normals !== null && normals !== undefined) ? normals : "-", (colors !== null && colors !== undefined) ? colors : "-", (uv !== null && uv !== undefined) ? uv : "-" ].join(";"); } function parsePrimitiveGeometry(ctx, primitiveInfo, geometryArrays) { const attributes = primitiveInfo.attributes; if (!attributes) { return; } switch (primitiveInfo.mode) { case 0: // POINTS geometryArrays.primitive = "points"; break; case 1: // LINES geometryArrays.primitive = "lines"; break; case 2: // LINE_LOOP // TODO: convert geometryArrays.primitive = "lines"; break; case 3: // LINE_STRIP // TODO: convert geometryArrays.primitive = "lines"; break; case 4: // TRIANGLES geometryArrays.primitive = "triangles"; break; case 5: // TRIANGLE_STRIP // TODO: convert console.log("TRIANGLE_STRIP"); geometryArrays.primitive = "triangles"; break; case 6: // TRIANGLE_FAN // TODO: convert console.log("TRIANGLE_FAN"); geometryArrays.primitive = "triangles"; break; default: geometryArrays.primitive = "triangles"; } const accessors = ctx.gltf.accessors; const indicesIndex = primitiveInfo.indices; if (indicesIndex !== null && indicesIndex !== undefined) { const accessorInfo = accessors[indicesIndex]; geometryArrays.indices = parseAccessorTypedArray(ctx, accessorInfo); } const positionsIndex = attributes.POSITION; if (positionsIndex !== null && positionsIndex !== undefined) { const accessorInfo = accessors[positionsIndex]; geometryArrays.positions = parseAccessorTypedArray(ctx, accessorInfo); } const normalsIndex = attributes.NORMAL; if (normalsIndex !== null && normalsIndex !== undefined) { const accessorInfo = accessors[normalsIndex]; geometryArrays.normals = parseAccessorTypedArray(ctx, accessorInfo); } const colorsIndex = attributes.COLOR_0; if (colorsIndex !== null && colorsIndex !== undefined) { const accessorInfo = accessors[colorsIndex]; geometryArrays.colors = parseAccessorTypedArray(ctx, accessorInfo); } } function parseAccessorTypedArray(ctx, accessorInfo) { const bufferView = ctx.gltf.bufferViews[accessorInfo.bufferView]; const itemSize = WEBGL_TYPE_SIZES[accessorInfo.type]; const TypedArray = WEBGL_COMPONENT_TYPES[accessorInfo.componentType]; const elementBytes = TypedArray.BYTES_PER_ELEMENT; // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. const itemBytes = elementBytes * itemSize; if (accessorInfo.byteStride && accessorInfo.byteStride !== itemBytes) { // The buffer is not interleaved if the stride is the item size in bytes. throw new Error("interleaved buffer!"); // TODO } else { return new TypedArray(bufferView._buffer, accessorInfo.byteOffset || 0, accessorInfo.count * itemSize); } } export {parseGLTFJSONIntoXKTModel};