Expand description
§brodo
Sparse volumes in three dimensions, in pure Rust. A grid stores values only where you activate voxels, in leaves of 8³, with a single value (a tile) standing for a whole region that is uniform. It is not tied to any one use: signed distance fields, fluid fields and smoke are all fields on its tree.
What sets it apart is that the set of active voxels, the Topology, is its own value,
and any number of fields of any type sit on one topology. You edit the topology once, and
every field on it is carried over to the new one; a field refuses a topology it does not
belong to.
- Tree and fields: a fixed four-level tree, tiles at every level. Editing a
Topologyreturns the new one and aRemap, which carries eachField<T>over. - Work on leaves: value accessors, each leaf’s 26 neighbours, a leaf gathered with ghost layers, and leaf-parallel updates that give the same result at any thread count.
- Analysis: connected components and flood fill, the Euler characteristic, and dilation and erosion, with 6- or 26-connectivity, tiles included.
- Files, whose bytes docs/formats.md specifies for a reader in any language:
freezepacks a grid into one aligned buffer, read in place, with no copy, as aGridView.- A cache file holds several grids with their names, voxel sizes and units. It is checked whole on reading, and read by copy or by memory mapping.
- An archive file is a cache file compressed, a floating grid’s values kept exact or quantised. It reads back whole, or as one grid’s region, decompressing only the parts that region needs.
- OpenVDB’s
.vdb: float and double grids, written for OpenVDB’s tools and read from files brodo and OpenVDB 12 to 14 write.
- Hermite data: where a surface crosses each voxel edge, with its normal and a group naming the face, beside the signs at the voxels’ corners: what a reader needs to place the surface within a voxel, sharp edges kept.
- Checking output: a slice drawn as PNG, and a field for ParaView, to look at a result.
Where to start:
| To | Use |
|---|---|
| make and edit the set of active voxels | topology::Topology and its edit |
| hold values on it | field::Field |
| label components, find the Euler characteristic, dilate or erode | analysis, morphology |
| save and load | cache, archive, vdb |
| read a saved grid in place | frozen::freeze, view::GridView |
| look at a field | slice, vtk |
Features, each a part a user may leave out or opt into:
| Feature | Default | Gives |
|---|---|---|
mmap | on | reading a cache file by memory mapping |
archive | off | the archive profile |
vdb | off | OpenVDB’s .vdb files |
slice-png | off | a slice as PNG |
vtk | off | a field for ParaView, as .vtu |
§Example
A solid ball in two leaves: its components and Euler characteristic counted, and the grid saved as a cache file’s bytes and read back:
use brodo::analysis::{Connectivity, label};
use brodo::cache::{CacheEntry, CacheFile, to_bytes};
use brodo::analysis::euler;
use brodo::field::Field;
use brodo::meta::GridMeta;
use brodo::topology::Topology;
// Two leaves side by side, and a solid ball of radius 5 about (8, 4, 4).
let empty = Topology::new();
let mut edit = empty.edit();
edit.activate([0, 0, 0])?;
edit.activate([8, 0, 0])?;
let (t, _remap) = edit.commit()?;
let mut solid = Field::new(&t, 0_u8);
for n in t.leaf_numbers() {
let voxels: Vec<_> = t.leaf_voxels(n)?.collect();
for (value, v) in solid.leaf_mut(&t, n)?.iter_mut().zip(voxels) {
let d2 = (v[0] - 8).pow(2) + (v[1] - 4).pow(2) + (v[2] - 4).pow(2);
*value = u8::from(d2 <= 25);
}
}
let inside = |x: u8| x == 1;
assert_eq!(label(&t, &solid, inside, Connectivity::TwentySix)?.count(), 1);
assert_eq!(euler(&t, &solid, inside, Connectivity::TwentySix)?, 1);
// Serialised as a cache file's bytes, and read back from a copy of them.
let meta = GridMeta::new("ball", [0.1; 3], "millimeter")?;
let bytes = to_bytes(&[CacheEntry::new(&meta, &t, &solid)?])?;
let file = brodo::cache::from_bytes(&bytes)?;
let grid = file.grids()[0];
assert_eq!(grid.meta().name, "ball");
assert_eq!(grid.view::<u8>()?.get([8, 4, 4])?, 1);The examples diffuse several fields on one topology, checked against the analytic solution, and build the union of two spheres’ distance fields:
cargo run -p brodo --example diffusion
cargo run -p brodo --example spheresIt is written by a human, its owner, together with AI coding agents, chiefly Claude Code and Antigravity in support: the owner decides and approves, the agents write under the repository’s rules, and every change passes the same checks and tests (the repository’s README says more).
Issues are welcome at https://github.com/akahmys/brodo/issues, a bug with the steps that show it or a request. Pull requests are not accepted: code written elsewhere cannot be shown free of copyleft sources, so fixes are written here from issues.
Licensed under either of MIT or Apache-2.0, at your option.
The design is in the repository’s ARCHITECTURE.md §4.
Modules§
- accessor
- Value accessors (ARCHITECTURE.md §4.4, ADR-0015): reads and writes by voxel that start from the regions last visited, so nearby accesses skip most of the tree. One per thread.
- analysis
- Topology analysis (ARCHITECTURE.md §4.5, ADR-0013): connected components of the voxels
whose value meets a condition, tiles and the background included, flood fill, and the
Euler characteristic (
euler, ADR-0024). - archive
- The archive profile (ARCHITECTURE.md §4.7, ADR-0016): grids with their metadata in one
file, each frozen buffer cut into chunks compressed apart by
lz4_flex’s block format, read back as the cache file of the same grids. This module parses untrusted bytes: it is listed inUNTRUSTED_INPUT, indexes nothing (CODING.md Rule 3), checks the whole file before it answers, and is fuzzed (TESTING.md T6). - cache
- The cache profile (ARCHITECTURE.md §4.7, ADR-0016): grids with their metadata in one
file, each grid’s frozen buffer on a 32-byte boundary. This module parses untrusted
bytes: it is listed in
UNTRUSTED_INPUT, indexes nothing (CODING.md Rule 3), checks the whole file before it answers, and is fuzzed (TESTING.md T6). - error
- The errors of brodo’s operations (CODING.md Rule 8).
- field
- A field: one value per voxel of a topology’s leaves, one per tile, and a background
(ARCHITECTURE.md §4.2, ADR-0014). It records its topology’s stamp and refuses any other
topology as
FieldError::StaleField. - frozen
- The frozen layout (ARCHITECTURE.md §4.6, ADR-0016) and
freeze, which packs a topology and a field into one 32-byte aligned buffer that aGridViewreads in place. - gather
- The gather (ARCHITECTURE.md §4.4, ADR-0015): a leaf and one ghost layer from its 26
neighbours, copied into a dense 10³ array, or one to three layers into a caller’s
Vec(gather_wide). A neighbour that is not a leaf gives its tile’s value or the background. A leaf of 8³ reaches only its 26 neighbours up to eight layers; a read beyond the window goes throughValueAccessor, faster there thanField::value_at. - grid
- A grid: one topology and one field on it, for single-field use (ARCHITECTURE.md §4.2). Editing a grid commits its topology and remaps its field in one step, so its field is never stale.
- hermite
- Hermite data on a grid’s voxel corners, a format any producer writes (ARCHITECTURE.md §4.9, ADR-0032): a sign at each lattice point and, on each edge whose ends differ, where the surface crosses it, its normal there and its group (Ju et al. 2002).
- kind
- A file’s kind, told by its first bytes, its magic number (docs/formats.md §2.1, §3.1, §8.1, §9.1; ADR-0056): each format’s magic number is here, and its module reads it from here, so that the kinds are told apart whichever features are built.
- manifest
- The leaf manifest (ARCHITECTURE.md §4.3, ADR-0014): what lies one leaf away from each leaf in each of the 26 directions, rebuilt with its topology at each commit.
- meta
- A grid’s metadata and bounding box (ARCHITECTURE.md §4.7, ADR-0016): what a file stores beside a grid’s topology and values.
- morphology
- Dilation and erosion of a topology (ARCHITECTURE.md §4.5, ADR-0025), on whole leaves: a leaf holds no per-voxel mask, so every voxel of a leaf or tile is active.
- parallel
- Leaf-parallel work (ARCHITECTURE.md §4.4, ADR-0015): an update that writes one field a
leaf at a time on
rayon’s threads, and a reduction whose result does not depend on the thread count. - slice
- Checking output (ARCHITECTURE.md §6.1, ADR-0018): a slice of a scalar field drawn as an image and encoded as PNG, for a person or an agent to look at (TESTING.md T8).
- topology
- A topology: the tree, a stamp unique in the process, and its leaves and tiles numbered in
the Morton order of their origins (ARCHITECTURE.md §4.2, ADR-0014). It is edited only
through a
TopologyEditor, whose commit gives a new topology and theRemapto it. - tree
- The sparse tree (ARCHITECTURE.md §4.1, ADR-0013): a root map, internal nodes of 32³ and 16³ children, and leaves of 8³ voxels. The tree holds topology only: which regions are leaves, which are tiles, and which are the background. Values belong to fields (§4.2).
- vdb
- OpenVDB files (ARCHITECTURE.md §4.7, ADR-0045, ADR-0046, ADR-0047): grids of
f32orf64written as one.vdbfile at file version 225, as OpenVDB 14 reads it, and read from the files brodo and OpenVDB 12 to 14 write byfrom_bytesandread, which this module re-exports from the private modulevdb_read, where reading lives. - view
- Reading a frozen grid in place (ARCHITECTURE.md §4.6, ADR-0016). This module parses
untrusted bytes: it is listed in
UNTRUSTED_INPUT, indexes nothing (CODING.md Rule 3), checks the whole buffer before any read, and is fuzzed (TESTING.md T6). - vtk
- Checking output (ARCHITECTURE.md §6.1, ADR-0018): a field written for ParaView as one
ASCII UnstructuredGrid (
.vtu), from VTK’s published file-format documentation.