Created
April 30, 2024 11:21
-
-
Save xeolabs/3eb7777900ff0b7d244a77bcdfeca127 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {math} from "../../viewer/scene/math/math.js"; | |
import {utils} from "../../viewer/scene/utils.js"; | |
import {core} from "../../viewer/scene/core.js"; | |
import {sRGBEncoding} from "../../viewer/scene/constants/constants.js"; | |
import {worldToRTCPositions} from "../../viewer/scene/math/rtcCoords.js"; | |
import {parse} from '@loaders.gl/core'; | |
import {GLTFLoader} from '@loaders.gl/gltf/dist/esm/gltf-loader.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 getMetaModelCorrections(metaModelJSON) { | |
const eachRootStats = {}; | |
const eachChildRoot = {}; | |
const metaObjects = metaModelJSON.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 { | |
} | |
} | |
} | |
return { | |
metaObjectsMap, | |
eachRootStats, | |
eachChildRoot | |
}; | |
} | |
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) { | |
const spinner = plugin.viewer.scene.canvas.spinner; | |
spinner.processes++; | |
parse(gltf, GLTFLoader, { | |
baseUri: options.basePath | |
}).then((gltfData) => { | |
const ctx = { | |
src: src, | |
metaModelCorrections: metaModelJSON ? getMetaModelCorrections(metaModelJSON) : null, | |
loadBuffer: options.loadBuffer, | |
basePath: options.basePath, | |
handlenode: options.handlenode, | |
backfaces: !!options.backfaces, | |
gltfData: gltfData, | |
scene: sceneModel.scene, | |
plugin: plugin, | |
sceneModel: sceneModel, | |
//geometryCreated: {}, | |
numObjects: 0, | |
nodes: [], | |
nextId: 0, | |
log: (msg) => { | |
plugin.log(msg); | |
} | |
}; | |
loadTextures(ctx); | |
loadMaterials(ctx); | |
loadDefaultScene(ctx); | |
sceneModel.finalize(); | |
spinner.processes--; | |
ok(); | |
}); | |
} | |
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; | |
} | |
// const alphaMode = material.alphaMode; | |
// switch (alphaMode) { | |
// case "NORMAL_OPAQUE": | |
// materialCfg.alphaMode = "opaque"; | |
// break; | |
// case "MASK": | |
// materialCfg.alphaMode = "mask"; | |
// break; | |
// case "BLEND": | |
// materialCfg.alphaMode = "blend"; | |
// break; | |
// default: | |
// } | |
// const alphaCutoff = material.alphaCutoff; | |
// if (alphaCutoff !== undefined) { | |
// materialCfg.alphaCutoff = alphaCutoff; | |
// } | |
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; | |
} | |
loadScene(ctx, scene); | |
} | |
function loadScene(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; i++) { | |
const node = nodes[i]; | |
loadNode(ctx, node, 0, null); | |
} | |
} | |
function countMeshUsage(ctx, node) { | |
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) { | |
error(ctx, "Node not found: " + i); | |
continue; | |
} | |
countMeshUsage(ctx, childNode); | |
} | |
} | |
} | |
const objectIdStack = []; | |
const meshIdsStack = []; | |
let meshIds = []; | |
function loadNode(ctx, node, depth, matrix) { | |
const gltfData = ctx.gltfData; | |
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; | |
} | |
} | |
const sceneModel = ctx.sceneModel; | |
if (node.name) { | |
meshIds = []; | |
let xktEntityId = node.name; | |
if (!!xktEntityId && sceneModel.objects[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 || sceneModel.objects[xktEntityId]) { | |
xktEntityId = "entity-" + ctx.nextId++; | |
} | |
objectIdStack.push(xktEntityId); | |
meshIdsStack.push(meshIds); | |
} | |
if (meshIds && node.mesh) { | |
const mesh = node.mesh; | |
let createEntity; | |
if (ctx.handlenode) { | |
const actions = {}; | |
if (!ctx.handlenode(ctx.sceneModel.id, node, actions)) { | |
return; | |
} | |
if (actions.createEntity) { | |
createEntity = actions.createEntity; | |
} | |
} | |
const worldMatrix = matrix ? matrix.slice() : math.identityMat4(); | |
const numPrimitives = mesh.primitives.length; | |
if (numPrimitives > 0) { | |
for (let i = 0; i < numPrimitives; i++) { | |
const primitive = mesh.primitives[i]; | |
if (primitive.mode < 4) { | |
continue; | |
} | |
const meshCfg = { | |
id: 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; | |
} | |
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; | |
} | |
sceneModel.createMesh(meshCfg); | |
meshIds.push(meshCfg.id); | |
} | |
} | |
} | |
if (node.children) { | |
const children = node.children; | |
for (let i = 0, len = children.length; i < len; i++) { | |
const childNode = children[i]; | |
loadNode(ctx, childNode, depth + 1, matrix); | |
} | |
} | |
// Post-order visit scene node | |
const nodeName = node.name; | |
if ((nodeName !== undefined && nodeName !== null) || depth === 0) { | |
if (nodeName === undefined || nodeName === null) { | |
ctx.log(`Warning: 'name' properties not found on glTF scene nodes - will randomly-generate object IDs in XKT`); | |
} | |
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) { | |
sceneModel.createEntity({ | |
id: xktEntityId, | |
meshIds: entityMeshIds | |
}); | |
} | |
meshIds = meshIdsStack.length > 0 ? meshIdsStack[meshIdsStack.length - 1] : null; | |
} | |
} | |
function error(ctx, msg) { | |
ctx.plugin.error(msg); | |
} | |
export {GLTFSceneModelLoader}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment