Art pipeline: Blender to sprite sheets

Characters and monsters are built in Blender and pre-rendered to 2D sprite sheets. The .blend file is the source of truth; everything under art/renders/ is generated.

Tools

Tool Install Purpose
Blender 5.2 LTS winget install --id BlenderFoundation.Blender --exact modelling, rigging, animation, rendering
uv winget install --id astral-sh.uv --exact runs the MCP server with uvx
MCP for Blender (ahujasid) .mcp.json in the project root already registers it for Claude Code lets Claude script Blender, fetch Poly Haven assets, run block-outs

One-time Blender add-on setup for MCP

Already done on this machine on 2026-10-16 (Blender 5.2.2, uv 0.13.1, add-on installed to %APPDATA%\Blender Foundation\Blender\5.2\scripts\addons\blender_mcp.py and enabled). On a new machine:

  1. Run uvx mcp-for-blender install-addon (copies the add-on into Blender's addons folder), or download addon.py from https://github.com/ahujasid/blender-mcp and install it through Edit > Preferences > Add-ons > Install. The installer's last message may crash on a Unicode arrow in a cp1252 console; the file is still copied.
  2. In Blender, hover the 3D viewport, press N to open its sidebar, pick the vertical "MCP for Blender" tab and click "Connect to MCP server". The add-on listens on localhost:9876 with no authentication, so keep it on this machine only.
  3. Start Claude Code in this folder; the blender MCP server from .mcp.json is picked up automatically (telemetry off, safe mode on). Save the .blend before letting Claude run code.

Blender is not on PATH by default; the full path is C:\Program Files\Blender Foundation\Blender 5.2\blender.exe.

Rendering a sheet

blender -b art/characters/<name>.blend --python art/tools/render_spritesheet.py -- --out art/renders/<name>

Options: --ppu 32 (pixels per Blender unit), --frame 64x64, --fps 12, --actions idle,walk (default: every action with a user or fake user), --armature Rig, --flip-normal-y, --keep-frames, and for weapon layers --hide <objects> and --socket <grip>:<direction> (below).

Outputs in art/renders/<name>/:

Rules that keep sheets consistent

Weapon layers

The body sheet is rendered without the held weapon and records where the weapon sits in every frame. The weapons are rendered once each, to their own sheet. The game draws the equipped one at the socket.

Hooded swordsman: the sword is four parts (Blade, Guard, Grip, Pommel) parented to the wrist bone hand.R, which sits at the end of arm.R.

Swing animations

art/tools/animate_attacks.py is a scripted first pass at the attack animations, made at the owner's request. A human animator can refine it or replace it. It rewrites hooded_swordsman.blend (Blender keeps a .blend1 backup):

blender -b art/characters/hooded_swordsman.blend --python art/tools/animate_attacks.py

Edit SWINGS in the script and run it again to change them; it replaces the atk_* actions each time. It also writes atk_up, atk_upfwd and atk_crouch (the directional swings) and, from POSES, the plain crouch pose (the legs are single bones, so the crouch is a wide low stance with the feet kept on the floor). - Strike ranges. STRIKE marks the part of each swing where the weapon actually cuts. It is stored on the action as a custom property strike and exported with the sheet. The game plays that part exactly while the hit window is open, and builds the hit area from those frames only. So a strike range that starts too early makes the hit area reach behind the character. - Leftover pose. After authoring, the script puts every bone back to its rest pose. The renderer also starts each action from rest, so a bone an action does not key (like the wrist in idle) never inherits another action's pose. - In the game, each combo step picks one (Items.SWING_ANIMS) and plays it stretched to the step's length.

Render the body after any animation change (it picks up every action):

blender -b art/characters/hooded_swordsman.blend --python art/tools/render_spritesheet.py -- --out art/renders/hooded_swordsman --hide Blade,Guard,Grip,Pommel --socket Grip:Blade

Weapons:

blender -b --python art/tools/render_weapons.py -- --out art/renders/weapons

Then copy both sheets into the game:

godot --headless --path game --script res://tools/sync_sprite_sheets.gd -- hooded_swordsman weapons
godot --headless --path game --import

Ironbound Wanderer (playable, from Meshy)

A playable character (the original kit), and the rig and animations Silas and Aurel are built on. Modelled, auto-rigged and animated in Meshy, then built into art/characters/ironbound_wanderer.blend by art/tools/import_meshy.py. The Meshy downloads live in art/characters/source/ironbound_wanderer/, which is git-ignored (about 120 MB): keep a copy of the zips to rebuild the .blend.

blender -b --factory-startup --python art/tools/import_meshy.py -- --character art/characters/source/ironbound_wanderer/Meshy_AI_The_Ironbound_Wandere_biped_Character_output.fbx --anim idle=art/characters/source/ironbound_wanderer/anim_idle.fbx --anim run=art/characters/source/ironbound_wanderer/Meshy_AI_The_Ironbound_Wandere_biped_Meshy_Merged_Animations.fbx:Run_02 --anim jump_full=art/characters/source/ironbound_wanderer/anim_jump.fbx --anim hurt_full=art/characters/source/ironbound_wanderer/anim_hurt.fbx --anim downed_full=art/characters/source/ironbound_wanderer/anim_downed.fbx --anim attack=art/characters/source/ironbound_wanderer/anim_slash.fbx --pin-height jump_full --clip jump=jump_full:15-20 --clip fall=jump_full:21-24 --clip hurt=hurt_full:3-13 --clip downed=downed_full:33-49 --drop jump_full,hurt_full,downed_full --strike attack=0.33,0.5 --socket-bone RightHand --socket-axis Z --out art/characters/ironbound_wanderer.blend
blender -b art/characters/ironbound_wanderer.blend --python art/tools/animate_attacks_meshy.py
blender -b art/characters/ironbound_wanderer.blend --python art/tools/render_spritesheet.py -- --out art/renders/ironbound_wanderer --armature Rig --socket Grip:Blade --frame 96x64
godot --headless --path game --script res://tools/sync_sprite_sheets.gd -- ironbound_wanderer
godot --headless --path game --import

The frame is 96x64 so the lying downed pose fits. Still missing: backdash (the game falls back to idle).

Playable characters: Silas, Aurel, Brunhal, Ansel, Kestrel, Coralie, Orsolya, Nemo, Ysolde and Tobias (built in Blender, no Meshy)

They are block-outs grown on the Meshy humanoid rig of ironbound_wanderer.blend, so they inherit every animation on it (the Meshy clips and the atk_* swings). art/tools/build_character.py removes the wanderer's mesh and builds a body from the bones (a Skin-modifier mesh, one radius per joint, each face coloured by its nearest bone: skin, shirt, sleeves, gloves, trousers, boots), a coat tube, a collar and the look's extras. Soft parts follow their two nearest bones; rigid ones follow one bone. It also adds GripL / BladeL on the left hand, the second weapon socket. Edit LOOKS to change colours or add a character; a look may scale joint radii ("radii"). The looks:

art/tools/animate_kits.py --kit <name> writes the kit animations with the same Poser as the swings (left arm, left weapon and a kicking leg added; KitPoser adds a straight air kick and a bent left elbow, "foreL"), and a backdash for all:

blender -b art/characters/ironbound_wanderer.blend --python art/tools/build_character.py -- --look silas --out art/characters/silas.blend
blender -b art/characters/silas.blend --python art/tools/animate_kits.py -- --kit silas
blender -b art/characters/silas.blend --python art/tools/render_spritesheet.py -- --out art/renders/silas --armature Rig --socket Grip:Blade --socket2 GripL:BladeL --frame 96x64
godot --headless --path game --script res://tools/sync_sprite_sheets.gd -- silas

(Every other character: the same with its name; each has its own .blend.) --socket2 records the left-hand socket as "sockets_l" in the atlas; the game draws the off-hand weapon (or Silas's second revolver) there. Every weapon is in weapons.blend: the revolvers (W_*_revolvers); Brunhal's maces and tower shield (W_flanged_mace, W_morning_star, W_tower_shield, whose long axis is the "blade" axis, the grip at its middle); Ansel's censers (W_iron_censer, W_pilgrim_censer: a handle, a chain and a pierced censer); Coralie's tomes (W_iron_tome, W_chained_codex, and W_open_tome, the open book drawn while she holds it open; the book's height is the blade axis, the grip at the middle of the spine, built small and scaled by TOME_SCALE); Ysolde's staves (W_rowan_staff, W_orrery_staff, the grip a third of the way up the shaft); and Tobias's canes (W_ashwood_cane, W_shepherds_crook, held near the top, the tip along +X). render_weapons.py adds whatever collection is missing to an existing file without touching the others. (weapons.blend and the weapon sheet are binary: when two branches each added weapons, take one side's file and run render_weapons.py again.)

Tobias's familiars have no sheet of their own: the hound draws the wolf enemy's sheet through a shader that maps its shading onto the palette's cyan ramp (Familiar.SPECTRAL_SHADER), and the moths and the crow are drawn in code (Familiar._draw_moths, _draw_crow).

Orsolya (human and wolf)

Orsolya Drav, the Moonblooded, has two sheets. Her human form is another build_character.py look on the Meshy rig ("orsolya": a short dark red coat with pale fur at the hem and collar, studded bracers, a wolf pelt over the left shoulder, belt charms, knee boots, a wild ash-blonde mane, cheek scars; the look's "boots_z" raises the boot line and "skull" rounds the head under the hair). animate_kits.py --kit orsolya writes howl, pounce and moonleap (air poses, AIR) and pounce_land; her Long Spear swings are the shared atk_*.

blender -b art/characters/ironbound_wanderer.blend --python art/tools/build_character.py -- --look orsolya --out art/characters/orsolya.blend
blender -b art/characters/orsolya.blend --python art/tools/animate_kits.py -- --kit orsolya
blender -b art/characters/orsolya.blend --python art/tools/render_spritesheet.py -- --out art/renders/orsolya --armature Rig --socket Grip:Blade --socket2 GripL:BladeL --frame 96x64

Her wolf form, orsolya_wolf.blend, is generated from the enemy wolf (never edit wolf.blend for it): make_orsolya_wolf.py removes the back growths and the enemy's actions, recolours the fur (ash-blonde / silver-grey, a darker grey back by rest-pose height, the veins turned down to a faint dark red), adds amber glowing eyes, a coat-red neckerchief with a silver charm and cheek scars (skinned like the body under them, with the same outline), and scales the rig to player size (SCALE 0.65: about 2 units nose to tail, 1 at the shoulder). animate_orsolya_wolf.py writes idle, run, jump, fall, maul, rush, bound, hurt and turn with animate_wolf.py's pose helpers (maul starts and ends on the idle stance: the game plays it four times in a row).

blender -b art/characters/wolf.blend --python art/tools/make_orsolya_wolf.py -- --out art/characters/orsolya_wolf.blend
blender -b art/characters/orsolya_wolf.blend --python art/tools/animate_orsolya_wolf.py
blender -b art/characters/orsolya_wolf.blend --python art/tools/render_spritesheet.py -- --out art/renders/orsolya_wolf --armature Wolf_rig --actions idle,run,jump,fall,maul,rush,bound,hurt,turn --frame 128x80
godot --headless --path game --script res://tools/sync_sprite_sheets.gd -- orsolya orsolya_wolf
godot --headless --path game --import

Previews (every frame, labelled, and both forms next to the enemy wolf at one scale) are in wiki/art/images/orsolya/. Like the other block-outs, the human jump and hurt frames touch the top of the 96x64 frame (the shared Meshy clips). (Aurel, Brunhal and Ysolde: the same with aurel / brunhal / ysolde.) --socket2 records the left-hand socket as "sockets_l" in the atlas; the game draws the off-hand weapon (or Silas's second revolver) there. The revolvers are in weapons.blend (W_*_revolvers), and so are Brunhal's maces and tower shield (W_flanged_mace, W_morning_star, W_tower_shield, whose long axis is the "blade" axis, the grip at its middle) and Ysolde's staves (W_rowan_staff, W_orrery_staff, the grip a third of the way up the shaft): render_weapons.py adds them to an existing file without touching the others. and Tobias's canes (W_ashwood_cane, W_shepherds_crook, held near the top, the tip along +X): render_weapons.py adds them to an existing file without touching the others. (weapons.blend and the weapon sheet are binary: when two branches each added weapons, take one side's file and run render_weapons.py again, which adds whatever collection is missing.)

Wolf (enemy)

art/characters/wolf.blend: a Skin-modifier block-out with Geometry Nodes growths on its back (tentacles that wiggle with scene time, bone spikes), a toon material with pulsing veins, an inverted-hull outline and a Rigify rig (Wolf_rig; edit Wolf_metarig and re-generate). Modifier order matters: BackGrowths (Geometry Nodes) must come before Armature, so the growths are scattered on the rest mesh and ride along instead of being re-rolled each frame.

art/tools/animate_wolf.py writes idle, run, leap_ready and leap (scripted first pass; edit the pose functions and run it again):

blender -b art/characters/wolf.blend --python art/tools/animate_wolf.py
blender -b art/characters/wolf.blend --python art/tools/render_spritesheet.py -- --out art/renders/wolf --armature Wolf_rig --actions idle,run,leap_ready,leap --frame 160x112
godot --headless --path game --script res://tools/sync_sprite_sheets.gd -- wolf
godot --headless --path game --import

Pass --armature and --actions: the file also has the meta-rig and the material pulse actions. In the normal pass the renderer switches outline shells (Solidify with flipped normals) off and swaps the materials Geometry Nodes assign too.

Baking textures

art/tools/bake_textures.py runs Smart UV Project on a mesh and bakes its flat base colour and its emission to PNGs next to the .blend (Cycles EMIT bake, so toon shading is not baked in), then saves the .blend to keep the UVs:

blender -b art/characters/wolf.blend --python art/tools/bake_textures.py -- --object Wolf_Blockout --size 1024 --hold VeinPulse=1.0

It writes wolf_albedo.png and wolf_emission.png. Label the nodes to bake BAKE_ALBEDO and BAKE_EMISSION (otherwise the Principled BSDF's Base Color and Emission Color are used); --hold pins an animated Value node, here the vein pulse at 1.0. Modifiers are off during the bake, so the Geometry Nodes growths are not in the textures. The sprite sheet does not use these textures.

Smoke test

blender -b --python art/tools/make_test_rig.py -- --out art/characters/test_dummy.blend
blender -b art/characters/test_dummy.blend --python art/tools/render_spritesheet.py -- --out art/renders/test_dummy