Reference Source

src/parsers/parseGLTFIntoXKTModel.js

import {utils} from "../XKTModel/lib/utils.js";
import {math} from "../lib/math.js";

import {parse} from '@loaders.gl/core';
import {GLTFLoader} from '@loaders.gl/gltf';
import {
    ClampToEdgeWrapping,
    LinearFilter,
    LinearMipMapLinearFilter,
    LinearMipMapNearestFilter,
    MirroredRepeatWrapping,
    NearestFilter,
    NearestMipMapLinearFilter,
    NearestMipMapNearestFilter,
    RepeatWrapping
} from "../constants.js";

/**
 * @desc Parses glTF into an {@link XKTModel}, supporting ````.glb```` and textures.
 *
 * * Supports ````.glb```` and textures
 * * For a lightweight glTF JSON parser that ignores textures, see {@link parseGLTFJSONIntoXKTModel}.
 *
 * ## Usage
 *
 * In the example below we'll create an {@link XKTModel}, then load a binary glTF model into it.
 *
 * ````javascript
 * utils.loadArraybuffer("../assets/models/gltf/HousePlan/glTF-Binary/HousePlan.glb", async (data) => {
 *
 *     const xktModel = new XKTModel();
 *
 *     parseGLTFIntoXKTModel({
 *          data,
 *          xktModel,
 *          log: (msg) => { console.log(msg); }
 *     }).then(()=>{
 *        xktModel.finalize();
 *     },
 *     (msg) => {
 *         console.error(msg);
 *     });
 * });
 * ````
 *
 * @param {Object} params Parsing parameters.
 * @param {ArrayBuffer} params.data The glTF.
 * @param {String} [params.baseUri] The base URI used to load this glTF, if any. For resolving relative uris to linked resources.
 * @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.includeTextures=true] Whether to parse textures.
 * @param {Boolean} [params.includeNormals=true] 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 non-PBR representation of the glTF.
 * @param {Object} [params.stats] Collects statistics.
 * @param {function} [params.log] Logging callback.
 @returns {Promise} Resolves when glTF has been parsed.
 */
function parseGLTFIntoXKTModel({
                                   data,
                                   baseUri,
                                   xktModel,
                                   metaModelData,
                                   includeTextures = true,
                                   includeNormals = true,
                                   getAttachment,
                                   stats = {},
                                   log
                               }) {

    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.numUVs = 0;
        stats.numTextures = 0;
        stats.numObjects = 0;
        stats.numGeometries = 0;

        parse(data, GLTFLoader, {
            baseUri
        }).then((gltfData) => {

            const ctx = {
                gltfData,
                nodesHaveNames: false, // determined in testIfNodesHaveNames()
                getAttachment: getAttachment || (() => {
                    throw new Error('You must define getAttachment() method to convert glTF with external resources')
                }),
                log: (log || function (msg) {
                }),
                error: function (msg) {
                    console.error(msg);
                },
                xktModel,
                includeNormals: (includeNormals !== false),
                includeTextures: (includeTextures !== false),
                geometryCreated: {},
                nextId: 0,
                geometriesCreated : {},
                stats
            };

            ctx.log("Using parser: parseGLTFIntoXKTModel");
            ctx.log(`Parsing normals: ${ctx.includeNormals ? "enabled" : "disabled"}`);
            ctx.log(`Parsing textures: ${ctx.includeTextures ? "enabled" : "disabled"}`);

            if (ctx.includeTextures) {
                parseTextures(ctx);
            }
            parseMaterials(ctx);
            parseDefaultScene(ctx);

            resolve();

        }, (errMsg) => {
            reject(`[parseGLTFIntoXKTModel] ${errMsg}`);
        });
    });
}

function parseTextures(ctx) {
    const gltfData = ctx.gltfData;
    const textures = gltfData.textures;
    if (textures) {
        for (let i = 0, len = textures.length; i < len; i++) {
            parseTexture(ctx, textures[i]);
            ctx.stats.numTextures++;
        }
    }
}

function parseTexture(ctx, texture) {
    if (!texture.source || !texture.source.image) {
        return;
    }
    const textureId = `texture-${ctx.nextId++}`;

    let minFilter = NearestMipMapLinearFilter;
    switch (texture.sampler.minFilter) {
        case 9728:
            minFilter = NearestFilter;
            break;
        case 9729:
            minFilter = LinearFilter;
            break;
        case 9984:
            minFilter = NearestMipMapNearestFilter;
            break;
        case 9985:
            minFilter = LinearMipMapNearestFilter;
            break;
        case 9986:
            minFilter = NearestMipMapLinearFilter;
            break;
        case 9987:
            minFilter = LinearMipMapLinearFilter;
            break;
    }

    let magFilter = LinearFilter;
    switch (texture.sampler.magFilter) {
        case 9728:
            magFilter = NearestFilter;
            break;
        case 9729:
            magFilter = LinearFilter;
            break;
    }

    let wrapS = RepeatWrapping;
    switch (texture.sampler.wrapS) {
        case 33071:
            wrapS = ClampToEdgeWrapping;
            break;
        case 33648:
            wrapS = MirroredRepeatWrapping;
            break;
        case 10497:
            wrapS = RepeatWrapping;
            break;
    }

    let wrapT = RepeatWrapping;
    switch (texture.sampler.wrapT) {
        case 33071:
            wrapT = ClampToEdgeWrapping;
            break;
        case 33648:
            wrapT = MirroredRepeatWrapping;
            break;
        case 10497:
            wrapT = RepeatWrapping;
            break;
    }

    let wrapR = RepeatWrapping;
    switch (texture.sampler.wrapR) {
        case 33071:
            wrapR = ClampToEdgeWrapping;
            break;
        case 33648:
            wrapR = MirroredRepeatWrapping;
            break;
        case 10497:
            wrapR = RepeatWrapping;
            break;
    }

    ctx.xktModel.createTexture({
        textureId: textureId,
        imageData: texture.source.image,
        mediaType: texture.source.mediaType,
        compressed: true,
        width: texture.source.image.width,
        height: texture.source.image.height,
        minFilter,
        magFilter,
        wrapS,
        wrapT,
        wrapR,
        flipY: !!texture.flipY,
        //     encoding: "sRGB"
    });
    texture._textureId = textureId;
}

function parseMaterials(ctx) {
    const gltfData = ctx.gltfData;
    const materials = gltfData.materials;
    if (materials) {
        for (let i = 0, len = materials.length; i < len; i++) {
            const material = materials[i];
            material._textureSetId = ctx.includeTextures ? parseTextureSet(ctx, material) : null;
            material._attributes = parseMaterialAttributes(ctx, material);
        }
    }
}

function parseTextureSet(ctx, material) {
    const textureSetCfg = {};
    if (material.normalTexture) {
        textureSetCfg.normalTextureId = material.normalTexture.texture._textureId;
    }
    if (material.occlusionTexture) {
        textureSetCfg.occlusionTextureId = material.occlusionTexture.texture._textureId;
    }
    if (material.emissiveTexture) {
        textureSetCfg.emissiveTextureId = material.emissiveTexture.texture._textureId;
    }
    const metallicPBR = material.pbrMetallicRoughness;
    if (material.pbrMetallicRoughness) {
        const pbrMetallicRoughness = material.pbrMetallicRoughness;
        const baseColorTexture = pbrMetallicRoughness.baseColorTexture || pbrMetallicRoughness.colorTexture;
        if (baseColorTexture) {
            if (baseColorTexture.texture) {
                textureSetCfg.colorTextureId = baseColorTexture.texture._textureId;
            } else {
                textureSetCfg.colorTextureId = ctx.gltfData.textures[baseColorTexture.index]._textureId;
            }
        }
        if (metallicPBR.metallicRoughnessTexture) {
            textureSetCfg.metallicRoughnessTextureId = metallicPBR.metallicRoughnessTexture.texture._textureId;
        }
    }
    const extensions = material.extensions;
    if (extensions) {
        const specularPBR = extensions["KHR_materials_pbrSpecularGlossiness"];
        if (specularPBR) {
            const specularTexture = specularPBR.specularTexture;
            if (specularTexture !== null && specularTexture !== undefined) {
                //  textureSetCfg.colorTextureId = ctx.gltfData.textures[specularColorTexture.index]._textureId;
            }
            const specularColorTexture = specularPBR.specularColorTexture;
            if (specularColorTexture !== null && specularColorTexture !== undefined) {
                textureSetCfg.colorTextureId = ctx.gltfData.textures[specularColorTexture.index]._textureId;
            }
        }
    }
    if (textureSetCfg.normalTextureId !== undefined ||
        textureSetCfg.occlusionTextureId !== undefined ||
        textureSetCfg.emissiveTextureId !== undefined ||
        textureSetCfg.colorTextureId !== undefined ||
        textureSetCfg.metallicRoughnessTextureId !== undefined) {
        textureSetCfg.textureSetId = `textureSet-${ctx.nextId++};`
        ctx.xktModel.createTextureSet(textureSetCfg);
        ctx.stats.numTextureSets++;
        return textureSetCfg.textureSetId;
    }
    return null;
}

function parseMaterialAttributes(ctx, material) { // Substitute RGBA for material, to use fast flat shading instead
    const extensions = material.extensions;
    const materialAttributes = {
        color: new Float32Array([1, 1, 1, 1]),
        opacity: 1,
        metallic: 0,
        roughness: 1
    };
    if (extensions) {
        const specularPBR = extensions["KHR_materials_pbrSpecularGlossiness"];
        if (specularPBR) {
            const diffuseFactor = specularPBR.diffuseFactor;
            if (diffuseFactor !== null && diffuseFactor !== undefined) {
                materialAttributes.color.set(diffuseFactor);
            }
        }
        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)) {
                    materialAttributes.color.set(diffuse);
                }
            }
            const transparency = values.transparency;
            if (transparency !== null && transparency !== undefined) {
                materialAttributes.opacity = transparency;
            }
            const transparent = values.transparent;
            if (transparent !== null && transparent !== undefined) {
                materialAttributes.opacity = transparent;
            }
        }
    }
    const metallicPBR = material.pbrMetallicRoughness;
    if (metallicPBR) {
        const baseColorFactor = metallicPBR.baseColorFactor;
        if (baseColorFactor) {
            materialAttributes.color[0] = baseColorFactor[0];
            materialAttributes.color[1] = baseColorFactor[1];
            materialAttributes.color[2] = baseColorFactor[2];
            materialAttributes.opacity = baseColorFactor[3];
        }
        const metallicFactor = metallicPBR.metallicFactor;
        if (metallicFactor !== null && metallicFactor !== undefined) {
            materialAttributes.metallic = metallicFactor;
        }
        const roughnessFactor = metallicPBR.roughnessFactor;
        if (roughnessFactor !== null && roughnessFactor !== undefined) {
            materialAttributes.roughness = roughnessFactor;
        }
    }
    return materialAttributes;
}

function parseDefaultScene(ctx) {
    const gltfData = ctx.gltfData;
    const scene = gltfData.scene || gltfData.scenes[0];
    if (!scene) {
        ctx.error("glTF has no default scene");
        return;
    }
    parseScene(ctx, scene);
}

function parseScene(ctx, scene) {
    const nodes = scene.nodes;
    if (!nodes) {
        return;
    }
    for (let i = 0, len = nodes.length; i < len; i++) {
        const node = nodes[i];
        countMeshUsage(ctx, node);
    }
    for (let i = 0, len = nodes.length; i < len && !ctx.nodesHaveNames; i++) {
        const node = nodes[i];
        if (testIfNodesHaveNames(node)) {
            ctx.nodesHaveNames = true;
        }
    }
    if (!ctx.nodesHaveNames) {
        ctx.log(`Warning: No "name" attributes found on glTF scene nodes - objects in XKT may not be what you expect`);
        for (let i = 0, len = nodes.length; i < len; i++) {
            const node = nodes[i];
            parseNodesWithoutNames(ctx, node, 0, null);
        }
    } else {
        for (let i = 0, len = nodes.length; i < len; i++) {
            const node = nodes[i];
            parseNodesWithNames(ctx, node, 0, null);
        }
    }
}

function countMeshUsage(ctx, node, level = 0) {
    if (!node) {
        return;
    }
    const mesh = node.mesh;
    if (mesh) {
        mesh.instances = mesh.instances ? mesh.instances + 1 : 1;
    }
    if (node.children) {
        const children = node.children;
        for (let i = 0, len = children.length; i < len; i++) {
            const childNode = children[i];
            if (!childNode) {
                ctx.error("Node not found: " + i);
                continue;
            }
            countMeshUsage(ctx, childNode, level + 1);
        }
    }
}

function testIfNodesHaveNames(node, level = 0) {
    if (!node) {
        return;
    }
    if (node.name) {
        return true;
    }
    if (node.children) {
        const children = node.children;
        for (let i = 0, len = children.length; i < len; i++) {
            const childNode = children[i];
            if (testIfNodesHaveNames(childNode, level + 1)) {
                return true;
            }
        }
    }
    return false;
}

/**
 * Parses a glTF node hierarchy that is known to NOT contain "name" attributes on the nodes.
 * Create a XKTMesh for each mesh primitive, and a single XKTEntity.
 */
const parseNodesWithoutNames = (function () {

    const meshIds = [];

    return function (ctx, node, depth, matrix) {
        if (!node) {
            return;
        }
        matrix = parseNodeMatrix(node, matrix);
        if (node.mesh) {
            parseNodeMesh(node, ctx, matrix, meshIds);
        }
        if (node.children) {
            const children = node.children;
            for (let i = 0, len = children.length; i < len; i++) {
                const childNode = children[i];
                parseNodesWithoutNames(ctx, childNode, depth + 1, matrix);
            }
        }
        if (depth === 0) {
            let entityId = "entity-" + ctx.nextId++;
            if (meshIds && meshIds.length > 0) {
                ctx.log("Creating XKTEntity with default ID: " + entityId);
                ctx.xktModel.createEntity({
                    entityId,
                    meshIds
                });
                meshIds.length = 0;
            }
            ctx.stats.numObjects++;
        }
    }
})();


/**
 * Parses a glTF node hierarchy that is known to contain "name" attributes on the nodes.
 *
 * Create a XKTMesh for each mesh primitive, and XKTEntity for each named node.
 *
 * Following a depth-first traversal, each XKTEntity is created on post-visit of each named node,
 * and gets all the XKTMeshes created since the last XKTEntity created.
 */
const parseNodesWithNames = (function () {

    const objectIdStack = [];
    const meshIdsStack = [];
    let meshIds = null;

    return function (ctx, node, depth, matrix) {
        if (!node) {
            return;
        }
        matrix = parseNodeMatrix(node, matrix);
        if (node.name) {
            meshIds = [];
            let xktEntityId = node.name;
            if (!!xktEntityId && ctx.xktModel.entities[xktEntityId]) {
                ctx.log(`Warning: Two or more glTF nodes found with same 'name' attribute: '${xktEntityId} - will randomly-generating an object ID in XKT`);
            }
            while (!xktEntityId || ctx.xktModel.entities[xktEntityId]) {
                xktEntityId = "entity-" + ctx.nextId++;
            }
            objectIdStack.push(xktEntityId);
            meshIdsStack.push(meshIds);
        }
        if (meshIds && node.mesh) {
            parseNodeMesh(node, ctx, matrix, meshIds);
        }
        if (node.children) {
            const children = node.children;
            for (let i = 0, len = children.length; i < len; i++) {
                const childNode = children[i];
                parseNodesWithNames(ctx, childNode, depth + 1, matrix);
            }
        }
        const nodeName = node.name;
        if ((nodeName !== undefined && nodeName !== null) || depth === 0) {
            let xktEntityId = objectIdStack.pop();
            if (!xktEntityId) { // For when there are no nodes with names
                xktEntityId = "entity-" + ctx.nextId++;
            }
            let entityMeshIds = meshIdsStack.pop();
            if (meshIds && meshIds.length > 0) {
                ctx.xktModel.createEntity({
                    entityId: xktEntityId,
                    meshIds: entityMeshIds
                });
            }
            ctx.stats.numObjects++;
            meshIds = meshIdsStack.length > 0 ? meshIdsStack[meshIdsStack.length - 1] : null;
        }
    }
})();

/**
 * Parses transform at the given glTF node.
 *
 * @param node the glTF node
 * @param matrix Transfor matrix from parent nodes
 * @returns {*} Transform matrix for the node
 */
function parseNodeMatrix(node, matrix) {
    if (!node) {
        return;
    }
    let localMatrix;
    if (node.matrix) {
        localMatrix = node.matrix;
        if (matrix) {
            matrix = math.mulMat4(matrix, localMatrix, math.mat4());
        } else {
            matrix = localMatrix;
        }
    }
    if (node.translation) {
        localMatrix = math.translationMat4v(node.translation);
        if (matrix) {
            matrix = math.mulMat4(matrix, localMatrix, math.mat4());
        } else {
            matrix = localMatrix;
        }
    }
    if (node.rotation) {
        localMatrix = math.quaternionToMat4(node.rotation);
        if (matrix) {
            matrix = math.mulMat4(matrix, localMatrix, math.mat4());
        } else {
            matrix = localMatrix;
        }
    }
    if (node.scale) {
        localMatrix = math.scalingMat4v(node.scale);
        if (matrix) {
            matrix = math.mulMat4(matrix, localMatrix, math.mat4());
        } else {
            matrix = localMatrix;
        }
    }
    return matrix;
}

function createPrimitiveHash(primitive) {
    const hash = [];
    const attributes = primitive.attributes;
    if (attributes) {
        for (let key in attributes) {
            hash.push(attributes[key].id);
        }
    }
    if (primitive.indices) {
        hash.push(primitive.indices.id);
    }
    return hash.join(".");
}

/**
 * Parses primitives referenced by the mesh belonging to the given node, creating XKTMeshes in the XKTModel.
 *
 * @param node glTF node
 * @param ctx Parsing context
 * @param matrix Matrix for the XKTMeshes
 * @param meshIds returns IDs of the new XKTMeshes
 */
function parseNodeMesh(node, ctx, matrix, meshIds) {
    if (!node) {
        return;
    }
    const mesh = node.mesh;
    if (!mesh) {
        return;
    }
    const numPrimitives = mesh.primitives.length;
    if (numPrimitives > 0) {
        for (let i = 0; i < numPrimitives; i++) {
            try {
                const primitive = mesh.primitives[i];
                const geometryId = createPrimitiveHash(primitive);
                if (!ctx.geometriesCreated[geometryId]) {
                    const geometryCfg = {
                        geometryId
                    };
                    switch (primitive.mode) {
                        case 0: // POINTS
                            geometryCfg.primitiveType = "points";
                            break;
                        case 1: // LINES
                            geometryCfg.primitiveType = "lines";
                            break;
                        case 2: // LINE_LOOP
                            geometryCfg.primitiveType = "line-loop";
                            break;
                        case 3: // LINE_STRIP
                            geometryCfg.primitiveType = "line-strip";
                            break;
                        case 4: // TRIANGLES
                            geometryCfg.primitiveType = "triangles";
                            break;
                        case 5: // TRIANGLE_STRIP
                            geometryCfg.primitiveType = "triangle-strip";
                            break;
                        case 6: // TRIANGLE_FAN
                            geometryCfg.primitiveType = "triangle-fan";
                            break;
                        default:
                            geometryCfg.primitiveType = "triangles";
                    }
                    const POSITION = primitive.attributes.POSITION;
                    if (!POSITION) {
                        continue;
                    }
                    geometryCfg.positions = primitive.attributes.POSITION.value;
                    ctx.stats.numVertices += geometryCfg.positions.length / 3;
                    if (ctx.includeNormals) {
                        if (primitive.attributes.NORMAL) {
                            geometryCfg.normals = primitive.attributes.NORMAL.value;
                            ctx.stats.numNormals += geometryCfg.normals.length / 3;
                        }
                    }
                    if (primitive.attributes.COLOR_0) {
                        geometryCfg.colorsCompressed = primitive.attributes.COLOR_0.value;
                    }
                    if (ctx.includeTextures) {
                        if (primitive.attributes.TEXCOORD_0) {
                            geometryCfg.uvs = primitive.attributes.TEXCOORD_0.value;
                            ctx.stats.numUVs += geometryCfg.uvs.length / 2;
                        }
                    }
                    if (primitive.indices) {
                        geometryCfg.indices = primitive.indices.value;
                        if (primitive.mode === 4) {
                            ctx.stats.numTriangles += geometryCfg.indices.length / 3;
                        }
                    }
                    ctx.xktModel.createGeometry(geometryCfg);
                    ctx.geometriesCreated[geometryId] = true;
                    ctx.stats.numGeometries++;
                }
                const xktMeshId = ctx.nextId++;
                const meshCfg = {
                    meshId: xktMeshId,
                    geometryId,
                    matrix: matrix ? matrix.slice() : math.identityMat4()
                };
                const material = primitive.material;
                if (material) {
                    meshCfg.textureSetId = material._textureSetId;
                    meshCfg.color = material._attributes.color;
                    meshCfg.opacity = material._attributes.opacity;
                    meshCfg.metallic = material._attributes.metallic;
                    meshCfg.roughness = material._attributes.roughness;
                } else {
                    meshCfg.color = [1.0, 1.0, 1.0];
                    meshCfg.opacity = 1.0;
                }
                ctx.xktModel.createMesh(meshCfg);
                meshIds.push(xktMeshId);
            } catch (e) {
                console.log(e);
            }
        }
    }
}

export {parseGLTFIntoXKTModel};