Skip to main content

Mesh & Cloud Comparison

VRGS provides two algorithms for measuring how one surface differs from another: Difference Map (closest-point Euclidean distance) and M3C2 (Multiscale Model-to-Model Cloud Comparison). Both produce per-vertex attributes on the source object that can be displayed, analysed in histograms, exported, or used as input to further attribute calculations.

This guide covers when to use each method, how to choose parameters, and how to interpret the results.

Quick start

Right-click a mesh, choose M3C2 Comparison..., accept the auto-suggested scales, and click OK. The signed distance and 95% level-of-detection appear as new attributes on the source mesh.

Choosing between Difference Map and M3C2

Difference MapM3C2
Distance typeUnsigned, EuclideanSigned, along the local surface normal
OutputOne attribute (Compare with <target>)Two attributes (M3C2 distance <target>, M3C2 LoD95 <target>)
Quantifies uncertainty?NoYes — 95% level of detection
Robust to noise?No — closest point flips between adjacent neighboursYes — averages a cylinder of points on each side
Sensitive to point spacing?YesLess so, because the projection scale (d/2) decouples from local sampling
SpeedFasterSlower (two BSP queries + a PCA fit per source point)
Best forCoarse "how far apart are they?" sanity checksQuantitative monitoring of change between two surveys

If you want a quick visualisation of "where do these two surfaces disagree?", use Difference Map. If you want to know whether the difference is real — i.e. larger than the combined noise of the two surveys — use M3C2.

Difference Map (closest-point)

For each vertex of the source object, finds the nearest vertex on the target object and writes that Euclidean distance to a LAYER_FLOAT attribute named Compare with <target name>.

Workflow

  1. Have at least two meshes loaded into memory.
  2. Right-click the source mesh in the project tree.
  3. Hover over Difference Map — a submenu lists every other in-memory mesh.
  4. Click the target mesh.

The visitor runs in the background; progress is reported in the debug log. When complete, the new attribute appears in the source mesh's attribute list and the active colour ramp can be switched to it.

Caveats

  • Distances are unsigned. A point 2 m above the target reads the same as a point 2 m below.
  • Closest-point matching is brittle at edges, holes and partial overlaps. A vertex that has no real correspondent in the target will still match the nearest available point — often producing a fictitious large distance. Mask or trim edges before using the result for monitoring.
  • Only the source object's m_Limits bounding box is used to skip points that are clearly outside the target. There is no other prefiltering.

M3C2 — Multiscale Model-to-Model Cloud Comparison

Implements the algorithm of Lague, Brodu & Leroux (2013). Conceptually:

  1. Around each source ("core") point P, fit a plane to the source neighbours within a radius D/2. The plane's smallest-eigenvector is the local normal N.
  2. Build a cylinder centred on P, axis along N, radius d/2, and half-length h.
  3. Project all source points inside the cylinder onto the axis. Their mean offset is μ₁, variance σ₁², count n₁.
  4. Do the same on the target cloud → μ₂, σ₂², n₂.
  5. Output:
    • Signed distance = μ₂ − μ₁ — positive when the target lies on the +N side of the source.
    • Level of detection at 95% = 1.96 · √(σ₁²/n₁ + σ₂²/n₂) — the smallest distance that can be distinguished from noise.

The two outputs are written as LAYER_FLOAT attributes M3C2 distance <target> and M3C2 LoD95 <target> on the source object. Points where the cylinder is empty in either cloud receive NaN and appear as missing data in colour ramps and histograms.

Workflow

  1. Have the source object selected and the target object loaded into memory. Both can be a triangulated mesh or a point cloud — meshes are treated as a cloud of their vertices.
  2. Right-click the source mesh in the project tree.
  3. Choose M3C2 Comparison....
  4. In the dialog:
    • Confirm the source name (read-only).
    • Pick a target from the combo (lists every other in-memory mesh and point cloud).
    • Accept or edit the three scales. Defaults are seeded from the source bounding-box diagonal:
      • Normal radius (D/2) ≈ diagonal / 100
      • Projection radius (d/2) ≈ diagonal / 200
      • Max distance ≈ diagonal / 20
  5. Click OK.

The visitor runs in the background. The two attributes appear when it finishes; switch the active colour ramp to M3C2 distance <target> to visualise the signed result.

Cancellation

There is no cancel button while M3C2 is running. For a 1 M-point source expect the order of seconds to a minute on a modern multi-core CPU, depending on neighbourhood density and the sizes you have set.

Parameters explained

The three scales are independent — they are not internally clamped to each other — but their ratios matter. Lague's recommended starting points are reproduced below.

Normal radius (D/2)

Radius of the source neighbourhood used for the PCA plane fit. The local normal is taken from the smallest-variance direction of the points inside this sphere.

Pick D/2 so that:

  • It is several times the local point spacing (typically 20–30 × spacing). Too small and the normal is dominated by noise; the algorithm rejects fits where fewer than 3 neighbours are found.
  • It is smaller than the surface curvature scale you care about. Setting D/2 too large smooths over real geomorphic features and makes the normal point in an averaged direction, which biases the signed distance.

Rule of thumb: start with the auto-suggested value (≈ diagonal/100) and increase it 2× at a time if the resulting distance map looks noisy along clearly continuous surfaces.

Projection radius (d/2)

Radius of the cylinder used to average source and target points along the local normal. Determines how many points contribute to μ₁ and μ₂.

Pick d/2 so that:

  • It is larger than the worst-case point spacing in either cloud, so the cylinder rarely empties.
  • It is smaller than the smallest feature you want to resolve. With d/2 = 1 m you cannot map a 50 cm fault scarp.

Rule of thumb: d/2 around D/4 to D/2 works well. Smaller cylinders give sharper results but higher noise (LoD95 grows like 1/√n).

Max distance

Half-length of the projection cylinder. Points whose projection onto N falls more than this distance from P are excluded from the averages. This serves three purposes:

  • Caps the BSP search radius — a tight Max distance makes M3C2 much faster.
  • Rejects the wrong surface in scenes where two unrelated surfaces are close (e.g. ground and overhanging foliage).
  • Bounds the maximum reportable distance.

Pick it as the largest plausible change between the two surveys, plus a margin. For glacier or coastal monitoring with metre-scale change set it to a few metres; for precise registration QA it can be a few centimetres.

Sign convention

The signed distance is μ₂ − μ₁ projected onto N. The PCA normal's sign is arbitrary at each point, so sign consistency along a surface is not guaranteed: the same physical "uplift" may appear as +2 cm at one core point and −2 cm at a neighbouring one if the local normal flips.

In practice, surfaces that are roughly co-oriented (within ~30°) produce consistent signs because PCA picks the same hemisphere. For more divergent surfaces, take the absolute value of the M3C2 distance for visualisation, or post-process by flipping signs against a reference direction.

Output attributes

After M3C2 completes, the source object has two new node attributes:

AttributeMeaningUnits
M3C2 distance <target>Signed distance (μ₂ − μ₁) along the local normalmodel units
M3C2 LoD95 <target>95% level of detectionmodel units

To visualise, set the source mesh's active attribute to M3C2 distance <target> and apply a diverging colour ramp (blue–white–red works well for signed change). Bring up the attribute histogram to see the distance distribution and the magnitude of the LoD95.

A common interpretation rule: a vertex's distance is significant only when |distance| > LoD95. Many M3C2 publications threshold the distance map against LoD95 and display only the significant points. You can replicate this in VRGS by using the Attribute Calculator to compute mask = (abs(distance) > LoD95) and gating the display by that mask.

Working with point clouds

Both the source and target can be point clouds, not just meshes. The right-click entry currently lives on mesh items, but the target combo in the dialog lists every in-memory mesh and point cloud, so you can compare a mesh to a cloud (e.g. mesh-from-photogrammetry vs raw laser-scan cloud) directly.

If you want a cloud as the source — e.g. compare a point cloud re-survey against a baseline mesh — invoke the visitor from a place that exposes it on point cloud items. (The underlying visitor accepts any CPointCloudBase; only the menu wiring is currently mesh-only.)

Performance tips

  • Tighter Max distance is the single biggest knob — every BSP query is bounded by √(d/2² + Max²), so halving Max cuts the cylinder bounding-sphere area roughly 4×.
  • Match D/2 and d/2 to the local sampling. Over-large radii return more points per cylinder than M3C2 needs and slow each step.
  • Decimate dense clouds before running M3C2 if you only need representative coverage. The algorithm scales linearly in source vertices and roughly linearly in cylinder occupancy.
  • The work parallelises across source points using std::execution::par, so multi-core CPUs help. There is no GPU path.

Common pitfalls

"All my LoD95 values are huge." Either the projection radius is too small (n is tiny so var/n explodes) or one of the clouds is genuinely very noisy. Increase d/2 first; if the LoD95 is still large, the change you are trying to detect is below the combined noise floor.

"The distance map is grainy/striped." The PCA normal is unstable. Increase D/2.

"Signed distance flips sign across a flat surface." Expected behaviour — see Sign convention above. Use abs() for visualisation, or run M3C2 with a reference direction enforced (a future feature; for now, flip via attribute calculator using a known reference vector).

"Lots of NaN holes." The cylinder is empty in one of the clouds at those points. Either increase d/2 or check that the two clouds actually overlap there. NaN at the boundary of a partial-overlap survey is correct — those points have no comparable target.

"It's much slower than Difference Map." Expected — M3C2 does two range queries plus a PCA fit per core point, versus one nearest-neighbour query for Difference Map. The trade-off is quantified uncertainty.

Algorithm and reference

Internally the M3C2 visitor:

  1. Builds a BSP tree on each cloud.
  2. For each source vertex, queries the source tree at radius D/2 and feeds the result to the PCA plane fit (DFNCore_PlaneFit::FitPlane).
  3. Queries each tree at the cylinder's bounding-sphere radius √(d/2² + h²), then applies the per-point cylinder filter and accumulates mean and population variance.
  4. Combines the two per-cloud statistics into the signed distance and LoD95 in a free-standing math kernel (m3c2::ComputeAtPoint) that is unit-tested independently of the visitor.

Reference

Lague D., Brodu N., Leroux J. (2013). Accurate 3D comparison of complex topography with terrestrial laser scanner: application to the Rangitikei canyon (N-Z). ISPRS Journal of Photogrammetry and Remote Sensing 82: 10–26. doi:10.1016/j.isprsjprs.2013.04.009

See also

  • Mesh context menu — full list of mesh right-click commands, including Difference Map and M3C2 Comparison....
  • Attributes — how per-vertex attributes are stored, displayed and combined.