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.
art/tools/make_tileset_kit.pybuilds 24 tiles as one collection each inart/environments/hollow_keep.blend: 16 wall variants by closed sides (N=1, E=2, S=4, W=8), 4 one-way planks, 4 back-wall variants. Running-bond bricks as bevelled, chipped blocks, keyed on their position modulo one tile so neighbours meet without a seam; capstones, quoins and lintels on open sides; a mortar backing plate.art/tools/render_tileset.pyrenders each tile's square with an orthographic front camera at 4x supersampling, box-filtered to 32 px, colour plus a camera-space normal sheet (the same encoding as the character sheets, so Light2D shades both alike). About 80 s for the 24 tiles.game/tools/sync_tilesets.gdwrites the Godot TileSet (CanvasTexture with the normal map, collision polygons by row,autotilemetadata) andlevel.gdpicks each wall, plank and back-wall variant from the map.
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.
- Hardware: RX 6950 XT (gfx1030) through ComfyUI-ZLUDA with HIP SDK 6.4 (7.2 does not work for this card). The first job after a HIP change compiles for ~40 min; then a 1280x768 image takes ~3 min. Models: SDXL base 1.0, sdxl-vae-fp16-fix, the Pixel Art XL LoRA, ControlNet Union SDXL promax.
art/tools/make_room_guides.pydraws, per room, from the stage JSON: a depth guide (solid near, planks a little behind, the back wall far; long plank runs as thick storey floors), a lineart guide of every solid edge, a gameplay mask, and for two-cell houses a colour block-in (one wall colour per storey, beams, posts, with brick grain and noise so img2img turns it into masonry).art/tools/paint_rooms.pydrives ComfyUI's HTTP API: ControlNet depth (0.6-0.95) and lineart (0.3-0.6), 28 steps DPM++ 2M Karras, CFG 6, the LoRA at 1.0; wide rooms in overlapping 1536 px strips; houses as img2img from the block-in at denoise 0.86 (0.72 kept the flat colours; txt2img lost the storeys). Prompts and per-room settings inart/environments/<stage>/rooms.json. Under ZLUDA the VAE decode needs cuDNN off (the CFZ toggle node).art/tools/pixelate_rooms.pybox-filters the paintings down and maps them onto one palette without dithering. First pass: 8 art px per tile shown x4, palette of 40 colours built from the paintings. Second pass: 16 px per tile shown x2.
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.
- City: an unguided AI-painted skyline (
skyin rooms.json), 768x576 art px shown x2, in a Parallax2D at scroll (0.15, 0.1). Outdoor rooms (stage JSONoutdoor) have no ceiling:level.gdkeeps 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. - 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. - Game layer:
art/tools/make_pixel_tileset.pydraws 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. - Foreground:
art/tools/make_foreground.pydraws 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:
- UI: one project theme (
game/assets/ui/add_theme.tres) with the Press Start 2P pixel font (8 px native, no anti-aliasing), cyan text, night buttons and inputs with a 1 px cyan border;WireBoxframes and bars; the drawn screens (gear, results, chapter picker) throughscripts/ui_style.gd. - Props and creatures:
art/tools/palette_scenes.pyrewrites their colour rects: explicit accents per prop node, dark ramp plus at most two magenta weak points per creature, violet second lights on bosses. - Feedback:
scripts/vfx.gdpixel sparks and dust, swing and shot trails, magenta telegraph glow on attack windups, screen shake and chromatic aberration (scripts/screen_fx.gd) on boss roars and slams.
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.
make_frost_kit.pybuilds tiles (used here for collision only) and the props.make_frost_rooms.pybuilds each room as a diorama from the stage JSON: the solid cells become rock masses with plates, cracks, course lines, snow caps or frost rims, icicles and quoin plates; plank runs become gratings with a cyan edge; the back wall comes from the room's theme (ten themes: courtyard, rampart, guardroom, cellar, gallery, chapel, tower, terrace, generator, gate) built from pieces like arches, windows, pilasters, banners, sconces, bunks, stairs, a generator, a console, an arc coil, a fountain, gargoyles, a gate with a portcullis; the room's dressing list places the kit props. Each room renders to a painting plus a normal sheet, and every glowing thing goes intolights.json.make_frost_strips.pyrenders the far spires, the street facades and the foreground silhouettes;make_frost_sky.pydraws the sky and the fog.level.gd(incremental on the Lantern Row code) shows five parallax strips, lights the paintings throughCanvasTexturenormals, turnslights.jsonintoPointLight2Ds (some flickering), and adds gusting snow.
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):
- Toon ramps of 4 or 5 palette steps instead of 3 (
toon_ninmake_abbey_rooms.py) for the rock, plates, walls, marble, wood and iron: the bevels and top faces pick up one more tone, the blocks read less like boxes, every pixel is still a palette entry. - A
wall_clutterpass in every theme: cyber pipes with flanges, valves and gauges, conduits with junction boxes and LEDs, wires hanging from the vault with small bulbs, and magenta-pod vines creeping over the stone, all hashed from a per-room seed (density per theme: the archive gets less, its shelves already fill the wall). - The ComfyUI pass is now a texturing pass: denoise 0.8 (was 0.45), LoRA 0.6
(was 1.0), ControlNet depth 0.9 and lineart 0.6, the style prompt asks for
"finely textured weathered stone masonry, cracked indigo and violet bricks,
grime and stains" and "intricate detail" left the negative. The composite
pushes the far layer back by 0.25 instead of 0.35 so more tones survive. At
0.45 the LoRA only flattened the wall; at 0.8 SDXL finally draws masonry,
stains and courses on the big surfaces, and the composite keeps the AI's
inventions off the playable layer and the set pieces (variants kept side by
side in
paint/:_v00.45,_v10.65,_v2/_v30.8).
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:
- B, in the render:
rim_and_shadow_rect/runin make_abbey_rooms.py draws a two-tone slate rim on every open top, a cyan-light edge on every shelf, and a void shadow band on the wall under every ledge. Helps, but the rock faces stay the diorama's and the shadow bars read heavy under thin shelves. - A+, the code-drawn game layer over the painting (the owner's method, step 3
of the plan): the stage scene shows its tiles again (
paint_hides_tilesfalse, the Lantern Row pixel tileset: lit tops, dark undersides, cyan plank edges over a hard shadow, quiet interiors) andlevel.gddraws them only inside the room rects, keeping the rock between rooms as invisible collision so the parallax strips still show there. Platforms pop at once and never share a colour with the back wall; the diorama's rock faces are covered, its set pieces and back walls stay.
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)
- (Done in E.) A Blender diorama per room rendered with its normal map; the hybrid with an AI painting only for the far background is still open.
- Painting each storey of a house separately with its own prompt, then assembling them with the beams drawn in code.
- Platforms as unique objects per run of planks (balcony, scaffold, broken beam) instead of a tile.
- Unique props (statues, banners, gates) from
generate_3dor Poly Haven through blender-mcp, rendered like the characters. - ControlNet from a Blender depth render instead of the JSON guide, so the AI paints over real geometry.
Owner's notes so far
- Liked: the painted mood of B, the four-layer depth of C, the HUD.
- Disliked: generic repeating tiles (A), invisible platforms and chunky x4 pixels (B), ceilings over outdoor rooms.
- Wants: a gothic city quarter with houses and shops in cutaway, low-fi cinematic 2D with a little parallax, platforms with variety "pour rendre ça vivant", more detail than x4.
- Screenshots of every Lantern Row room:
godot --path game -- --host --port 7790 --stage lantern_row --stageshots <path>saves<path>_R01.png...<path>_map.pngfor review.