Expand description
Intra-mesh vertex weld + index dedup applied at the per-element mesh source
(build_mesh_data), collapsing the faceted-brep per-face vertex duplication
while keeping creases (distinct normals) split.
Intra-mesh vertex weld + index dedup, applied at the mesh SOURCE.
The faceted-brep mesher emits geometry per IfcFace with no cross-face
vertex sharing, so a closed shell duplicates every shared corner once per
incident face (~3-6x). That is the direct cause of the ~8x-larger GLBs the
reference-extractor comparison flagged on structural (faceted-brep-heavy)
models, and it inflates every downstream mesh (render, export, analysis).
This weld collapses vertices that share an identical f32 position AND a
coinciding (quantized) normal into one, then remaps indices.
It runs AT LEAST once per element, in the OBJECT frame, at the last moment
before the placement is baked in: [weld_mesh] from apply_placement and
[weld_sub_mesh] from apply_submesh_placement. Every element — voided or
not, faceted brep or swept solid, single-item or per-style sub-meshes —
therefore arrives welded in its MeshData. Some are welded a SECOND time,
post-bake, by [weld] called from build_mesh_data; its doc says which and
why. Because it keys on the quantized
normal, coincident positions carrying DISTINCT normals (a crease / cube
corner) stay split, so flat shading is preserved (a cube keeps its 24
vertices). Triangles and the AABB are preserved exactly (welded vertices sit
at identical positions; triangle count and winding are unchanged).
§Why the object frame, and not after the bake (#4103)
The position part of the key is the raw f32 BIT PATTERN, so what the weld merges depends on the magnitude of the coordinates it is handed. An f32 ULP is ~6e-8 m at 0.5 m and ~2e-6 m at 30 m, so welding baked world coordinates silently applies an epsilon that grows with the element’s distance from the origin, and welding the same source geometry at two different placements merges two different sets of vertices.
That broke a contract the pipeline depends on. Every occurrence of one
IfcRepresentationMap is a clone of ONE cached source mesh
(router::mapped_item), and every direct-solid rep_identity is a hash of
the mesh BEFORE placement (router::processing::direct_rep_identity), so
occurrences of one representation are meant to be bit-identical. A post-bake
weld rewrote each of them differently, and instancing::collate_refs refuses
a group whose members disagree on vertex count, so nothing ever collated:
ten armchairs sharing one IfcRepresentationMap shipped as ten full meshes
with vertex counts spread over 0.9%, and a cached Parquet artifact came out
three times the size of its own IFC.
In the object frame the key no longer depends on the element’s PLACEMENT, so occurrences that differ only by where they were put weld identically. Placement is rigid, so nothing that merges here would have failed to merge after the bake for a geometric reason; only the accidental collisions go away, and those were never intended.
§What this does NOT fix, measured
Three things survive, all narrower than the bug above but none zero.
A per-occurrence IfcMappedItem MappingTarget is baked in by
router::mapped_item BEFORE the mesh reaches apply_placement, so the weld
still sees target-transformed coordinates. Occurrences of one
IfcRepresentationMap whose targets differ by enough to move the f32
exponent still weld to different vertex counts, and collate_refs still
refuses that group. Measured on the issue_4103_shared_map_buffer_identity
fixture with the offsets moved from the placement into the target: 8/8/8
vertices when the placement varies (fixed), 8/4/4 when the target varies
(unfixed). Closing that means welding the cached source once, before the
target bake, and suppressing this weld for a mesh already welded — a
different change.
The invariant this weld establishes — no two vertices sharing a (position bits, quantized normal) key — holds in the frame the weld RAN in, not necessarily in the world buffer that ships. A rigid placement can map two vertices that are distinct in the object frame onto one f32 world position; main merged those post-bake and this deliberately does not, because doing so is exactly the placement-dependence being removed. Measured across the six ara3d models: 104 of 15,211 shipped meshes carry at least one such duplicate key, 494 of 1,093,616 vertices (0.68% and 0.045%); AC20-FZK-Haus has none. A post-bake weld to remove them would reintroduce #4103.
Welding is not the only stage that can make two occurrences disagree. Anything
that edits INDICES after the bake can too, and two do: degenerate::clean
compares a triangle height computed from f32 world positions against an
ABSOLUTE threshold, and mesh_orient‘s adjacency grid is 10 um, which is
finer than the f32 world grid at 128 m and beyond (one ULP there is 1.5e-5 m).
Either can drop or reorient a different triangle in one occurrence than in
another, and instancing::group rejects a group whose members’ index buffers
differ, which is the #4103 symptom reached by another route. UNMEASURED: this
was reasoned from the thresholds, not observed, and the fixture here uses
half-unit squares that cannot exercise it. Recorded so the next person
investigating a stubborn collation rejection does not assume the weld is the
only candidate.
§Per-vertex attributes
MeshData’s only per-vertex-parallel arrays are positions, normals, and
(for textured meshes, #961) uvs. The weld carries the UVs through the same
remap so they stay 1:1 with the welded positions, AND folds the (quantized)
UV into the merge key: two vertices at the same position + normal but
DIFFERENT UVs are a legitimate texture SEAM and must stay split, or the
texture mapping tears. An untextured mesh (uvs == None) contributes a
constant (0, 0) UV, so its key is effectively position + normal and it gets
the full weld benefit (steel faceted breps unaffected).
Deterministic and cross-arch (native == wasm32): first-seen order over the original vertex array, integer keys (f32 position bits + a quantized normal + a quantized UV), no float comparison, FMA-free.
Functions§
- weld
- Weld a mesh’s vertices in place, carrying
uvsthrough the same remap and returning them still 1:1 with the welded positions. - weld_
indexed - Weld
positions/normals(3 floats per vertex, equal length), optionaluvs(2 floats per vertex), and remapindices.