Skip to main content

fabriks

A level-of-detail (LOD) wire format for 3D meshes. fabriks partitions surface collections into an octree of Parquet files, allowing viewers to fetch only the required spatial detail per frame without running a dedicated server.

It is strictly a serializer: zero network dependencies, client code, or storage opinions. Hand it an abstract store, and it writes the same layout to disk, S3, GCS, or memory.


Key Properties

  • Octree Partitioning: Level 0 is full detail. Each coarser level combines 8 finer cells into 1 cell with a quarter of the face count.
  • Dual Catalogs: catalog/cells.parquet (spatial index read once at mount) and catalog/objects.parquet (inverted index mapping object IDs to cell keys).
  • Seamless Boundaries: Vertices on cell faces stay pinned during decimation—fine cells meet coarse neighbors without visual cracks.
  • Range-Query Optimized: Cells map to individual Parquet row groups sorted in Morton order for efficient byte-span fetches.

Layout & Partitioning

my-collection/
  fabriks.json                  <- Root manifest (written last; atomic completion signal & checksums)
  catalog/cells.parquet        <- Spatial index (maps level & cell_key to row group byte locators)
  catalog/objects.parquet      <- Identity index (maps object IDs to cell keys for isolation/extraction)
  level0/part-00000.parquet    <- Level 0 full-detail geometry
  level1/part-00000.parquet    <- Decimated coarse-level geometry (L=1, L=2, ...)
  • Spatial Octree: Space is partitioned into uniform 3D cells (cell_size). Each parent cell at level L >= 1 merges 8 child cells (2×2×2) from level L - 1.
  • Seam Locking: Vertices on cell boundary planes stay pinned during decimation so fine and coarse cells tile seamlessly without visual gaps.
  • Morton Ordering: Geometry row groups are sorted along a Z-order curve (Morton space) for spatially compact, range-query-friendly byte fetches.
  • Signable Paths: Every directory and file name is letters, digits, - and . — no =, no ~, nothing else outside RFC 3986's unreserved set. A key spelled this way percent-encodes to itself, so SigV4's canonical request is the same string whichever SDK, proxy or hand-rolled presigner builds it, and a presigned fetch cannot fail with SignatureDoesNotMatch over an encoding disagreement.

Install

pip install fabriks              # Core writer/reader (trimesh, fast-simplification, scipy, shapely)
pip install 'fabriks[obstore]'   # + S3 / GCS / Azure support via obstore
pip install 'fabriks[meshopt]'   # + MESHOPT blob decoding

Sizing & Writing

import trimesh
from obstore.store import LocalStore
import fabriks

objects = {
    7: trimesh.creation.icosphere(radius=18.0).apply_translation([200, 160, 60]),
    3: trimesh.creation.box(extents=[40, 24, 16]).apply_translation([90, 70, 40]),
}

# Derive grid dimensions from byte targets (optional)
grid_plan = fabriks.plan_grid(objects, cell_bytes=16 * 1024, layer_bytes=128 * 1024)

# Write meshes directly to a store
manifest = fabriks.write_meshes(
    objects,
    store=LocalStore("/data"),
    prefix="my-collection",
    **grid_plan.as_kwargs(),
)

Reading & Planning

import fabriks
from obstore.store import LocalStore

collection = fabriks.open_collection(LocalStore("/data"), "my-collection")

# Camera or voxel error budget planning
camera = fabriks.Camera.perspective((0, 0, 500), fov_y=0.8, viewport_height=1080)
plan = collection.plan(camera=camera, pixel_budget=1.0)

# Synchronous batch fetch
for cell in collection.read_cells([(e.level, e.cell) for e in plan]):
    draw(cell.vertices, cell.faces)

# Reassemble single object across cells
mesh = collection.object_mesh(7)
collection.release()

Async Reading

collection = await fabriks.aopen_collection(S3Store(...), "my-collection")
plan = collection.plan(camera=camera)
cells = await collection.aread_cells([(e.level, e.cell) for e in plan], concurrency=16)

Foreign Formats

fabriks.contrib reads the formats a model usually arrives in and writes them back out.

from fabriks.contrib import read_glb, write_obj
import fabriks

imported = read_glb("bunny.glb", store, "bunny", levels=4)

collection = fabriks.open_collection(store, "bunny")
blob = write_obj(collection, level=2)          # or write to a path: write_obj(collection, "out.obj")
Format Identity Notes
GLB per object One file; reads from bytes or a path. The best round trip here.
GLTF per object Several files — write_gltf returns {name: bytes}, which read_gltf takes back.
OBJ per object Identity rides on o markers; materials are skipped.
PLY / STL / OFF single object No object names in the format; a write merges every object into one mesh.

3MF and DAE are deliberately absent — trimesh reads neither without networkx / pycollada, and a function that is always an ImportError is a worse contract than one that is not there.

The fit. A collection lives in a positive, whole-number voxel space; a mesh file is usually centred on the origin and a couple of units across, which fabriks refuses outright and could not partition anyway. So read_* scales the model so its longest side spans resolution voxels (512 by default) and shifts it clear of the origin. The transform and each object's source name ride on the object catalog as extra columns — which the format allows on purpose — so write_* inverts the import's own numbers and a round trip lands back on the source's coordinates. Pass fit=VoxelFit.identity() for a model already in voxel coordinates.

Identity. An object named with a plain integer keeps that number as its id, so a collection written out and read back comes home with the ids it left with. Any other name is assigned the lowest free id and its string is kept in the catalog's name column, readable as collection.objects[id].name.

Round trips are not exact. Positions are 16-bit quantized per cell, so geometry comes back within about one quantum (cell_size / 65535) of where it went in, never on it. Compare bounds and shape, not vertex arrays — object_mesh welds an object's pieces back together, so neither vertex count nor order survives either.

Surfaces only. Normals, UVs, materials and vertex colours are not carried: a renderer computes vertex normals from positions and winding. Hard edges authored as split vertices do survive that — splitting is topology, and topology round trips — but only while an object stays inside a single cell. Once it spans cells, clipping and the position-only weld fuse the split, and a flat-shaded box comes back smoothed. NORMALS.md is what the decision buys, what it costs, and the measurements either way.


Configuration

Protocol Options Notes
Stores LocalStore, S3Store, DirectoryStore, MemoryStore Implements put, get, list, and optional get_range.
Simplifiers "QUADRIC" (default), "GREEDY" QUADRIC uses fast-simplification with preserve_border=True. Custom simplifiers implement fabriks.Simplifier.
Codecs codec: NONE, MESHOPT


compression: NONE, ZSTD | Default is NONE/NONE (raw little-endian arrays for zero-copy GPU upload). |


Verification

report = fabriks.verify(collection, tier="geometry")  # "structure" | "blobs" | "geometry"
if not report:
    print(report)

Coordinates & Conventions

fabriks addresses dimensions strictly by array slot (0, 1, 2). Inputs from (z, y, x) volumes pass directly via cell_size=(z_size, y_size, x_size) without transposition. Axis labels in column names are slot identifiers, not physical coordinate claims.

Metadata

Release files for fabriks 1.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fabriks 1.3.1
File Size Uploaded
fabriks-1.3.1.tar.gz 111.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fabriks 1.3.1
File Interpreter ABI Platform
fabriks-1.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 245.6 kB

Release files / fabriks-1.3.1.tar.gz

Download URL fabriks-1.3.1.tar.gz
Size 111.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1817f3c3db1b68a681180dff3c991d86e1ee7fec09a872624a6e0531ffc3fd8d
BLAKE2b-256 checksum
How to use checksums
629fe03ee7313273c870bd13a24a0d2dc417c5ee85719d7cbb3639ca443e12ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fabriks-1.3.1-py3-none-any.whl

Download URL fabriks-1.3.1-py3-none-any.whl
Size 133.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f0aa205612371118396753338526edcbb5ce0aa59e507c13700796e9dc5050bb
BLAKE2b-256 checksum
How to use checksums
4fed7946e4f7d612980adf38f857b85b0e54efc35d5978da0c3e188756a66e49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

1.3.1 This release

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page