examples
Six programs and one command line
Each program below compiles as it stands against 0.9.4. They are in the release under examples/, and the test suite builds all six so that they cannot rot silently.
1. Look before you act
Walkthrough: output, failure modes, variations
Most bad results come from running the wrong operation on a mesh nobody measured. One pass of Stats answers whether the mesh is even suitable for what you planned.
#include <ridgeforge/io.hpp>
#include <ridgeforge/stats.hpp>
#include <cstdio>
int main(int argc, char** argv) {
if (argc < 2) return 1;
auto m = rf::io::read(argv[1]);
if (!m) { std::fprintf(stderr, "%s\n", m.message()); return 2; }
const rf::Stats s = rf::Stats::of(*m);
std::printf("%zu verts %zu tris %zu components\n",
s.vertices, s.triangles, s.components);
std::printf("closed=%d manifold=%d oriented=%d loops=%zu\n",
s.closed, s.manifold, s.oriented, s.boundary_loops);
if (s.closed && s.manifold)
std::printf("genus %d, volume %.4f\n", s.genus, s.volume);
if (s.non_manifold_edges)
std::printf("warning: %zu non-manifold edges\n", s.non_manifold_edges);
return 0;
}
The two conditionals are the point. Printing s.genus unconditionally is how a report ends up claiming that a scanned car door has genus −4318.
2. Clean a scan
Walkthrough: output, failure modes, variations
Order matters here, and it is always the same order: weld first so that adjacency exists at all, then throw away what is degenerate, then make the winding agree, and only then close holes.
#include <ridgeforge/io.hpp>
#include <ridgeforge/repair.hpp>
#include <ridgeforge/stats.hpp>
int main() {
auto loaded = rf::io::read("torso_raw.stl"); // STL has no sharing
if (!loaded) return 2;
rf::Mesh& m = *loaded;
const rf::Scalar diag = m.bounds().extent().x; // scale reference
rf::Repair::weld_vertices(m, diag * 1e-5f);
rf::Repair::remove_degenerate(m, diag * diag * 1e-10f);
rf::Repair::remove_duplicate_triangles(m);
rf::Repair::remove_small_components(m, 50);
if (auto r = rf::Repair::orient_consistently(m); !r)
std::fprintf(stderr, "orientation: %s\n", r.message());
rf::Repair::fill_holes(m, 400); // loops up to 400 edges
return rf::io::write(m, "torso_clean.ply") ? 0 : 3;
}
Both tolerances are relative to the model's own size, which is the only way to write a pipeline that survives a change of units. The literal 50 is a judgement about speckle, not a general constant: on a mesh of small genuine parts it would delete the parts.
3. An LOD chain
Walkthrough: output, failure modes, variations
Three levels, each decimated from the previous one rather than from the original, which keeps the chain consistent and is also four times faster.
#include <ridgeforge/decimate.hpp>
#include <ridgeforge/io.hpp>
#include <string>
int main() {
auto loaded = rf::io::read("bracket.ply");
if (!loaded) return 2;
rf::Mesh m = std::move(*loaded);
rf::DecimateOptions opt;
opt.preserve_boundary = true;
opt.preserve_ridges = true; // a CAD part: keep the arrises
opt.ridge_angle_deg = 25.0f; // lower than default, sharper part
opt.max_aspect_ratio = 12.0f;
for (int level = 1; level <= 3; ++level) {
opt.target_ratio = 0.4f; // 40 % of the current level
rf::Decimator dec{opt};
auto rep = dec.run(m);
if (!rep) return 5;
std::printf("lod%d: %zu tris, error %.3e, target %s\n", level,
rep->triangles_after, rep->max_applied_error,
rep->target_reached ? "met" : "blocked by locks");
rf::io::write(m, "bracket_lod" + std::to_string(level) + ".ply");
}
return 0;
}
With preserve_ridges on, a part made mostly of sharp edges reaches a floor where almost every vertex is locked. The third level then prints blocked by locks and comes out larger than 6.4 % of the input. That is the correct behaviour; if you need to go below the floor, raise ridge_angle_deg for the coarse levels instead of turning preservation off entirely.
4. Ridges to a polyline file
Walkthrough: output, failure modes, variations
OBJ can hold lines as well as faces, so the extracted ridges can be written as a file that any viewer will display over the surface.
#include <ridgeforge/io.hpp>
#include <ridgeforge/ridge.hpp>
#include <cstdio>
int main() {
auto loaded = rf::io::read("statue.ply");
if (!loaded) return 2;
rf::RidgeOptions ro;
ro.min_angle_deg = 42.0f; // scan noise shows up below ~40
ro.min_edges = 8;
ro.include_boundary = false;
auto set = rf::RidgeExtractor{ro}.run(*loaded);
if (!set) return 5;
std::FILE* f = std::fopen("statue_ridges.obj", "w");
if (!f) return 3;
for (const rf::Vec3& p : loaded->positions)
std::fprintf(f, "v %.6g %.6g %.6g\n", p.x, p.y, p.z);
for (const auto& line : set->lines) {
std::fputs("l", f);
for (rf::Index v : line.vertices) std::fprintf(f, " %u", v + 1u);
if (line.closed) std::fprintf(f, " %u", line.vertices[0] + 1u);
std::fputc('\n', f);
}
std::fclose(f);
return 0;
}
Two details that bite: OBJ indices start at one, and a closed polyline does not repeat its first vertex in vertices, so the loop has to close it explicitly.
5. Report self-intersections
Walkthrough: output, failure modes, variations
#include <ridgeforge/io.hpp>
#include <ridgeforge/selfint.hpp>
int main(int argc, char** argv) {
auto m = rf::io::read(argv[1]);
if (!m) return 2;
rf::SelfIntersection::FindOptions fo;
fo.ignore_adjacent = true;
fo.max_pairs = 1000; // enough to decide, cheap to get
auto pairs = rf::SelfIntersection::find(*m, fo);
if (!pairs) return 5;
std::printf("%zu intersecting pairs%s\n", pairs->size(),
pairs->size() == fo.max_pairs ? " (truncated)" : "");
for (std::size_t i = 0; i < pairs->size() && i < 10; ++i)
std::printf(" tri %u x tri %u\n", (*pairs)[i].a, (*pairs)[i].b);
return pairs->empty() ? 0 : 5;
}
Capping max_pairs turns an unbounded question into a bounded one. For a yes-or-no gate in a pipeline, set it to one.
6. A time budget
Walkthrough: output, failure modes, variations
The progress callback is also the cancellation channel, which makes a deadline four lines of code.
#include <ridgeforge/decimate.hpp>
#include <chrono>
int main() {
using clock = std::chrono::steady_clock;
const auto deadline = clock::now() + std::chrono::seconds(5);
rf::DecimateOptions opt;
opt.target_ratio = 0.05f;
opt.progress = [deadline](float frac, const char* stage) {
if (clock::now() < deadline) return true;
std::fprintf(stderr, "out of time at %.0f%% in %s\n", frac * 100, stage);
return false; // -> Status::Cancelled
};
// ... load, run, and accept a partially decimated but valid mesh
}
A cancelled decimation leaves a valid mesh with fewer collapses applied. That is the whole reason cancellation is safe to use in a batch job: the worst case is a coarser level than you wanted, never a broken file.
The rfmesh tool
Built with -DRF_BUILD_TOOLS=ON. It covers the common pipelines without writing any C++, and it is the fastest way to produce a reproducer for a bug report.
# what am I looking at
rfmesh info scan.ply
# clean, then decimate to 80k triangles keeping sharp edges
rfmesh clean --weld 1e-5 --min-component 50 --fill-holes 400 scan.ply -o clean.ply
rfmesh decimate --target-tris 80000 --ridges --ridge-angle 35 clean.ply -o lod.ply
# gate a pipeline: non-zero exit when the mesh intersects itself
rfmesh check --self-intersect --max-pairs 1 lod.ply
# ridges as a separate polyline file, 8 threads
rfmesh ridges --ridge-angle 42 --min-edges 8 --jobs 8 statue.ply -o ridges.obj
| Flag | Applies to | Meaning |
|---|---|---|
--target-tris N | decimate | absolute triangle target |
--target-ratio F | decimate | fraction of the input, 0 to 1 |
--max-error F | decimate | quadric error ceiling |
--ridges | decimate | lock sharp edges while collapsing |
--ridge-angle DEG | decimate, ridges | dihedral threshold, default 40 |
--min-edges N | ridges | discard chains shorter than this |
--no-preserve-boundary | decimate, remesh | allow boundary vertices to move |
--weld TOL | clean | weld tolerance in model units |
--min-area F | clean | degenerate triangle threshold |
--fill-holes N | clean | close loops up to N edges |
--min-component N | clean | drop components below N triangles |
--edge-length F | remesh | target edge length, 0 means the input mean |
--iterations N | remesh | default 5 |
--self-intersect | check | search for intersecting pairs |
--max-pairs N | check | stop after N pairs; 1 makes it a gate |
--jobs N | all | thread count, 0 means all cores |
--format obj|ply|stl | all | override detection by extension |
--ascii | all | write ascii where the format allows it |
--quiet, --verbose | all | suppress or expand the progress line |
Exit codes
| Code | Meaning |
|---|---|
| 0 | success, and for check it also means nothing was found |
| 1 | bad usage: unknown flag, missing argument, contradictory options |
| 2 | input could not be read or parsed |
| 3 | output could not be written |
| 4 | interrupted |
| 5 | the operation reported a failure, or check found something |
An unknown flag is an error rather than a warning, deliberately: a batch script that silently ignores --target-trs would write out an unprocessed mesh and claim success.