Closed wlinna closed 4 months ago
It seems that all the errors are related to the _BLOCKID attribute
Hey @wlinna, the geometric representation we use is not necessarily glTF compatible due to the blockID
attribute. If you remove that attribute from the geometry before exporting, would it work for you?
Hello
If you remove that attribute from the geometry before exporting, would it work for you?
That's what I'm wondering myself. However, I don't know what blockID
represents and thus I don't know what I'd be losing. However, that's what I'm going to try next.
Is blockID
documented somewhere?
the geometric representation we use is not necessarily glTF compatible due to the blockID attribute.
But having a custom attribute shouldn't be a problem in itself. At my day job our meshes have custom attributes and GLTFExporter
has no trouble dealing with them. Because of this, I'm inclined to think that something is wrong with the way blockID
is stored / expressed.
Hm, blockID is just another Three.js BufferAttribute, so I'm not sure what can be wrong with it. š¤
We use fragments to express geometry, which is a simple wrapper we created around Three.js instancedMeshes to work efficiently with BIM data. The concept is simple: use instancing for geometries that repeat, and merge geometries that are unique (like walls, slabs, etc) to reduce draw calls.
We use the blockID attribute to distinguish meshes with unique geometries that are merged to reduce the draw calls (e.g. walls or slabs) at the vertex level. It's not ideal having an int per vertex, but it's the easiest way to make this work with three-mesh-bvh, which necessary for things like real time clipping planes.
Thanks for the response
We use the blockID attribute to distinguish meshes with unique geometries that are merged to reduce the draw calls (e.g. walls or slabs) at the vertex level. It's not ideal having an int per vertex, but it's the easiest way to make this work with three-mesh-bvh, which necessary for things like real time clipping planes.
Good to know. My application needs the meshes to be separated, so I need to use blockID
to undo the batching.
Hm, blockID is just another Three.js BufferAttribute, so I'm not sure what can be wrong with it. š¤
The problem is related to the byteStride
of bufferview. It has to be divisible by 4
, but in the export it is 2
. GLTFExporter
computes it like this:
case WEBGL_CONSTANTS.SHORT:
case WEBGL_CONSTANTS.UNSIGNED_SHORT:
componentSize = 2;
break;
...
bufferViewDef.byteStride = attribute.itemSize * componentSize;
Here's what the glTF spec says:
For performance and compatibility reasons, each element of a vertex attribute MUST be aligned to 4-byte boundaries inside a bufferView (i.e., accessor.byteOffset and bufferView.byteStride MUST be multiples of 4).
Perhaps it would be for the best to change blockID
from Uint16
to Uint32
since apparently using Uint16
might have undesired performance/compatibility implications.
Whether you do it or not doesn't concern me anymore, however, because I need to undo the batching anyway and thus remove blockID
anyway.
Turns out that blockID
isn't the only problem. I tried removing them and then exporting like before, but the results are not good
group.traverse(obj => {
console.log(obj.constructor.name);
if (!(obj instanceof THREE.Mesh)) {
return;
}
const geom = obj.geometry as THREE.BufferGeometry;
geom.deleteAttribute('blockID');
}
When I zoom out, it looks like the objects that are likely instanced are 1000 times larger and stacked together. I don't know how the instances are handled in fragments
/ components
, so any pointers would be much appreciated
It turns out that the GLTFExporter
supplied by components
does not include (GLTFMeshGpuInstancing)[
https://github.com/mrdoob/three.js/blob/d04539a76736ff500cae883d6a38b3dd8643c548/examples/jsm/exporters/GLTFExporter.js#L146C15-L146C36].
So part of the problem should be fixed by upgrading the Three.js dependency. I'm probably going to try make separate meshes though.
I ended up turning all the instances to separate Mesh objects (that just happen to share the geometry) to avoid any compatibility issues, and that works fine.
Hi @wlinna I am curious to see how you solved this issue. I am also trying to export glbs or gltfs from certain IFC classes (walls, slabs, roofs, I want to create a lite or shell mesh that I can load on top of mapbox) but I am not being successful at doing so. @agviegas any advice would be greatly appreciated. Thanks!
Hello, the process of exporting is surprisingly complicated, although I haven't tested with the latest version because I can't install it without errors. So I use "openbim-components": "1.2.0"
However, I managed to implement a my own solution that has served me well since. Feel free to modify my code. My code includes only uses two very basic materials (because my application doesn't need material information), so you'll likely need to write better material generation code (if you can share your improvement, I'd appreciate it)
Here's the relevant TypeScript code. Feel free to ask about the code if you have trouble using / understanding it.
Note that if you don't need to separate the components, you might be able to get away with something much simpler. In that case, removing blockID
attribute from each geometry might be a sufficient alternative to the whole separateByBlockId
operaiton.
async function exportGlb() {
if (!fragments.groups.length) {
return;
}
const group = fragments.groups[0];
const clone = cloneAndUnravel(group);
// You only need this if you use fragmentIfcLoader.settings.webIfc.COORDINATE_TO_ORIGIN = true;
group.coordinationMatrix.invert().decompose(clone.position, clone.quaternion, clone.scale);
const exporter = new GLTFExporter();
// Options copied from examples. They don't have to be like this I think
const opts = {
trs: false,
onlyVisible: false,
truncateDrawRange: true,
binary: true,
maxTextureSize: 0
};
const glb = await exporter.parseAsync(clone, opts) as ArrayBuffer;
const blob = new Blob([glb]);
const file = new File([blob], name + '.glb');
const objUrl = URL.createObjectURL(file);
...
}
class ItemData {
position: number[] = [];
index: number[] = [];
oldToNew = new Map<number, number>();
addNewPos(oldIdx: number, x: number, y: number, z: number) {
this.position.push(x, y, z);
this.oldToNew.set(oldIdx, (this.position.length / 3) - 1);
}
addIndex(oldIdx: number) {
const newIdx = this.oldToNew.get(oldIdx);
if (newIdx == null) {
throw new Error('Index not found error');
}
this.index.push(newIdx);
}
}
function cloneAndUnravel(group: FragmentsGroup) {
const serializer = new Serializer();
const exported = serializer.export(group);
const clone = serializer.import(exported);
const properties = group.properties!;
const itemMap = new Map<string, ItemData>();
clone.traverse(obj => {
if (!(obj instanceof THREE.Mesh)) {
return;
}
const geom = obj.geometry as THREE.BufferGeometry;
if (obj instanceof FragmentMesh) {
const matrix = new THREE.Matrix4;
for (let instI = 0; instI < obj.count; ++instI) {
obj.getMatrixAt(instI, matrix);
separateByBlockId(obj.fragment, instI, geom, matrix, itemMap);
}
return;
}
throw new Error('This never happens');
});
clone.clear();
const outGeoms = new Map<string, THREE.BufferGeometry>();
for (const [key, itemData] of itemMap.entries()) {
const newGeom = new THREE.BufferGeometry();
newGeom.setIndex(itemData.index);
newGeom.setAttribute('position', new THREE.Float32BufferAttribute(itemData.position, 3));
const cleaned = BufferGeometryUtils.mergeVertices(newGeom);
outGeoms.set(key, cleaned);
}
const myMat = new THREE.MeshStandardMaterial({ color: 0x666666 });
const windowMat = new THREE.MeshStandardMaterial({ color: 0x0101FE, opacity: 0.7 });
for (const [expressId, geom] of outGeoms.entries()) {
const props = properties[Number(expressId)];
const ifcType = props.type;
let mat = myMat;
if (ifcType === WEBIFC.IFCWINDOW || ifcType === WEBIFC.IFCWINDOWSTANDARDCASE) {
mat = windowMat;
}
let newMesh = new THREE.Mesh(geom, mat);
newMesh.name = props.GlobalId?.value ?? expressId;
newMesh.userData.expressId = expressId;
newMesh.updateMatrix();
clone.add(newMesh);
}
return clone;
}
function separateByBlockId(fragment: Fragment, instanceId: number, bufGeom: THREE.BufferGeometry, matrix: THREE.Matrix4, itemMap: Map<string, ItemData>) {
const origVerts = bufGeom.getAttribute('position');
const blockIdBuf = bufGeom.getAttribute('blockID');
const origIndex = bufGeom.index as THREE.Uint32BufferAttribute;
const vec = new THREE.Vector3;
for (let i = 0; i < origVerts.count; ++i) {
let blockId = blockIdBuf.getX(i);
let expressId = fragment.getItemID(instanceId, blockId);
if (expressId.includes('.')) {
// const test = fragment.getItemID(instanceId, blockId);
console.warn("Investigate: weird expressId: " + expressId);
expressId = expressId.substring(0, expressId.lastIndexOf('.'));
}
let blockData = itemMap.get(expressId) ?? new ItemData;
itemMap.set(expressId, blockData);
vec.set(origVerts.getX(i), origVerts.getY(i), origVerts.getZ(i)).applyMatrix4(matrix);
blockData.addNewPos(i, vec.x, vec.y, vec.z);
}
let currentBlockId = -1;
for (let iIndex = 0; iIndex < origIndex.count; ++iIndex) {
let idx = origIndex.getX(iIndex);
let blockId = blockIdBuf.getX(idx);
if (iIndex % 3 === 0) {
currentBlockId = blockId;
} else if (blockId != currentBlockId) {
throw new Error('The blockId should not change mid-triangle!');
}
let expressId = fragment.getItemID(instanceId, blockId);
if (expressId.includes('.')) {
expressId = expressId.substring(0, expressId.lastIndexOf('.'));
}
let blockData = itemMap.get(expressId) ?? new ItemData;
itemMap.set(expressId, blockData);
blockData.addIndex(idx);
}
}
Hi, this week we will release a new version of components/fragments, maybe it helps. We have gotten rid of the BlockID attribute, so now the mesh
attribute of a component is a mere three.js InstancedMesh, so exporting it to GLTF should be a pure Three.js matter.
If after the update of components & fragments to 1.5.0 the issue remains, let me know and we'll see what we can do. Cheers!
@agviegas Interesting! But how are the different meshes with identical materials batched then, if at all? Won't the additional draw calls become an issue? Or do I have to figure out a new way to split merged meshes (since I need to have them separated before processing)?
Merged meshes don't exist anymore. Now, everything is instanced. So each unique element remains its own mesh. We have created a streaming system that can open multi gigabyte models on any devices in seconds at 60 fps. The draw calls are always under control because items that are not visible for the camera are removed from the scene, and later from memory (until seen again).
Needless to say, this system is also compatible with the rest of components and part of this library. This is what we are releasing this week.
Getting rid of merged meshes has made everything much cleaner and easier. Also, this year we are creating a BIM modeller, and this approach will make everything much easier to work with and to maintain.
Thanks, @wlinna I'll give it a try. Looking forward to the new release.
Very interesting. I have a couple of questions if you don't mind me asking:
Btw, I wonder how well will occlusion culling work when the model has a limited number of good occluders. Industrial facility models often seem to lack them. Not that it matters in my use case, just curious.
Sure! This will be in the docs as a step-by-step example once we release it next week.
Thanks for explaining!
What I actually need is to load the model to the scene, manipulate it a little bit such as by hiding certain components, and then convert it to GLB so that my WASM module can run some operations on it.
Thankfully 1.4 series still allows using IfcFragmentLoader. I really hope the ability to load the whole model to the scene will never be removed or deprecated.
1.4.11 has this rather nasty bug though :/ https://github.com/ThatOpen/engine_components/issues/314
Hey @wlinna thanks for your patience. I think it's solved. Using the fragmentsManager tutorial as a starting point, and modifying the load function so that it exports the result with your config:
async function loadFragments() {
if (fragments.groups.size) {
return;
}
const file = await fetch(
"https://thatopen.github.io/engine_components/resources/small.frag",
);
const data = await file.arrayBuffer();
const buffer = new Uint8Array(data);
const group = fragments.load(buffer);
world.scene.three.add(group);
uuid = group.uuid;
const exporter = new GLTFExporter();
const opts: GLTFExporterOptions = {
trs: true,
binary: true,
onlyVisible: true,
};
const glb = (await exporter.parseAsync(group, opts)) as ArrayBuffer;
const blob = new Blob([glb]);
const exportedFile = new File([blob], "example.glb");
// I changed download to take name separately to make TypeScript happy
const a = document.createElement("a");
a.download = "exported.glb";
a.href = URL.createObjectURL(exportedFile);
a.click();
a.remove();
}
This is the result:
https://github.com/ThatOpen/engine_components/assets/56475338/3f4757f0-8af4-4ace-b8c4-adb1a046f7e5
Let me know if you have any issues. Cheers!
Describe the bug š
I am trying to export the fragments to glTF, but the exported glb-file is invalid. glTF validator produces lots of errors and the file doesn't open in Blender
Whether it's because the fragments are somehow special or because the GLTFExporter of Three.js 152 specifically is broken I know not. However, in other contexts I have used GLTFExporter without problems for years. Either way, something should be done on the openbim-components side.
Reproduction ā¶ļø
https://657dfca31ade9164da46e2de--bim-building-simplification.netlify.app/
Steps to reproduce š¢
~(Sorry for not being able to give a reproduction site at the moment. My repro site seems to break when built with Vite and deployed to Netlify. Maybe later)~
EDIT: Reproduction steps with the reproduction site
Old steps (the reproduction steps explained in general)
System Info š»
Used Package Manager š¦
npm
Error Trace/Logs š
glTF validator output
```{ "uri": "example.glb", "mimeType": "model/gltf-binary", "validatorVersion": "2.0.0-dev.3.8", "validatedAt": "2023-12-16T16:13:37.458Z", "issues": { "numErrors": 630, "numWarnings": 0, "numInfos": 0, "numHints": 0, "messages": [ { "code": "VALUE_NOT_IN_RANGE", "message": "Value 2 is out of range.", "severity": 0, "pointer": "/bufferViews/2/byteStride" }, { "code": "VALUE_NOT_IN_RANGE", "message": "Value 2 is out of range.", "severity": 0, "pointer": "/bufferViews/6/byteStride" }, { "code": "VALUE_NOT_IN_RANGE", "message": "Value 2 is out of range.", "severity": 0, "pointer": "/bufferViews/10/byteStride" }, ... { "code": "MESH_PRIMITIVE_ACCESSOR_UNALIGNED", "message": "Vertex attribute data must be aligned to 4-byte boundaries.", "severity": 0, "pointer": "/meshes/0/primitives/0/attributes/_BLOCKID" }, { "code": "MESH_PRIMITIVE_ACCESSOR_UNALIGNED", "message": "Vertex attribute data must be aligned to 4-byte boundaries.", "severity": 0, "pointer": "/meshes/1/primitives/0/attributes/_BLOCKID" }, ... ], "truncated": false }, "info": { "version": "2.0", "generator": "THREE.GLTFExporter", "resources": [ { "pointer": "/buffers/0", "mimeType": "application/gltf-buffer", "storage": "glb", "byteLength": 2062856 } ], } } ```Validations ā