Files
TheriapolisV3/theriapolis-tile-generation-handoff.md
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

13 KiB
Raw Blame History

Theriapolis — Tile Generation Handoff

This is the working knowledge accumulated while populating Content/Gfx/tactical/ with art generated through the Pixellab MCP service. Read this whole file before generating any new tiles — there are several non-obvious failure modes and the ones we already burned credits on have specific fixes documented below.


1. Where tiles live

Content/Gfx/tactical/
    surface/   ← edge-to-edge ground textures, fully opaque
        grass_0.png   grass_1.png   grass_2.png         (user manual)
        tallgrass_0.png  tallgrass_1.png                (user manual)
        dirt_0.png    dirt_1.png                        (user manual)
        gravel_0.png  gravel_1.png                      (user manual)
        sand_0.png    sand_1.png    sand_2.png          (Pixellab)
        snow_0.png    snow_1.png    snow_2.png          (Pixellab)
        rock_0.png    rock_1.png    rock_2.png          (Pixellab — see §6 known issues)
        marsh_0.png   marsh_1.png   marsh_2.png         (Pixellab — third reroll)
        mud_0.png     mud_1.png     mud_2.png           (Pixellab)
        cobble_0.png  cobble_1.png  cobble_2.png        (Pixellab)
        floor_0.png   floor_1.png   floor_2.png         (Pixellab)
        wall_0.png    wall_1.png    wall_2.png          (Pixellab)
        shallowwater_0.png … _2.png                     (Pixellab)
        deepwater_0.png   … _2.png                      (Pixellab)
        troddendirt_0.png … _2.png                      (Pixellab — see §3.5)

    deco/      ← single objects with transparent background
        tree.png     bush.png     snag.png              (Pixellab — forest set)
        flower.png   crop.png     reed.png              (Pixellab)
        rock.png     boulder.png                        (Pixellab)

Filenames are lowercased enum names matching TacticalSurface and TacticalDeco in TacticalTile.cs. The _N suffix is a variant index — the renderer picks one per cell from a deterministic per-tile Variant byte. If only <name>.png exists (no suffix), every cell uses it. The atlas auto-loads anything in these folders; missing files fall back to procedurally-generated colored squares from TacticalAtlas.cs.

Provenance matters. The user manually placed grass/tallgrass/dirt/gravel to set the visual style baseline. Do not overwrite those. Anything the user produced in the Pixellab web UI also takes precedence over MCP-generated tiles. When in doubt, ask before saving over an existing file.


2. The Pixellab MCP setup

The MCP server pixellab provides programmatic access to PixelLab AI's generators. Tools we use:

  • mcp__pixellab__create_tiles_pro — surface tiles. Always returns 16 variations regardless of what you describe. Async (~30 s without style_images, ~7-8 min with style_images).
  • mcp__pixellab__get_tiles_pro — poll status + retrieve URLs.
  • mcp__pixellab__create_map_object — single decoration sprite with transparent background. Async (~30-90 s).
  • mcp__pixellab__get_map_object — poll/retrieve.

Each call returns a job ID immediately; the work happens server-side. Use ScheduleWakeup with the job ID(s) in the prompt to come back when the work is likely done — don't poll in a tight loop.

MCP scope quirk. When you run the Pixellab CLI installer, it adds the server entry to whichever directory you were cd-ed into at the time. If the project Claude Code sees doesn't have the entry, the tools won't appear. Fix by editing ~/.claude.json directly — find the project entry under projects[<your-project-path>].mcpServers and ensure pixellab is in there. A full Claude Code restart is required for changes to take effect.

Auth. The bearer token in the config is reusable indefinitely until rotated. Don't leak it — rotate any token that gets pasted in chat or read into a tool result.


3. The generation workflow

3.1 Surface tiles — use style_images mode, ALWAYS

Without style_images, create_tiles_pro bakes a 13 pixel dark border frame into every tile (the algorithm literally "draws tile shape outlines and has AI fill them with pixel art"). The border is sometimes pure black, sometimes a dark grey/purple, and the existing TacticalAtlas.StripBorderPixels only catches some of it. Don't rely on the strip; just bypass the bordering algorithm by providing a clean reference.

The reference image has to be a clean, edge-to-edge, fully-opaque 32×32 PNG with no border itself. The user's grass_0.png works perfectly. Encode it base64 and pass it as style_images.

3.2 Avoid the baked-in shadow gradient

create_tiles_pro defaults to tile_view: "low top-down" which adds ~30% pseudo-3D depth. The AI renders that depth as a dark shadow on the bottom and right edges of every tile (interior brightness drops by 5090 points on those edges). The border detector misses this because the shadow color isn't black-uniform — it's just darker than the interior.

Tiled together, every tile shows a diagonal grid of dark seams. Always set:

"tile_view":         "top-down",   // not "low top-down"
"tile_depth_ratio":  0,             // force flat
"style_options":     { "color_palette": false, "outline": false, "detail": true, "shading": false }

shading: false in style_options also helps suppress the depth shadow.

3.3 Force opaque coverage in the prompt

If you say "no plants no rocks no transitions" the AI sometimes interprets that as "make most of the tile transparent and put a small clump of content in the middle". This produced a marsh-v2 batch where every tile was 3080% transparent. Fix by explicitly demanding full opacity:

"A fully opaque solid 32x32 ground tile … every single pixel must be opaque earth, no transparency anywhere, no holes…"

3.4 Decoration tiles — use create_map_object, not create_tiles_pro

Decorations are individual props (trees, bushes, etc.) with transparent backgrounds. They composite over surface tiles. create_map_object is designed for this — it returns a single PNG with proper transparency. Standard params:

{ "width": 32, "height": 32,
  "view": "high top-down",
  "outline": "single color outline",
  "shading": "medium shading",
  "detail": "medium detail" }

3.5 Avoid description traps that bake in directional features

For troddendirt we said "wheel ruts" in the description. The AI obliged by drawing very prominent vertical stripes that read as wood planks when tiled. Subtle features → safe. Specific features → problematic, because the AI baked them centered/directional and they don't tile.

Rule of thumb: if you want the texture to feel like a road but tile seamlessly in any orientation, leave directional language out of the prompt and let the natural variation in the 16 tiles give you variety.


4. The tile-analyze command

Run it on any folder of 32×32 PNGs:

dotnet run --project Theriapolis.Tools -- tile-analyze --dir /tmp/pixellab-<surface> --sheet /tmp/<surface>-sheet.png

Output is a per-tile table + an optional 4×-upscaled labeled contact sheet. Each label shows:

  • brd:N — number of edges (04) where ≥80% of perimeter pixels are dark-uniform "border" pixels (max channel ≤ 95, max - min ≤ 25). Should be 0 for a clean tile.
  • op:NN% — fraction of pixels with α ≥ 128. Should be 100% for a surface tile, 3070% for a decoration sprite.
  • sh t/b/l/r — interior brightness minus edge brightness, per side. Positive = edge is darker than interior. Anything > 30 is a baked-in shadow gradient that will show as a grid of dark seams when tiled. This is the single most useful number for triage; the border detector misses shadows.

Labels are GREEN if the tile passes everything, ORANGE if it fails any check. The full implementation lives in TileAnalyze.cs; the detector methods (CountBorderEdges, CountOpaqueFraction, ShadowScores) are public so you can call them directly from any future Tools command.

Spot-check existing saved tiles

Same command works on Content/Gfx/tactical/surface/:

dotnet run --project Theriapolis.Tools -- tile-analyze \
    --dir Content/Gfx/tactical/surface

Use this to sweep for the shadow issue before assuming any saved tile is clean. The history of what's been spot-checked vs not is in §6.


5. End-to-end recipe: generating one new surface

  1. Read this whole document first.
  2. Verify Pixellab MCP is loaded (/mcp in Claude Code shows it).
  3. Get the base64 of Content/Gfx/tactical/surface/grass_0.png (the reference image). The previous session has this cached at /tmp/grass_0_b64.txt if it survived.
  4. Call create_tiles_pro with:
    • description: include "fully opaque … every single pixel must be opaque … edge-to-edge" language; avoid directional feature language.
    • style_images: [{"base64": "...", "width": 32, "height": 32}]
    • tile_view: "top-down", tile_depth_ratio: 0, style_options: {color_palette:false, outline:false, detail:true, shading:false}
  5. Note the returned tile_id. Schedule a ScheduleWakeup for ~480 s.
  6. On wakeup, get_tiles_pro(tile_id). If still processing, reschedule another 240 s. Once complete, download all 16 PNGs from the B2 URLs.
  7. Run tile-analyze --dir <download-dir> --sheet <out-sheet.png>.
  8. Show the contact sheet to the user along with the table. Recommend 3 picks that:
    • Pass all detector checks (border 0, opaque 100%, shadow ≤ 30).
    • Don't have strong directional features (wheel ruts, plank lines, centered swirls — these tile poorly).
    • Aren't visually identical to each other (variant rotation needs real variety).
  9. After user confirms, save the picks as <surface>_0/1/2.png in Content/Gfx/tactical/surface/.
  10. Rebuild Desktop (dotnet build Theriapolis.Desktop) so the runtime bin/Debug/net8.0/Gfx/ copy refreshes.

6. Known issues / deferred work

  • rock_0.png has a -87 shadow on the right edge. Detected in a spot-check but not yet rerolled. Use the §3.2 flat-shading params. rock_1 and rock_2 haven't been spot-checked at all — verify before rerolling so we know if all three need replacing.
  • mud_0 and cobble_0 are borderline (shadow scores in the high 20s). Just under the 30 threshold. Acceptable for now but candidates for a quality pass later.
  • Marsh history. The marsh surface took three rerolls:
    • v1: came back as ponds with reeds and grey borders (asked for "reedy growth" — AI placed reeds on a smaller central pond shape).
    • v2: came back as transparent decoration sprites (the prompt said "no reeds no plants" too forcefully — AI made everything except the moss patches transparent).
    • v3 (current): correct after explicit "fully opaque every pixel" wording. This is the canonical example of the failure modes in §3.3 + §3.5.

7. Quick reference — current generation params we know work

# Surface tiles (create_tiles_pro)
{
    "description": "<see §3.3 — emphasize opacity + edge-to-edge>",
    "style_images": '[{"base64": "<grass_0.png base64>", "width": 32, "height": 32}]',
    "style_options": '{"color_palette": false, "outline": false, "detail": true, "shading": false}',
    "tile_view": "top-down",
    "tile_depth_ratio": 0
}

# Decoration sprites (create_map_object)
{
    "description": "<see request doc>",
    "width": 32, "height": 32,
    "view": "high top-down",
    "outline": "single color outline",
    "shading": "medium shading",
    "detail": "medium detail"
}

These are the configurations that produced clean tiles after we worked through the failure modes. Deviate only when you have a reason and you've re-checked with tile-analyze.


8. Architecture context

The tactical render path that consumes these tiles is:

  1. TacticalChunkGen stamps TacticalSurface and TacticalDeco enum values into each tactical tile based on biome, road polylines, settlement footprints.
  2. TacticalAtlas loads PNGs from Content/Gfx/tactical/{surface,deco}/ keyed by lowercased enum name. Falls back to procedural color squares for anything missing.
  3. TacticalRenderer draws each tile's surface texture, then its decoration if any, in chunk-by-chunk passes.

The art request document for human artists is theriapolis-tactical-tile-art-request.md — it's the same content list we're satisfying with Pixellab, just framed for a human painter. Keep both docs in sync if the surface or decoration enums change.