RasmahRasmah

Volume meshing

Overview

A surface mesh stops at the boundary; a volume mesh fills the inside. It is the mesh a finite-element solver actually integrates over, so its quality — the shape of its tetrahedra — directly controls the accuracy and stability of a simulation.

This chapter covers the three ways to build a volume mesh, then the numbers that tell you whether the result is any good.

The main entry point

tetrahedralize turns any shape into a tetrahedral mesh, choosing the kernel for you:

Rasmah.tetrahedralizeFunction
tetrahedralize(g, n; conform=false, method=:marching) -> TetMesh

Volume-mesh an implicit geometry (ImplicitGeometry) or a Feature into a tetrahedral TetMesh. This is the main volume-meshing entry point.

Theory

A volume mesh fills the inside of a shape — the region $s(\mathbf{x}) \le 0$ of its signed distance field — with tetrahedra, the simplest 3D cells. A surface mesh only needs to capture a boundary; a volume mesh must also cover the interior, because that is where a finite-element solver integrates its weak form.

Algorithm

By default (method = :marching) the region is extracted with marching_tetrahedra on an n×n×n grid over the shape's bounding box (plus 5% padding). With conform = true the boundary vertices are additionally snapped onto the true surface and the interior relaxed (conforming_tetmesh). method = :delaunay selects the native Delaunay pipeline (delaunay_tetmesh) instead. A Feature is converted to its SDF first, so the parameterisation stays differentiable.

Keyword options

  • conform = false — snap boundary vertices to the surface and relax the interior.
  • method = :marching:marching (marching tetrahedra) or :delaunay (native Delaunay tetrahedralization).

The n = nothing keyword overload falls back to the shared default_resolution.

Example

julia> t = tetrahedralize(sphere(1.0), 16);

julia> size(t.elements, 2) > 0
true

See also

surface_mesh for the surface counterpart, and marching_tetrahedra / conforming_tetmesh / delaunay_tetmesh for the individual kernels.

source
tetrahedralize(b::BRep, n; conform=false, order=1) -> TetMesh

Tetrahedralize a BRep through its signed-distance field. order = 1 gives the linear mesh; order = 2 inserts edge/face nodes with boundary nodes projected onto the BRep surface (isoparametric quadratic geometry). The returned mesh carries the BRep face sets (see face_sets).

source
vtk(tetrahedralize(sphere(1.0), 12))

Marching tetrahedra

The default kernel samples the SDF on a grid and extracts the region $s \le 0$ as tetrahedra — the volume analogue of marching cubes:

Rasmah.marching_tetrahedraFunction
marching_tetrahedra(f, lo, hi, n; parallel = true) -> TetMesh

Extract the f ≤ 0 region of the SDF-like callable f over [lo, hi] as a tetrahedral mesh using marching tetrahedra on an n×n×n grid.

source
g = to_sdf(sphere(1.0))
lo, hi = bounds(g)
vtk(marching_tetrahedra(x -> g(x), lo, hi, 12))

Boundary-conforming volume meshing

Marching tetrahedra places boundary vertices on grid edges, leaving a slight "staircase" that does not lie on the true surface. Conforming meshing snaps every boundary vertex onto the exact surface and relaxes the interior, so the boundary follows the geometry:

Rasmah.conforming_tetmeshFunction
conforming_tetmesh(f, lo, hi, n; relax = true, iters = 3) -> TetMesh

Boundary-conforming volume mesh: freeze marching tetrahedra, deform, snap boundary vertices to f's zero set, then (optionally) relax the interior.

source
g = to_sdf(sphere(1.0))
lo, hi = bounds(g)
m = (hi .- lo) ./ 20
vtk(conforming_tetmesh(x -> g(x), lo .- m, hi .+ m, 16))

Structured meshes

For a simple box there is no need to march: the grid is the mesh, each cell split into six tetrahedra in a way that guarantees every cell has positive volume:

Rasmah.tetrahedralize_boxFunction
tetrahedralize_box(w, h, t, nx::Int, ny::Int, nz::Int)
tetrahedralize_box(; width, height, thickness, nx::Int, ny::Int, nz::Int)

A uniform tetrahedral mesh of a box of dimensions width × height × thickness (the box spans [0, w] × [0, h] × [0, t]), subdivided into nx × ny × nz elements along the x, y and z axes respectively.

source
tetrahedralize_box(b::BRep, nx::Int, ny::Int, nz::Int)

A uniform tetrahedral mesh of an axis-aligned box BRep (e.g. from box(…)), subdivided into nx × ny × nz elements. The returned mesh carries the BRep face sets (see face_sets), so BRepFaceSet boundary conditions resolve automatically.

source
vtk(tetrahedralize_box(2.0, 1.0, 1.0, 4, 2, 2))

When the box is a BRep (e.g. from box(…)), the mesh also carries the BRep's face labels, so boundary conditions resolve automatically in solve:

Rasmah.face_setsFunction
face_sets(m::TetMesh) -> Union{Vector{FaceSet},Nothing}

The per-BRep-face FaceSets attached to a mesh produced by tetrahedralize(::BRep, …) / tetrahedralize_box(::BRep, …)face_sets[i] is the FaceSet of boundary faces belonging to BRep face i (1-based). Returns nothing when the mesh carries no such labels.

source

Quality

A tetrahedron is only as good as its shape. A sliver — a tetrahedron squashed nearly flat — makes the solver's stiffness matrix ill-conditioned. The scaled Jacobian measures shape on a scale from $1$ (perfect right corner) down through $0$ (degenerate) to negative (inverted):

Rasmah.tet_scaled_jacobianFunction
tet_scaled_jacobian(nodes, e) -> Real
tet_scaled_jacobian(nodes, e1, e2, e3, e4) -> Real

Scaled Jacobian det/(‖b1‖‖b2‖‖b3‖) of a tetrahedron: 1 right-corner, 1/√2 ≈ 0.707 regular, 0 degenerate, negative inverted.

source
t = tetrahedralize(sphere(1.0), 16)
tet_scaled_jacobian(t.nodes, t.elements[:, 1])
0.4082482904638628

To see the whole picture at once, summarise every element:

Rasmah.quality_reportFunction
quality_report(m::TetMesh) -> NamedTuple

Quality summary: min plus a few percentiles of the scaled Jacobian over all elements.

source
quality_report(tetrahedralize(sphere(1.0), 16))
(min = 9.229264727954907e-6, p5 = 0.005254529989311553, p50 = 0.4082482904638628, p95 = 0.6946703689125308, max = 0.7071067811865495, mean = 0.327852155923039)

The report exposes the worst element (min) and the spread of the rest. A regular tetrahedron has scaled Jacobian $1/\sqrt{2} \approx 0.707$, so a max near $0.707$ and a min far below it means a few slivers among healthy cells.

Next steps

Meshes built this way are clean, but meshes imported from scanners rarely are. For that, see Mesh repair.