Reference Source

src/plugins/GLTFLoaderPlugin/GLTFPerformanceModelLoader.js

import {math} from "../../viewer/scene/math/math.js";
import {utils} from "../../viewer/scene/utils.js";
import {core} from "../../viewer/scene/core.js";
import {buildEdgeIndices} from '../../viewer/scene/math/buildEdgeIndices.js';
import {worldToRTCPositions} from "../../viewer/scene/math/rtcCoords";

/**
 * @private
 */
class GLTFPerformanceModelLoader {

    constructor(cfg) { // TODO: Loading options fallbacks on loader, eg. handleGLTFNode etc
        cfg = cfg || {};
    }

    load(plugin, performanceModel, src, options, ok, error) {
        options = options || {};
        loadGLTF(plugin, performanceModel, src, options, function () {
                core.scheduleTask(function () {
                    performanceModel.scene.fire("modelLoaded", performanceModel.id); // FIXME: Assumes listeners know order of these two events
                    performanceModel.fire("loaded", true, false);
                });
                if (ok) {
                    ok();
                }
            },
            function (msg) {
                plugin.error(msg);
                if (error) {
                    error(msg);
                }
                performanceModel.fire("error", msg);
            });
    }

    parse(plugin, performanceModel, gltf, options, ok, error) {
        options = options || {};
        parseGLTF(plugin, gltf, "", options, performanceModel, function () {
                performanceModel.scene.fire("modelLoaded", performanceModel.id); // FIXME: Assumes listeners know order of these two events
                performanceModel.fire("loaded", true, false);
                if (ok) {
                    ok();
                }
            },
            function (msg) {
                performanceModel.error(msg);
                performanceModel.fire("error", msg);
                if (error) {
                    error(msg);
                }
            });
    }
}

const loadGLTF = (function () {

    return function (plugin, performanceModel, src, options, ok, error) {
        const spinner = plugin.viewer.scene.canvas.spinner;
        spinner.processes++;
        plugin.dataSource.getGLTF(src, function (json) { // OK
                spinner.processes--;
                parseGLTF(plugin, json, src, options, performanceModel, ok, error);
            },
            error);
    };

    function getBasePath(src) {
        const i = src.lastIndexOf("/");
        return (i !== 0) ? src.substring(0, i + 1) : "";
    }
})();

const parseGLTF = (function () {

    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
    };

    return function (plugin, json, src, options, performanceModel, ok) {
        const ctx = {
            src: src,
            loadBuffer: options.loadBuffer,
            handleGLTFNode: options.handleGLTFNode,
            json: json,
            scene: performanceModel.scene,
            plugin: plugin,
            performanceModel: performanceModel,
            geometryCreated: {},
            numObjects: 0,
            nodes: []
        };
        const spinner = plugin.viewer.scene.canvas.spinner;
        spinner.processes++;
        loadBuffers(ctx, function () {
            loadBufferViews(ctx);
            freeBuffers(ctx); // Don't need buffers once we've created views of them
            loadMaterials(ctx);
            spinner.processes--;
            loadDefaultScene(ctx);
            performanceModel.finalize();
            ok();
        });
    };

    function loadBuffers(ctx, ok) {
        const buffers = ctx.json.buffers;
        if (buffers) {
            let numToLoad = buffers.length;
            for (let i = 0, len = buffers.length; i < len; i++) {
                loadBuffer(ctx, buffers[i], function () {
                    if (--numToLoad === 0) {
                        ok();
                    }
                }, function (msg) {
                    ctx.plugin.error(msg);
                    if (--numToLoad === 0) {
                        ok();
                    }
                });
            }
        } else {
            ok();
        }
    }

    function loadBuffer(ctx, bufferInfo, ok, err) {
        const uri = bufferInfo.uri;
        if (uri) {
            ctx.plugin.dataSource.getArrayBuffer(ctx.src, uri, function (data) {
                    bufferInfo._buffer = data;
                    ok();
                },
                err);
        } else {
            err('gltf/handleBuffer missing uri in ' + JSON.stringify(bufferInfo));
        }
    }

    function loadBufferViews(ctx) {
        const bufferViewsInfo = ctx.json.bufferViews;
        if (bufferViewsInfo) {
            for (let i = 0, len = bufferViewsInfo.length; i < len; i++) {
                loadBufferView(ctx, bufferViewsInfo[i]);
            }
        }
    }

    function loadBufferView(ctx, bufferViewInfo) {
        const buffer = ctx.json.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) {
        const buffers = ctx.json.buffers;
        if (buffers) {
            for (let i = 0, len = buffers.length; i < len; i++) {
                buffers[i]._buffer = null;
            }
        }
    }

    function loadMaterials(ctx) {
        const materialsInfo = ctx.json.materials;
        if (materialsInfo) {
            for (let i = 0, len = materialsInfo.length; i < len; i++) {
                const materialInfo = materialsInfo[i];
                const material = loadMaterialColorize(ctx, materialInfo);
                materialInfo._rgbaColor = material;
            }
        }
    }

    function loadMaterialColorize(ctx, materialInfo) { // Substitute RGBA for material, to use fast flat shading instead
        const colorize = new Float32Array([1, 1, 1, 1]);
        const extensions = materialInfo.extensions;
        if (extensions) {
            const specularPBR = extensions["KHR_materials_pbrSpecularGlossiness"];
            if (specularPBR) {
                const diffuseFactor = specularPBR.diffuseFactor;
                if (diffuseFactor !== null && diffuseFactor !== undefined) {
                    colorize.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)) {
                        colorize.set(diffuse);
                    }
                }
                const transparency = values.transparency;
                if (transparency !== null && transparency !== undefined) {
                    colorize[3] = transparency;
                }
                const transparent = values.transparent;
                if (transparent !== null && transparent !== undefined) {
                    colorize[3] = transparent;
                }
            }
        }
        const metallicPBR = materialInfo.pbrMetallicRoughness;
        if (metallicPBR) {
            const baseColorFactor = metallicPBR.baseColorFactor;
            if (baseColorFactor) {
                colorize.set(baseColorFactor);
            }
        }
        return colorize;
    }

    function loadDefaultScene(ctx) {
        const json = ctx.json;
        const scene = json.scene || 0;
        const defaultSceneInfo = json.scenes[scene];
        if (!defaultSceneInfo) {
            error(ctx, "glTF has no default scene");
            return;
        }
        preprocessScene(ctx, defaultSceneInfo);
        loadScene(ctx, defaultSceneInfo);
    }

    function preprocessScene(ctx, sceneInfo) {
        const nodes = sceneInfo.nodes;
        if (!nodes) {
            return;
        }
        const json = ctx.json;
        for (let i = 0, len = nodes.length; i < len; i++) {
            const glTFNode = json.nodes[nodes[i]];
            if (!glTFNode) {
                error(ctx, "Node not found: " + i);
                continue;
            }
            countMeshUsage(ctx, i, glTFNode);
        }
    }

    function loadScene(ctx, sceneInfo) {
        const nodes = sceneInfo.nodes;
        if (!nodes) {
            return;
        }
        const json = ctx.json;
        for (let i = 0, len = nodes.length; i < len; i++) {
            const glTFNode = json.nodes[nodes[i]];
            if (!glTFNode) {
                error(ctx, "Node not found: " + i);
                continue;
            }
            countMeshUsage(ctx, glTFNode);
        }
        ctx.plugin.viewer.scene.canvas.spinner.processes++;
        for (let i = 0, len = nodes.length; i < len; i++) {
            const glTFNode = json.nodes[nodes[i]];
            loadNode(ctx, glTFNode, null);
        }
        ctx.plugin.viewer.scene.canvas.spinner.processes--;
    }

    function countMeshUsage(ctx, glTFNode) {
        const json = ctx.json;
        const mesh = glTFNode.mesh;
        if (mesh !== undefined) {
            const meshInfo = json.meshes[glTFNode.mesh];
            if (meshInfo) {
                meshInfo.instances = meshInfo.instances ? meshInfo.instances + 1 : 1;
            }
        }
        if (glTFNode.children) {
            const children = glTFNode.children;
            for (let i = 0, len = children.length; i < len; i++) {
                const childNodeIdx = children[i];
                const childNodeInfo = json.nodes[childNodeIdx];
                if (!childNodeInfo) {
                    error(ctx, "Node not found: " + i);
                    continue;
                }
                countMeshUsage(ctx, childNodeInfo);
            }
        }
    }

    function loadNode(ctx, glTFNode, matrix) {

        const json = ctx.json;
        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, math.mat4());
            } else {
                matrix = localMatrix;
            }
        }

        if (glTFNode.rotation) {
            localMatrix = math.quaternionToMat4(glTFNode.rotation);
            if (matrix) {
                matrix = math.mulMat4(matrix, localMatrix, math.mat4());
            } else {
                matrix = localMatrix;
            }
        }

        if (glTFNode.scale) {
            localMatrix = math.scalingMat4v(glTFNode.scale);
            if (matrix) {
                matrix = math.mulMat4(matrix, localMatrix, math.mat4());
            } else {
                matrix = localMatrix;
            }
        }

        if (glTFNode.mesh !== undefined) {

            const meshInfo = json.meshes[glTFNode.mesh];

            if (meshInfo) {

                let createEntity;

                if (ctx.handleGLTFNode) {
                    const actions = {};
                    if (!ctx.handleGLTFNode(ctx.performanceModel.id, glTFNode, actions)) {
                        return;
                    }
                    if (actions.createEntity) {
                        createEntity = actions.createEntity;
                    }
                }

                const performanceModel = ctx.performanceModel;
                const worldMatrix = matrix ? matrix.slice() : math.identityMat4();
                const numPrimitives = meshInfo.primitives.length;

                if (numPrimitives > 0) {

                    const meshIds = [];

                    for (let i = 0; i < numPrimitives; i++) {
                        const primitiveInfo = meshInfo.primitives[i];
                        if (primitiveInfo.mode < 4) {
                            continue;
                        }
                        const meshCfg = {
                            id: performanceModel.id + "." + ctx.numObjects++
                        };

                        const materialIndex = primitiveInfo.material;
                        let materialInfo;
                        if (materialIndex !== null && materialIndex !== undefined) {
                            materialInfo = json.materials[materialIndex];
                        }
                        if (materialInfo) {
                            meshCfg.color = materialInfo._rgbaColor;
                            meshCfg.opacity = materialInfo._rgbaColor[3];

                        } else {
                            meshCfg.color = new Float32Array([1.0, 1.0, 1.0]);
                            meshCfg.opacity = 1.0;
                        }

                        if (createEntity) {
                            if (createEntity.colorize) {
                                meshCfg.color = createEntity.colorize;
                            }
                            if (createEntity.opacity !== undefined && createEntity.opacity !== null) {
                                meshCfg.opacity = createEntity.opacity;
                            }
                        }

                        loadPrimitiveGeometry(ctx, primitiveInfo, meshCfg);
                        math.transformPositions3(worldMatrix, meshCfg.localPositions, meshCfg.positions);
                        const origin = math.vec3();
                        const rtcNeeded = worldToRTCPositions(meshCfg.positions, meshCfg.positions, origin); // Small cellsize guarantees better accuracy
                        if (rtcNeeded) {
                            meshCfg.origin = origin;
                        }

                        performanceModel.createMesh(meshCfg);
                        meshIds.push(meshCfg.id);
                    }

                    if (createEntity) {
                        performanceModel.createEntity(utils.apply(createEntity, {
                            meshIds: meshIds
                        }));
                    } else {
                        performanceModel.createEntity({
                            meshIds: meshIds
                        });
                    }
                }
            }
        }

        if (glTFNode.children) {
            const children = glTFNode.children;
            for (let i = 0, len = children.length; i < len; i++) {
                const childNodeIdx = children[i];
                const childNodeInfo = json.nodes[childNodeIdx];
                if (!childNodeInfo) {
                    error(ctx, "Node not found: " + i);
                    continue;
                }
                loadNode(ctx, childNodeInfo, matrix);
            }
        }
    }

    function loadPrimitiveGeometry(ctx, primitiveInfo, geometryCfg) {
        const attributes = primitiveInfo.attributes;
        if (!attributes) {
            return;
        }
        geometryCfg.primitive = "triangles";
        const indicesIndex = primitiveInfo.indices;
        if (indicesIndex !== null && indicesIndex !== undefined) {
            const accessorInfo = ctx.json.accessors[indicesIndex];
            geometryCfg.indices = loadAccessorTypedArray(ctx, accessorInfo);
        }
        const positionsIndex = attributes.POSITION;
        if (positionsIndex !== null && positionsIndex !== undefined) {
            const accessorInfo = ctx.json.accessors[positionsIndex];
            geometryCfg.localPositions = loadAccessorTypedArray(ctx, accessorInfo);
            geometryCfg.positions = new Float64Array(geometryCfg.localPositions.length);
        }
        const normalsIndex = attributes.NORMAL;
        if (normalsIndex !== null && normalsIndex !== undefined) {
            const accessorInfo = ctx.json.accessors[normalsIndex];
            geometryCfg.normals = loadAccessorTypedArray(ctx, accessorInfo);
        }
        if (geometryCfg.indices) {
            geometryCfg.edgeIndices = buildEdgeIndices(geometryCfg.localPositions, geometryCfg.indices, null, 10); // Save PerformanceModel from building edges
        }
    }

    function loadAccessorTypedArray(ctx, accessorInfo) {
        const bufferViewInfo = ctx.json.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.
            error("interleaved buffer!"); // TODO
        } else {
            return new TypedArray(bufferViewInfo._buffer, accessorInfo.byteOffset || 0, accessorInfo.count * itemSize);
        }
    }

    function error(ctx, msg) {
        ctx.plugin.error(msg);
    }
})();

export {GLTFPerformanceModelLoader}