5.2.2. Asset Baker¶
1. Overview¶
Asset Baker is Atlas' containerized Blender headless HP→LP bake worker. It turns messy AI-generated high-poly meshes — the interpenetrating shells/flaps, texture distortion, and missing normal maps that make img2mesh output unusable as game/web assets — into clean low-poly GLBs with a baked BaseColor + tangent-normal map. The pipeline is the industry HP→LP bake: voxel-remesh → decimate → fresh Smart-UV → selected-to-active bake of color + normal from the original.
It is a distinct service from the Asset Worker (#343): that Node/glTF-Transform worker does weld/simplify/compress and cannot voxel-remesh, regenerate UVs, or bake textures/normals — those need Blender. Asset Baker rides the same content-addressed MinIO artifact schema and sits behind the same /assets/* route family, so generate → image→3D → bake (this) → optimize (asset-worker) speak one job idiom while remaining separate containers (Blender vs Node images).
It is disabled by default (ASSET_BAKER_SOURCE=disabled); the recommended enabled mode is container-cpu. Cycles bakes on CPU by design: deterministic, runs anywhere (CI, Linux prod), measured 30–200 s/asset at 2k textures, and GPU-contention-safe (Docker on macOS can't pass Metal into a container anyway). GPU (container-gpu) and managed localhost are deferred until separate lifecycle/performance evidence exists. The Blender image is ~1.5–2.5 GB, so the service is track-membered (gen-ai-creative) and never always-on.
There is no MCP surface, by design: this is a deterministic batch stage whose instructions are job parameters, not a conversational agent. (The blender-mcp addon also refuses to start under blender -b, and making the one reliably-deterministic pipeline stage agent-dependent would forfeit reproducibility/CI-testability. The agentic Blender need belongs to the separate blender-mcp service.)
2. Access¶
| Surface | URL | Notes |
|---|---|---|
| Direct API | http://localhost:${ASSET_BAKER_PORT} |
Host-side FastAPI service when ASSET_BAKER_SOURCE=container-cpu. |
| Kong alias | http://asset-baker.localhost:${KONG_HTTP_PORT} |
Requires ./start.sh --setup-hosts; generated only when the service is enabled. |
| Internal API | http://asset-baker:8096 |
Used by sibling containers through ASSET_BAKER_ENDPOINT. |
| Health | GET /health |
Returns 200 only when the configured Blender executable is available; otherwise returns 503. |
| Metrics | GET /metrics |
Prometheus request counters and duration histograms; intentionally unauthenticated for in-network scraping. |
Every route except GET /health and GET /metrics requires Authorization: Bearer ${ASSET_BAKER_API_TOKEN}. Atlas generates the token on first startup; requests fail closed with 503 if authentication is not configured.
3. Configuration¶
| Variable | Default | Purpose |
|---|---|---|
ASSET_BAKER_SOURCE |
disabled |
Enables the containerized worker when set to container-cpu. |
ASSET_BAKER_IMAGE |
python:3.12.9-slim |
Base image; the Dockerfile downloads pinned Blender on top. |
ASSET_BAKER_BLENDER_VERSION |
4.3.2 |
Pinned headless Blender (≥ 4.3) installed in the image; bundles Python + numpy. |
ASSET_BAKER_BLENDER_SHA256 |
4da1c956...a5592e6 |
Published SHA-256 verified before the pinned Blender archive is extracted. |
ASSET_BAKER_PORT |
computed | Host port assigned by Atlas' topology allocator. |
ASSET_BAKER_API_TOKEN |
generated | Bearer token required by upload, reference-bake, and artifact-download routes. |
ASSET_BAKER_ALLOWED_INPUT_BUCKETS |
(blank) | Optional comma- or space-separated MinIO bucket allowlist. Blank follows MINIO_BUCKET_ASSET_INPUTS. |
ASSET_BAKER_ARTIFACT_DIR |
/data/artifacts |
Local cache path for baked outputs and direct downloads. |
ASSET_BAKER_MINIO_ENABLED |
true |
Writes baked GLB + textures to MinIO when enabled. |
ASSET_BAKER_MINIO_BUCKET |
asset-baker |
Output bucket for content-addressed artifacts. |
ASSET_BAKER_MINIO_ENDPOINT |
auto-managed | Internal MinIO S3 API endpoint. |
ASSET_BAKER_MINIO_ACCESS_KEY / ASSET_BAKER_MINIO_SECRET_KEY |
empty | Optional credential override; compose otherwise uses the generated MINIO_ASSET_BAKER_* scoped account. |
ASSET_BAKER_TARGET_TRIS |
39000 |
Default low-poly triangle budget (overridable per request). |
ASSET_BAKER_TEX_SIZE |
2048 |
Default baked texture resolution (square). |
ASSET_BAKER_CANONICAL_SIZE |
4.0 |
Canonical max-dimension each mesh is scaled to before remesh. |
ASSET_BAKER_BRIGHTNESS_MIN |
0.05 |
Mean-brightness QA gate; a baked color mean below this is refused. |
ASSET_BAKER_TIMEOUT_SECONDS |
600 |
Hard wall-clock ceiling per bake subprocess. |
ASSET_BAKER_MAX_UPLOAD_MB |
200 |
Maximum accepted GLB input size (MiB). |
ASSET_BAKER_CONCURRENCY |
1 |
Maximum concurrent bakes; default 1 (one bake saturates CPU). |
Enable from the CLI with:
./start.sh --asset-baker-source container-cpu
The default ASSET_BAKER_SOURCE=disabled keeps the ~2 GB Blender worker out of normal starts until a creative or 3D workflow needs it.
3.1. Pinned Blender runtime¶
The image installs a pinned, checksum-verified headless Blender at build time (services/asset-baker/app/Dockerfile). The build is reproducible and source-host-independent:
| Property | Value |
|---|---|
| Version | ASSET_BAKER_BLENDER_VERSION (default 4.3.2) |
| Artifact | blender-<version>-linux-x64.tar.xz |
| Integrity | ASSET_BAKER_BLENDER_SHA256 (default 4da1c956…a5592e6), verified with sha256sum -c before extraction |
| Architecture | linux/amd64 (x86_64) only — Blender ships no official linux-arm64 build; the Dockerfile fails fast with an actionable message on any other architecture (build with --platform=linux/amd64 on Apple Silicon) |
| License | Blender is GNU GPL-2.0-or-later; it runs as a separate headless subprocess (blender -b), not linked into Atlas code, so it imposes no license obligation on Atlas itself |
Download source. The canonical download.blender.org returns HTTP 403 to automated clients (#505), so the build fetches from the official Blender mirror network, trying each in turn (ftp.nluug.nl, mirror.clarkson.edu, mirrors.ocf.berkeley.edu, mirrors.dotsrc.org) until one succeeds. Because the SHA-256 is verified regardless of host, the mirror is interchangeable and a single mirror outage does not break the build. A clean docker build therefore succeeds without any developer cache, and a build-time smoke step asserts blender --version matches the pinned version.
Updating the pin. Pick the new version, download its blender-<version>-linux-x64.tar.xz and the matching blender-<version>.sha256 from any mirror above, then set ASSET_BAKER_BLENDER_VERSION + ASSET_BAKER_BLENDER_SHA256 (in .env, or the service.yml defaults) to the new version and its published checksum.
4. API Contract¶
4.1. Uploaded GLB¶
POST /assets/bake accepts multipart/form-data:
curl -H "Authorization: Bearer ${ASSET_BAKER_API_TOKEN}" \
-F file=@mesh.glb \
"http://localhost:${ASSET_BAKER_PORT}/assets/bake"
| Field | Required | Notes |
|---|---|---|
file |
yes | Raw .glb file (plain GLB, not EXT_meshopt_compressed). |
target_tris |
no | Low-poly triangle budget for the decimate stage. Defaults to ASSET_BAKER_TARGET_TRIS. |
tex_size |
no | Baked BaseColor/Normal texture resolution (square). Defaults to ASSET_BAKER_TEX_SIZE. |
canonical_size |
no | Canonical max-dimension normalization scale. Defaults to ASSET_BAKER_CANONICAL_SIZE. |
mode |
no | bake (full HP→LP + texture bake, default) or skip (foliage bypass — normalize + export, no remesh/bake). |
4.2. MinIO Referenced GLB¶
POST /assets/bake/ref accepts JSON:
{
"input": {"bucket": "raw-assets", "key": "incoming/cottage.glb"},
"params": {"target_tris": 15000, "tex_size": 2048, "mode": "bake"}
}
The request bucket must be listed in ASSET_BAKER_ALLOWED_INPUT_BUCKETS; other buckets return 403 before MinIO is contacted.
Populate the default raw-assets bucket with the generated
MINIO_ASSET_INGEST_ACCESS_KEY and MINIO_ASSET_INGEST_SECRET_KEY; that
identity can write inputs without receiving MinIO root or processor-output access.
4.3. Response¶
Both endpoints return a content-addressed artifact envelope — the baked LP GLB plus its BaseColor/Normal textures:
{
"status": "succeeded",
"sha256": "<lp-glb-sha256>",
"artifact": {
"storage": "minio",
"bucket": "asset-baker",
"key": "bake/<sha256>.glb",
"uri": "s3://asset-baker/bake/<sha256>.glb",
"content_type": "model/gltf-binary"
},
"textures": [
{"role": "basecolor", "storage": "minio", "bucket": "asset-baker", "key": "bake/<sha>.png", "content_type": "image/png"},
{"role": "normal", "storage": "minio", "bucket": "asset-baker", "key": "bake/<sha>.png", "content_type": "image/png"}
],
"summary": {"mode": "bake", "faces_in": 98000, "tris_out": 15000, "shells_kept": 1, "color_mean": 0.34, "duration_s": 72.0},
"download_url": null
}
When ASSET_BAKER_MINIO_ENABLED=false, artifacts are stored under ASSET_BAKER_ARTIFACT_DIR/bake/<sha256>.{glb,png} and download_url=/assets/artifacts/<sha256>.glb. A skip (foliage) bake emits textures: [] and color_mean: null.
Failure statuses: 400 (empty / non-GLB), 413 (over ASSET_BAKER_MAX_UPLOAD_MB), 422 (bake failed — including the black-bake QA gate), 429 (worker busy — bounded concurrency), 504 (bake timeout).
5. Architecture & Wiring¶
The worker spawns a headless Blender subprocess (blender -b -P bake.py) per request and enforces bounds (size, timeout, concurrency=1, temp-file cleanup) around it. The bake pipeline (bake.py, ported from DayDreams' battle-tested spikes/one-cell/bake_lp.py) runs per source:
- Import → join → canonical-normalize. All meshes are joined and scaled to
canonical_sizemax-dimension before remeshing — raw GLBs aren't meter-scale and the relative voxel/ray heuristics explode without it (a 0.99 m "cottage" otherwise remeshes to 8.3 M faces). The base is rested atz=0. - Voxel-remesh fuses the interpenetrating shells/flaps that cause tilt/floaters into one watertight surface.
- Debris-shell drop removes loose fragments below
MIN_SHELL_FACES. - Decimate to
target_tris(skipped if already under). - Uniform smooth shading (hard normals corrupt cage baking) + clear seams.
- Smart-UV Project by angle + pack — seam-based unwrap fails on remeshed surfaces.
- Two-pass selected-to-active bake (tight + far-fallback, numpy composite of missed texels) of DIFFUSE/COLOR and TANGENT normal from the original into fresh textures.
- Mean-brightness QA gate, then export
<name>_LP.glb+BaseColor.png+Normal.png.
Hard-won correctness rules encoded as tested invariants (each cost DayDreams a real shipped defect):
- Metallic neutralization — img2mesh sources ship
metallic=1.0; a fully-metallic BSDF has no diffuse, so COLOR bakes come out pure black. The worker neutralizes metallic on the bake source in-memory only. - Mean-brightness gate — a baked color mean below
ASSET_BAKER_BRIGHTNESS_MINis a silently-failed bake and is refused with a non-zero result (422), never shipped. Re-enforced at the worker boundary. - Canonical-scale normalization before remeshing (above).
- Smart-UV by angle only — seam-based unwrap fails on remeshed surfaces.
- Foliage bypass (
mode=skip) — thin leaf-shell meshes fragment into shards under remesh+decimate, so foliage is normalized and re-exported without remesh/bake. Rule: buildings/props →bake; foliage →skip. - Per-source isolation + JSON summary — one bad asset never aborts a batch; a machine-readable summary drives the gate and content-addressing.
Outputs are SHA-256 content-addressed and written to MinIO (bake/<sha256>.{glb,png}) or the local artifact cache, reusing the Asset Worker (#343) schema. The service is intentionally separate from the media gateway; image-to-3D tickets submit raw provider GLB to this worker rather than embedding bake logic in each adapter.
6. Dependencies & Integrations¶
6.1. Current — Upstream (this service calls)¶
| Service | Category |
|---|---|
| minio | data |
6.2. Current — Downstream (services that call this)¶
| Service | Category |
|---|---|
| kong | infra |
| prometheus | infra |
6.3. Architecture diagram¶
Open the full-size diagram for a full-screen view.
6.4. Future — Missing pair integrations¶
- Backend media operations should call Asset Baker after hosted image-to-3D providers return raw GLB, before Asset Worker's optimize pass, forming the
generate → image→3D → bake → optimizechain. - Once a durable media-operation/Celery contract exists (#339), the bake step should ride the shared
queued|running|succeeded|failed|cancelled|timeoutoperation idiom instead of the current synchronous bounded API.
6.5. Future — Candidate new services¶
- A
creative-3dwizard track should split fromgen-ai-creativeonce the 3D fleet (gateway, image→3D, bake, segment, terrain) reaches ~5+ services.
6.6. Future — Unused features in this service¶
- GPU baking (
container-gpu, Cycles OPTIX/CUDA) is a fleet-scale optimization, deferred until separate performance evidence exists. Thebake.py--gpupath exists but is not exposed as a source variant. - Managed host
localhost(a/Applications/Blender.appon the developer machine, mirroring the ComfyUI managed-MPS pattern) is deferred until separate lifecycle evidence exists.
7. Troubleshooting¶
400 Input must be a GLB file: use a.glbfilename and a binary GLB payload; decompress EXT_meshopt GLBs to plain GLB first (Blender 4.3 can't import meshopt).422 baked color is black: the mean-brightness QA gate fired — typically ametallic=1source (neutralized automatically, but check the source's textures) or dead UVs. The bake is refused rather than shipping a black asset.- Thin foliage fragments into shards: pass
mode=skip(foliage bypass) — buildings/props bake, foliage skips. 429 Bake worker is busy: bounded concurrency (ASSET_BAKER_CONCURRENCY, default 1) rejected a concurrent bake; retry after the in-flight bake finishes.504bake timeout: raiseASSET_BAKER_TIMEOUT_SECONDSor lowertex_size/target_trisfor very heavy assets.401 Invalid Asset Baker bearer token: passAuthorization: Bearer ${ASSET_BAKER_API_TOKEN}.403 Input bucket is not allowed: add the intended bucket toASSET_BAKER_ALLOWED_INPUT_BUCKETS; do not broaden the list to unrelated or private buckets.- MinIO upload failure: confirm
MINIO_SOURCE=container,ASSET_BAKER_MINIO_BUCKET, and the generatedMINIO_ASSET_BAKER_*credentials. - Kong alias missing: confirm
ASSET_BAKER_SOURCE=container-cpu, run./start.sh --setup-hosts, and regenerate routes through the normal startup flow.