Voxel octree storage for .NET

Encode voxel data.
Query it in place.

Tedd.Voxtree encodes one cubic voxel channel as a versioned, contiguous byte sequence. Build from z-fastest linear input or Morton Encoding (also known as the Z-order curve or Lebesgue curve), then query owned or caller-managed storage without expanding the tree first. Group independent channels in immutable chunks, then place them in bounded sparse regions or signed 64-bit streamed worlds.

dotnet add package Tedd.Voxtree
  • .NET Standard 2.1
  • .NET 10
  • .NET 11 preview
  • LGPL-2.1
Tedd.Voxtree · Chunk inspectorimmutable snapshot
ENCODED CHANNEL Material · chunk (-1, 0, 2)
Octree<UInt32>

LEVELS532³ voxels

INPUT LAYOUTMortonZ-order curve

STORAGETreeVersioned bytes

REGION MAPUniform leaves and subdivided regions
channel 0
POINT QUERY tree.Get(13, 7, 21) 42
Read the encoded snapshot directly. No dense expansion is required for point lookup.

Sparse voxel treeUniform octants collapse; incompressible data stays dense

SIMD-enabled pathsRuntime-vectorized scans and equality operations on .NET 10/11

Zero-copy / allocation-freeBorrowed lookup and caller-buffer build paths

Concurrent read modelImmutable publication and shared-lock world reads

Memory layout and format invariants

Input order and storage lifetime are explicit contracts.

The encoder accepts a complete cubic channel. It does not infer dimensions, strides, component packing, or buffer ownership.

Dimensions

side = 1 << levels

The source length must equal side³. Partial volumes and non-power-of-two sides are rejected. The maximum single channel is 512³ values.

Linear indexing

(x × side²) + (y × side) + z

z changes fastest, followed by y, then x. This order is the DenseVoxelLayout.Linear input contract.

Morton indexing

DenseVoxelLayout.Morton

Morton encoding maps three-dimensional coordinates to a one-dimensional index by interleaving coordinate bits. Conversion writes to caller-provided spans.

Encoded representation

One contiguous byte sequence

Uniform regions collapse to value leaves. If a tree would not be smaller, the encoder selects a dense payload. The format contains offsets and no process-local pointers.

Type and ownership model

Each layer has one storage responsibility.

Dense buffers hold uncompressed values. An octree encodes one channel. A chunk publishes related channels. A world maps chunk addresses to memory and persistent storage.

  1. 01

    Edit

    Dense buffers

    Use z-fastest linear storage or Morton encoding. DenseVoxelBlockSpan overlays caller-owned, channel-major memory.

  2. 02

    Encode

    Octree / OctreeSpan

    Octree<T> owns its encoded byte array. OctreeSpan<T> is a stack-bound view over borrowed, immutable bytes.

  3. 03

    Compose

    OctreeChunk

    Publish an immutable set of independently encoded channels with common dimensions. Rebuild one channel while retaining the others.

  4. 04

    Stream

    WorldEntity

    Map signed global coordinates to power-of-two chunks by arithmetic shift. Load absent residents from configured storage on demand.

Why choose it for engine code

Predictable memory behavior without flattening the world.

Use Tedd.Voxtree when voxel data is read more often than it is rewritten and chunk lifetime must be explicit. These guarantees concern representation, ownership, and allocation; throughput remains workload- and hardware-dependent.

⨯

SIMD-enabled paths

Vectorized scans where the runtime supports them

The .NET 10 and .NET 11 builds use runtime-vectorized uniform-region detection and equality search/count operations. The .NET Standard 2.1 target uses portable scalar fallbacks with the same format and API.

⌁

Zero-copy and allocation control

Caller-owned build and query buffers

Build into Span<byte> and query borrowed bytes through OctreeSpan<T>. Valid caller-buffer builds and spatial queries allocate no managed heap memory; owned builds allocate one final array.

⇄

Multi-threading contract

Immutable publication with concurrent readers

Octree<T> publishes new immutable arrays lock-free. OctreeWorld<T> permits concurrent readers, serializes writers, and supplies batch scopes. Mutable editors remain exclusive to one worker.

▱

Multiple channels

Encode attributes independently

Keep material, collision, lighting, fluid, or application-defined fields in separate encodings. A channel update can reuse every unchanged channel and preserve their uniform regions.

⋮⋮

Bulk mutation

Decode once and encode once

A hot chunk retains mutable, channel-major Morton-encoded values across repeated edits. Readers retain the previous immutable snapshot until commit; stale publication is rejected.

✣

Deferred point edits

Keep small writes beside compressed chunks

DeferredOctreeChunk<T> retains pooled point edits and materializes only affected channels on overflow. A synchronized store exposes deduplicated chunks for bounded worker repackaging. Sparse mode is configurable and enabled by default.

⊞

Sparse worlds

Allocate chunks, not a world-sized array

OctreeWorld<T> stores sparse outer branches and resident chunks without a complete chunk-reference grid. It also distinguishes unloaded space from known air.

↧

Streaming and persistence

Bound the resident working set

Load chunks on demand, track dirty snapshots, save atomically, and evict older residents against a soft serialized-byte target. Compression and LOD namespaces are explicit storage options.

◎

Encoded spatial queries

Query regions without full expansion

Execute point lookup, filtered existence and counts, bounded hit enumeration, region decode, and nearest-match search directly against encoded regions. Reusable neighborhood caches refresh only entering slabs during local movement.

Synchronous world persistence

Configure storage and make I/O behavior explicit.

With storage configured, Get may synchronously load a nonresident chunk. UnloadChunk writes a dirty chunk before release. SaveAllChunks writes dirty residents only.

  • Signed addressing. Negative global coordinates resolve to chunk and local coordinates by arithmetic shift.
  • Self-describing files. Chunk files contain codec, LOD, coordinates, lengths, format version, and CRC32 metadata.
  • Defined accounting. EstimatedResidentBytes counts serialized packet bytes, not CLR object or process memory.
World.csC#
var storage = new ChunkStorageOptions("world/chunks")
{
    Compression = ChunkCompression.Brotli,
    CompressionLevel = CompressionLevel.Fastest
};

var world = new WorldEntity(
    chunkSize: 32,
    channelCount: 4,
    storage,
    maxResidentBytes: 512L * 1024 * 1024);

world.SetChunk(-1, 0, 2, chunk);
world.SaveAllChunks();
world.UnloadChunk(-1, 0, 2);

uint material = world.Get(
    channel: 0, x: -1, y: 7, z: 64);

Ownership and lifetime

Buffer lifetime is part of the API contract.

Octree<T>Owns one final encoded byte array. Builds atomically publish a new immutable array; concurrent reads retain a valid snapshot.
OctreeSpan<T>Borrows caller memory. As a ref struct, it cannot cross await or yield, or be stored on the managed heap.
HotOctreeChunk<T>Owns mutable, channel-major, Morton-encoded values for one exclusive edit session.
DeferredOctreeChunk<T>Owns a compressed snapshot plus pooled point edits. At capacity, affected channels become dense while untouched channels remain encoded. Disabling deferred writes expands every channel on the first write.
OctreeWorld<T>Represents a bounded sparse region, distinguishes unloaded from known-air space, and synchronizes access with reader/writer locking.

Operational constraints

Know which guarantees are provided.

Tedd.Voxtree does not generate or select LOD data, simulate entities, or define the meaning of unloaded space. The application owns those policies.

WorldEntity, HotOctreeChunk, and standalone DeferredOctreeChunk owners are not thread-safe. DeferredChunkStore synchronizes its operations, while OctreeWorld uses a ReaderWriterLockSlim and provides explicit read and write batches. The library does not create worker threads; the application controls scheduling and partitions mutable editors by worker.

One encoded channel is limited to levels 0 through 9: a maximum side length of 512 and 134,217,728 dense values. World depth and chunk addressing are separate from this per-channel limit.

Review the complete capability matrix ↗

Tedd.Voxtree 2.x

Start with an explicit
storage contract.

Select the dense layout, value width, ownership model, and world synchronization policy before integrating the query path.