Modules | C++

Volume

Dense scalar grids — signed distance fields, boolean CSG, slicing, and isosurface extraction.

The Volume module works on a dense scalar field sampled on a regular, axis-aligned grid. It generates such a field from a mesh or an analytic solid, combines fields with CSG operators, slices one onto a plane, and extracts the geometry a field's level set names.

Include the module with:

#include <trueform/volume.hpp>
EntryReturnsDoes
tf::make_sphere_sdftf::volume_bufferThe analytic sphere field
tf::make_mesh_sdftf::volume_bufferThe signed distance field of a closed mesh
tf::make_booleantf::volume_bufferUnion / intersection / difference of two fields
tf::make_isosurfacetf::polygons_bufferA 3D field's level set as a triangle mesh
tf::make_isocontourstf::curves_bufferA 2D field's level set, or a slice plane's, as polylines
tf::make_volume_slicetf::volume_buffer (2D)The field resampled onto an oriented plane
tf::make_resampled_volumetf::volume_bufferThe field regridded onto a stated grid
volume_basic.cpp
auto field = tf::make_sphere_sdf<float>({64, 64, 64},
                                        tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                                        tf::point<float, 3>{-3.2f, -3.2f, -3.2f},
                                        tf::point<float, 3>{0.f, 0.f, 0.f}, 1.5f);

auto mesh = tf::make_isosurface(field.volume());   // the zero level set

A volume boolean is a sample-wise combine of two fields: exact on the zero level set, approximate away from it, and bound by the grid's resolution. The CSG module's mesh boolean is exact everywhere and carries no resolution. Use fields when the input is already a field, when the shapes are offsets or blends, or when a self-intersecting input must be resolved by resampling; use CSG when the operands are meshes you want back unchanged.

The Carrier

TypeRole
tf::volume_buffer<T, Coord, Dims>Owns the samples in T — any arithmetic scalar, int16/uint16/uint8 included — on a Coord grid. Coord defaults to T, Dims to 3
tf::volume<Policy>The non-owning view every entry takes. buffer.volume() yields it
volume_carrier.cpp
tf::volume_buffer<float> field({64, 64, 64}, {0.1f, 0.1f, 0.1f},
                               {-3.2f, -3.2f, -3.2f});
field(10, 12, 14) = -0.5f;               // sample at voxel (x, y, z)

auto vol = field.volume();               // the view the entries take
vol.dims();                              // {64, 64, 64}
vol.spacing();                           // one voxel step along x, y, z
vol.origin();                            // local-space position of sample (0,0,0)
vol.voxel_count();                       // 262144
vol.point_at(10, 12, 14);                // origin + (x, y, z) * spacing

Samples are stored with the first axis varying fastest: linear_index(x, y, z) == x + dims[0] * (y + dims[1] * z). A view over memory you already own is built without a copy:

volume_view.cpp
std::vector<float> samples(64 * 64 * 64);
auto vol = tf::make_volume(samples.data(), std::array<int, 3>{64, 64, 64},
                           tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                           tf::point<float, 3>{-3.2f, -3.2f, -3.2f});

tf::make_volume_buffer<OutputCoordinateType>(vol) copies a view into its own buffer: stated, the request becomes both types of the copy; unstated, the copy keeps the view's own two.

Sample Type and Coordinate Type

A volume is a sampled function, so it carries two types: sample_type (what a sample is) and coordinate_type (where samples stand — the scalar of spacing, origin and every emitted position). They are the same by default. State them apart for measurements whose grid is not their own scale — int16 CT counts on a fractional-millimetre grid:

volume_two_types.cpp
tf::volume_buffer<std::int16_t, float> ct({512, 512, 300},
                                          {0.7f, 0.7f, 1.5f},
                                          {0.f, 0.f, 0.f});

Every entry emits in one type: the OutputCoordinateType template argument, or — when you state none — its own input's coordinate type. That is the volume's for every entry that takes one, and the mesh's for tf::make_mesh_sdf, which takes no volume. tf::make_sphere_sdf carries no such request at all: its RealType template argument is the field's type outright, deduced from the tf::point arguments or stated.

The emitted type must be floating, so an integer-sampled field emits in the grid's floating coordinate type unless you ask otherwise. Each sample is read through exactly one cast into that type, so a crossing always lands on the edge it belongs to.

An entry that also names an index type takes Index first and OutputCoordinateType second, the slot order tf::triangulated and the other geometry entries use:

volume_request.cpp
auto mesh   = tf::make_isosurface(vol);                   // float samples -> float mesh
auto wide64 = tf::make_isosurface<std::int64_t>(vol);     // 64-bit face indices
auto wide   = tf::make_isosurface<int, double>(vol);      // double coordinates

Axis Count

The axis count is read off the carrier by tf::coordinate_dims_v, never passed. Each entry constrains on what its own operation means:

EntryAxes
tf::make_isosurface3 — a level set of a 3D field is a surface
tf::make_isocontours (grid overload)2 — a level set of a 2D field is a curve
tf::make_volume_slice3 in, 2 out — a plane through a 3D field
tf::make_mesh_sdf3 — a closed surface encloses a 3D solid
tf::make_boolean, tf::make_sphere_sdf, tf::make_resampled_volumeany

Posed Volumes

The grid is axis-aligned in its own local space. World placement is a tagged frame, as it is for any form:

volume_posed.cpp
auto frame = tf::make_frame(scanner_pose);        // e.g. a NIfTI affine
auto posed = vol | tf::tag(frame);
auto mesh  = tf::make_isosurface(posed);          // world-space triangles

The geometry-emitting entries emit through the stated frame, and a reflecting frame (negative determinant) swaps each triangle's corners so the surface stays wound outward. Positional inputs — a slice plane's origin and axes — stay in the volume's local space. The exception is tf::make_resampled_volume: for a posed volume its target origin and grid are world space.

Signed Distance Fields

A signed distance field is negative inside the solid, positive outside, and zero on its surface; extracting the isosurface at 0 recovers the shape.

make_sphere_sdf

volume_sphere_sdf.cpp
auto ball = tf::make_sphere_sdf<float>({64, 64, 64},
                                       tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                                       tf::point<float, 3>{-3.2f, -3.2f, -3.2f},
                                       tf::point<float, 3>{0.f, 0.f, 0.f}, 1.5f);

// any axis count — a 2D dims states a disc
auto disc = tf::make_sphere_sdf<float>(std::array<int, 2>{64, 64},
                                       tf::point<float, 2>{0.1f, 0.1f},
                                       tf::point<float, 2>{-3.2f, -3.2f},
                                       tf::point<float, 2>{0.f, 0.f}, 1.5f);
ParameterTypeDescription
dimsstd::array<int, Dims>Number of samples along each axis
spacingtf::point<RealType, Dims>Physical size of one grid step along each axis
origintf::point<RealType, Dims>Local-space position of sample (0, ..., 0)
centertf::point<RealType, Dims>Sphere centre, in the volume's local frame
radiusRealTypeSphere radius

RealType is the field's sample and coordinate type, stated at the call. Each sample holds distance(point, center) - radius; the result is a tf::volume_buffer<RealType, RealType, Dims>.

make_mesh_sdf

volume_mesh_sdf.cpp
auto field = tf::make_mesh_sdf(polygons, {64, 64, 64},
                               tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                               tf::point<float, 3>{-3.2f, -3.2f, -3.2f});

// the banded mode: the band measured, the far field swept
auto fast = tf::make_mesh_sdf(polygons, {64, 64, 64},
                              tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                              tf::point<float, 3>{-3.2f, -3.2f, -3.2f},
                              tf::mesh_sdf_mode::banded);
ParameterTypeDescription
OutputCoordinateTypetemplate argumentThe sample and coordinate type of the field. Unstated: the mesh's own coordinate type
polygonstf::polygons<Policy>A closed surface mesh, optionally tagged
dimsstd::array<int, 3>Number of samples along x, y, z
spacingpoint-like, 3DPhysical size of one voxel step along x, y, z
originpoint-like, 3DPosition of sample (0, 0, 0), in the mesh's own frame — world space when it is frame-tagged
configtf::mesh_sdf_configThe magnitude mode; default measures every sample

Each sample takes the distance to the nearest point of the surface, signed by exact crossing parity on the integer lattice: negative inside by winding, so an inverted shell inverts its field and nested shells stay solid inside. A sample landing exactly on the surface gets distance zero, and its sign is decided the same way on every run.

The surface must be closed — an open mesh has no inside, and the parity sign is undefined on one. Repair an open mesh with tf::make_outer_shell first.

A mesh carrying a tf::tree is queried through it; a mesh without one gets a tree built for the call. Tag the mesh when you already have a tree or will call more than once:

volume_mesh_sdf_tagged.cpp
tf::aabb_tree<int, float, 3> tree(polygons, tf::config_tree(4, 4));
auto form  = polygons | tf::tag(tree);
auto field = tf::make_mesh_sdf(form, {64, 64, 64},
                               tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                               tf::point<float, 3>{-3.2f, -3.2f, -3.2f});

The grid samples the mesh in world space when the form carries a frame tag and in its local frame otherwise, so dims, spacing and origin describe the grid in that same space.

Banded mode

tf::mesh_sdf_config is implicitly constructible from a tf::mesh_sdf_mode, so a call site that only chooses the mode writes it directly.

FieldDefaultMeaning
modetf::mesh_sdf_mode::exactexact measures every sample; banded measures a band and sweeps the rest
band2Banded only: voxels of exactly measured magnitude on each side of the surface
volume_mesh_sdf_band.cpp
auto wide = tf::make_mesh_sdf(polygons, {64, 64, 64},
                              tf::point<float, 3>{0.1f, 0.1f, 0.1f},
                              tf::point<float, 3>{-3.2f, -3.2f, -3.2f},
                              tf::mesh_sdf_config{tf::mesh_sdf_mode::banded, 4});

Banded mode measures only the samples within band voxels of the surface and propagates the far field with a seeded distance sweep — an order of magnitude faster at routine grids. The sign is exact everywhere and the magnitude is exact inside the band. Beyond it, on grids whose lines resolve the surface, the 99th percentile of the error is under about one voxel and shrinks with resolution; the deep interior near the medial axis may locally undershoot by a few voxels. A feature no grid line meets is not found when the band is measured, so its neighbourhood takes the swept far field's value instead.

Isosurface Extraction

make_isosurface

volume_isosurface.cpp
auto mesh   = tf::make_isosurface(vol);                // the zero level set
auto offset = tf::make_isosurface(vol, 0.25f);         // an offset surface
auto sharp  = tf::make_isosurface(vol, 0.f,
                                  tf::isosurface_method::dual_contouring);
ParameterDefaultDescription
volThe 3D scalar volume
iso0The isovalue to extract, in the type the call emits in
config{flying_edges, refine = true, stabilizer = 0.01}tf::isosurface_config

The result is a welded, indexed tf::polygons_buffer of triangles — no soup, no post-hoc weld. A corner is inside when sample < iso, so a signed distance field (negative inside) yields outward windings. For an SDF a nonzero isovalue is an offset surface: +d inflates by d, -d deflates by it.

tf::isosurface_methodWhat it places
flying_edges (default)One vertex per crossing grid edge: the fastest regular output, defined for any scalar field
dual_contouringOne vertex per surface component of a cell, fitted to the field's own crossings, so creases and corners survive. Assumes a distance-like field

Dual contouring's output is manifold by construction, including where two contour arcs of one grid face join the same pair of cells. Reach for it on an SDF of a machined or faceted shape, where flying edges rounds every crease off to the grid.

tf::isosurface_config is implicitly constructible from a tf::isosurface_method, so a call site that only chooses the method writes it directly:

FieldDefaultMeaning
methodflying_edgesWhich extractor produces the surface
refinetrueDual contouring only: refit each feature vertex to the planes its neighbourhood's crossings state
stabilizer0.01Dual contouring only: the dimensionless pull toward the crossing centroid. Must be finite and nonnegative
volume_config.cpp
tf::isosurface_config config;
config.method = tf::isosurface_method::dual_contouring;
config.refine = false;
auto mesh = tf::make_isosurface(vol, 0.f, config);

Boolean CSG of Fields

make_boolean

volume_boolean.cpp
auto merged = tf::make_boolean(a.volume(), b.volume(),
                               tf::volume_boolean_op::union_);
auto carved = tf::make_boolean(a.volume(), b.volume(),
                               tf::volume_boolean_op::difference);
auto surface = tf::make_isosurface(carved.volume());

This is the library's tf::make_boolean, distinguished by the carriers it takes: volumes and a tf::volume_boolean_op rather than meshes and a tf::boolean_op.

OperationCombinatorMeaning
tf::volume_boolean_op::union_min(a, b)A ∪ B — inside either field
tf::volume_boolean_op::intersectionmax(a, b)A ∩ B — inside both
tf::volume_boolean_op::differencemax(a, -b)A \ B — inside A, outside B

The combinators are exact on the zero level set, so the extracted isosurface is exactly the boolean of the two solids; away from the surface min/max under- and over-estimate distance, which matters only if you offset the result afterwards.

Both operands must carry the same axis count. Unsigned sample types are refused at compile time — an SDF has a negative inside — as is any non-floating emitted type.

The shared grid

The combine happens on one grid. Matching grids (identical dims, spacing and origin) and matching poses combine sample-wise with no interpolation. Anything else — a different grid, a different pose — resamples both operands through tf::make_resampled_volume onto a common world-axis-aligned grid spanning the union of their world domains to within one truncated step, at the finer of the two local spacings (a scaling pose does not enter that election), capped so far-apart fine grids cannot exhaust memory. Pose equality is exact matrix equality in the type the call emits in, so poses differing below that type's resolution are one pose.

Out of an operand's domain is outside its solid: every operand reads the far-outside sentinel past its own box, so no field is continued outward past the grid it was sampled on — a solid that reaches its own domain boundary stops there.

An empty operand is the empty set and the algebra answers for it: A ∪ ∅ = A, A ∩ ∅ = ∅, A \ ∅ = A, ∅ \ B = ∅.

Posed operands

With any posed operand the call returns the field and the pose it stands in — the shared pose on the matched path, identity for the world-axis-aligned general result:

volume_posed_boolean.cpp
auto [merged, pose] = tf::make_boolean(a | tf::tag(pose_a), b | tf::tag(pose_b),
                                       tf::volume_boolean_op::union_);

Unposed operands return the buffer alone.

Slicing and Resampling

make_volume_slice

volume_slice.cpp
auto slice = tf::make_volume_slice(vol, tf::point<float, 3>{-3.2f, -3.2f, 0.f},
                                   tf::point<float, 3>{1.f, 0.f, 0.f},
                                   tf::point<float, 3>{0.f, 1.f, 0.f},
                                   {256, 256}, {0.025f, 0.025f});

auto flat = tf::make_isocontours(slice.volume(), 0.f);   // in the slice's 2D frame
ParameterTypeDescription
voltf::volume<Policy>The 3D scalar volume
plane_originpoint-like, 3DLocal-space position of slice grid node (0, 0)
u, vpoint-like, 3DUnit directions of the slice grid's i and j axes
dims2std::array<int, 2>Number of slice grid nodes along u and v
spacing2std::array<Real, 2>Slice grid step along u and v, in the type the call emits in

The slice of a volume is a volume one dimension down, and that is what comes back. Node (i, j) samples the volume at plane_origin + i * spacing2[0] * u + j * spacing2[1] * v, so the slice's own 2D frame has its origin at (0, 0) and axes u, v. The plane is stated in the volume's local space; a tagged frame does not move it.

Nodes outside the volume's box receive a sentinel above the field's maximum rather than a clamped boundary sample, so contours extracted from the slice terminate cleanly at the volume boundary.

make_resampled_volume

volume_resample.cpp
auto coarse = tf::make_resampled_volume(field.volume(), {32, 32, 32},
                                        tf::point<float, 3>{0.5f, 0.5f, 0.5f},
                                        tf::point<float, 3>{-3.2f, -3.2f, -3.2f});

Each target node at origin + index * spacing takes the field's multilinear value. An untagged volume regrids in its own local space, clamped at the edge. A posed volume regrids through its frame: the target grid is world space, each node maps back through the frame's inverse into the field's local space, and nodes outside the posed domain take a sentinel above the field's maximum. The result stands on the grid that was asked for and carries no pose of its own.

Two reads, two contracts — and tf::make_boolean always takes the posed one: an untagged operand is tagged with an identity frame before it is resampled, so every operand reads the far-outside sentinel past its own box instead of clamping. That is what makes out-of-domain mean outside the solid.

Isocontours

make_isocontours (2D grid)

volume_isocontours_2d.cpp
auto rings = tf::make_isocontours(disc.volume(), 0.f);

std::array<float, 3> levels{-0.25f, 0.f, 0.25f};
auto many = tf::make_isocontours(disc.volume(), tf::make_range(levels));

The level set of a 2D volume's bilinear field, connected into polylines — the same product tf::make_isocontours yields for a scalar field on a mesh, one dimension down. A single isovalue or a range of them.

make_isocontours (slice plane)

volume_slice_contours.cpp
std::array<float, 3> levels{-0.25f, 0.f, 0.25f};
auto curves = tf::make_isocontours(vol,
                                   tf::point<float, 3>{-3.2f, -3.2f, 0.f},
                                   tf::point<float, 3>{1.f, 0.f, 0.f},
                                   tf::point<float, 3>{0.f, 1.f, 0.f},
                                   {256, 256}, {0.025f, 0.025f},
                                   tf::make_range(levels));

The slice-then-contour composition, with every contour point lifted into world space for a posed volume and into the volume's local frame otherwise. Take tf::make_volume_slice plus the 2D overload instead when you want the curves in the slice's own 2D frame.

The grid and its step are numbers in the type the call emits in, so a braced pair is what both entries take. The plane's origin and axes are geometry and accept any point form — a tf::point of either width, or a view into a mesh's points — which is why those are spelled out.

Both overloads return a tf::curves_buffer of connected polylines; a closed contour repeats its first index last. The crossings are welded, so the connection is topological rather than a coordinate search.

Reading and Writing Volumes

tf::read_nifti and tf::write_nifti move volumes through NIfTI-1 as .nii or .nii.gz, the compression decided by the path's extension. See I/O for the full contract.

volume_nifti.cpp
auto ct = tf::read_nifti<std::int16_t>("scan.nii.gz");
if (ct) {
  auto surface = ct.posed
                     ? tf::make_isosurface(
                           ct.volume.volume() | tf::tag(ct.frame), 300.f)
                     : tf::make_isosurface(ct.volume.volume(), 300.f);
}

A file holds one sampled function with one placement, and the read splits it: spacing and any axis-aligned translation land on the grid — in the file's stated spatial unit, unconverted — while an orientation the axis-aligned grid cannot absorb comes back as a frame to tag with, posed set. A frame tag transforms per point, so consume the volume bare when posed is false.

Relationship to Iso and CSG

VolumeIsoCSG
Field lives ona regular grida mesh's vertices
Level set of a 3D fieldtf::make_isosurface → triangle mesh
Level set of a 2D fieldtf::make_isocontours → 2D curvestf::make_isocontours → 3D curves on the mesh
Booleanssample-wise on fields, resolution-boundexact on meshes, resolution-free

The two tf::make_isocontours families are the same operation on different carriers: a scalar field's level set, connected into polylines. The Volume module's overloads take a grid; the Iso module's take a mesh plus per-vertex scalars.