Skip to main content

boxglb

Write glTF 2.0 / GLB files from axis-aligned boxes. Pure Python, no dependencies.

You already have the model — panel dimensions from a configurator, a pallet layout, the output of a greedy mesher. boxglb turns those numbers into a file that opens in Blender, three.js, Windows 3D Viewer, Google Scene Viewer or any browser, and stops there. It is a serializer, not a modeller.

pip install boxglb

Example

from boxglb import Box, Material, write_glb

materials = {
    "oak":   Material("oak", base_color=(0.76, 0.60, 0.42, 1.0), roughness=0.8),
    "glass": Material("glass", base_color=(0.8, 0.9, 1.0, 0.25)),
}

shelf = [
    Box("side_left",  pos=(0.00, 0, 0),      size=(0.018, 2.0, 0.30),  material="oak"),
    Box("side_right", pos=(0.98, 0, 0),      size=(0.018, 2.0, 0.30),  material="oak"),
    Box("shelf_1",    pos=(0.018, 0.4, 0),   size=(0.964, 0.018, 0.30), material="oak"),
    Box("door",       pos=(0.018, 0, 0.282), size=(0.964, 2.0, 0.018), material="glass"),
]

stats = write_glb(shelf, materials, "shelf.glb")
# {'triangles': 48, 'nodes': 2, 'materials': 2,
#  'bbox_m': [0.998, 2.0, 0.3], 'origin_m': [0.0, 0.0, 0.0], 'bytes': 5572}

Everything is in metres, because that is what glTF defines. No scale negotiation, which is the question FBX and OBJ leave open. pos is the minimum corner, not the centre. Alpha below 1.0 sets alphaMode: BLEND automatically — no special material names are involved.

Runnable version: examples/shelf.py.

Why this and not something else

dependencies boxes → GLB UV generation
trimesh numpy, 54 MB installed, Python ≥ 3.10 yes no — creation.box() has uv = None; the GLB carries only POSITION
pygltflib dataclasses-json, deprecated you write buffers and accessors you
gltflib dataclasses-json same you
boxglb none yes per face, at world scale

Verified 2026-08-28 by installing each into a clean venv and running this shelf.

Textures work without you writing an unwrap. UVs are planar projections in world coordinates scaled by texture_scale_m, so a wood texture tiles at physical size instead of stretching to fit each panel:

Material("oak", texture="oak.jpg", texture_scale_m=0.5)   # one tile per half metre

That is the reason to choose boxglb even where numpy is already installed. The second reason is narrower: zero dependencies matter when a graphics stack in the image costs more than the task — a backend returning previews, a lambda, CI, an air-gapped box with no wheels for its architecture.

What this package does not do

Deliberately, permanently:

  • arbitrary meshes — axis-aligned rectangular volumes only
  • rotation or transforms — there is no matrix anywhere in the code
  • node hierarchy — every node sits flat in one scene
  • animation, skinning, morph targets
  • cameras and lights
  • smooth shading — normals are per face by construction
  • reading GLB — it writes only

If you need any of these, you need a different library, and this list is here so you find that out before installing rather than after.

Known behaviour worth knowing

Boxes sharing a material are merged. By default all boxes of one material become a single mesh and node named by the material key — that is why the shelf above reports nodes: 2 for four boxes. Box.name therefore does not reach the file; the names above exist for the readability of your own code. Pass group_by_material=False to get one node per box named by Box.name. It costs file size and draw calls: a hundred boxes become a hundred meshes and four hundred accessors.

Texture direction depends on the face. X faces get U=y, V=-z; Z faces get U=x, V=-y. Grain on a side panel runs at 90° to grain on a front panel. For wood this is often what you want; it is stated here because it surprises people.

UVs are world coordinates. A scene translated hundreds of metres from the origin produces large UV values and loses float32 precision.

Vertices are not welded — 24 per box. That is the price of per-face normals, and it is what makes texturing possible at all.

API

def write_glb(
    boxes: list[Box],
    materials: dict[str, Material],
    out_path: str | os.PathLike[str],
    scene_name: str = "model",
    texture_root: Path | None = None,   # resolves relative texture paths
    group_by_material: bool = True,
) -> Stats

Box(name, pos, size, material) — all in metres, size strictly positive.

Material(name, base_color=(0.8, 0.8, 0.8, 1.0), metallic=0.0, roughness=0.7, texture=None, texture_scale_m=1.0, double_sided=True)

The key of the materials dict is the identifier Box.material points at, and it names the mesh and node. Material.name is the label written into the glTF material and may differ: {"oak": Material("American walnut")} is a valid, deliberate choice.

Stats is a TypedDict — triangles, nodes, materials, bbox_m, origin_m, bytes. Note that nodes counts glTF nodes, not boxes.

Bad input is rejected before anything is written, with a message that names the offender: non-positive dimensions, an unknown material key, a texture that does not resolve, a texture that is not PNG or JPEG, an empty box list, a missing output directory.

The package ships py.typed and passes mypy --strict.

Requirements

Python 3.9 or newer. Nothing else.

License

MIT

Metadata

Release files for boxglb 0.1.0

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

Source distribution (sdist)

Source distribution for boxglb 0.1.0
File Size Uploaded
boxglb-0.1.0.tar.gz 29.1 kB Details

Built distribution (wheel)

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

Total release size: 39.6 kB

Release files / boxglb-0.1.0.tar.gz

Download URL boxglb-0.1.0.tar.gz
Size 29.1 kB
Tags Source
SHA-256 checksum
How to use checksums
26d75c507df9722f9fcbb674ee7d8ac83e6c7adac8a3187ea2c6be5b19cd906c
BLAKE2b-256 checksum
How to use checksums
2e21142071dbce1cae2537e1440d1a135101feb947b8fe8fe7948d13b9cf0446
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release files / boxglb-0.1.0-py3-none-any.whl

Download URL boxglb-0.1.0-py3-none-any.whl
Size 10.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
624aad028a7246c62f6d35a7adb5af55560eadf6790a430b8e68a90d1f3e1267
BLAKE2b-256 checksum
How to use checksums
6ca4089b75874061cc6ff5166f6288878b45efaa6162dac0a8759c3e444a3c3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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