Mesh preparation for DRI Voyager.
The pipeline shells out to several command-line tools that must be on PATH.
# System tools
brew install imagemagick # `mogrify` — texture normalization
brew install ktx # KTX-Software >= v5 (`ktx create`) — KTX2 encoding
brew install node # Node 24+ LTS (for the tools below)
# Node tools
npm install -g gltfpack # meshoptimizer geometry decimation/compression
# Python package. Optional extras:
# [validate] — pymeshlab, for the Hausdorff decimation gate
# [preview] — trimesh + pyrender, to render the model-preview thumbnail
pip install .
pip install '.[validate,preview]'
# KTX2 embed helper (Node): install its deps once, in the installed package dir
npm install --prefix "$(python -c 'import preppy, pathlib; print(pathlib.Path(preppy.__file__).parent / "node")')"The KTX2-into-glb embed step runs a bundled Node helper
(preppy/node/embed.mjs, using @gltf-transform/core + meshoptimizer) rather
than the gltf-transform CLI, so it needs node (24+ LTS) plus that helper's npm
deps installed once as shown above.
KTX-Software must be >= v5.0.0 —
toktxwas removed in v5 and the pipeline usesktx create. Ifbrew install ktxis unavailable on your platform, grab a release from https://github.com/KhronosGroup/KTX-Software/releases.
Verify everything is installed and new enough:
preppy-check-tools # all CLI tools + optional model-preview backend
preppy-check-tools preview # just probe the model-preview render toolchainThe preview line is informational: it renders a tiny offscreen frame to confirm
the preview extra and a GL backend actually work. When it reports SKIP, the
pipeline still runs — thumbnails fall back to a texture crop.
preppy turns source OBJs + textures into one self-contained .glb
per variant (meshopt geometry with embedded KTX2) plus a viewer-native
per-object manifest.
preppy -i config.json -o out/The input config is a flat array of objects, each with a flat variants[]
(one mesh + its texture(s) per variant). See
templates/prep-models.schema.json for the
full schema and templates/mvs-example.json for a
worked example. Each variant's texture(s) are resolved transitively from its
OBJ's map_Kd — no texture paths in the config normally.
Relative obj paths resolve against --data-root (default: the current working
directory), so the config file can live anywhere:
preppy -i config.json -o out/ --data-root /path/to/meshes/Output layout (per-object directory named by prefix, defaults to id):
out/
index.json # optional host archive listing
<prefix>/
manifest.json # the scene the viewer loads
<prefix>_<suffix>.<hash>.glb # one self-contained glb per variant
<prefix>_thumb.jpg # default-variant thumbnail (model preview)
Asset filenames carry an inputs+config content hash by default (served
immutable); manifest.json / index.json keep stable names and hold the
current hashed URIs. Useful flags:
| Flag | Effect |
|---|---|
--ktx2-mode {etc1s,uastc} |
KTX2/Basis codec (default etc1s) |
-s/--decimate-error FLOAT |
gltfpack -si target (default 0.2) |
--no-decimate |
meshopt-compress without simplifying |
--nodata-fill COLOR |
default atlas no-data fill (hex, # optional) to back-fill from the nearest chart pixel |
--no-hash-names |
stable asset names (not cacheable immutable) |
--data-root DIR |
root that relative obj paths resolve against (default: CWD) |
--uri PREFIX |
absolute-URL prefix for manifest uris |
--prune |
delete hashed assets no longer referenced by a manifest |
--prune-keep N |
with --prune, also keep the newest N prior generations of each variant (rollover window; default 0) |
--thumbnail-mode {render,texture} |
thumbnail source: a rendered model preview of the default variant (default; needs the preview extra, falls back to texture if unavailable) or a texture center-crop |
--preview-bg COLOR |
background the rendered preview composites over (hex, # optional; default 222222) |
--keep-tmp |
keep intermediate PNG/KTX2/geometry files |
The default <prefix>_thumb.jpg is a rendered preview of the default variant's
model (a 3/4 view with computed normals), not a crop of its texture atlas. It is
a proxy: it renders the OBJ geometry with the normalized textures rather than
the delivered meshopt/KTX2 glb, so it is recognizable but not a pixel-identical
capture of what <dri-viewer> shows. Rendering needs the preview extra
(trimesh + pyrender) and a working offscreen GL backend; when either is
missing the run falls back to the texture center-crop (--thumbnail-mode texture) automatically.
Run preppy -h for the complete list.
Serve the catalog with a two-tier cache policy so cache-busting is safe:
- hashed assets (
….<hash>.glb/.ktx2) never change under a given name — cache themimmutable, effectively forever; manifest.json/index.jsonkeep stable names — serve themno-cache(always revalidate) so a viewer immediately picks up new hashed URIs.
A rebuild that changes an input mints new hashed names and rewrites the manifest;
the viewer revalidates the manifest and pulls the new immutable assets. Pair this
with --prune --prune-keep N so a manifest already served to an in-flight client
during a rollover can still resolve its (now-previous-generation) hashed URIs.
See docs/hosting/htaccess.example for a
ready-to-adapt Apache configuration (final host TBD; tune to your deployment).
pip install -e '.[test]' # pytest (+ pymeshlab/pyrender via [validate]/[preview])
python -m pytest tests/Tests that need the external toolchain (magick/ktx/gltfpack/node), pymeshlab,
or an offscreen-GL backend skip cleanly when those are absent, so a bare run still
covers the pure logic. CI runs this across Python 3.11–3.13 (plus a manual integration
job that exercises the full toolchain); see .github/workflows/ci.yml.
A prebuilt multi-arch (amd64/arm64) image bundling the full toolchain is published
to the GitHub Container Registry on every push to develop (tag edge) and on
release tags (vX.Y.Z, latest):
docker pull ghcr.io/educelab/preppy:edge
# The image's default command is `preppy -h`; run the pipeline over a mounted dir:
docker run --rm -v "$PWD":/data -w /data ghcr.io/educelab/preppy:edge \
preppy -i config.json -o out/
# Any of the console scripts work as the command, e.g.:
docker run --rm ghcr.io/educelab/preppy:edge preppy-check-toolsTo build it locally (see Dockerfile):
docker build -t preppy .The
<dri-viewer>web component that consumes this pipeline's output lives in a separate repository (dri-voyager), with its own TypeScript/Vitest/Playwright suite. This repo is the model-preparation pipeline only.
The single-object preppy-obj2glb tool (and its convert.py core) still uses
obj2gltf + gltf-pipeline to emit a Draco-compressed GLB. It is deprecated
in favor of the delivery pipeline above and will be removed. To keep using it
during the transition:
npm install -g obj2gltf gltf-pipeline