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:
- Run
uvx mcp-for-blender install-addon(copies the add-on into Blender's addons folder), or downloadaddon.pyfrom 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. - 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.
- Start Claude Code in this folder; the
blenderMCP server from.mcp.jsonis 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>/:
<name>_color.png: one row per action, one column per frame, RGBA, no anti-aliasing.<name>_normal.png: same layout; camera-space normals encoded asn * 0.5 + 0.5(red = screen right, green = screen up, blue = toward the camera). Load it as thenormal_textureof aCanvasTexturein Godot for 2D lighting.<name>.json: frame size, pixels per unit,origin(the feet pixel in each frame), and per action its row, frame count, fps and loop flag. The Godot side buildsSpriteFramesfrom this file; see "Importing Blender sprite sheets" inwiki/game/game-prototype.md.
Rules that keep sheets consistent
- Front view camera: +X is screen right, +Z is up, ground is z = 0. Build characters facing +X; the game flips the sprite for the other direction.
- One world scale for everything: 32 px per unit. A 1.8-unit human fits a 64x64 frame.
Big monsters use a bigger
--frame, not a different--ppu. - Name actions the way the game expects them (
idle,walk,jump,fall,attack_1,hurt,death). Looping is inferred from the name. - Weapons and visible gear are separate sprites drawn at a socket recorded per frame, so loot does not multiply the body animations (see "Weapon layers").
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
- Wrist bone. It adds
hand.Rif it is missing and moves the sword parts onto it, so a swing can turn the blade without the arm. The older actions do not keyhand.R, so they look as before. - Swing actions. It writes the
atk_*actions, among themslash,rising,thrust,jab,overhead,slam,spin,lash,lash_downandplunge. Each is a few keyframes in screen terms: - where the arm points (so where the hand is) and where the blade points, in degrees clockwise from screen right;
- lean for the spine and hips, steps for the legs, the off arm and the cape;
- a lunge or crouch offset for the whole body.
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
--hideleaves those objects out of both passes.--socket GRIP:DIRECTIONrecords, per frame, the GRIP object's origin as the grip pixel, and the direction to the DIRECTION object's origin as the weapon's angle (degrees, 0 = screen right, clockwise, as in Godot). It also records whether the grip is on the camera's side of the body. Each action in the JSON gets"sockets": [[x, y, angle, front], ...], one entry per frame.
Weapons:
blender -b --python art/tools/render_weapons.py -- --out art/renders/weapons
art/characters/weapons.blendis the source: one collectionW_<id>per weapon, ids matching the game's item bases. Each model has its grip at the origin, points along +X, and lies in the XZ plane facing the camera.--rebuildregenerates the file. The Short Blade is the swordsman's own sword laid flat; the others are block-outs in the swordsman's materials, for refining by hand.- Output:
weapons_color.png,weapons_normal.png(all weapons side by side) andweapons.json, which gives each weapon'sregionin the sheet and itsgrippixel.
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.
- One FBX per animation. Meshy's "Merged Animations" FBX only had real keys
for its run (
Run_02); the other takes were two still keys. Download each animation on its own (*_withSkin.fbx). Library animations and text-to-motion clips come with different object transforms; the importer detects which. - Retargeting. Bones are matched by hierarchy position (Meshy's animation files use Mixamo names); each bone's world rotation away from rest is copied. Hip travel is removed for loops and clamped for one-shots.
- Clips. The jump is cut into
jump(rising) andfall, with the hips held at standing height (--pin-height).hurtanddownedare the useful parts of longer clips. - Weapon socket.
Grip/Bladeempties sit onRightHand; the blade follows the bone's +Z (up and forward in the guard stance).
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
- Swings.
art/tools/animate_attacks_meshy.pywrites theatk_*swings and thecrouchpose from the sameSWINGS/POSEStables asanimate_attacks.py, solved in world space on the Meshy rig: each key starts from the idle combat stance, the right arm and hand point where the table says, and the feet stay planted (two-bone IK) through lunges and the crouch. Re-run it afterimport_meshy.py, which rebuilds the .blend without them. Meshy's own slash stays asattack, the fallback for a swing a weapon lacks.
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:
- Silas: hat, belt, holsters, star, beard. Aurel: hair, ponytail, scabbards, red trim.
- Brunhal: broader joints, pauldrons, a braid and a chest emblem.
- Ansel: a long robe, a tabard, a beard and a round bald head (
"skullcap"). - Kestrel: slimmer (radii below 1), a short fitted jacket as the coat, a messy undercut of tufts, soot on the cheeks, a scarf with a tail, a bandolier of stars, chest pockets and cloth wraps from wrist to elbow.
- Coralie: a knee-length grey outer robe over an ankle-length oxblood one, a leather apron with needles and spools, a bun pierced by two bone needles, a grey streak, and an iron-bound book chained to her belt.
- Orsolya: see "Orsolya (human and wolf)" below.
- Nemo: leaner, a short ragged mantle for a coat (
"ragged"tears its hem), a tall hood, a smooth porcelain mask with two slits, bandage-coloured arms and four small cracked beast masks on the belt. The worn mask is one look for now; a head-socket layer per mask could come later. - Ysolde: slender, a long coat, short braids, spectacles up on the forehead, a high collar, silver chart stars and lines on the coat, a harness and rolled maps on her back.
- Tobias: lean (radii below 1), a long hunting coat, a fur collar, a drooping felt hat with three feathers, a long grey moustache and brows, the leash coiled at his waist with a loop at each hip, five pouches and a bone whistle.
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:
- Silas:
atk_shoot_r/_l(forward, up, up-forward, low),atk_fan,reload,kick,lasso,coin. Aurel:atkl_*(every swing with the hands swapped),cast,dash. - Brunhal:
guard,bash,charge,plant,taunt(her mace swings are the sharedatk_overhead/atk_slash/atk_slam). - Ansel:
atk_downfwdandhook(his lashes and spin are the sharedatk_lash,atk_lash_down,atk_rising,atk_spin). - Kestrel:
wall_slide,wall_jump,wall_run(loops),roll(four frames: the roll lasts 0.2 s),dive_kick(a straight leg at 45 degrees in the air; its strike starts at 0.6 so the leg is out for the whole hit) andthrow(her dagger swings are the sharedatk_jab/atk_thrust). - Coralie:
book_open(the tome held up in front) andcast(her slams areatk_overhead/atk_slam; Leaping Verse playsatk_plungethenatk_slam). - Orsolya:
howl,pounce,pounce_land,moonleap(and the wolf's own sheet, below). - Nemo:
grab,glide,spit,slam,leap,lungeandguard(the Bone Shield). - Ysolde:
cast(staff raised, left hand thrown forward) andblink(curled up; the game draws it faint); her staff strikes are the sharedatk_thrust/atk_slash. - Tobias:
whistle(left hand to his lips),point(sends the crow) andden(crouched low, held); his cane raps are the sharedatk_overhead/atk_jab.
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