Hey @ryan_veenstra,
I’ve made some progress! Your tips about working with Revit were pivotal in helping me get to this stage — thank you so much for pointing me in the right direction.
Since preserving the GlobalId is critical in my scenario, I’d like to share the approach I used, focusing specifically on retaining this metadata. I’ll also briefly touch on how to extend this method to include additional properties if needed.
Steps:
#1. Convert IFC to GLB using IfcOpenShell (Installation - IfcOpenShell 0.8.0 documentation)
The --use-element-guid parameter ensures that the GlobalId becomes the name attribute in the resulting GLB file, which is key for subsequent steps:
IfcConvert.exe path/to/ifc.ifc path/to/output.glb --use-element-guid
#2. Convert GLB to GLTF using CesiumGS glTF Pipeline (gltf-pipeline/README.md at main · CesiumGS/gltf-pipeline · GitHub)
Use the following command to transform the GLB file into a GLTF file:
npx gltf-pipeline -i path/to/glb.glb -o path/to/output.gltf
#3. Embed Metadata in the GLTF File
Here, I use a script to loop through the GLTF JSON structure and add the GlobalId (from the name field) as a property. At this stage, you can also pre-extract additional properties from the IFC file, use the GlobalId as key and embed them alongside the GlobalId. This is especially useful if you need other attributes available for interaction in the viewer.
For example, in my case, we use the GlobalId as a key to link viewer objects with data stored in our system. This step also allows the customization of base mesh names if needed.
#4. Upload to Cesium Ion
When uploading to Cesium Ion, ensure the correct data type is specified:
Web Interface: Use the “Architecture, Engineering, or Construction model (BIM/CAD)” option.
API: Include “type”: “3DTILES” and “options”: { “sourceType”: “BIM_CAD” } in your request payload.
Output:
With this process, the GlobalId (or other custom properties) becomes accessible in the click event of the mesh in the Cesium viewer. This workflow can handle GLTF files in both single-file (.gltf) and paired-file (.gltf + .bin) formats — both work seamlessly in Cesium Ion.
A great advantage of this method is its flexibility. You can embed any set of properties in the GLTF file. For example, I tested it with a simple restroom model created in Blender, adding mock metadata, and it worked perfectly.
Below, I’ve included a sample script illustrating step #3 and an interface demonstrating how to embed the GlobalId into the GLTF file from its name provided by --use-element-guid from the step #1.
This script is a proof of concept and can certainly be improved, but I hope it serves as a starting point for others exploring this approach.
import fs from "fs";
const startTime = Date.now();
interface EXT_structural_metadata {
class: string;
properties: { ifcGUID: string; [key: string]: any };
}
interface GltfSchema {
extensionsUsed?: string[];
extensions?: { [key: string]: any };
nodes: Array<{
name?: string;
extensions?: { EXT_structural_metadata?: EXT_structural_metadata; [key: string]: any };
}>;
}
function updateGltf(filePath: string): void {
const gltfData: GltfSchema = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (!gltfData.extensionsUsed) {
gltfData.extensionsUsed = [];
}
if (!gltfData.extensionsUsed.includes("EXT_structural_metadata")) {
gltfData.extensionsUsed.push("EXT_structural_metadata");
}
if (!gltfData.extensions) {
gltfData.extensions = {};
}
if (!gltfData.extensions.EXT_structural_metadata) {
gltfData.extensions.EXT_structural_metadata = {
schema: {
id: "guidFromNameGenerated",
classes: {
guidFromName: {
name: "guidFromName",
properties: {
ifcGUID: {
name: "ifcGUID",
type: "STRING",
required: false,
},
},
},
},
},
};
}
gltfData.nodes.forEach((node) => {
if (node.name) {
if (!node.extensions) {
node.extensions = {};
}
if (!node.extensions.EXT_structural_metadata) {
node.extensions.EXT_structural_metadata = {
class: "guidFromName",
properties: {
ifcGUID: node.name,
},
};
}
}
});
fs.writeFileSync(filePath, JSON.stringify(gltfData, null, 2), "utf8");
}
const filePath = "./file.gltf";
updateGltf(filePath);
const endTime = Date.now();
const timeExpendInSeconds = (endTime - startTime) / 1000;
console.log("seconds to convert:", filePath, timeExpendInSeconds);
Thank you again!
Please let me know if you have any suggestions on better handle the situation.
Best regards,
Kharel