Skip to content

Creating meshes

Primitives, parametric surfaces, sweeps and triangulations from ordito.creation and ordito.polyline.

Primitive gallery

Sixteen builders from ordito.creation, each returning a vertex buffer and a flat face buffer on the requested device: box, icosphere, uv_sphere, capsule, cone, cylinder, annulus, torus, the Platonic solids (tetrahedron, octahedron, dodecahedron, icosahedron), the superquadrics super_ellipsoid and super_toroid, and the two open patches sphere_cap and grid. is_watertight confirms which of them are closed.

import math

import ordito as od

c = od.creation
shapes = {
    "box": c.box(extents=(1.6, 1.2, 1.0), device=device),
    "icosphere": c.icosphere(2, device=device),
    "uv_sphere": c.uv_sphere(count=(12, 20), device=device),
    "capsule": c.capsule(height=1.2, radius=0.5, count=(16, 16), device=device),
    "cone": c.cone(0.8, 1.6, sections=24, device=device),
    "cylinder": c.cylinder(0.7, 1.6, sections=24, device=device),
    "annulus": c.annulus(0.5, 1.0, height=0.6, sections=32, device=device),
    "torus": c.torus(1.0, 0.35, 32, 16, device=device),
    "tetrahedron": c.tetrahedron(device=device),
    "octahedron": c.octahedron(device=device),
    "dodecahedron": c.dodecahedron(device=device),
    "icosahedron": c.icosahedron(device=device),
    "super_ellipsoid": c.super_ellipsoid(0.3, 0.6, device=device),
    "super_toroid": c.super_toroid(0.3, 1.0, device=device),
    "sphere_cap": c.sphere_cap(math.pi / 3, subdivisions=3, device=device),
    "grid": c.grid(count=(9, 9), extents=(2.0, 2.0), device=device),
}
closed = [name for name, (v, f) in shapes.items() if od.validation.is_watertight(v, f)]
print(f"{len(closed)} of {len(shapes)} are watertight")
print("open:", ", ".join(name for name in shapes if name not in closed))
Output
14 of 16 are watertight
open: sphere_cap, grid

Primitive gallery

Modelled on: trimesh: creation · Open3D: mesh primitives · PyVista: geometric objects

Parametric surfaces

parametric_surface samples sixteen classical surfaces: minimal surfaces, surfaces of revolution and immersions of non-orientable ones. Seams and poles are glued in the index buffer, so the topology is exact at any resolution: a Trimesh reports the Euler characteristic, the number of boundary loops and whether the surface is orientable. Each face is coloured by the side that faces the camera, green for the front of its winding and blue for the back. On a non-orientable surface the two colours meet along a seam that no consistent winding can remove.

import ordito as od

kinds: list[od.creation.ParametricSurfaceKind] = [
    "bohemian_dome", "bour", "boy", "catalan_minimal", "conic_spiral", "cross_cap",
    "dini", "enneper", "figure8_klein", "henneberg", "klein", "kuen",
    "mobius", "plucker_conoid", "pseudosphere", "roman",
]  # fmt: skip
surfaces = {}
for kind in kinds:
    surfaces[kind] = od.Trimesh(*od.creation.parametric_surface(kind, 80, 80, device=device))
closed = [k for k, m in surfaces.items() if not m.boundary_loops]
print("closed:", ", ".join(closed))
print("non-orientable:", ", ".join(k for k, m in surfaces.items() if not m.is_orientable))
Output
closed: bohemian_dome, boy, cross_cap, figure8_klein, roman
non-orientable: boy, cross_cap, figure8_klein, henneberg, mobius, roman

Parametric surfaces

Modelled on: PyVista: parametric objects

Revolve, extrude and sweep

Three ways to turn a 2-D outline into a solid. revolve spins a profile about the Z axis: an open profile that starts and ends on the axis gives a closed vase, and a closed section revolved through part of a turn gets capped ends. extrude_polygon triangulates a star and raises it into a prism. sweep_polygon carries the same star along a trefoil knot, rolling it twice around the path's tangent on the way.

import numpy as np
import warp as wp

import ordito as od

def vec2(points: np.ndarray) -> wp.array[wp.vec2]:
    return wp.array(points, dtype=wp.vec2, device=device)

# A vase: radius against height, from the axis at the bottom to the axis at the top.
height = np.linspace(0.0, 2.0, 40)
radius = 0.55 + 0.25 * np.sin(2.6 * height + 0.6) - 0.1 * height
profile = np.concatenate([[[0.0, 0.0]], np.stack([radius, height], 1), [[0.0, 2.0]]])
vase = od.creation.revolve(vec2(profile), sections=64)

# A closed section revolved through three quarters of a turn, with capped ends.
t = np.linspace(0.0, 2.0 * np.pi, 48, endpoint=False)
section = np.stack([1.0 + 0.3 * np.cos(t), 0.5 * np.sin(t)], 1)
ring = od.creation.revolve(vec2(section), angle=1.5 * np.pi, cap=True, sections=48)

angle = np.linspace(0.0, 2.0 * np.pi, 10, endpoint=False)
radii = np.where(np.arange(10) % 2, 0.45, 1.0)
star = np.stack([radii * np.cos(angle), radii * np.sin(angle)], 1)
prism = od.creation.extrude_polygon(vec2(star), 0.5)

s = np.linspace(0.0, 2.0 * np.pi, 301)
knot = np.stack(
    [np.sin(s) + 2 * np.sin(2 * s), np.cos(s) - 2 * np.cos(2 * s), -np.sin(3 * s)], 1
)
swept = od.creation.sweep_polygon(
    vec2(0.45 * star),
    wp.array(knot, dtype=wp.vec3, device=device),
    angles=wp.array(np.linspace(0.0, 4.0 * np.pi, 301), dtype=wp.float32, device=device),
)
for name, (v, f) in [("vase", vase), ("ring", ring), ("prism", prism), ("knot", swept)]:
    print(
        f"{name:>5}: {f.shape[0] // 3:5d} faces, watertight {od.validation.is_watertight(v, f)}"
    )
Output
 vase:  5120 faces, watertight True
 ring:  4700 faces, watertight True
prism:    36 faces, watertight True
 knot:  6000 faces, watertight True

Revolve, extrude and sweep

Modelled on: trimesh: creation · PyVista: extrude rotate · MeshLib: extrude

Polygon triangulation

triangulate_polygon fills a simple 2-D polygon by ear clipping on the device, using only the polygon's own vertices: an n-gon becomes n - 2 triangles. extrude_triangulation raises the triangulation into a watertight solid, and polyline_triangulate does the same filling for a closed planar loop in 3-D, whatever plane it lies in. Interior rings (holes) are not supported, so the gear's bore is left out.

import numpy as np
import warp as wp

import ordito as od

# A 16-tooth gear outline, counter-clockwise.
teeth, steps = 16, np.array([0.0, 0.18, 0.5, 0.68])
angle = 2 * np.pi * (np.arange(teeth)[:, None] + steps).ravel() / teeth
radius = np.tile([0.8, 1.0, 1.0, 0.8], teeth)
outline = np.stack([radius * np.cos(angle), radius * np.sin(angle)], axis=1)

ring, faces = od.polyline.triangulate_polygon(wp.array(outline, dtype=wp.vec2, device=device))
print(f"{ring.shape[0]} vertices -> {faces.shape[0] // 3} triangles")
solid = od.creation.extrude_triangulation(ring, faces, height=0.3)
print(f"extruded: watertight {od.validation.is_watertight(*solid)}")

# The same outline on a tilted plane in 3-D.
tilt = od.transform.matrix_to_numpy(od.transform.rotation_matrix((1.0, 1.0, 0.0), 0.9))[:3, :3]
loop = np.c_[outline, np.zeros(len(outline))] @ tilt.T
loop_faces = od.polyline.polyline_triangulate(wp.array(loop, dtype=wp.vec3, device=device))
print(f"tilted loop: {loop_faces.shape[0]} triangles")
Output
64 vertices -> 62 triangles
extruded: watertight True
tilted loop: 62 triangles

Polygon triangulation

Modelled on: libigl 604 · MeshLib: contour triangulation · trimesh: creation

Terrain triangulation

Scattered survey points of a height field are triangulated in the plane by delaunay_triangulation, which returns the triangulation that maximizes the smallest angle. Its faces index the input points, so lifting them back to their heights turns the 2-D triangulation into the terrain surface (a 2.5-D mesh).

import warp as wp

import ordito as od
from examples import data

points = data.load_points("terrain_points", device)
plan = wp.array(points.numpy()[:, :2], dtype=wp.vec2, device=device)
faces = od.reconstruction.delaunay_triangulation(plan)
terrain = od.Trimesh(points, faces)
print(f"{points.shape[0]} points -> {terrain.n_faces} triangles")
print(f"boundary loops: {len(terrain.boundary_loops)}, Euler {terrain.euler_characteristic}")
Output
4000 points -> 7973 triangles
boundary loops: 1, Euler 1

Terrain triangulation

Modelled on: PyVista: delaunay_2d · MeshLib: terrain triangulation