Environment rendering methods: what was tried, what came of it

A log of every method used to produce the game's visuals, with the owner's verdict, so the next experiment can be compared with the last. Newest at the bottom. Each entry says how to reproduce it. See wiki/art/art-direction.md for the target look and CLAUDE.md for the tools.

A. Blender tile kit rendered to a tileset (2026-10-17)

Branch feature/hollow-keep-tiles (not merged). Same pipeline as the characters: build in Blender, pre-render to sheets.

Result: a coherent, lit stone wall that matches the characters' lighting; the wolf's and player's normal maps and the tiles react to the same torches. Verdict (owner): "très cool mais... trop générique". One repeating brick motif everywhere, no composed silhouettes, nothing breaks the grid. Kept as the way to make a collision-accurate tileset; rejected as the main look.

B. AI-painted rooms, guided by the stage JSON (2026-10-17)

Branch feature/lantern-row. One painting per stage room, from ComfyUI.

Result: strong mood at a glance (the owner: "quand même très cool"), the collision lines land where the guides put them. Three problems the owner named: platforms almost invisible (same colours as the background), pixels far too big at x4, and the cutaway houses read as one room, not three storeys (ControlNet alone does not make storeys; the block-in helped, half-way). Also: each room is its own painting, so the style drifts between rooms unless the palette pass is strict, and anything non-ASCII or hand-tuned per room costs a 3 min render to check.

C. Four layers on the art direction document (2026-10-18)

Branch feature/lantern-row, the current prototype, built on B.

  1. City: an unguided AI-painted skyline (sky in rooms.json), 768x576 art px shown x2, in a Parallax2D at scroll (0.15, 0.1). Outdoor rooms (stage JSON outdoor) have no ceiling: level.gd keeps the ceiling and the rock above as invisible collision so the city shows through, and the project's clear colour is the palette's night.
  2. Rooms: the B paintings, repainted to the ADD prompts (indigo, neon cyan and magenta, ruined gothic fused with machinery) and pushed back by pixelate_rooms.py --background 0.35: everything but the neon lights darkened, then snapped to the palette's background colours only. Outdoor rooms are cut to transparent above a jagged skyline (sky_rows) so their facades stand against the city.
  3. Game layer: art/tools/make_pixel_tileset.py draws the collision tileset in code, 16 px per tile shown x2, from the palette's slate ramp (which the background never uses): lit wall tops, dark undersides, neon-cyan plank edges over a hard shadow, quiet wall interiors.
  4. Foreground: art/tools/make_foreground.py draws black rubble, railings, cables and the odd arch pillar as a strip in a faster Parallax2D (1.25).

Result: platforms read at once, the city gives depth, the pixels are twice as fine. The owner found it "pas mal". Still open: the AI backgrounds remain generic per room (a street, a market...) with no composed set pieces; the houses' storeys are still faint; props and creatures are palette-coloured placeholder rectangles, not drawn art.

D. Everything else on the palette (2026-10-18)

Same branch. Not rendering as such, but part of what the owner sees:

E. Blender dioramas rendered to pixels (2026-10-18)

Branch claude/dead-cells-art-pipeline-ffuzx1 (this merge). The "Blender diorama per room" idea from the list above, taken all the way: no AI painting at all, every layer rendered from Blender with toon materials on the palette, as the Frost Court prologue (ten rooms, selectable on the start screen next to Lantern Row for a side-by-side test).

tools/art_pipeline/ (this branch). Every material is a toon ramp of three palette entries (toon_materials.py); the render is one EEVEE sample with no filter, so a pixel is exactly a palette colour and the result needs no post-processing.

Good: deterministic and diffable, no GPU or external service, the palette is enforced by construction, lights and normal maps work on the paintings, a room's identity is a function you can read and tune, a change re-renders one room in ten seconds, and the same machinery can produce another stage from a new theme set. Bad: the first version of a theme is sparse (it took four passes to reach the current density), the pieces are geometric by nature (no painterly texture, no hand-drawn irregularity), and an artist cannot touch the result directly: the .blend is generated, the edits live in Python.

Where this leaves us

Method 3 reaches the layering and per-room identity of method 2 with the repeatability of method 1. It is the best fit for the project's rule that Blender files are the canonical source and everything is scripted. Method 2 still wins on painterly richness per room where a human picks the result; a hybrid (a diorama as the img2img start image for ComfyUI, then snap) is the obvious next experiment if the owner wants the AI texture back without losing the collision match.

Reproduce (see wiki/art/blender-dioramas-pipeline.md): python game/levels/make_frost_court.py, then blender --background --python tools/art_pipeline/make_frost_rooms.py, make_frost_strips.py, python tools/art_pipeline/make_frost_sky.py, sync the frost_court_tiles tileset, import, and --stage frost_court. About four minutes for the ten rooms; one room re-renders in ten seconds.

The same pipeline also made the seated lobby players (one sprite layer per costume piece, LayeredSprite in Godot), the spike this branch started with.

F. Dioramas as the ComfyUI start image (2026-10-18, in progress)

Branch feature/silent-abbey, chapter 3, The Silent Abbey (four rooms from the owner's brief: vestibule, library shaft, stained-glass gallery, grimoire sanctuary). The owner's hybrid: method E's deterministic Blender geometry for every layer, then ComfyUI only to add painted texture on top of it, then the palette snap.

Step 1 (done): game/levels/make_silent_abbey.py, the grey-box layout, with a theme and a dressing list per room; level.gd gained the "slain" door gate (the exit niche opens when the Ossuary Sentinel dies).

Step 2 (done): tools/art_pipeline/make_abbey_rooms.py imports make_frost_rooms.py (rock masses, back-wall pieces, the room builder's shape) and adds what the abbey needs: four theme builders, sixteen dressing props (hooded statues weeping magenta, neon tubes on sagging cables, bookshelves, spiral-stair stumps, mechanical vines, leaded windows with a few lit cells, the clock face, the altar and the book-and-engine hybrid, candle drifts), walkable planks drawn as library shelves with books under them, and one-tile solids on a floor drawn as furniture (oak tables in the gallery, marble steps in the sanctuary) instead of rock. New passes per room, into art/environments/silent_abbey/guides/: <id>_color.png (the painting over black, the img2img start image), <id>_depth.png (a depth material, near = white) and <id>_lines.png (edges where depth or normal jump, computed from the two renders). lights.json gained "strobe" (hard machine flashes behind the gallery windows; level.gd plays them). 27 s for the four rooms.

Result after step 2 (pure E, before any painting): the rooms read as four places at once, and the lights carry the mood. Open: the solids are still the slate rock masses of Frost Court (indigo/violet stone was the brief), the vestibule is sparse above the arches, no sky or parallax strips yet (step 4).

Step 3 (done): the ComfyUI pass over the Blender passes. rooms.json in art/environments/silent_abbey/ (one prompt per room, depth 0.8, lineart 0.55, img2img denoise 0.45); paint_rooms.py unchanged except two overrides (--denoise, --first-variant). The repaint takes 30-130 s per strip on the warm GPU. Then the new art/tools/composite_rooms.py: the AI repaint is box-filtered to 16 px per tile, pushed back and snapped to the palette's background colours (as pixelate_rooms.py --background), blown back up x2, and used only where the depth guide is far (the back wall); wherever the depth says playable layer or mid-depth set piece (statues, tables, candles, the engine) the Blender pixels are kept untouched, so collision, cyan edges and props stay crisp. The normal sheets stay Blender's.

Verdict so far (owner to confirm): the hybrid keeps everything method C lost (platforms, props) but at denoise 0.45 the LoRA adds little texture, it mostly flattens and greys the back wall before the palette snap pulls it back to indigo; 0.65 (paint/R01_v1.png) looks much the same. ControlNet from real depth and lineart holds the architecture perfectly, which is the gain over method B's 2D guides. The far layer's x2 pixels against the x1 playable layer read as depth. Worth trying next: a higher denoise with a stronger prompt for texture only on big flat walls, or dropping the LoRA for the wall pass.

Second pass on step 3 (same day), after the owner's three notes (too blocky, walls too flat, not enough mess):

Third pass (owner's zoom on a vestibule arch: "beaucoup plus blocky", and "beaucoup de dithering dans ton downscale"): the arch heads were six slabs in Blender, so the lineart guide was a staircase and ControlNet painted a staircase; pointed_arch in make_abbey_rooms.py now uses 16 slabs (window heads 14) and every theme goes through it. The speckle was the palette snap on a textured repaint, one pixel at a time; composite_rooms.py now blurs the downscaled repaint a little before the snap (--smooth 0.7) and runs a 3x3 mode filter after it (--despeckle 3), so the far layer comes out as flat shapes with no isolated pixels (the ADD's "no dithering"). Note that the variant number feeds the seed, so each repaint differs; _v4 is the current pick.

Step 4 (done): the layers around the rooms, as Frost Court's five strips. tools/art_pipeline/make_abbey_strips.py (imports make_frost_strips' machinery) renders far.png (broken spires, a buttressed nave with a dim rose, the ADD's dead machines: cooling towers, a gantry, masts with cyan blinks), mid.png (the abbey's outer cloister: pointed arcade on pillars, buttresses, a few lit windows, ducts and a dormant turbine) and foreground.png (black silhouettes: pillars carrying pointed arches, low pews, lecterns, candelabra, balustrades, chains with lanterns and censers). Two lessons from the first render: the bottom silhouettes must stay under one tile tall or they sit on the players' feet, and the pillars want to be thin and rare (every 48 units, 1.2 wide) or they hide the action. fog.png is make_frost_sky's bands.

The sky: the owner's plan was an AI-painted skyline. Three ComfyUI attempts (paint/sky_v0..v2): with the stage's style line the model paints a whole ruined abbey with ground and arches, even when the prompt says "only the sky", and that fights the rooms. rooms.json entries can now carry their own style line, and with a sky-only style the third attempt is the one: moon, flat clouds, a far citadel on its hill, misty lowlands, large flat shapes. It is box-filtered to 768x576, blurred 0.7, pushed back 0.3, snapped to the background palette and mode-filtered like the room far layers (the same recipe as composite_rooms.py; one Pillow line, see the strips script's docstring). make_frost_sky's procedural banded night remains the deterministic fallback. The stage scene fixes the sky vertically (sky_scroll (0.15, 1)) since the strip is as tall as the stage.

Step 5 (done): the enemies were already placed by the layout; the husks got the scholars' rags (a "skin" entity prop read by husk.gd), the tickwraiths found the abbey clock, and --abbeytest proves the Sentinel's death latches door D1 open and the exit clears the chapter.

Platform readability (owner's note, the Dead Cells rules: rim light on every standable top, hard shadow under every ledge, the playable layer on its own palette ramp with the screen's highest contrast). Two options were tried:

A+ is the current state; B's rims stay in the render (they help the pure method E look and the ComfyUI depth guides). Owner to confirm.

Step 6 (plumbing done, models pending): unique props from generated or downloaded 3D models. tools/art_pipeline/import_prop.py takes a GLB/FBX/OBJ (Meshy through blender-mcp's generate_3d, Poly Haven, Sketchfab), joins and normalises it (faces the camera, --height tiles tall, feet at z = 0), snaps its base-colour texture texel by texel to the palette's background and stone ramps, multiplies it by a three-step band of the lit intensity (the toon chain with a texture: form from the bands, detail from the texture) and saves it as prop_<name> into art/environments/silent_abbey_props.blend (images packed). make_abbey_rooms.py loads that library and lets a library prop override the geometric builder of the same dressing name (--no-library to compare, --library to point elsewhere).

First real prop (2026-10-19): blender-mcp's add-on has no Meshy and its other generators are off, but its Poly Haven library only needed the scene toggle blendermcp_use_polyhaven. Poly Haven's "Gothic Statue" (Benny Weimer, CC0: a robed, veiled figure with a book and chains, mossy stone) was imported at 1K, exported to art/environments/polyhaven/gothic_statue.glb (the canonical source) and run through import_prop.py as hooded_statue, 2.7 tiles tall. It replaces the seven block-out statues of the vestibule and the sanctuary; the brief's magenta tears come back as a library extra (weeping_extra: a stream, a pool, a light) drawn over any statue of that name. The textured, sculpted figure reads far better than the cone-and-sphere one at the same cost.

Meshy (2026-10-19): the owner generated "Silent Sentinel" on meshy.ai from the step-6 prompt (hooded cleric, hands joined, hood over the face, on a plinth; art/environments/meshy/silent_sentinel.glb, 2K baked texture). Imported as hooded_statue at 2.7 tiles it is the brief exactly, and its texture lands in the indigo and purple ramps on its own; the Poly Haven figure stays in the library as crowned_statue for later dressing. Lesson from the swap: the ComfyUI far layer must be repainted after a prop changes shape, or the old silhouette shows beside the new one as a pale ghost (the guides moved, the repaint did not). import_prop.py now saves the library through a temp file, since Blender will not overwrite a file it has just loaded props from.

The grimoire engine followed the same day: the owner's Meshy "Neon Grimoire" (art/environments/meshy/neon_grimoire.glb: an open book on an engine underside, a crystal floating over the pages) imported as grimoire_engine at 4 tiles with the violet ramp allowed (--family ...,violet, so the crystal lands in violet instead of stone), and a library extra (engine_extra) gives it the brief's life: a magenta core light and a magenta-light core where the crystal floats, cyan sparks around it, a cold rune light low on the block. The sanctuary was re-rendered, repainted (v7) and composited. Of the unique props only the terminals remain block-outs.

Reproduce: python game/levels/make_silent_abbey.py, then blender --background --python tools/art_pipeline/make_abbey_rooms.py --, blender --background --python tools/art_pipeline/make_abbey_strips.py -- (then the sky and fog with make_frost_sky's functions, see the strips script), python art/tools/paint_rooms.py art/environments/silent_abbey (ComfyUI up; the settings live in rooms.json), python art/tools/composite_rooms.py art/environments/silent_abbey --background 0.25 --pick R01=4 ... --to-game game/assets/paint/silent_abbey, import, --stage silent_abbey --stageshots <path>.

Comparison of the three stage methods (A, C, E)

Method 1: Blender tile kit Method 2: pixel tileset + ComfyUI rooms Method 3: Blender dioramas to pixels
Stage Hollow Keep (chapter 1) Lantern Row (prologue) Frost Court (prologue, POC)
Branch / docs main; art/tools/make_tileset_kit.py, render_tileset.py feature/lantern-row (merged here); wiki/art/art-direction.md, art/tools/paint_rooms.py this branch; wiki/art/blender-dioramas-pipeline.md
What is drawn 24 tiles (16 walls, 4 planks, 4 back walls) 24 tiles drawn in code + one AI painting per room + sky + foreground strips one painting per room rendered from a 3D diorama + sky, far, mid, fog, foreground strips
Source of truth art/environments/hollow_keep.blend (generated by script) art/environments/lantern_row/rooms.json (prompts) + the picked variants game/levels/frost_court.json (theme + dressing per room) + the Python builders
Tools needed Blender Pillow; ComfyUI with SDXL, a pixel-art LoRA and ControlNet on a GPU Blender (or the bpy wheel), Pillow
Palette free (shaded stone) cyber_fantasy.gpl, snapped after painting cyber_fantasy.gpl, by construction (every material is three palette entries)
Shading smooth: soft sun, 4x supersampling, normal maps lit in game the painting's own lighting, flat in game; the tiles have a lit rim hard 3-band toon, no anti-aliasing, normal maps lit in game, per-room light list
Per-room identity none: the same bricks everywhere strong, but what the model decides strong and controlled: a theme function + a dressing list
Time per stage minutes to render hours of prompting and picking variants per room, plus repaints ~4 min to render ten rooms; the time goes into writing theme builders
Repeatable yes, deterministic no: every run paints differently, seeds help a little yes, deterministic, every change is a diff
Edits change the kit script, re-render repaint the room (and lose the parts you liked) edit one builder or one dressing line, re-render that room
Collision the tiles themselves the tiles (drawn as the crisp playable layer) the tiles, invisible (paint_hides_tiles); the painting draws the solids
Weak spots generic, no identity, looks like every other tileset walls and floors never quite match the painting; AI artefacts; not reproducible dioramas are only as rich as the builders; big surfaces can look empty until dressed

Screenshots at the same camera setting, Hollow Keep / Lantern Row / Frost Court, one per zoom level: wiki/art/images/render_method/compare_room.png, compare_nearby.png, compare_whole_map.png; full-size single shots and the start screen next to them; the whole comparison as a PDF: wiki/art/images/render_method/render_method.pdf.

Not tried yet (ideas on the table)

Owner's notes so far