QSlim¶
qslim simplifies a triangle mesh to a target number of vertices. It collapses the edges of
least quadric error first (Garland and Heckbert 1997),
each to the point that minimizes the error, so flat regions lose their vertices first and sharp
features keep theirs. With an envelope, no collapse may move the surface farther than a given
distance from the input.
Use it to reduce a mesh to a vertex budget while keeping its shape. For a coarse mesh with uniform, well-shaped triangles, use isotropic remeshing instead; shortest edge collapse is the simpler criterion, faster but blind to the shape.
A first run¶
The Octocat (18,944 vertices) down to a tenth of its vertices, inside an envelope of 0.2% of its bounding box diagonal:
{
"application": "qslim",
"input": "Octocat.obj",
"output": "octocat_qslim.obj",
"target_rel": 0.1,
"eps_rel": 0.002,
"num_threads": 4
}
The input path is relative to the JSON file; the output, octocat_qslim.obj, is written to
the working directory.
octocat = wm.read_mesh(str(DATA / "Octocat.obj"))
outputs, report = wm.qslim(
{"input": "octocat", "target_rel": 0.1, "eps_rel": 0.002, "num_threads": 4},
{"octocat": octocat},
)
simplified = outputs["mesh"]
print(len(octocat["V"]), "->", len(simplified["V"]), "vertices")
On files instead, with the same options as the JSON file:
wmtk::MeshMap meshes{{"octocat", wmtk::io::read_mesh(data / "Octocat.obj")}};
auto [outputs, report] = wmtk::components::qslim::qslim(
{{"input", "octocat"}, {"target_rel", 0.1}, {"eps_rel", 0.002}, {"num_threads", 4}},
meshes);
const wmtk::MeshData& simplified = outputs.at("mesh");
std::cout << meshes.at("octocat").V.rows() << " -> " << simplified.V.rows() << " vertices\n";
wmtk::io::write_mesh("octocat_qslim.msh", simplified);
Link wmtk::qslim.
It prints 18944 -> 1891 vertices, give or take a few: see
threads.
The input on the left, the result on the right: drag to rotate, scroll to zoom. Every example is in the gallery.
Key settings¶
The target: target_rel, target_abs¶
target_rel (default 0.1) is the target number of vertices as
a fraction of the input's; target_abs, when positive,
replaces it with an absolute count. The collapses stop when the mesh reaches the target, or when
no collapse is left that the envelope allows, whichever comes first.
The envelope: eps_rel¶
eps_rel is the envelope's thickness, relative to the input's
bounding box diagonal. The default, -1, is no envelope: the collapses go all the way to the
target, wherever that takes the surface. With an envelope, a collapse that would leave part of
the surface outside it is refused, and a thin envelope stops the simplification before the
target. On the Octocat, with target_rel 0.01 (189 vertices):
# The envelope against the target: with a thin envelope the collapses stop before the target.
for eps_rel in [-1, 0.01, 0.002]:
outputs, _ = wm.qslim(
{"input": "octocat", "target_rel": 0.01, "eps_rel": eps_rel, "num_threads": 4},
{"octocat": octocat})
print(f"eps_rel {eps_rel:g}: {len(outputs['mesh']['V'])} vertices")
eps_rel |
vertices left |
|---|---|
| -1 (none) | 188 |
| 0.01 | 267 |
| 0.002 | 1,139 |
So the envelope is what decides how far a simplification can go while the result still approximates the input; the target is an upper bound.
The open boundary: boundary_eps_rel¶
On a mesh with an open boundary, the surface envelope alone holds the boundary loosely: the
outline can retract along the surface. A positive
boundary_eps_rel adds a tube around the input's
boundary that every boundary edge must stay in, and places collapses at the boundary on the
outline. Set it to about eps_rel when the outline matters.
The envelope test: use_sample_envelope¶
The envelope test is exact by default. use_sample_envelope
switches to a sampled test, which is much faster on pathological inputs, where one triangle
covers millions of tiny ones (some Thingi10K models). The envelope is the same; the test is
approximate.
Threads and reproducibility¶
The envelope test is most of the time: on the Octocat with eps_rel 0.002, one thread takes
11.1 s, 8 threads 1.5 s. Without an envelope the same run takes 0.5 s. The examples set
num_threads to 4. With more than one thread, the order of
the collapses depends on the schedule, and the result changes slightly from run to run (1,887 to
1,894 vertices here); num_threads 0 runs sequentially and gives identical results.
Outputs¶
- The simplified mesh:
outputon the command line (defaultout.obj),outputs["mesh"]in memory. - With
report, a JSON file with the target, the vertex and triangle counts and the run time. - With
throw_on_fail, the run fails when the target is not reached instead of writing the mesh it got to. - With
history_output, the history of the collapses, from which tracking maps points between the input and the output.
Pitfalls¶
- A misspelled option is ignored.
qslimruns with the default of an option it does not know;wmtk_appand the Python bindings log a warning (qslim has no option ...). Editors catch it beforehand with the JSON Schemas. - The target is not reached. The envelope refuses the remaining collapses: thicken it, or
accept the larger mesh.
throw_on_failturns this into an error. - Duplicate vertices. The input is welded first (vertices within 1e-5 of the diagonal are merged), so a mesh written face by face simplifies as one surface.
See also¶
- Reference: every option of
qslim - Shortest edge collapse, isotropic remeshing
- Garland and Heckbert 1997, Surface Simplification Using Quadric Error Metrics