Phase 12: texture atlas + UV-mapped 3D mesh textures

Fixes a real gap left over from Phase 11: mesh.rs's 3D mesher was still
using the hand-picked flat palette instead of texture-averaged colors.
Adds worker/src/atlas.rs to pack downloaded block textures into a single
PNG atlas + UV rect map, threads tile-relative UV and atlas-rect buffers
through the mesh binary format (v2, hard break — meshes are a
regenerable render cache), serves the atlas from MinIO via two new api
routes, and adds a custom Babylon shader that falls back to flat vertex
colors per-fragment for untextured quads. glTF export intentionally
stays vertex-color-only (documented reasoning in gltf-export.js) since
standard glTF materials can't express that same per-fragment fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-10 00:41:15 +02:00
parent f8216b6e77
commit 6ad8051eae
16 changed files with 843 additions and 44 deletions
+61 -1
View File
@@ -90,7 +90,7 @@ to thread a server-scoped palette through `main.rs`'s batch loop instead of one
already has every loaded mod's texture assets, just unused server-side. The `forge-1_12_2` mod
leaf (Enigmatica 2, this project's primary target) extracts them once at startup
(`BlockAssetExtractor`, best-effort: guesses each block's texture by its registry-name path
segment, not a real blockstate/model JSON resolution — that's Phase 12's job) and ships two new
segment, not a real blockstate/model JSON resolution — that's Phase 13's job, see below) and ships two new
WS messages after connecting: `block_registry` (numeric id -> registry name, needed since a
numeric `blockId` alone is meaningless without the mod list that assigned it) and `block_textures`
(the extracted PNGs, batched). The api stores both (`api/src/textures.ts`: registry rows in a new
@@ -103,6 +103,66 @@ implement extraction yet either — `BackendConnection#sendBlockRegistry`/`#send
on the shared interface (so any leaf can adopt them later with no protocol change), but only the
1.12.2 leaf calls them so far, matching this phase's Enigmatica-2-focused verification target.
### Texture atlas + UV-mapped 3D meshes (Phase 12)
Phase 11 only fed texture-averaged colors into the 2D tile path — `worker/src/mesh.rs`'s 3D
section mesher still called the plain hand-picked `palette::color_for`, so the 3D viewer's "now
accurate" claim in that phase's writeup wasn't actually true yet. Fixed first: `mesh.rs` now calls
`color_for_textured` too, same as the 2D path.
On top of that, `worker/src/atlas.rs` packs every block texture the worker already downloads (see
Phase 11 above) into a single RGBA PNG atlas (one native `16x16` tile per texture, deterministically
laid out in a square-ish grid) plus a `texture name -> normalized [u0,v0,u1,v1]` rect map, cached
to disk like the palette. It does its own jar download rather than sharing Phase 11's — a small,
one-time, cached duplicate fetch, accepted to keep the two build paths independent. Unlike the
palette's post-hoc `texturepacks/` overlay, the atlas always rebuilds (and caches under its own
`-<pack>` suffixed filename) when `TEXTURE_PACK` is set, since there's no cheap way to patch one
tile back out of an already-packed image.
`mesh.rs` now emits two new per-vertex buffers alongside the existing position/normal/color ones:
tile-relative `uv` (unbounded — a merged quad spanning N blocks has that UV coordinate run 0..N,
not 0..1) and `atlasRect` (the same 4 floats repeated for all 4 vertices of a quad; `[0,0,0,0]`
sentinel when the block has no atlas entry — the frontend falls back to the flat vertex color for
that quad). This is a hard break in the section-mesh binary wire format (v2) — safe to do without
any migration, since rendered meshes are a fully regenerable cache (MinIO + a Postgres pointer
row), not a durable artifact; an old-format blob just gets silently overwritten the next time that
section's dirty-chunk job runs.
The atlas PNG + UV-map JSON are uploaded once at worker startup to fixed, version-agnostic MinIO
keys (`atlas/current.png`, `atlas/current.json` — matches the worker-wide-only scope already
established for the palette/texturepack in Phase 11) and served by `api` at `GET /api/atlas.png`
/ `GET /api/atlas.json` (404 until a worker with `ACCEPT_MINECRAFT_EULA=true` has built one).
The live Babylon 3D viewer (`frontend/src/public/js/mesh.js`) uses a custom unlit `ShaderMaterial`
(nearest-neighbor sampling, mipmaps disabled — bilinear/mip blending would bleed a tile's edge
pixels into its atlas neighbor) whose fragment shader `fract()`s the tile-relative UV to repeat a
single atlas tile across a merged quad, and falls back to the plain vertex color per-fragment when
`atlasRect` is the `[0,0,0,0]` sentinel — this per-fragment branch is what makes the live viewer
strictly more capable than the exported glTF here (see below), and is what actually makes greedy
meshing (which merges many blocks into one quad) compatible with per-block texture tiling at all.
Falls back entirely to the pre-Phase-12 flat-color `StandardMaterial` if no atlas was ever
uploaded (fetch 404/error). **Not yet empirically verified against a real running worker + browser**
(no headless-GL environment available in this dev setup) — in particular `invertY`'s row-order
assumption against `atlas.rs`'s top-down PNG rows is unconfirmed, worth checking on first live
test, same "flagged, not yet live-tested" caveat this project already carries for the Xaero
waypoint format (Phase 4).
The client-side region-export mesher (`voxel-mesh.js`) gained the identical UV/atlasRect output
(ported by hand from `mesh.rs`, same pattern as `block-colors.js` mirroring `palette.rs` — see the
new `block-textures.js` mirroring `block_names.rs`), but **`gltf-export.js` deliberately does not
embed the atlas texture into exported glTFs** — still vertex-color-only, unchanged from before this
phase. Reason: standard glTF materials only support one fixed formula (`baseColorTexture *
baseColorFactor * COLOR_0`, no branching), so the live viewer's per-fragment vertex-color fallback
for untextured quads isn't expressible in a way that works in arbitrary external viewers (Blender,
generic glTF web viewers) — properly supporting it needs either a reserved always-white atlas tile
baked into `atlas.rs` or splitting merged geometry into per-material primitives, both real scope,
deliberately deferred rather than shipping some faces textured and others visibly wrong.
**Still not done** (see the plan's phase list): real non-cube block/blockentity geometry via
blockstate/model JSON parsing (`BlockAssetExtractor`'s texture matching is still a filename
convention guess, not a real model resolution) — renumbered to **Phase 13** once the atlas/UV
scope above turned out to be its own full phase; Thaumcraft remains the named stress test for it.
## Running
```