@xeokit/xeokit-sdk
Version:
3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision
656 lines (616 loc) • 23.1 kB
JavaScript
import { parse } from "../../external.js";
import { GLTFLoader, postProcessGLTF } from "../../external.js";
import { sRGBEncoding } from "../../viewer/scene/constants/constants.js";
import { core } from "../../viewer/scene/core.js";
import { math } from "../../viewer/scene/math/math.js";
import { worldToRTCPositions } from "../../viewer/scene/math/rtcCoords.js";
import { utils } from "../../viewer/scene/utils.js";
import {
ClampToEdgeWrapping,
LinearFilter,
LinearMipMapLinearFilter,
LinearMipMapNearestFilter,
MirroredRepeatWrapping,
NearestFilter,
NearestMipMapLinearFilter,
NearestMipMapNearestFilter,
RepeatWrapping
} from "../../viewer/scene/constants/constants.js";
/**
* @private
*/
class GLTFSceneModelLoader {
constructor(cfg) {
cfg = cfg || {};
}
load(plugin, src, metaModelJSON, options, sceneModel, ok, error) {
options = options || {};
loadGLTF(plugin, src, metaModelJSON, options, sceneModel, function () {
core.scheduleTask(function () {
sceneModel.scene.fire("modelLoaded", sceneModel.id); // FIXME: Assumes listeners know order of these two events
sceneModel.fire("loaded", true, false);
});
if (ok) {
ok();
}
},
function (msg) {
plugin.error(msg);
if (error) {
error(msg);
}
sceneModel.fire("error", msg);
});
}
parse(plugin, gltf, metaModelJSON, options, sceneModel, ok, error) {
options = options || {};
parseGLTF(plugin, "", gltf, metaModelJSON, options, sceneModel, function () {
sceneModel.scene.fire("modelLoaded", sceneModel.id); // FIXME: Assumes listeners know order of these two events
sceneModel.fire("loaded", true, false);
if (ok) {
ok();
}
},
function (msg) {
sceneModel.error(msg);
sceneModel.fire("error", msg);
if (error) {
error(msg);
}
});
}
}
function loadGLTF(plugin, src, metaModelJSON, options, sceneModel, ok, error) {
const spinner = plugin.viewer.scene.canvas.spinner;
spinner.processes++;
const isGLB = (src.split('.').pop() === "glb");
if (isGLB) {
plugin.dataSource.getGLB(src, (arrayBuffer) => { // OK
options.basePath = getBasePath(src);
parseGLTF(plugin, src, arrayBuffer, metaModelJSON, options, sceneModel, ok, error);
spinner.processes--;
},
(err) => {
spinner.processes--;
error(err);
});
} else {
plugin.dataSource.getGLTF(src, (gltf) => { // OK
options.basePath = getBasePath(src);
parseGLTF(plugin, src, gltf, metaModelJSON, options, sceneModel, ok, error);
spinner.processes--;
},
(err) => {
spinner.processes--;
error(err);
});
}
}
function getBasePath(src) {
const i = src.lastIndexOf("/");
return (i !== 0) ? src.substring(0, i + 1) : "";
}
function parseGLTF(plugin, src, gltf, metaModelJSON, options, sceneModel, ok, error) {
const spinner = plugin.viewer.scene.canvas.spinner;
spinner.processes++;
parse(gltf, GLTFLoader, {
...(options.parseOptions || { }),
baseUri: options.basePath
}).then((gltfData) => {
const processedGLTF = postProcessGLTF(gltfData);
const ctx = {
src: src,
entityId: options.entityId,
metaModelJSON,
autoMetaModel: options.autoMetaModel,
globalizeObjectIds: options.globalizeObjectIds,
metaObjects: [],
loadBuffer: options.loadBuffer,
basePath: options.basePath,
handlenode: options.handlenode,
backfaces: !!options.backfaces,
gltfData: processedGLTF,
scene: sceneModel.scene,
plugin: plugin,
sceneModel: sceneModel,
//geometryCreated: {},
numObjects: 0,
nodes: [],
nextId: 0,
entityPerMesh: options.entityPerMesh,
log: (msg) => {
plugin.log(msg);
}
};
loadTextures(ctx);
loadMaterials(ctx);
if (options.autoMetaModel) {
ctx.metaObjects.push({
id: sceneModel.id,
type: "Default",
name: sceneModel.id
});
}
loadDefaultScene(ctx);
sceneModel.finalize();
if (options.autoMetaModel) {
plugin.viewer.metaScene.createMetaModel(sceneModel.id, {
metaObjects: ctx.metaObjects
});
}
spinner.processes--;
ok();
}).catch((err) => {
if (error) error(err);
});
}
function loadTextures(ctx) {
const gltfData = ctx.gltfData;
const textures = gltfData.textures;
if (textures) {
for (let i = 0, len = textures.length; i < len; i++) {
loadTexture(ctx, textures[i]);
}
}
}
function loadTexture(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.sceneModel.createTexture({
id: textureId,
image: texture.source.image,
flipY: !!texture.flipY,
minFilter,
magFilter,
wrapS,
wrapT,
wrapR,
encoding: sRGBEncoding
});
texture._textureId = textureId;
}
function loadMaterials(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 = loadTextureSet(ctx, material);
material._attributes = loadMaterialAttributes(ctx, material);
}
}
}
function loadTextureSet(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;
}
switch (material.alphaMode) {
case "OPAQUE":
break;
case "MASK":
const alphaCutoff = material.alphaCutoff;
// Default from the spec https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-material
textureSetCfg.alphaCutoff = (alphaCutoff !== undefined) ? alphaCutoff : 0.5;
break;
case "BLEND":
break;
default:
break;
}
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.id = `textureSet-${ctx.nextId++};`
ctx.sceneModel.createTextureSet(textureSetCfg);
return textureSetCfg.id;
}
return null;
}
function loadMaterialAttributes(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,
doubleSided: true
};
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;
}
}
materialAttributes.doubleSided = (material.doubleSided !== false);
return materialAttributes;
}
function loadDefaultScene(ctx) {
const gltfData = ctx.gltfData;
const scene = gltfData.scene || gltfData.scenes[0];
if (!scene) {
error(ctx, "glTF has no default scene");
return;
}
const nodes = scene.nodes;
if (!nodes) {
return;
}
(function accumulateMeshInstantes(nodes) {
nodes.forEach(node => {
const mesh = node.mesh;
if (mesh) {
mesh.instances ||= 0;
mesh.instances += 1;
}
if (node.children) {
accumulateMeshInstantes(node.children);
}
});
})(nodes);
if (ctx.entityPerMesh) {
const sceneIds = new Set();
(function createSceneMeshesAndEntities(nodes, parentId, parentMatrix, dep) {
return nodes.reduce(
(hadMesh, node) => {
const baseId = node.name ?? "Node";
const maybeGlobalize = id => ctx.globalizeObjectIds ? math.globalizeObjectId(ctx.sceneModel.id, id) : id;
let entityId = maybeGlobalize(baseId);
let nextPostfixId = 1;
while (ctx.sceneModel.objects[entityId] || sceneIds.has(entityId)) {
entityId = maybeGlobalize(`${baseId}.${(nextPostfixId++).toString().padStart(4, "0")}`);
}
sceneIds.add(entityId);
const matrix = parseNodeMatrix(node, parentMatrix);
const meshEntity = node.mesh && (function() {
const meshIds = [ ];
parseNodeMesh(node, ctx, matrix, meshIds);
return (meshIds.length > 0) && ctx.sceneModel.createEntity({
id: entityId,
meshIds: meshIds,
isObject: true
});
})();
const hasMesh = (node.children && createSceneMeshesAndEntities(node.children, entityId, matrix, dep + 1)) || meshEntity;
if (hasMesh && ctx.autoMetaModel) {
ctx.metaObjects.push({
id: entityId,
name: entityId,
type: "Default",
parent: parentId
});
}
return hadMesh || hasMesh;
},
false);
})(nodes, ctx.sceneModel.id, null, 0);
return;
}
// Create a SceneMesh for each mesh primitive, and a SceneModelEntity for the root node and each named node.
const meshIdsStack = [];
let meshIds = null;
(function createSceneMeshesAndEntities(nodes, depth, parentMatrix) {
nodes.forEach(node => {
const nodeName = node.name;
let entityId = (((nodeName !== undefined) && (nodeName !== null) && nodeName)
||
((depth === 0) && ("Node." + String(ctx.nextId++).padStart(4, "0"))));
if (entityId) {
entityId = ctx.globalizeObjectIds ? math.globalizeObjectId(ctx.sceneModel.id, entityId) : entityId;
while (ctx.sceneModel.objects[entityId]) {
entityId = nodeName + "." + String(ctx.nextId++).padStart(4, "0");
entityId = ctx.globalizeObjectIds ? math.globalizeObjectId(ctx.sceneModel.id, entityId) : entityId;
}
meshIdsStack.push(meshIds);
meshIds = [];
}
const matrix = parseNodeMatrix(node, parentMatrix);
if (node.mesh) {
parseNodeMesh(node, ctx, matrix, meshIds);
}
if (node.children) {
createSceneMeshesAndEntities(node.children, depth + 1, matrix);
}
if (entityId) {
if (meshIds.length > 0) {
ctx.sceneModel.createEntity({
id: entityId,
meshIds: meshIds,
isObject: true
});
if (ctx.autoMetaModel) {
ctx.metaObjects.push({
id: entityId,
type: "Default",
name: nodeName ? nodeName : "Node",
parent: ctx.sceneModel.id
});
}
}
meshIds = meshIdsStack.pop();
}
});
})(nodes, 0, 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) {
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;
}
/**
* 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) {
const mesh = node.mesh;
if (!mesh) {
return;
}
const numPrimitives = mesh.primitives.length;
if (numPrimitives > 0) {
for (let i = 0; i < numPrimitives; i++) {
const primitive = mesh.primitives[i];
const meshCfg = {
id: ctx.sceneModel.id + "." + ctx.numObjects++
};
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 = new Float32Array([1.0, 1.0, 1.0]);
meshCfg.opacity = 1.0;
}
const backfaces = ((ctx.backfaces !== false) || (material && material.doubleSided !== false));
switch (primitive.mode) {
case 0: // POINTS
meshCfg.primitive = "points";
break;
case 1: // LINES
meshCfg.primitive = "lines";
break;
case 2: // LINE_LOOP
meshCfg.primitive = "lines";
break;
case 3: // LINE_STRIP
meshCfg.primitive = "lines";
break;
case 4: // TRIANGLES
meshCfg.primitive = backfaces ? "triangles" : "solid";
break;
case 5: // TRIANGLE_STRIP
meshCfg.primitive = backfaces ? "triangles" : "solid";
break;
case 6: // TRIANGLE_FAN
meshCfg.primitive = backfaces ? "triangles" : "solid";
break;
default:
meshCfg.primitive = backfaces ? "triangles" : "solid";
}
const POSITION = primitive.attributes.POSITION;
if (!POSITION) {
continue;
}
meshCfg.localPositions = POSITION.value;
meshCfg.positions = new Float64Array(meshCfg.localPositions.length);
if (primitive.attributes.NORMAL) {
meshCfg.normals = primitive.attributes.NORMAL.value;
}
if (primitive.attributes.TEXCOORD_0) {
meshCfg.uv = primitive.attributes.TEXCOORD_0.value;
}
if (primitive.indices) {
meshCfg.indices = primitive.indices.value;
}
if (matrix) {
math.transformPositions3(matrix, meshCfg.localPositions, meshCfg.positions);
} else { // eqiv to math.transformPositions3(math.identityMat4(), meshCfg.localPositions, meshCfg.positions);
meshCfg.positions.set(meshCfg.localPositions);
}
const origin = math.vec3();
const rtcNeeded = worldToRTCPositions(meshCfg.positions, meshCfg.positions, origin); // Small cellsize guarantees better accuracy
if (rtcNeeded) {
meshCfg.origin = origin;
}
ctx.sceneModel.createMesh(meshCfg);
meshIds.push(meshCfg.id);
}
}
}
function error(ctx, msg) {
ctx.plugin.error(msg);
}
export { GLTFSceneModelLoader };