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
+24
View File
@@ -99,6 +99,30 @@ export const app = new Elysia()
set.headers["content-type"] = "application/octet-stream";
return new Response(stream as any);
})
// Phase 12: the texture atlas (packed block-texture PNG + UV rect map) worker uploads once at
// startup to a fixed, version-agnostic key — no per-server/pointer-row lookup needed since it's
// worker-wide, same scope limitation as the Phase 11 texture palette (see README). 404s (not a
// 500) until a worker with ACCEPT_MINECRAFT_EULA=true has actually built and uploaded one.
.get("/api/atlas.png", async ({ set }) => {
try {
const stream = await minio.getObject(TILE_BUCKET, "atlas/current.png");
set.headers["content-type"] = "image/png";
return new Response(stream as any);
} catch {
set.status = 404;
return { error: "atlas_not_built" };
}
})
.get("/api/atlas.json", async ({ set }) => {
try {
const stream = await minio.getObject(TILE_BUCKET, "atlas/current.json");
set.headers["content-type"] = "application/json";
return new Response(stream as any);
} catch {
set.status = 404;
return { error: "atlas_not_built" };
}
})
// See link.ts's doc comment: the session token comes back in the body, not an httpOnly
// cookie, and is sent back via this header on subsequent requests.
.post("/api/link/redeem", async ({ body, set }) => {