Skip to content

Mesh Remeshing¤

Mesh Remeshing

This example demonstrates how to remesh a triangle mesh with COMPAS CGAL, and the three ways trimesh_remesh can treat the boundary of an open mesh.

A RhinoVault funicular shell is triangulated and remeshed to the same coarse target edge length three ways, shown side by side (each drawn over the faint original for reference):

  • Left — protect_boundary=False (default): boundary edges are re-sampled to the target length, so the shell's open perimeter is coarsened and its corners rounded.
  • Middle — protect_boundary=True: every boundary edge is constrained, so the whole perimeter is preserved verbatim while the interior is coarsened.
  • Right — keep_points=<4 corners>: only the four corner vertices are pinned (matched by coordinate). The rest of the boundary is still re-sampled, but those four points (shown in red) survive exactly.

protect_boundary and keep_points are complementary: the first keeps the entire boundary curve, the second keeps only the specific vertices you name. The companion parameter protect_sharp_edges_angle_deg similarly constrains interior feature edges whose dihedral angle exceeds a threshold.

Key Features:

  • Loading PLY mesh files
  • Remeshing with a target edge length via trimesh_remesh
  • Preserving the whole boundary with protect_boundary
  • Pinning only specific vertices with keep_points
  • Side-by-side visualization of the three results over the original
from pathlib import Path

from compas.datastructures import Mesh
from compas.geometry import Pointcloud
from compas.geometry import Translation
from compas_viewer import Viewer
from compas_viewer.scene.tagobject import Tag

from compas_cgal.meshing import trimesh_remesh

# =============================================================================
# Input mesh
# =============================================================================

input_file = Path(__file__).parent.parent.parent / "data" / "rhinovault_mesh_0.ply"

mesh = Mesh.from_ply(input_file)
mesh.quads_to_triangles()

VF = mesh.to_vertices_and_faces()

# =============================================================================
# Footprint bounds + the four base corners
# =============================================================================

coords = mesh.vertices_attributes("xyz")
xmin = min(x for x, _, _ in coords)
xmax = max(x for x, _, _ in coords)
ymin = min(y for _, y, _ in coords)
ymax = max(y for _, y, _ in coords)
zmax = max(z for _, _, z in coords)

# The four base corners: the boundary vertices nearest the xy-extremes of the
# footprint. Their coordinates are what we hand to ``keep_points``.
boundary_xyz = [mesh.vertex_coordinates(v) for v in mesh.vertices_on_boundary()]


def nearest_corner(tx, ty):
    return min(boundary_xyz, key=lambda p: (p[0] - tx) ** 2 + (p[1] - ty) ** 2)


corners = [
    nearest_corner(xmin, ymin),
    nearest_corner(xmax, ymin),
    nearest_corner(xmax, ymax),
    nearest_corner(xmin, ymax),
]

# =============================================================================
# Remesh three ways to a coarse target (larger than the input boundary spacing)
# =============================================================================

TARGET_EDGE_LENGTH = 3
ITERATIONS = 10

# 1) Default: the open perimeter is coarsened and its corners rounded.
V0, F0 = trimesh_remesh(VF, TARGET_EDGE_LENGTH, ITERATIONS, protect_boundary=False)

# 2) protect_boundary=True: every boundary edge is constrained, so the whole
#    perimeter is preserved verbatim while the interior is coarsened.
V1, F1 = trimesh_remesh(VF, TARGET_EDGE_LENGTH, ITERATIONS, protect_boundary=True)

# 3) keep_points: only the four corners are pinned — the rest of the boundary
#    is still re-sampled, but those four vertices survive exactly.
V2, F2 = trimesh_remesh(VF, TARGET_EDGE_LENGTH, ITERATIONS, keep_points=corners)

# =============================================================================
# Visualize — three panels side by side, each over the faint original
# =============================================================================

dx = 1.3 * (xmax - xmin)  # offset between panels
xmid = 0.5 * (xmin + xmax)

viewer = Viewer(width=1600, height=900)

labels = ["protect_boundary = False", "protect_boundary = True", "keep_points = 4 corners"]
for i, (V, F) in enumerate([(V0, F0), (V1, F1), (V2, F2)]):
    shift = Translation.from_vector([i * dx, 0.0, 0.0])
    remeshed = Mesh.from_vertices_and_faces(V.tolist(), F.tolist()).transformed(shift)
    original = mesh.transformed(shift)
    viewer.scene.add(original, show_points=False, opacity=0.25)
    viewer.scene.add(remeshed, show_points=True)
    viewer.scene.add(Tag(labels[i], (xmid + i * dx, ymin - 2.0, zmax + 1.0), height=30))

# Emphasize the four kept corners on the third panel.
kept = Pointcloud(corners).transformed(Translation.from_vector([2 * dx, 0.0, 0.0]))
viewer.scene.add(kept, pointcolor=(1.0, 0.0, 0.0), pointsize=20, show_points=True)

viewer.show()