src/parsers/parseGLTFJSONIntoXKTModel.js
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};