What "Same Vertex" Means in an STL, and Why Four Tools Answer It Differently
Published 2026-09-11
You run a model through one repair tool and it reports a handful of holes. You run the same file through another and it reports thousands, or none. Nothing about the file changed, and neither tool is broken.
The reason is a gap in the STL format that gets glossed over: an STL never states that two triangles share a corner. That has to be worked out after the file is read, and every tool works it out differently.
What the file actually contains
Our test object is a sphere, 100 mm across, closed by construction. Its true topology is not in question:
2,562 vertices, 5,120 faces, 0 boundary edges, 0 non-manifold edges, 1 shell, watertight, volume 522,467 mm³.
Exported to STL and read back with no reconstruction at all, the same object reads as:
15,360 vertices, 5,120 faces, 15,360 boundary edges, 5,120 shells, not watertight.
Same file, same triangles. 15,360 is exactly 5,120 × 3: every triangle brought its own three corners and shares none of them. Every one of its three edges is a boundary edge, because as far as the reader can tell nothing is attached to anything.
For comparison, the same sphere exported to three formats that store an explicit vertex index, read back with no merging applied:
| Format | Vertices | Boundary edges | Watertight |
|---|---|---|---|
| PLY | 2,562 | 0 | yes |
| OBJ | 2,562 | 0 | yes |
| GLB | 2,562 | 0 | yes |
| STL | 15,360 | 15,360 | no |
The first three match the source exactly, with no reconstruction step, because they say which vertices are shared. Related reading on what else separates them: OBJ vs STL.
The thing we expected to be the culprit, and was not
STL stores coordinates as 32 bit floats, while most modelling tools work in 64 bit. The obvious hypothesis is that this round trip nudges coordinates apart, so corners that were identical in the source no longer match afterwards.
We tested it. It is not what happens. Counting the coordinates in our STL that match bit for bit gives exactly 2,562 distinct points, which is the truth value. Rounding to 32 bit is deterministic: the same input produces the same output every time, so a corner written three times is written identically three times.
On a clean file, every merge strategy we tested recovered the mesh perfectly, including the strictest possible one. If files were always clean, tools would always agree.
What actually decides the answer
Files are not always clean. When a mesh has been through a scanner, a tessellator, a boolean operation or a conversion chain, the same logical corner can get written more than once with a slightly different value. That noise is far too small to see or to print. It is not too small to change the answer.
Here is the same sphere with three levels of coordinate noise, read at a range of merge tolerances. The mesh is recovered correctly only where marked.
| Merge tolerance | Noise 0.000001 mm | Noise 0.0001 mm | Noise 0.01 mm |
|---|---|---|---|
| exact match | 5,120 shells | 5,120 shells | 5,120 shells |
| 0.0001 mm | 14 shells | 5,119 shells | 5,120 shells |
| 0.001 mm | 3 shells | 1,508 shells | 5,120 shells |
| 0.01 mm | 1 shell, 104 boundary edges | 18 shells | 5,117 shells |
| 0.1 mm | correct | correct | 1,472 shells |
| 1 mm | correct | correct | 5 shells, 592 boundary edges |
Three things fall out of this table.
Exact matching fails completely on any amount of noise. The first row is identical in all three columns: noise a millionth of a millimetre wide is enough to make a strict reader see 5,120 separate objects. A tool that only merges identical coordinates will tell you a perfectly good model is shattered.
The tolerance has to be far larger than the noise, not just larger. With noise of a millionth of a millimetre, a tolerance a hundred times bigger still left 14 shells. Full recovery needed 0.1 mm, which is a hundred thousand times the noise. The reason is in how the merge is implemented: rounding coordinates to a number of decimal places puts two points either side of a rounding boundary into different buckets no matter how close together they are. A tolerance behaves like a grid, not like a radius.
The window can close entirely. In the last column no setting we tested worked. At 1 mm the mesh still had 592 boundary edges, and the shortest real edge in this model is 3.46 mm, so there is very little room left before merging starts destroying geometry rather than repairing it. This is what a file that “cannot be fixed in any tool” actually looks like from the inside.
Four tools, four definitions of the same point
None of this is hidden. Each tool documents what it does, and the documented behaviours are genuinely different from each other, not just differently tuned.
| Tool | What it treats as the same point |
|---|---|
| MeshLab, Remove Duplicate Vertices | Identical coordinates only. The documentation is explicit: if two vertices have the same coordinates they are merged into one. No tolerance. |
| MeshLab, Merge Close Vertices | Anything nearer than a threshold. Documented as “like a unify duplicated vertices but with some tolerance”, with the default described as 1/10000 of the bounding box diagonal. |
| Blender, Merge by Distance | Each cluster of vertices closer to each other than a given distance, merged together. A cluster rule rather than a pairwise one. |
| trimesh, the library we measured with | Coordinates rounded to a number of decimal places, with equal results treated as the same point. |
(Sources: the pymeshlab filter list for both MeshLab filters, and the Blender manual for Merge by Distance.)
Note that MeshLab alone ships two of these. Running the wrong one of the two, on a file with any noise at all, changes almost nothing and leaves you looking at a report full of holes.
What to do with a number a tool gives you
Ask what the tool did before it counted. A hole count without a stated merge rule is not a property of the file. Two reports that disagree are usually two different questions, both answered correctly.
Do not trust an exact-match count on anything that has been converted. If a report says every triangle is its own shell, that is the signature of a strict reader on a noisy file, not a broken model.
If you can choose the format, choose one that stores the connections. This whole class of ambiguity is absent from PLY, OBJ and GLB. For printing specifically, 3MF also carries them, along with the unit of length that STL does not record either.
When a file resists every tool, suspect the window has closed. If the noise in the coordinates is within an order of magnitude of the smallest real feature, no merge distance separates repair from damage. At that point the fix is upstream: re-export at higher precision, or go back to the source geometry rather than the mesh.
Our own STL viewer reports boundary edges, shells and watertightness for a file you drop into it, and merges with a tolerance that scales to the model’s own size rather than a fixed number, for the reason the table above shows: a tolerance that is sensible on a 100 mm part means something completely different on a 10 mm one. If you need to fix what it finds rather than just see it, how to repair a broken STL covers the order of operations, and non-manifold edges covers the failure that sealing holes does not clear.
Method
Control object: an icosphere at subdivision level 4, scaled to 100 mm across, closed and manifold by construction, giving known truth values. Built and measured with trimesh 5.1.0 and numpy 2.5.2 on 8 September 2026. The STL was read with processing disabled so no merging was applied on load, then merged at each tolerance in turn and re-measured. Coordinate noise was added as independent Gaussian offsets on every stored vertex, seeded for reproducibility. Shell counts come from connected component analysis, not from watertight splitting.
Tolerances are quoted as absolute distances in millimetres. Because the merge implementation converts a tolerance to a number of decimal places, only powers of ten produce distinct behaviour, so the sweep stops at 1 mm rather than repeating one measurement under several labels.
Not measured here: what any named tool actually reports. MeshLab and Blender behaviour above is quoted from their own documentation, not observed, and none of the numbers in the tables were produced by either.
Full results, method and limitations: The merge tolerance that decides whether an STL reads as watertight.
The raw output is published as JSON, produced
by scripts/measure-weld-tolerance.py.
Frequently asked questions
Why do two programs report different numbers of holes in the same STL?
Because an STL does not record which triangles share a corner. It stores every triangle as three independent points, so a reader has to reconstruct the connections by deciding which coordinates count as the same point. That decision is a per-tool choice, and the tools do not make it the same way. On our test file, reading it with no reconstruction at all reported 15,360 boundary edges and 5,120 separate shells for a sphere that is actually closed, has zero boundary edges and is a single shell.
What does merging or welding vertices actually do?
It collapses coordinates that are close enough into one shared point, which is what turns a pile of loose triangles back into a surface with real connections. Until that happens, topology checks are meaningless: every triangle looks like an isolated island with three open edges. This is why a repair tool can report thousands of holes in a file that prints perfectly, and why the same file can come back clean from a different tool.
What tolerance should I use when merging vertices?
It has to be larger than the coordinate noise in the file and smaller than the smallest real feature, and that window can be narrow or entirely closed. On our sphere, whose shortest edge is 3.46 mm, coordinate noise of a millionth of a millimetre still needed a tolerance of 0.1 mm to fully recover the mesh. With noise of 0.01 mm no tolerance we tested recovered it: at 1 mm the mesh still showed 592 boundary edges and 5 shells.
Does MeshLab merge vertices by default?
MeshLab ships two separate filters that answer the question differently. Remove Duplicate Vertices merges only vertices with identical coordinates, with no tolerance at all. Merge Close Vertices merges anything nearer than a threshold, documented as defaulting to 1/10000 of the bounding box diagonal. Which one you run changes the answer you get, and on a file with any coordinate noise the exact-match filter changes almost nothing.
Which 3D file formats avoid this problem?
Any format that stores an explicit vertex index, which is most of them. We exported the same sphere to PLY, OBJ and GLB and read each back with no merging applied: all three reported 2,562 vertices, zero boundary edges and a watertight mesh, matching the source exactly. STL was the only format in the test that needed reconstruction before it could be analysed at all.