rf-geom / ridgeforge Public

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;
}
Expect the chain to stall

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
FlagApplies toMeaning
--target-tris Ndecimateabsolute triangle target
--target-ratio Fdecimatefraction of the input, 0 to 1
--max-error Fdecimatequadric error ceiling
--ridgesdecimatelock sharp edges while collapsing
--ridge-angle DEGdecimate, ridgesdihedral threshold, default 40
--min-edges Nridgesdiscard chains shorter than this
--no-preserve-boundarydecimate, remeshallow boundary vertices to move
--weld TOLcleanweld tolerance in model units
--min-area Fcleandegenerate triangle threshold
--fill-holes Ncleanclose loops up to N edges
--min-component Ncleandrop components below N triangles
--edge-length Fremeshtarget edge length, 0 means the input mean
--iterations Nremeshdefault 5
--self-intersectchecksearch for intersecting pairs
--max-pairs Ncheckstop after N pairs; 1 makes it a gate
--jobs Nallthread count, 0 means all cores
--format obj|ply|stlalloverride detection by extension
--asciiallwrite ascii where the format allows it
--quiet, --verboseallsuppress or expand the progress line

Exit codes

CodeMeaning
0success, and for check it also means nothing was found
1bad usage: unknown flag, missing argument, contradictory options
2input could not be read or parsed
3output could not be written
4interrupted
5the 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.