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.tetrahedralize — Function
tetrahedralize(g, n; conform=false, method=:marching) -> TetMeshVolume-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
trueSee also
surface_mesh for the surface counterpart, and marching_tetrahedra / conforming_tetmesh / delaunay_tetmesh for the individual kernels.
tetrahedralize(b::BRep, n; conform=false, order=1) -> TetMeshTetrahedralize 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).
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_tetrahedra — Function
marching_tetrahedra(f, lo, hi, n; parallel = true) -> TetMeshExtract 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.
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_tetmesh — Function
conforming_tetmesh(f, lo, hi, n; relax = true, iters = 3) -> TetMeshBoundary-conforming volume mesh: freeze marching tetrahedra, deform, snap boundary vertices to f's zero set, then (optionally) relax the interior.
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_box — Function
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.
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.
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_sets — Function
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.
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_jacobian — Function
tet_scaled_jacobian(nodes, e) -> Real
tet_scaled_jacobian(nodes, e1, e2, e3, e4) -> RealScaled Jacobian det/(‖b1‖‖b2‖‖b3‖) of a tetrahedron: 1 right-corner, 1/√2 ≈ 0.707 regular, 0 degenerate, negative inverted.
t = tetrahedralize(sphere(1.0), 16)
tet_scaled_jacobian(t.nodes, t.elements[:, 1])0.4082482904638628To see the whole picture at once, summarise every element:
Rasmah.quality_report — Function
quality_report(m::TetMesh) -> NamedTupleQuality summary: min plus a few percentiles of the scaled Jacobian over all elements.
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.
