agent-art
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Agent Art
Pixel art for AI agents. An MCP server with a live web UI that lets any agent (Claude, GPT, DeepSeek, local models) make real pixel art: sprites, characters, tilesets, seamless textures, icons, UI, isometric and HD-2D scenes, and animations. Every asset is built from tools the agent can reason about: shapes, form shading, Voronoi cells, outlines, lighting and rigs. It looks at its own work as an image, gets a critique, fixes what's wrong and exports PNGs, GIFs and spritesheets for any engine.
Works with weak models too. Tool descriptions are compact, and details load on demand through
art_help. Images are sized to their token cost. Errors print the correct usage. Rig templates turn "draw a walking character" into "draw six parts, call one preset".The same asset in every view and frame. Rigs draw each part once per view (side/front/back plus mirrored views) and generate frames by tweening poses, at any frame rate.
humanoid_hdadds detailed 8-direction characters with per-view poses, two-bone IK, foot planting, follow-through and RotSprite sampling.Low-res to high-res. 8×8 icons up to 2048² scenes. Shading and cells are geometry-based, so they scale. Scale2x/EPX upscaling and RotSprite rotation keep edges clean.
Characters that hold guns right. Declare a
weaponon a rig and both hands stay on the grips in every frame, view and carry state (aim, ready, sprint, reload, slung, holster, ...), with reach limits, no clipping through the body, muzzle discipline and depth-driven draw order.checkvalidates every clip in all 8 directions. The same solver puts first-personfp_armsonfp_rifle/fp_pistol.You can watch, steer and review. The web UI updates live as the agent draws: an activity timeline of every tool call with before/after snapshots, an asset browser, viewers for canvases, animations, rigs, 3D, audio and sheets, an asset graph (
art_graph) and review notes you pin on a frame or part (art_review), which the agent reads and resolves. Your edits go through the same API, so the agent sees them.3D and audio too.
art_modelturns pixel textures into low-poly.glbmap kits, props, ships and vehicles. Hard-surface detail (greebles, pipes and cables, trim sheets, PBR, pixel-correct height normals, booleans, morphs) and render-cost tools (baked AO, LODs, impostors, collision/occluder proxies, room/portal culling) keep sci-fi scenes rich and fast in a browser. Rigid-part rigs (first-person rifle, pistol and arms templates, doors, turrets) get poses, animation clips and sockets in the.glb, or render to pixel sprite frames that 2D arms follow with IK.art_audiomakes SFX (including firearms and sci-fi), music and mixes, and reports what they sound like.Zero dependencies. Plain Node.js 20+, no native modules. The web UI's 3D viewer uses a vendored copy of three.js r170 (MIT).
Example: Sanctum Burn, a co-op 2.5D survivor game. All of its art was made by an agent with Agent Art.
Install
# Claude Code
claude mcp add agent-art -s user -- npx -y @rusticstack/agent-art mcp
# any MCP client (stdio)
{ "mcpServers": { "agent-art": { "command": "npx", "args": ["-y", "@rusticstack/agent-art", "mcp"] } } }Then ask the agent: "open the art UI and make a 32×32 knight with a walk cycle".
The MCP process hosts the UI at http://localhost:7420. If an agent-art serve is already running on the same workspace, it uses that one, so the UI outlives agent sessions and several agents can share it. If that server goes away mid-session, the MCP process looks for another server on the same workspace (ports 7420-7440) and otherwise hosts one itself, on the same workspace; it logs each switch to stderr.
From a source checkout: npm start (UI), npm test. The included .mcp.json registers the server for Claude Code in this folder.
Upgrading to 0.7
Compact replies:
art_infoandart_riginfo,createandupdatereply with a summary. Rig cells come ascells.rows(the y of each view's band) pluscells.parts([x, w, h]), so a cell is[x, rows[view], w, h]. Passfull: truefor the old full reply orfields: [...]for just those fields.Animation tags: preset animations are always tagged
<preset>_<view>:walkin the side view is nowwalk_side(it used to bewalkfor the first view).humanoid_hd: the near (_f) limbs now sit on the correct screen side in the 3/4, front and back views, and in the front and back views the thighs draw above the pelvis, so renders of non-side views change.mirror_pairs: an arm held by IK on a non-foot end (a weapon grip, a hand on a prop) keeps its side instead of swapping. Setmirror_hands: trueon a key to swap on purpose.preview: the ground line is drawn at the feet, not at the root.art_critique: HD sprites (64x96 and up) get a 48-128 colour budget instead of 32, and ordered dither there is not flagged as noise.styleoverrides the detection.Material layers: canvas layers named
height,metalorroughare material channels inart_model(normal map, metalness, roughness) and no longer show in the base colour. Rename such layers to keep them as paint.Package size: the npm package grows from about 360 kB to about 770 kB packed (2.6 MB unpacked), because the web UI vendors three.js r170 (MIT).
Related MCP server: Aseprite MCP Server
How an agent works with it
Palette first:
art_palette rampbuilds hue-shifted shading ramps.art_projectshares a palette and style notes across a whole asset set.Block in flat silhouettes with
art_drawcommands:rect,ellipse,polygon,line,curve,grid(pixel rows plus a legend),fill,text, ...Form and surface:
shadegives volume shading from one light direction, based on the shape's geometry, with no pillow shading.cellsmakes tileable stones, scales and bricks.noisemakes dithered textures with exact color weights.outlineandbevelfinish edges.Look and check:
art_viewrenders the image with a grid, seam-check tiling and onion skin.art_critiquescores orphan pixels, noise, contrast, colour count, seams, weak outline, jaggies, pillow shading, and animation drift (volume, ground line, palette, duplicate frames).Export:
art_export(PNG).art_animation export(GIF, or a spritesheet plus Aseprite-style JSON).art_sheetgives a contact sheet of a whole set.
Characters, views and animations: art_rig
// 1. start from a template (humanoid | humanoid_hd | blob | flyer): only a starting point
{ "action": "create", "name": "knight", "frame": [32, 32], "template": "humanoid",
"parts": { "head": { "w": 14, "h": 11 }, "cape": { "w": 8, "h": 12, "pivot": [4, 0], "parent": "body", "at": [-2, -6], "z": -4 } } }
// 2. draw each part in the cells the reply lists (one row per view) on canvas "knight_parts"
// 3. generate animations: frames are assembled from the same part pixels, so the knight never drifts
{ "action": "animate", "name": "knight", "preset": "walk", "view": "side", "frames": 16, "fps": 20 }Templates set up parts, joints, views, poses and presets: humanoid has idle, blink, walk, run, attack, hurt, jump and death; blob has idle, hop, blink, hurt and death; flyer has fly, hurt and death. Every size, joint, view, pose and preset can be changed. You can add parts (tails, capes, weapons) or remove them. For anything else (quadrupeds, vehicles, props with moving parts), define your own rig. Poses set per-part offsets, rotation, squash and stretch (sx/sy), variants (a blinking head) and visibility. mirror_pairs swaps left and right limbs for the second half of a walk, and plant_feet keeps feet on the ground line.
High-detail characters: humanoid_hd
For detailed sprites in the style of late-90s pre-rendered games or Replaced (64x96 up to 160x240 frames, rich shading, 12-24 frame cycles), template: "humanoid_hd" gives 21 parts: pelvis (root), torso, chest, neck, head, upper_arm/forearm/hand and thigh/shin/foot on both sides, plus optional weapon (child of hand_f), gear_back, gear_hip and cape slots. Limbs are parent chains, so elbows and knees bend. It draws five views (side, front_side, front, back_side, back) and mirrors three more, for 8 directions. Presets: idle (12 frames), walk (16), run (12), crouch_walk (16), aim (1), shoot (6), reload (24), hurt (8), death (20), throw (12) and blink.
// the template already gives the cape and gear_hip secondary motion; this loosens the cape
{ "action": "create", "name": "soldier", "frame": [128, 192], "template": "humanoid_hd",
"secondary": { "cape": { "stiffness": 0.15, "damping": 0.2, "gravity": 0.5 } } }
// draw the cells, then one canvas per preset and view (frame strips are at most 8192 px tall)
{ "action": "animate", "name": "soldier", "preset": "reload", "view": "front_side", "canvas": "soldier_reload_front_side" }
// a custom cycle from the template's poses, with planted feet and cape follow-through
{ "action": "animate", "name": "soldier", "animation": "patrol", "view": "front_side", "frames": 16, "fps": 12,
"keys": [{ "pose": "walk_contact" }, { "pose": "walk_pass" }, { "pose": "walk_contact", "mirror_pairs": true }, { "pose": "walk_pass", "mirror_pairs": true }],
"plant": true, "secondary": true, "canvas": "soldier_patrol_front_side" }
// a pose with a view-specific override and two-bone IK (hand on the weapon's foregrip)
{ "action": "pose", "name": "soldier", "pose": "brace", "parts": { "chest": { "rot": -4 } },
"views": { "front": { "head": { "rot": 0 } } },
"ik": { "hand_b": { "target": { "part": "weapon", "at": [20, -3] }, "bend": -1 } } }Per-view poses:
views: {front: {part: {...}}}is merged over the base pose in that view; mirror views use their source view, and tweening blends the overrides. The template's poses come with front, back and 3/4 overrides already, so forward swings foreshorten (thigh and shinsywithscale_children) instead of splaying sideways.Two-bone IK:
ik: {end: {target: [x, y] | {part, at: [dx, dy]}, bend: 1 | -1, rot?, weight?}}in a pose or key rotates the end's parent and grandparent so the end pivot reaches the target. It is solved per frame after FK, in each view.rotsets the end part's world rotation afterwards, for example to keep a gun level. Targets tween between keys, and a key without the entry fades back to FK.Foot planting:
animate {plant: true}(aliasik_feet) keeps stance feet on the ground through every contact frame with leg IK. A standing foot stays put and a walking stance foot moves at constant speed.plant: ["foot_f"]picks the feet; rigfeetsets the default.Secondary motion: rig
secondary: {cape: {stiffness, damping, gravity?}}plusanimate {secondary: true}adds lag and follow-through to capes, straps and pouches. Loops are simulated until they repeat, so they stay seamless.scale_children: a part'ssx/syalso moves its children's joints (foreshortening, breathing); humanoid_hd has it on.Rotation sampling: rig
sampling: "rotsprite"(the humanoid_hd default) supersamples the Scale2x image, keeps only the part's own colours and cleans up holes and stray pixels, so thin limbs keep their thickness from frame to frame."smooth"does the same on the 1x cell, and"nearest8"is the old behaviour, still the default for existing rigs.Follow another animation (
ik_from):animate {ik_from:{canvas, animation, map:{hand_r:"socket_grip_r", hand_l:{anchor:"socket_grip_l", bend:-1}}, offset?}}takes per-frame IK targets from another canvas's anchors, for example anart_model renderof a gun clip, and copies its frame count, timing and loop. A map value is an anchor name or{anchor, bend?, rot?, at?, weight?}. Give the rig the render's frame size (or anoffset). See the end-to-end example.art_animation export {format: "spritesheet", json: false}skips the sidecar JSON.Equipment sets: give parts variants (
chest: ["plate"],head: ["helmet", "helmet_blink"]) and optionally a rigvariant_sets: {heavy: {chest: "plate", head: "helmet"}}. Thenanimate {..., variant: "heavy"}renders every part with that set, so armour sets need no sheet swapping. A pose variant such asblinkcombines as<set>_<variant>(helmet_blink) when the part has one.Tags: preset animations are always tagged
<preset>_<view>(walk_side,walk_front). Passanimationto choose your own.Compact replies:
create,updateandinforeply with a summary. Cells come asrows(the y of each view's band) plusparts([x, w, h]), so a cell is[x, rows[view], w, h].full: truereturns everything, andfields: ["poses"]returns only those fields.art_infoworks the same way.
See docs/rig-hd-guide.md for a step-by-step guide. Skill: rig-hd.
Held weapons: weapon
Without constraints, generated sprites hold guns wrong: the support hand misses the foregrip, arms don't reach, the rifle clips through the torso, the gun swaps hands halfway through a walk cycle, and it vanishes outside aim poses. Declare the weapon and the rig handles it in every frame of every pose, clip and view:
{ "action": "create", "name": "soldier", "frame": [128, 192], "template": "humanoid_hd",
"weapon": { "class": "rifle", "attachments": { "vgrip": "vertical_grip" } } }
// draw "weapon" and "att_vgrip" in the side row only, pointing right; then
{ "action": "check", "name": "soldier" } // every preset x 8 directions -> pass / fail + failing frames
{ "action": "animate", "name": "soldier", "preset": "walk", "view": "front_side", "carry": "ready_high", "canvas": "soldier_walk_fs" }Weapon definition: named points in the weapon cell:
grip_main(trigger hand),grip_support(handguard; it may slide back along the rail),stock(shoulder pocket contact),muzzle,optic(eye line) andmag. Classes (rifle,carbine,smg,shotgun,dmr,sniper,lmg,pistol) give defaults as fractions of the cell. Attachments are sub-parts that mount on sockets (underbarrel,top,muzzle). A vertical or angled grip or a bipod takes overgrip_support, a suppressor themuzzle, an optic theoptic.attachpicks the active ones, andanimate {attach: []}overrides them per call.Hands: both hands solve onto their grips with two-bone IK every frame, and the wrists turn to each grip's angle. The support hand of a pistol cups the trigger hand. Mirrored views, 3/4 views and front and back views all hold, because the weapon and arms are solved once in a 3D body proxy and projected into each view.
Reach: arm lengths come from the rig. The shoulders protract a little. If that isn't enough, the support hand slides back along the rail and then the weapon moves back along its axis, or the frame is flagged. Limbs never stretch, and elbows always bend down and out.
Dead zone: the torso, head and legs (and any free arm) are capsules the weapon may not enter. The weapon is pushed out forward first, then outward, turning about the stock when it is shouldered.
standoffsets the minimum gap between chest and receiver. When aiming, the stock sits in the shoulder pocket and the head tilts for a cheek weld to the optic line.Carry states: long guns have
aim,ready_low,ready_high,hip,sprint,reload,slung,one_hand_low,dropandholster. Pistols haveaim,compressed,low,reload,one_hand_lowandholster. Outside firing states the muzzle points down or forward-low, never at the character's own body. Set the state withcarryon a pose or key,weapon.poses(by pose name),weapon.carry(the default) oranimate {carry}, and add your own withweapon.states. The humanoid_hd presets come mapped: walk and idle useready_low, runsprint, aim and shootaim, reloadreload, and throwslung(a pistol is holstered). The weapon stays visible unless it is holstered.Mirroring:
mirror_pairsswaps legs and free arms but keeps held hands and the weapon on their side. The key optionmirror_hands: truedeliberately swaps handedness.Front-view readability: a barrel pointing at the camera is canted outward with a length floor (
foreshorten: {floor, cant, back_cant} | false). In back-facing views the muzzle angles out past the silhouette on the weapon side.Back view:
Gun: yawed outward in 3D and projected, so its pitch is kept, and it has one length (
back_floor), so runtime sheet cells match the frames.Arms: they follow the projected 3D arm, so the elbow sits out and below the shoulder.
Draw order: arms holding the gun in front of the body are drawn under the whole trunk, and the support shoulder protracts.
Weapon check: it flags a raised elbow and an arm drawn over the torso from behind.

Layering: the weapon is drawn over the torso when it is held in front of it and under it when behind, using projected depth.
weapon.layer: {above, below, pin}overrides that.layers {name, pose | preset + frame, view}lists the draw order with the reason for each z, andexplain {part_a, part_b}says why one part is drawn over another.debug_renderreturns a frame as one layer per part; the web UI's Layers view uses both.Validation:
checkmeasures every frame: hand-to-grip distance, limb over-extension, inverted elbows, weapon-body penetration, muzzle flags, layering and weapon visibility.animateruns the same checks and by default refuses to write a clip with a violating frame.constraints: "fix"auto-corrects instead (muzzle down, stronger cant, flipped elbows, draw order), and"warn"writes the frames and reports.Front aim and reload: aimed at the camera, a long gun's barrel stays short and is canted inward, so the muzzle sits near the centre line. It is drawn over the forearms and the trigger hand and under the support hand, which is further forward on the handguard; a pistol's slide goes over both hands. The head tilts onto the stock.
humanoid_hdwith a weapon adds amagpart (draw it in the side row) andmag_hand, which draws the same cell in the support hand. The reload goes grab, strip (mag in the hand), chest pouch, seat, charging handle, with the eventsmag_out,mag_pouch,mag_in,chargeandready.
The same system works for first-person art_model arms: see Guns held by arms. Skill: weapon-handling.
Depth, draw order and linked cells
Depth model:
depth: trueon a rig decides the order of each arm chain against the chest frame by frame, using the weapon solver's 3D proxy. An arm in front of the chest's front surface goes over it, one behind its back surface goes under it, and in between the authored order stays. Clips decide with hysteresis, so a limb that hovers at the surface does not flicker. A bone that crosses the surface is split into two pieces with their own z (a forearm reaching across the chest). Rules:depth: {rules: [{part, ref, chain?, split?, views?}], margin?}.z keyframes: a key
{pose, z: {forearm_b: 9}}sets the order for that key. It steps at the tween midpoint, and a pose z wins over the depth model.zcheck:
zcheck {name, clips?, views?, window?}finds z-flicker (two overlapping parts that swap order and swap back withinwindowframes) and depth violations.animatestores each frame's order from bottom to top, and spritesheet JSON writes it asframes[].order(withframes[].events).Linked and shared cells:
parts.p.link: {back_side: "side"}(or{view, flip}) draws another view's cell, so it is not drawn twice.parts.p.art: "mag"draws another part's cell.hide: truekeeps a part hidden until a pose setshide: false.Cross-view checks:
consistency {name}compares every part across views: empty cells, palette drift, bone length drift, pivots and joints off the drawing, and rest silhouette height.derive {name, from: "side", to: ["front_side"], parts?, flip?}copies cells as a start to paint over.Stable rotation:
animate {stable: true}quantizes each part's drawing angle in 3° steps with hysteresis, so slow limbs do not shimmer. Joint positions stay exact.
Animation library: art_anim
Author motion once per skeleton and render it on every character. A skeleton is a rig definition without art, with named proportions and masks. Clips belong to a skeleton. A skin is a drawn rig bound to a skeleton. batch renders skins × loadouts × clips × views straight into game strips and a manifest.
{ "action": "skeleton", "name": "hd", "template": "humanoid_hd", "frame": [128, 192], "weapon": { "class": "rifle" },
"proportions": { "heavy": { "width": 1.15, "spine": 1.04 }, "lean": { "width": 0.9, "legs": 1.04 } } }
{ "action": "skin", "name": "mec", "skeleton": "hd", "proportions": "heavy",
"equipment": { "pack": { "part": "gear_back" } }, "loadouts": { "light": { "equip": [] }, "heavy": { "equip": ["pack"] } } }
// draw the cells of rig "mec" (art_rig consistency checks the views agree), then
{ "action": "clip", "skeleton": "hd", "name": "limp", "layers": [{ "clip": "walk" }, { "clip": "flinch", "additive": true }] }
{ "action": "batch", "skins": ["mec", "sev"], "loadouts": "all", "out": "C:/game/public/assets", "front": "auto" }Skeletons: a skeleton is made from a template, from an existing rig (
from_rig) or from parts. The template presets become the clip library, pluswalk_aim(walk legs with the aim upper body as a masked layer),flinch(an additive upper-body hit) andwalk_hit. The maskslower,upper,arms,headandallare built in; add your own withmasks. The depth model is on by default.Clips: keys (named poses or inline parts,
carry,z),frames,fps,loop,events: [{t, name}],plantandsecondary, pluslayers: [{clip, mask?, weight?, additive?}]. An override layer blends the masked parts toward the layer (IK ends included). An additive layer adds its offset from its first frame or itsrefpose.Skins:
skincreates a rig with cells sized for the proportions, or binds an existing rig (rig,map). Clips retarget to the skin's proportions automatically.equipmentmaps items to parts or variants, andloadoutspick items and sets.Retargeting:
retarget {clip, from, to, map?}copies a clip to another skeleton. Named poses are inlined (keeping their carry state), translations scale with hip height, frame-pixel IK targets move with the root, and rotations carry over.Batch: writes
characters/<set>/<clip>_<view>.pngas horizontal strips.front: "auto" | [parts]adds__frontstrips with the parts drawn over the weapon. It also mergessprites.<set>intomanifest/characters.json:frame,anchor(the ground point),pad,skeleton,skin,loadout,class, and per animfile,front,frames,fps,loop,eventsand the draworder(only where it changes).pad: "auto"(the default) grows the set's frame symmetrically until every frame fits, for example a rifle barrel past the 128 px side frame, and records the padding. The engine anchors onanchor. Stable rotation is on by default.Runtime weapon layers: the game draws the gun, so one character export works with every gun of its handling class (pistol, smg, rifle, lmg, sniper, launcher).
batch {weapon: "runtime"}writes the body strip without the gun and the__frontstrip, which holds what is drawn over the gun. Each anim getsclassand a per-frameweaponsocket:{x, y, rot, pitch, order, hidden?}in the set's frame pixels.pad,frameandanchorare unchanged.weapon {name, class, skin}makes a standalone gun asset with its grips at the class geometry. Usefrom_skinto copy a character's gun instead.conformchecks the grips against the class geometry.weapon_sheetwritesweapons_world/<name>.pngwith rows for the five views and columns for pitch, without the magazine. The magazine gets its own overlay,<name>_mag.png, which the engine leaves out onmag:falseframes such as a reload. There is also one overlay sheet per attachment. Each sheet is at most 1024 px. It mergesmanifest/weapons_world.json.compositeshows the baked frame next to the runtime frame.weapon_checkverifies that the hands stay on the grips of every gun in the class.The class table, grip geometry (in body units), carry positions and
conformlive insrc/handling.js. They are shared with other pipelines, such as first-person viewmodels.
Melee:
melee {skeleton, weapon?, shield?}writes a melee clip set onto a humanoid_hd skeleton.Strikes per weapon: knife slash / stab / combo, blade and heavy swings, baton strike with a shock tip, rifle butt-stroke, pistol whip, punches, kick and shove. Also block and parry, hit reactions in four directions, stagger, knockdown and get-up.
Paired takedowns (
takedown_rear,disarm_front,shield_knockdown) are attacker / victim clips that share one alignment.batchaddsanims.<clip>_<view>.meleeto the characters manifest: phases, events (windup, active start / end, impact, recovery, sound, effect), a hit capsule per active frame and view with the swept polygon since the previous frame, damage type, stamina, root motion, combo windows and paired metadata.melee_checkvalidates event order, sweep coverage, foot slide and contact alignment.melee_sheetrenders the eight weapon models (art_model template:"melee_<id>": combat knife, machete, baton, stun baton, vibro-blade, breaching axe and hammer, riot shield) into world sheets of views × pitch with the grip on one pixel.First-person melee clips (gun bash, knife combo, punch, baton strike, shield up and bash) carry the same events and per-frame capsules in
manifest/fp.json.art_help {topic:"melee"}has the manifest fields and the Three.js and Roblox runtime notes. Demos:docs/melee/(node scripts/melee-demos.js).


glTF:
gltf {skin, clips?, path?}exports a skinned cutout mesh: one quad per part from the base-view cells, one joint per part, and the rest pose as the bind pose. Each clip becomes an animation, a hidden part is scaled to 0, and events go in the animation extras.
Skill: anim-library. art_help {topic:"library"} has every option.
Export paths: an absolute path is used as is, and ./x or ../x resolves against the MCP server's working directory. A bare relative path goes to the workspace exports folder, and the reply now says so. art_animation export {layout: "files"} writes one horizontal strip (and JSON) per tag; the default rows stacks one row per tag and says so in the reply.
3D environments and props (HD-2D): art_model
Turn pixel art into low-poly game assets in the HD-2D style (pixel textures on low-poly meshes, nearest filtering), exported as glTF 2.0 .glb with no dependencies. The focus is modular map kits; characters stay 2D billboards.
// a kit piece: tileable textures repeat at 1 texel = 1 px, so every piece keeps 32 px per unit
{ "action": "create", "name": "wall_ruin", "parts": [
{ "type": "wall", "w": 32, "h": 64, "d": 8, "texture": { "canvas": "stone", "overlay": "grime" }, "broken": { "seed": 3 }, "holes": "crack" },
{ "type": "rubble", "texture": "stone", "count": 6, "at": [0, 0, 10] } ], "preview": true }
// a diorama section from placed pieces (cell = 32 px grid, turn = 90° steps)
{ "action": "create", "name": "crypt", "parts": [
{ "type": "model", "model": "floor_a", "array": { "count": 4, "cell_step": [1, 0, 0] } },
{ "type": "model", "model": "wall_ruin", "cell": [1, 0, -1] } ] }
{ "action": "export", "name": "crypt" } // exports/crypt.glb + crypt.meta.json (or instances:true)Kit pieces:
floor,wall,corner,pillar,column,arch(gothic or round; frame, solid or ring),vault,stairs,ledge,balustrade,roof,rubble,box,cylinder/prism,plane,shape(any outline). Props:extrude(sprite to solid; side walls take the edge pixel's colour;depth_mapgives voxel relief) andlathe(revolve a right-half silhouette). Layouts:modelplaces other models.Ruins:
brokengives jagged tops or edges,holestakes a mask canvas whose painted pixels become cracks and breaches, andrubblescatters chunks. Outlines are smooth polygons (marching squares, then simplified) and arch curves get a segment count from their size.style:"voxel"(arches:voxel:true) keeps the old blocky look.Ships and vehicles:
wedge(box whose top is resized and moved:top_w,top_d,shift:[x,z]; the default is a ramp),boxwithtaper(front face scaled for a nose),bevel(chamfer px) orinsets(recessed or raised panels for intakes, hatches and hangar mouths, without CSG; also theinsetpart),cylinderwithr_top/r_bottom(frustum; 0 gives acone),shapeon planexy,xzorzy(side profile[z,y], extruded along x; unknown planes are an error), andloft(ashapewhose far cross-section is scaled byscale_end, so a hull narrows toward the nose). See Ships and vehicles.Smooth geometry:
latheprofiles are simplified before revolving (collinear runs merged, then Douglas-Peucker withsimplifypx, default 0.75), so a pixel silhouette gives a few clean rings instead of one ring per pixel row.World-space UVs: tiled faces get their UVs from the world position after placement, planar per face direction, at 1 texel = 1 px. The texture lattice starts at a grid-cell edge (
uv_origin, default[16,0,16], settable per model). Offsets, turns, arrays and nested layouts all continue the texture across touching pieces.Overlays: grime, decay, moss, blood and edge wear become decal shells on a second UV set (glTF
TEXCOORD_1, strength inCOLOR_0alpha, alpha MASK). Example:overlays:[{texture:"grime", where:["bottom","corners"], falloff:20}, {texture:"moss", where:"top"}].mapisworld(the overlay's own scale) orface/piece/surface(stretched 0..1). Overlays go on a part or on the whole model (model-leveloverlays, kept bycreateandupdate). Decal shells are tessellated adaptively (detaildefaults from the falloff and face size, 16-64 px), and uniform areas such aswhere:"all"are not split at all.Hard-surface detail and render cost: greebles, sweeps (pipes, cables), trim sheets, PBR materials, pixel-correct height normals, baked AO, booleans, morph targets, LODs, impostors, collision/occluder proxies and room/portal cull data; see Hard-surface detail and render cost.
Connected textures (CTM):
art_model ctmbuilds an OptiFine/Continuity-style tileset; see below.Textures: set them per face (
faces,top,side), use a colour, or add a grimeoverlay. Emissive comes from a layer namedemissive; that layer is left out of the base colour, so a glow-only pixel is black in the albedo and lit only by the emissive texture.normal_maps:trueadds normal maps.Preview:
art_model previewrenders a contact sheet with a software rasterizer. It includes the 38° game camera plus front, side, top and iso views.Export: NEAREST samplers, REPEAT wrap, embedded PNGs, alpha MASK (cutoff 0.5) only when a triangle actually samples a transparent texel (unused tileset slots no longer force it), and emissive/normal textures. The file is Y-up with 32 px = 1 unit (
scale). For a Z-up engine setobject.rotation.x = Math.PI / 2or export withup:"z". Options: one baked mesh,instances:true(shared meshes and nodes), oratlas:true.
art_help {topic:"3d"} has every option plus recipes for a gothic arch kit, a gargoyle relief, chains, lathed candles, CTM marble floors, wall trims, moss and a spaceship. Skills: 3d-kit, ship-model, hard-surface.
Rigid-part rigs, animation clips and pre-rendered sprites
For first-person weapons and arms, or any prop with moving parts (doors, chests, turrets), a model can be a rig of rigid parts, PSX / Half-Life 1 style (no skinning). The same rig exports as glTF nodes and animations or renders to pixel-art sprite layers.
// by hand: name = its own node, parent = nesting, pivot = the joint
{ "action": "create", "name": "door", "parts": [
{ "type": "box", "name": "frame", "w": 20, "h": 34, "d": 2 },
{ "type": "box", "name": "leaf", "parent": "frame", "w": 16, "h": 32, "d": 1, "at": [0, 1, 0], "pivot": [-8, 0, 0] } ],
"sockets": { "handle": { "part": "leaf", "at": [6, 16, 1], "look": [0, 0, 1] } } }
{ "action": "pose", "name": "door", "pose": "open", "parts": { "leaf": { "rot": [0, -80, 0] } } }
{ "action": "animate", "name": "door", "clip": "swing", "keys": [ { "t": 0, "pose": "rest" }, { "t": 0.8, "pose": "open", "ease": "smooth" } ],
"events": [ { "t": 0, "name": "creak" } ] }
{ "action": "export", "name": "door" } // a node per part + socket_handle + one glTF animation per clipNodes: a part with
namebecomes its own glTF node with its own mesh;parentnests it under another named part, and its transform is local to the parent (translation = joint offset, rotation, scale).pivot:[x,y,z]is the joint. Unnamed parts merge into the root mesh or into their named parent's mesh;rig:{merge:false}makes every part a node.createandinforeport the node tree. Models without names export exactly as before.Poses and clips:
posestores per-node offsets from the rest pose (rotdegrees,atpx in the parent's frame,scale;basestarts from another pose).animatestores a clip of keys in seconds (ease:linear,smoothorstep, applied to the motion into the key), withloop,durationandevents:[{t, name}];keys:nulldeletes it. Export writes one glTF animation per clip, with translation, rotation (quaternion) and scale channels per moved node. All-linear clips use LINEAR samplers at the key times, all-step clips use STEP, and anything else is baked at 30 fps. Events go in the animation'sextras.preview {clip, frames, view}shows a clip as a contact sheet.Sockets:
sockets:{muzzle:{part, at, look?}, sight, grip_r, grip_l, mag_well}export as empty child nodessocket_<name>, for aligning ADS, spawning muzzle flashes and placing hands.meta.jsonlists each node's parent-space transform, so a Roblox import maps it to Motor6D joints (C0 = translation and rotation).Templates: starter rigs in view space (origin = the eye, -Z forward, 32 px = 1 m):
fp_rifle(receiver, barrel, handguard, stock, magazine, bolt, charging handle, optic, trigger; clipsfire,reload_tactical,reload_empty),fp_pistol(frame, slide, barrel, magazine, trigger; clipsfire,reload) andfp_arms(clavicle > upper arm > forearm > hand > fingers per side; posesgrip_rifleandgrip_pistolput the palms on the weapons' grip sockets). The weapons have the five sockets above. Override anything withtextures(slots such asmetal,polymer,sleeve,glove),setby part name,add,remove,poses,clipsandsockets. Thefppreview view is the matching camera (eye at the origin, 60°).Sprite renders (
render): each clip sample, pose,anglesentry orsweep:{yaw:[a,b,step], pitch:[..]}grid cell becomes one frame of a canvas animation. Frames use nearest texels with no anti-aliasing and a transparent background, plus an optionallight,quantize(median-cutcolors:n, a colour list orpalette:"project", with ordereddither) and a 1 pxoutline. The camera isfp(the default),side,front,iso,top,gameor{pos, look, fov}.onlyandhiderender layers (for example the gun without its magazine) with the same fitted camera, so the layers stack.Anchors: every rendered frame stores each socket's screen position and rotation in
meta.anchors, and spritesheet export writes them toframes[].anchors. A 2D arm rig follows them withart_rig animate {ik_from}(seeart_rig).Known limitation: the
fpcamera drops triangles that cross its near plane instead of clipping them, so a part that reaches behind the eye (a long stock, a shoulder) can vanish in some frames. Shorten it, move the weapon forward, or render that layer withcamera:"side".
End to end, a first-person rifle from the template to 2D sprite layers with arms (texture canvases must exist before create):
// art_create_canvas: a texture (paint it with the tileable-texture skill)
{ "name": "gun_metal", "width": 16, "height": 16, "background": "#2c2e33" }
// art_model: the rig, a custom reload clip, the .glb
{ "action": "create", "name": "ar", "template": "fp_rifle", "textures": { "metal": "gun_metal" }, "set": { "magazine": { "h": 5 } } }
{ "action": "animate", "name": "ar", "clip": "reload_fast", "keys": [ { "t": 0, "pose": "rest" },
{ "t": 0.25, "pose": "reload_tilt", "ease": "smooth" },
{ "t": 0.5, "pose": "reload_tilt", "parts": { "magazine": { "at": [0, -9, 1.5] } }, "ease": "smooth" },
{ "t": 0.9, "pose": "reload_tilt", "ease": "smooth" }, { "t": 1.2, "pose": "rest", "ease": "smooth" } ],
"events": [ { "t": 0.5, "name": "mag_out" }, { "t": 0.9, "name": "mag_in" } ] }
{ "action": "preview", "name": "ar", "clip": "reload_fast", "frames": 6, "view": "side" }
{ "action": "export", "name": "ar" } // exports/ar.glb: nodes, socket_* nodes, animations fire, reload_*, reload_fast
// art_model: pixel sprite frames (hide/only give stackable layers, sweep gives turn frames)
{ "action": "render", "name": "ar", "canvas": "ar_reload", "clip": "reload_fast", "fps": 12, "size": [160, 120],
"camera": "fp", "quantize": { "colors": 16, "dither": true }, "outline": "#14141a" }
// art_rig: 2D arms with the same frame size; draw the parts in the listed cells, then follow the grips
{ "action": "create", "name": "arms2d", "frame": [160, 120], "views": ["side"], "root": [80, 119], "parts": {
"body": { "w": 2, "h": 2, "pivot": [1, 1] },
"upper_r": { "w": 8, "h": 32, "pivot": [4, 0], "parent": "body", "at": [68, 0] },
"fore_r": { "w": 7, "h": 30, "pivot": [3, 0], "parent": "upper_r", "at": [0, 30] },
"hand_r": { "w": 8, "h": 8, "pivot": [4, 0], "parent": "fore_r", "at": [0, 28] },
"upper_l": { "w": 8, "h": 32, "pivot": [4, 0], "parent": "body", "at": [-30, 0] },
"fore_l": { "w": 7, "h": 30, "pivot": [3, 0], "parent": "upper_l", "at": [0, 30] },
"hand_l": { "w": 8, "h": 8, "pivot": [4, 0], "parent": "fore_l", "at": [0, 28] } } }
{ "action": "animate", "name": "arms2d", "animation": "reload_fast", "canvas": "arms_reload",
"ik_from": { "canvas": "ar_reload", "animation": "reload_fast",
"map": { "hand_r": { "anchor": "socket_grip_r", "bend": -1 }, "hand_l": "socket_grip_l" } } }
// art_animation: one spritesheet per layer; json frames[].anchors line them up in-engine
{ "canvas": "ar_reload", "action": "export", "format": "spritesheet" }The full pipeline (gun and magazine layers, angle sweep, 3D arms) is in art_help {topic:"3d"} under "Rigs & clips" and "Pre-rendered sprites". Skills: fp-weapon, prop-rig (doors, chests, turrets).
Guns held by arms (first person)
Create fp_rifle or fp_pistol with weapon and the fp_arms hands solve onto the gun instead of following a fixed pose:
{ "action": "create", "name": "arms", "template": "fp_arms" }
{ "action": "create", "name": "gun", "template": "fp_rifle", "weapon": { "attach": ["vertical_grip"] } }
{ "action": "render", "name": "arms", "canvas": "arms_low", "camera": "fp", "hold": { "weapon": "gun", "carry": "ready_low", "both": true } }
{ "action": "check", "name": "arms", "hold": { "weapon": "gun" } } // every carry state and clip -> pass / fail
{ "action": "animate", "name": "arms", "clip": "reload_empty", "hold": { "weapon": "gun", "carry": "reload", "bake_weapon": "reload_empty_held" } }Weapon:
weapon.grips: {grip_main, grip_support, stock, muzzle, optic, mag}names a socket or a{part, at, look, up}point for each (defaults: the template'sgrip_r,grip_l,muzzleandsightsockets). The attachmentsvertical_grip,angled_grip,bipodandsuppressoradd parts and sockets and take over the grip or the muzzle. Withoutweaponthe templates are unchanged.Hold: two-bone IK puts each palm on its grip, with the wrist turned to the grip's
lookandup. The carry states arehip,aim(the optic on the screen centre, stock welded),ready_low,ready_high,sprint,reload(the support hand on the magazine) andholster, plus pistolcompressedandlow.weapon.statesadds your own, withbase. A grip out of reach moves the gun back or slides the support hand, never stretching the arm. A gun inside the head, chest or shoulder capsules is pushed out. The muzzle must point down outside firing states, andfix: truelowers it.Output:
render {hold}andanimate {hold}return ahold/heldreport (corrections, issues, pass).animate {hold}bakes the solved arms into a clip, andbake_weaponalso writes the matching gun clip, so the exported glTF plays back without the solver.checkadds visibility: how much of the gun is on screen and how much the arms hide.
Ships and vehicles
{ "action": "create", "name": "scout", "origin": "bottom", "preview": true, "parts": [
{ "type": "loft", "points": [[-12,0],[12,0],[10,10],[-10,10]], "d": 64, "scale_end": [0.3,0.4], "color": "#8a94a6" },
{ "type": "wedge", "w": 40, "h": 3, "d": 24, "top_w": 8, "shift": [16,0], "at": [14,2,0], "mirror": "x" },
{ "type": "cone", "r": 3, "h": 8, "rotate": [90,0,0], "at": [0,5,32] },
{ "type": "cylinder", "r_bottom": 5, "r_top": 4, "h": 10, "rotate": [-90,0,0], "at": [10,5,-32], "mirror": "x" },
{ "type": "box", "w": 10, "h": 6, "d": 14, "bevel": 2, "glass": true, "color": "#9fd8ff", "at": [0,10,8] },
{ "type": "box", "w": 6, "h": 6, "d": 6, "at": [0,0,-20], "insets": [{ "face": "back", "rect": [1,1,4,4], "depth": 3, "texture": "#f80" }] },
{ "type": "cylinder", "r": 1.5, "h": 4, "at": [6,10,-10], "array": { "count": 4, "rotate": [0,90,0], "about": [0,10,-10] } } ] }Transform order: every part is scaled, then rotated about the fixed axes X, then Y, then Z (
rotate:[x,y,z]in degrees), then turned (turn, 90° steps about y). All of this happens about the part's own origin (its local0,0,0, bottom-centre for most builders), or aboutpivot:[x,y,z](local px) when given. Then the part is moved byat+cell× grid. Onextrudeandlathea two-elementpivot:[x,y]is still the canvas pivot.Mirror:
mirror:"x"(or"y","z", or a list for every combination) also emits a copy mirrored across the model'sx=0plane. Winding and normals are fixed, so the copy is not inside out.Radial arrays:
array:{count, rotate:[0,60,0], about:[x,y,z]}rotates copy k by k steps about that model-space point (engine rings, rotor blades). Withoutabout, copies turn in place as before.Translucency:
opacity:0..1exports glTFalphaMode:"BLEND", withbaseColorFactoralpha = opacity multiplied by the texture alpha.glass:trueis BLEND at opacity 0.35 (unless set), double-sided and glossy. The material extras carrydepthWrite:false. The image is shared with the opaque material, and the preview blends translucent parts after the opaque ones.Cylinder decals:
wrap:"once"puts one copy of the texture round the side, centred on +z.u_offset(in turns, + toward +x) rotates it, so a decal can face any side.wrap:"repeat"gives a whole number of copies (repeat:n, or a default from the circumference), so there is no seam.tile:falsekeeps the old stretched mapping.Insets:
insets:[{face, rect:[u,v,w,h], depth, texture?, side?}]on a box.rectis in px from the face's top-left as seen from outside.depth > 0cuts a recess (the face gets a hole, four inner walls and a floor), anddepth < 0adds a raised panel. Panels may touch the face border, for a hangar mouth.Origin: the default
origin:"auto"keeps part coordinates, so kit pieces stay on the grid; this did not change. For a standalone prop, ship or vehicle useorigin:"bottom"(centred on X/Z, bottom on y=0).createsays so in itsnexthint when the model is off-centre.
Hard-surface detail and render cost
Ships, stations, carrier interiors and cities that read like a big-budget sci-fi universe at a pixel-texture budget, and stay fast in a browser. Everything below is opt-in: models that use none of it export byte-for-byte as before.

{ "action": "trim", "canvas": "hull_trim" } // a 64-wide trim sheet: plate, bevel, bolts, seam, hazard, grate, rib, pipe, rubber, light
{ "action": "create", "name": "hull_a", "origin": "bottom", "parts": [
{ "type": "box", "id": "hull", "w": 160, "h": 40, "d": 96, "bevel": 3, "texture": "hull", "material": "painted_metal", "trim": "hull_trim:bevel",
"greebles": [ { "face": "top", "seed": 11, "grid": 2, "density": 0.35, "avoid": [[64, 0, 32, 96]] },
{ "face": "right", "seed": 12, "density": 0.3, "size": [2, 8], "kinds": { "vent": 3, "pipe": 2, "bolts": 2, "panel": 3 } } ] },
{ "type": "box", "w": 52, "h": 22, "d": 30, "at": [0, 0, 36], "boolean": "subtract", "target": "hull" }, // hangar mouth
{ "type": "tube", "path": [[-82, 30, 44], [-82, 30, -40], [-60, 30, -46]], "r": 2, "trim": "hull_trim:pipe", "mirror": "x" },
{ "type": "tube", "from": [-12, 50, 30], "to": [-12, 50, -30], "sag": 3, "r": 0.6, "sides": 4 }, // hanging cable
{ "type": "cylinder", "r_bottom": 9, "r_top": 7, "h": 14, "rotate": [-90, 0, 0], "at": [-50, 20, -48], "mirror": "x", "material": "bare_metal" } ] }
{ "action": "preview", "name": "hull_a", "views": ["game"], "ao": true } // also lod:1|2, morph:{open:1}, bump
{ "action": "export", "name": "hull_a", "bake_ao": true, "lods": [1, 0.5, 0.2], "impostor": { "grid": 8, "size": 64 }, "collision": "hull", "occluder": "boxes" }PBR materials:
materialon a part is a preset (painted_metal,bare_metal,rubber,plastic,glass,emissive_panel,fabric) or{metal, rough}, exported as glTFmetallicFactor/roughnessFactor(the old default stays metal 0, rough 1). Canvas layers namedmetalandrough(grey = value, unpainted = the factor) become ametallicRoughnessTexture; canvasmeta.materialsets a whole canvas.atlas:truepacks PBR textures into one metallicRoughness atlas.Height and normals: a
heightlayer (or a same-size<canvas>_hcanvas withbump) gives a pixel-correct normal map: no blur, only texels withinbevelof a drop to a lower neighbour tilt, so a raised panel gets a crisp rim and the low side stays flat.bump:{strength, bevel, auto:"luminance"|"panels"}derives height from brightness steps or dark panel lines when you have no height map. glTF convention (green = up).Baked AO:
bake_ao:true|{mode:"texture"|"vertex"|"both", samples, distance, texel}ray-casts hemispheres against the model (BVH) into an AO lightmap (occlusionTexture, its own uniqueTEXCOORD_1, NEAREST) and/orCOLOR_0. Contact shadows land in crevices, under greebles and in insets. Bakes are deterministic.cast_shadow:false/receive_shadow:falseon a part split it into a node with those flags inextras.Greebles: a
greeblepart, orgreebles:[{face, ...}]on a box/wall/floor, scatters non-overlapping pieces (panel vent pipe conduit antenna bolts hatch heatsink nozzle dome cables tank block, or your own models via"model:<name>"/greeble_kind) with a seed, density, size and height ranges, an optional panelgrid, and anavoidmask or rectangles. Pieces merge into the model's materials, orinstanced:trueexports them withEXT_mesh_gpu_instancing(three.jsInstancedMesh). LODs drop them (drop_lod:"auto": flat pieces at LOD 1, tall ones at LOD 2).Sweeps:
tube/sweepsweeps a circle or a customprofilealong a polyline, Catmull-Rom or Bezierpathwith rotation-minimising frames, mitred corners,twist, taper (r_end), caps and texture wrap;from/to/saghangs a cable. Pipes, hoses, railings, conduits.Trim sheets:
art_model trim {canvas}paints a sheet with colour, height, metal, rough and emissive layers, or marks your own (strips:{name:[y,h]}).trim:"sheet:strip"maps a part's bevels, inset walls and thin faces to that strip: u along the face at world scale, v across the strip. Hulls and interiors share one texture.Mesh ops:
boolean:"subtract"|"union"|"intersect"(BSP CSG per closed piece; results are edge-manifold, world UVs continue on the cut walls), modelmerge(weld + drop hidden faces between touching pieces), and modelmorphs:{open:{set:{door:{rotate:[0,-90,0]}}}}(door= part index orid; a partnamemakes a rig node, and rigs cannot morph yet) or{model:"variant"}as glTF morph targets with names inmesh.extras.targetNames.LODs:
lods:[1,0.5,0.2]runs quadric edge collapse onto existing vertices. Each triangle keeps its own UVs, so flat regions stay pixel-exact. Material and open borders are kept, and hidden faces between pieces are dropped first. The levels go into<name>_lods > <name>_LOD0..nnodes (orlod_mode:"files"), andmeta.lods.levels[i].distanceis where that level's error falls below one screen pixel (lod_screen:{height:720, fov:70, px:1}).Impostors:
impostor:{grid:8, size:64}(hemi-octahedral) or{layout:"ring", angles:8|16}renders albedo, world-normal and depth atlases plus<name>.impostor.json(cells, directions, encode formula, quad size, switch distance) for far-LOD billboards.Proxies and stats:
collision:"hull"|"boxes"andoccluder:"boxes"(conservative, inside the surface) export as hidden named nodes, with the boxes also listed inmeta.json; they are not counted in triangles.stats:trueadds per-part triangles and bounds.Cull data: a layout's
cull:{rooms:[{name, min, max}]}exports oneroom_<name>node per room, finds doorways automatically where rooms touch, and writesmeta.cull(rooms, portals, PVS). The engine draws the camera's room plus its PVS, or walks the portals through the frustum.

node docs/surface/build-demos.mjs rebuilds these renders. art_help {topic:"3d"} has every option plus recipes for hull greebling, a corridor with one trim sheet, a pipe run and LOD + impostor for a city block. Skill: hard-surface.
Materials v2: render data for modern renderers
Opt-in per part, canvas or model; models that use none of it export the same bytes as before.
Material extensions: presets
painted_hull(clearcoat),brushed_metal(anisotropy + streaked direction map),glass_canopy(transmission + volume + ior),cloth(sheen),neon(emissive strength),skin(diffuse transmission + SSS thickness); or fieldsemissive:{color, strength, animate},clearcoat,anisotropy,transmission,ior,sheen,subsurface,height(parallax height texture),detail(tiling detail albedo + normal).art_model material {name, material, preset|set}edits model materials after creation.Baked light: light parts (
{type:"light", kind:"point"|"spot"|"area"}),export {lightmap}= a coloured, quantised, dithered lightmap on TEXCOORD_1 (sun, sky, lights, emissive bounce),probes= SH9 light probes on a grid,bake_ao:{bent:true}= bent normals,thickness= SSS thickness.Geometry and vertex data: MikkTSpace-compatible TANGENT, part
displace(adaptive, crack-free true displacement from the height map),vertex_colors(paint / masks / AO sets),uv2/uv3.Packing and profiles: ORM packing (
gltf_orm,roblox,unity_mask), texture dedupe,export {profile:"three"|"roblox"|"minimal"}(Roblox: SurfaceAppearance maps <= 1024 px, meshes < 20k triangles, a report of what was dropped). Every v2 export runs an in-repo structural glTF validator.Channels:
preview {channel:"normal"|"orm"|"lightmap"|"bent"|"emissive"|"height"|...|"all"}, and a channel dropdown in the web UI 3D viewer.

docs/materials/README.md has every demo (hull clearcoat, brushed metal, glass canopy, fabric seat, neon sign, parallax, detail maps) and the three.js hookups (lightmap, probes, pixel-snapped parallax, detail, bent normals, emissive flicker, SSS). art_help {topic:"materials"}.
Connected textures (CTM)
{ "action": "ctm", "name": "marble_ctm", "ctm": { "base": "marble", "method": "full", "border": { "width": 2, "style": "bevel" } } }
{ "action": "create", "name": "hall", "parts": [ { "type": "floor", "texture": "marble_ctm", "cell": [0, 0, 0], "array": { "count": 4, "cell_step": [1, 0, 0] } } ] }Methods:
full: the 47-tile blob set.compact: 5 tiles, combined by quarters.horizontal,verticalandhorizontal+vertical.top.random: weighted, picked deterministically from the world cell.variants:[...]lists the canvases; withoutbase, the first variant is the base.repeat: one big pattern over cols × rows cells.overlay: tiles that creep in from neighbouring cells of another material.
Tileset sources: a base texture plus a border style, or a hand-drawn
template. A template holds either all the tiles, or the 5 compact tiles, which are expanded to 47.Bake: every world-tiled face using the tileset is split into world cells. Each cell takes its tile from its 8 neighbours across all placed pieces. A neighbour counts when it is on the same plane and has the same connect key:
material,texture, ortagviactm_tag. Walls fold around vertical corners.Export: writes
<name>.ctm.json(method, tile order, masks, connect rule) and the tileset PNGs, for re-tiling at runtime.
Neighbour bits: N=1 NE=2 E=4 SE=8 S=16 SW=32 W=64 NW=128 (N = up on walls, −Z on floors). Reference selection for engines:
/**
* ctmTileFor(neighbors, method, opts) -> tile index into the tileset (compact: [tl, tr, br, bl]
* indices of the 5 compact tiles). neighbors: 8-bit mask (N1 NE2 E4 SE8 S16 SW32 W64 NW128),
* an array of 8 booleans in that order, or {n, ne, e, se, s, sw, w, nw}.
* opts: cell [x, y, z?] (random, repeat), weights (random), seed (random), repeat [cols, rows].
*/
function ctmTileFor(neighbors, method = 'full', opts = {}) {
const K = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
let m = 0;
if (typeof neighbors === 'number') m = neighbors & 255;
else if (Array.isArray(neighbors)) neighbors.forEach((v, i) => { if (v) m |= 1 << i; });
else K.forEach((k, i) => { if (neighbors && neighbors[k]) m |= 1 << i; });
const blob = (x) => {
let r = x & 85; // edges N E S W
if (x & 1 && x & 4 && x & 2) r |= 2;
if (x & 4 && x & 16 && x & 8) r |= 8;
if (x & 16 && x & 64 && x & 32) r |= 32;
if (x & 64 && x & 1 && x & 128) r |= 128;
return r;
};
const N = !!(m & 1), E = !!(m & 4), S = !!(m & 16), W = !!(m & 64);
const cell = opts.cell || [0, 0, 0];
const hash = () => {
let h = (opts.seed ?? 1) ^ 0x9e3779b9;
for (const v of [cell[0] | 0, cell[1] | 0, (cell[2] ?? 0) | 0]) { h = Math.imul(h ^ v, 0x85ebca6b); h ^= h >>> 13; h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16; }
return (h >>> 0) / 4294967296;
};
switch (method) {
case 'full':
case 'overlay': {
if (!ctmTileFor.blobIndex) {
const masks = [...new Set([...Array(256).keys()].map(blob))].sort((a, b) => a - b);
ctmTileFor.blobIndex = new Map(masks.map((x, i) => [x, i]));
}
// overlay: bits mean "the other material is there"; its tiles are the blob set of the complement
return ctmTileFor.blobIndex.get(blob(method === 'overlay' ? ~m & 255 : m));
}
case 'compact': {
const q = (v, h, d) => (v && h ? (d ? 1 : 4) : v ? 3 : h ? 2 : 0);
return [q(N, W, m & 128), q(N, E, m & 2), q(S, E, m & 8), q(S, W, m & 32)];
}
case 'horizontal': return (W ? 1 : 0) | (E ? 2 : 0);
case 'vertical': return (N ? 1 : 0) | (S ? 2 : 0);
case 'horizontal+vertical': return (N ? 1 : 0) | (E ? 2 : 0) | (S ? 4 : 0) | (W ? 8 : 0);
case 'top': return N ? 0 : 1;
case 'random': {
const w = opts.weights && opts.weights.length ? opts.weights : [1];
let r = hash() * w.reduce((a, b) => a + b, 0);
for (let i = 0; i < w.length; i++) { r -= w[i]; if (r < 0) return i; }
return w.length - 1;
}
case 'repeat': {
const [cw, ch] = opts.repeat || [1, 1];
const mod = (a, n) => ((a % n) + n) % n;
return mod(cell[0], cw) + mod(cell[1], ch) * cw;
}
default: throw new Error(`unknown CTM method "${method}"`);
}
}Ships, interiors, industrial kits and megastructures
Big-budget sci-fi set pieces at a pixel-texture budget (renders: docs/ships/, rebuilt by node docs/ships/build-demos.mjs). Designs from the templates and generators are original and seeded.
Walkable hero ship.
create {template:"gunship"}builds an original 25 m VTOL gunship (maker ORVANE, class TERN) at true scale (32 px = 1 m) with a walkable interior in the same model. The exterior is a faceted, chined fuselage wedge with a dorsal spine, side sponsons, a heavy chin, separate bevelled armour slabs, armoured wing pylons and faceted VTOL nacelles. Inside: a corridor with chamfered portal arches, grating and blue edge strips, a weapon rack, personal storage and a component bay, a round airlock with a boarding ladder, a hold and a cockpit behind multi-pane glazing. Renders:docs/ships/hero_*.png, rebuilt bynode docs/ships/build-hero.mjs.Player metrics and
walk_check. One shared set of metrics (1.8 m capsule, doors at least 1.2 x 2.1 m, corridors at least 1.4 x 2.3 m, stairs at most 0.2 m per step and 38 deg, slopes 35 deg, ramps 20 deg, hull at least 0.15 m thick) drives the kit andart_model walk_check {name}, which reports blocked doors, headroom (including seated head clearance), unreachable areas, floor gaps, interior parts poking through the hull and metric violations.export {walk:true}adds a navmesh node, box colliders per room, door / ladder / lift trigger volumes, seats and spawn points.Walkable hull capabilities.
hull {shell, openings}gives the hull a real wall with cut openings and visible rims;glazingsets mullioned glass into an opening;ring_dooris a round airlock door;portal_arch,locker_wall,screen_arm,pilot_seat,terminal,stair_runand clutter pieces furnish interiors;decaladds stencils, serials, labels, arrows and chevrons;figureis a 1.8 m scale silhouette.Kit textures by default.
kit_<name>and tintedkit_<name>_<rrggbb>references resolve to built-in painted pixel textures (with normal maps and metal/rough) when there is no canvas of that name, so kit pieces, templates and generators are textured out of the box.preview {mood:true, ao:true}lights them per pixel with the model's own lights plus key, fill and rim light.Hull from design curves.
{type:"hull", length, plan:[[t, half_width]], profile:[[t, top, bottom]], sections:[[t, "hex"|"wedge"|"box"|2.6|{points, chamfer}]]}lofts a closed hull (t 0 = tail, 1 = nose). Polygon corners stay hard creases (creasesets the smoothing angle).panel_linescuts grooves,platesraises armour,carve:{mask}recesses quads from a canvas, anduv:"fit"maps one livery canvas over the hull. The surface stays closed.Components.
on_hull:{hull, t, u, lift?, mirror?}puts any part or assembly on the hull surface. Assemblies are one part that expands into several:canopy,engine,thruster,wing,fin,landing_gear,ramp,cargo_door,turret,rotary_cannon,hardpoint,antenna,sensor_dome,rcs_quad,intake,vent,armor_plateandcontainer_rack. A named assembly becomes rig nodes with clips (gear_retract/gear_deploymerged over every leg,canopy_open,ramp_open,wings_fold,vtol_down,<turret>_scan) and sockets (<gear>_contact,<turret>_muzzle, plumes, hardpoints). An unnamed assembly is static in the given state (open,deployed).Templates.
create {template:"fighter"|"dropship"|"hauler"|"capital_ring", seed, textures:"kit"}builds a complete ship that you refine by part name.art_model kitpaints the kit textures (hull, deck, wall, screen, glow, hazard, rock and more, with height, metal, rough and emissive layers).art_model liverypaints paint masks, stripes, hazard zones, decals, a seeded original emblem and wear onto a canvas mapped over the hull.Interiors. The interior kit snaps to a 64 px deck grid:
corridor(straight, L, T, X, end),room,door(bulkhead, blast, hatch),airlock,ladder,lift,seat,console(MFD screen markers for UI textures),bridge,engine_room,quarters,cargo_bay,hangar,med_bayandarmoury.generate {kind:"interior", hull:<ship>}fits a deck plan inside the hull: it places doors where the exterior ramp and hatches are, writes window parts back onto the ship, and adds cull rooms for portal culling.Exteriors and city. The kit includes
tower,dish,tank,silo,crane,gantry,pipeline,scaffold,truss(warren, pratt, x or k bracing),catwalk,landing_pad,hangar_shell,hab_module,rock_platform,shaft,atrium,bridge_walk,lift_shaft,cable_web,pipe_wall,market_stallandsign_frame.generate {kind:"mining_colony"|"market", seed}lays out instanced kit models, with walkable floors in the report.Neon and light.
neon {text}builds emissive tube text from the pixel font, or from a canvas of 1 px strokes.signis a lit sign box andholoan additive card.lightexports node extras so the game can place real lights.fog_cardandgod_rayare additive banded falloff planes.preview {mood:true}lights a dark scene with the model's own light markers.Budgets.
budget {name}reports the scale class, triangles, a LOD estimate, draw calls and the costliest parts. Export options:budget:"fighter"refuses an over-budget export.split:truekeeps every mesh under 20 000 triangles (the Roblox MeshPart limit).static:true|"<pose>"bakes a rigged ship so lods, impostor and cull apply.
Damage and destruction
art_model damage gives a model seeded, nested damage states (scuffed, damaged, wrecked or custom amounts): dents that respect part rigidity, lost or hinged panels with an inner frame and sparking wires, bent parts, shattered glass, scorch, wear, bullet holes, hull breaches, and fx sockets for sparks, smoke, fire, steam and leaks. art_model fracture pre-cuts parts into watertight chunks with mass, centre and collision hulls. model_export {damage:true} writes the states as nodes (with LODs), blend masks and a manifest; damage:{profile:"roblox"} writes MeshParts with baked decals. Details: art_help {topic:"damage"}.
![]()
World-space UVs at runtime (three.js)
The same mapping the exporter bakes, for runtime-placed tiles. Shown for a Z-up scene; for Y-up swap up to (0,1,0) and the cap branch accordingly.
const pxPerUnit = 32, texSize = 32; // texels per tile
material.onBeforeCompile = (shader) => {
shader.uniforms.uvScale = { value: pxPerUnit / texSize };
shader.uniforms.uvOrigin = { value: new THREE.Vector3(0.5, -0.5, 0) }; // uv_origin [16,0,16] px, glTF Y-up -> Z-up
shader.vertexShader = shader.vertexShader
.replace('#include <common>', `#include <common>
uniform float uvScale;
uniform vec3 uvOrigin;`)
.replace('#include <uv_vertex>', `#include <uv_vertex>
vec3 wp = (modelMatrix * vec4(position, 1.0)).xyz - uvOrigin;
vec3 wn = normalize(mat3(modelMatrix) * normal);
vec3 up = vec3(0.0, 0.0, 1.0);
vec3 t = abs(dot(wn, up)) > 0.999 ? vec3(1.0, 0.0, 0.0) : normalize(cross(up, wn));
vec3 b = abs(dot(wn, up)) > 0.999 ? (dot(wn, up) > 0.0 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, -1.0, 0.0)) : cross(wn, t);
vMapUv = vec2(dot(wp, t), -dot(wp, b)) * uvScale;`);
};Sound effects and music: art_audio
Agents cannot hear, so art_audio turns audio into things they can read: an analysis image (waveform with beat grid and onsets, log spectrogram, loudness and energy per section, goniometer, band balance; view:"mix" overlays stem spectra and marks masking) and a listener report in plain words: what a listener taps along to versus the written BPM, the drum feel per section (half-time, blast, double-kick, four-on-the-floor, ...), the key/mode and chord progression, timbre words (dark, muddy, harsh, distorted and which stem, reverb size), what stands out or is buried, what gets repetitive, and how it compares with a genre profile and the stated intent. critique ranks issues by audibility and names the parameter to change.
SFX: 58 seeded recipes (gore, combat, holy, UI, monsters, sfxr classics, plus FPS/sci-fi below) with
vars(pitch, length, intensity, variation, wet, target) andvariantsfor batches; or sfxr params, or your own layers. Normalised to the peaktarget(default -1 dBFS), head silence trimmed.FPS and sci-fi SFX: firearms (
gunshot_rifle,_pistol,_marksman,_shotgun,_smg,_lmg,_suppressed) layer crack, body, thump, sub, mechanics and an environment tail (vars.environment: "outdoor"|"indoor"|"canyon"). Weapon handling:mag_out,mag_in,bolt_rack,dry_fire,shotgun_pump,casing_drop. Sci-fi:laser_bolt,plasma_shot,shield_hit,glitch_tear,teleport_swell,servo,drone_rotor(loop),hologram_ui,alarm_klaxon(loop),radio_squelch_on/_off,bullet_whiz,ricochet, andfootstep(vars.surface: metal|dirt|concrete|snow|grate).SFX options:
fx:[{type:"dc"|"eq"|"comp"|...}],reverb:{preset, wet},targetandloop:true(a seamless crossfaded loop with loop points on export) work on recipes and custom layers alike. Two extra layer kinds:glide(a tone or noise band along a frequency path, with AM, vibrato and a swell) andglitch(stutter, bitcrush shards, dropouts).infoon a recipe asset shows the resolved layers to copy and edit;deletetakes wildcards andnames:[...].Song updates: re-sending a saved song with only
tracksor onlysectionsmerges them in (nullremoves one).Music: a compact text notation (
tempo,key e phrygian,track gtr metal_guitar gain=-3 hp=80,section verse 8,gtr: E5' E5' _ F5!), generators (@doom_riff,@organ_progression,@blast_beat,@dread_ambient), instrument presets as JSON patches (pipe organ with wind and chiff, formant choir, modal bells, Karplus-Strong guitars double-tracked through amp and cab, bowed strings, metal and industrial kits), humanised timing, round robin, sends, buses, automation, sidechain, a cathedral reverb, and a mastering chain to a LUFS target with a true-peak ceiling. Loops wrap their reverb tail so the seam is clean.Song text + JSON: send
texttogether with JSONtracks,busesormaster; the JSON deep-merges over the parsed text, so notes stay in the text and mix tweaks go in JSON.Analysis: kick and snare detection is gated (a low-band transient with a sharp, noisy attack near the loudest hits), so choirs, wind, organ, drones and sub bass do not read as double-kick or backbeat. Distortion is measured from harmonic and intermodulation structure on loud, pitched frames, not spectral flatness, so reverb tails and drum kits do not count. The reverb RT60 comes from free decays and has a
reverb_confidence.loop_seamis only reported for a real discontinuity; a level change at the seam isloop_level_change. One-shots getsweep_octaves, which tells a laser or power-up glide from an impact.Export: WAV 16/24/32-bit float with a
smplloop chunk and a JSON sidecar; mp3/ogg/opus through ffmpeg (AGENT_ART_FFMPEGor PATH). Stems and adaptive-music intensity layers with a crossfade manifest. Renders run at 44.1 kHz;sample_rate:48000delivers 48 kHz through a windowed-sinc resampler with sample-exact loop points.
art_audio { action:"sfx", name:"gib", recipe:"gib_burst", variants:3 }
art_audio { action:"song", name:"crypt", text:"<notation>", intent:"gothic cathedral doom", genre:"cathedral_doom" }
art_audio { action:"analyze", name:"crypt" } // image + listener report
art_audio { action:"export", name:"gib_*", format:"ogg" }
art_audio { action:"sfx", recipe:"gunshot_marksman", variants:4, vars:{ environment:"canyon" } }
art_audio { action:"sfx", name:"rifle_in", recipe:"gunshot_rifle", vars:{ environment:"indoor", intensity:0.8 }, reverb:{ preset:"room", wet:0.2 } }
art_audio { action:"sfx", name:"hum", recipe:"drone_rotor", loop:true }
art_audio { action:"sfx", name:"zap", layers:[{ kind:"glide", pts:[[0,2400],[0.25,300]], dur:0.3 }, { kind:"glitch", at:0.05, dur:0.2, gain:-9 }], fx:["dc", { comp:{ ratio:4 } }], target:-3 }art_help {topic:"music"} has the full worked "gothic cathedral doom" track; topics audio, mixing and perception cover the rest.
Planets, ground terrain and space backdrops: art_planet
Planets you can see from orbit and land on, with pixel-art textures (palette ramps, ordered dither, NEAREST, every texture at most 1024 px) for a browser three.js game, and kept portable to Roblox. Each planet comes from one seeded field (3D noise on the unit sphere). The orbit maps, cube faces, mesh LODs, quadtree tiles and ground patches all sample that same field, so they agree.

Planets (
planet): presetsice,toxic,bloom,lava,desert,gas(latitude bands, storms, rings) andbarren. Any preset field can be overridden. Biome rules (where: {water, height, lat, moisture}select a paletteramp, mappedbyheight, moisture, latitude, noise or depth), noise (fbm,ridged,billow, domain warp), continents, craters, canyons, emissive seams (lava rivers, ice crevasses), night-side city lights,hue, andpalette(quantise to a list or"project:<name>").Maps: equirect (three.js
equirectUv) and cube faces (px nx py ny pz nz, OpenGL /CubeTextureLoaderorder). Each set has albedo, height (8-bit and 16-bit packed), tangent-space normal, roughness (glTF metallicRoughness), emissive glow, night lights, and a cloud layer with alpha. Rings come as a banded alpha strip.Meshes: a cube-sphere on an integer lattice, so shared face edges are bit-identical and every LOD is watertight. The
.glbholds a root node carrying the axial tilt, with<name>_lods(LOD0..n with switch distances), a cloud shell, an atmosphere shell (shader parameters inextras) and a ring annulus. A quadtree tile set (tiles/<face>/<level>/<x>_<y>.glbplus<name>.tiles.jsonwith bounds, geometric error, children and skirt depth) lets an engine stream detail near the camera. For far LODs there is an octahedral impostor atlas (#7bakeImpostor) and a pre-lit disc sprite.Systems (
system): an original star with 4 to 8 bodies (distinct biomes, orbits, moons and names), written as.system.jsonwith a palette strip render.Ground (
terrain {planet, lat, lon}): a landing-zone patch in a local frame (+X east, +Z south, metres). Its base elevation and biome come from the planet field. On top of that come ground detail, ridged-multifractal mountain ranges on parts of the horizon, dunes on desert worlds, mesas and escarpments, then a thermal pass (scree at cliff bases) and a droplet hydraulic pass (gullies). The landing pad stays flat. The ground uses per-biome pixel tilesets painted with Agent Art's owncells,noise,scatterandlineops (cracked plates, sand ripples, gravel, dried mud, ice crust, toxic crust, moss, snow, glowing basalt, regolith), with 4 variants and 2 transition tiles per layer at 32 px/m. Materials blend per texel through a noise-perturbed splat lookup with noise-masked, dithered organic borders and a 1 px rim, and steep faces get strata bands. Output: chunk.glbs (8 px/m, LODs, skirts, collision), 32 px/m detail decals around the pad, a horizon ring out to the far ranges with fog and sky colours, an RGBA splat (bytes sum to 255), the tileset, masks and noise for a runtime ground shader,heightfield.json, clustered GPU-instanced props (boulder clusters with size falloff at cliff bases and in gullies, debris, rock spires, shrub and grass clumps, crystal outcrops), rock arches, colonyanchors, and overview / eye-height (1.7 m) / wide renders.Kits:
rocks(chunky low-poly boulders, rocks, slabs, shards and pebbles, plus crystals, grass cards and themesa,butte,spireandarchformations, each with LODs from #7's simplifier) andasteroids(potato, contact-binary, shard and cratered shapes; a belt, ring or cluster field exported as instance transforms with spin, plus an instanced.glb).Backdrops:
skybox(layered, domain-warped nebula gas, dust lanes, an optional galaxy band, star classes and bright-star sprites; 6 faces plus an equirect, 512 to 1024 px) andfx(a sun with corona, a lens-flare sheet, a debris card, dust-storm frames and lightning frames).
art_planet { action:"system", seed:2251, count:6 }
art_planet { action:"planet", name:"phalo", preset:"bloom", seed:<from the system json>, rings:{ inner:1.45, outer:2.25 } }
art_planet { action:"terrain", planet:"phalo", lat:14, lon:32, size:512 }
art_planet { action:"skybox", name:"ember_sky", preset:"ember", size:512 }
art_planet { action:"asteroids", name:"belt", field:{ shape:"belt", count:1200 } }Orbit (atmosphere, clouds, rings) | Landing zone | Nebula skybox | Asteroid belt |
|
|
|
|

art_help {topic:"planets"} lists every option. The planets-space skill has the reference three.js snippets: the atmosphere shader (fresnel rim and day/night terminator), night lights, quadtree tile streaming, the far-planet sphere-on-quad impostor, terrain loading and a pixel ground shader. node docs/planets/build-demos.mjs rebuilds the renders and docs/planets/demo-stats.json.
First-person viewmodels: art_fp
First person built from separate pixel sprites, so the gun reads as 3D with no 3D models in the game (the fp v2 runtime contract). The arms are animated once per handling class (pistol, smg, rifle, dmr, shotgun, lmg, sniper, launcher, unarmed, knife, baton, grenade, med, tablet). Guns, magazines, attachments and items are yaw × pitch sprite sweeps that the game turns with look sway and snaps onto per-frame sockets. Adding a gun or a scope never touches the arms.

Arms: a 3D skeleton in eye space (clavicle, upper arm, forearm, palm, thumb, index and grouped fingers, both sides) solved with two-bone IK onto the anchors the game draws, so both palms stay on the grip and support at every frame. Finger presets cover trigger, support under the handguard, vertical grip, angled grip, magazine, charging-handle pinch, pistol cup, knife hammer grip, grenade hold, pin pull, open palm and fist. Every bone is a part sprite rendered once per view bucket (forearm foreshortening, back of the hand or palm) and placed with RotSprite rotation, so frames stay palette-stable. Skins (
mec,severance) swap sleeves, bracers and gloves without re-animating.Sweeps: an
art_modelgun, magazine, attachment or item rendered over a yaw × pitch grid into sheets of at most 1024 px, with every anchor per cell (grip,support,mag_well,muzzle,optic,muzzle_mount,underbarrel,sight,mount). One camera for all of them, so pixels per metre match. Moving parts export as state variants (rifle@bolt_back,pistol@slide_locked,shotgun@pump_back), and optics add a lens layer (att:scope_4x@lens).Clips per class: idle, walk, sprint, fire and fire_ads (recoil kick and recovery), ads_in and ads_idle (iron sight on the frame centre), reload and reload_empty (mag out, pouch, new mag, seat, charging handle) with events, charge, draw, holster, inspect, jam_clear, firemode, check_mag, melee, a one-handed grenade throw over the held gun, use_med and interact. There is a shotgun shell loop, a sniper bolt cycle, an LMG cover, knife slash, stab and a three-hit combo, baton strike with riot shield up and bash (the shield is a second held item,
item2), grenade cook and throw, med bandage, stim and kit, and tablet raise and use.@vgrip/@agripvariants move the support hand onto a foregrip.Check: hands on every gun of a class, the ADS sight centred to 1 px, magazine hand-offs, anchor continuity across the sweep, grip geometry against the class (
handling.jsconform) and attachment fits.
art_fp { action:"sweep", out:"public/assets" } // fp_guns / fp_mags / fp_attach
art_fp { action:"batch", out:"public/assets", skins:["mec","severance"] } // fp: arms strips + sockets
art_fp { action:"composite", out:"public/assets", class:"rifle", clip:"reload_empty", frame:14, attachments:{ optic:"scope_4x", muzzle:"compensator", underbarrel:"angled_grip" } }
art_fp { action:"check", out:"public/assets" }Look sway (the gun turning through its sweep) | Inspect | Pistol reload |
|
|
|
art_help {topic:"fp"} has the details. node scripts/fp-demos.js rebuilds docs/fp/.
Canvas op options worth knowing
art_canvas cropkeeps every frame, animation and tag of a frame strip, with the rect given in frame coordinates.rotate {angle, expand:true}grows the canvas so rotated content is not cut off.outlineis safe to re-run: it skips pixels next to existing outline-coloured pixels (skip_existing).clearwith no region clears the whole layer.stamp_visible {keep_visible:true}keeps the source layers.ellipse {thickness}draws a ring.text {font:"5x7"|"7x9"}gives larger fonts.noisetakes a"keep"colour and only erases under a mask witherase:true.adjust {alpha:-0.3}is a change in opacity (set_alphasets it directly).mask:"!#hex"means everything but that colour.glow {select:"value"}lets saturated neon bloom.gradient {dither:false}turns dithering off.art_draw {commands_file}runs a JSON batch from a file.art_critique {tileable:true}reportsseams:{ok, edge, interior, worst}. Spritesheet export takesjson:false.art_view {tile:3, max_size:300}zooms the tiled preview to fillmax_size.
Tools
Tool | What it does |
| New canvas: size, frames, background, palette, project |
| Batch of drawing commands as one undo step. |
| The canvas as an image: zoom/region, pixel grid, |
| Heuristic quality report and score for a frame, an animation or every frame |
| Usage and examples for any command ( |
| Multi-view, multi-animation characters from parts and poses (templates incl. |
| 3D: modular HD-2D kit pieces, ruins, extruded/lathed props, ships and vehicles, and dioramas, previewed from the game camera, exported to |
| Sound effects, music, mixing and mastering; analysis images, listener reports and critiques; WAV/mp3/ogg/opus export |
| First-person viewmodels: rigged 2D arms per handling class, gun / magazine / attachment / item sweeps with anchors, arms batch with per-frame sockets, runtime-identical composite and check |
| Planets (orbit maps, cube-sphere LODs, quadtree tiles, clouds, rings, atmosphere, impostors), original star systems, matching landing-zone terrain with splat maps, instanced props and colony anchors, rock and asteroid kits, nebula skyboxes and space FX sprites |
| Exact pixels as text rows plus a legend, in the format |
| Named colors per canvas or project. |
| Group assets with a shared palette and notes; contact sheets to check consistency |
| Layers with blend modes (multiply, screen, add, overlay, ...), opacity, order, merge, flatten, |
| Resize, crop, scale ( |
| Frame ops, tags with per-frame durations, forward/reverse/pingpong, filmstrip, GIF/spritesheet export |
| Asset graph: what is part of what ( |
| Review notes the user leaves in the web UI (a box or pin on a frame, view, part or audio range, with a category and a "compare with" asset). |
| Retire superseded assets in bulk, safely. |
| The rest |
| Optional: Minecraft Java resource packs (vanilla texture names and sizes for reference, pack build plus zip) |
Drawing commands (art_help {op} for each): grid, pixels, line, curve, polygon, rect, ellipse, fill, replace, remap, text, gradient, noise, scatter, cells, shade, adjust, outline, bevel, flip, rotate (RotSprite for any angle), shift, mirror, copy (across canvases: scale, tint, opacity), clear, quantize.
Light and 2.5D: light, fog, rays, glow, blur, vignette, cast_shadow, iso_tile, iso_box.
Every command takes layer, frame, mask (opaque / transparent / a colour / layer:name), blend and symmetry.
Skills
The recipes behind art_help {skill} are also served as MCP prompts and can be installed as agent skills:
npx @rusticstack/agent-art skills --out ~/.claude/skills # tileable-texture, sprite, rig, rig-hd, weapon-handling, walk-cycle, iso-scene, hd2d-scene, 3d-kit, ship-model, hard-surface, ships-structures, fp-weapon, prop-rig, ui-icons, minecraft-pack, sfx-pack, music-track, planets-spaceCLI (no MCP needed)
agent-art tools # tool list + instructions (for agents without MCP)
agent-art tool art_draw - --out view.png <<'EOF' # call any MCP tool; image replies go to --out
{"canvas":"stone","commands":[{"op":"cells","size":5,"colors":["#555","#777","#999"]}],"preview":true}
EOF
agent-art op create '{"name":"stone","width":16}' # raw engine ops
agent-art batch steps.jsonl --out-dir renders # many ops in order (an absolute "out" is used as is)
agent-art help shade # docs for a command, topic or skillWeb UI
The UI shows everything the agent does, live, and lets you steer it.

Activity timeline. Every MCP tool call is an entry: tool, action, key arguments, duration, result summary or error, and before/after thumbnails of the canvases and models it changed. Filter by tool, by the open asset, by errors or by source, and search. Clicking an entry opens the asset as it was after that call;
⇆compares before and after. Dock it right or at the bottom (P).Asset browser. One tree of the workspace: pinned items, groups, projects, canvases (layers and animation tags), rigs (parts canvas, clips, poses), 3D models (named parts, clips), audio and exported files. Each item can be hidden (eye), pinned or soloed; hidden and pinned state is saved per workspace. New assets appear as the agent makes them.
Viewers. Canvas editor (zoom at the cursor, pan, pixel grid, 3×3 tile, frame scrubber, onion skin, per-layer view, the paint tools). Animation player with fps, loop and a per-view grid that plays all 8 directions of a rig animation as a compass. Rig viewer with the skeleton drawn from the per-frame part anchors, plus pose previews per view. 3D viewer (three.js, vendored) with orbit, wireframe, vertex normals, UV checker, glTF clips, sockets, pivots and triangle stats. Audio player with waveform, spectrum, loop and range selection, and the analysis report the agent reads. Sprite sheet / export preview with frames and anchors. Project overview grid.
Layers view. The draw order of a rig clip or a canvas for the current view and frame (name, z, the rule that placed it), view-only eye and solo, a debug tint by draw order with overlap highlighting, hover to see which part drew a pixel and what is underneath, and a z-order lane chart across frames.
Asset graph. Force-directed or layered view of
art_graph: thumbnails, colours by kind, relation labels, filters by relation or tag, collapsible groups, review badges.Review notes. Press
M, drag a box or click a pin (on a canvas, an animation view, a rig clip, the 3D view or an audio range) and leave a note:fix,redo,inconsistent(across views),layering,style,questionorapprove, with a priority and an optional asset/view to compare with. The agent sees open notes inart_review listand as one line in the reply of any tool that touches that asset; it marks themfixedand you accept or reopen them. Approving an asset makes the agent's replies warn before it changes it.Compare (
C) puts two assets or two states side by side. Follow (F) switches the viewer to whatever the agent is touching; turn it off to browse while events keep streaming.Resizable panes, keyboard shortcuts (
?), dark theme, and a narrow-screen layout with drawers. Thumbnails are cached by version and load lazily, so hundreds of canvases stay fast.
The agent's side of the graph and the notes:
// art_graph: record a relation, then ask what is connected (a few hundred characters)
{ "action": "link", "from": "helmet_side", "rel": "variant_of", "to": "helmet_front", "attrs": { "view": "side" } }
{ "action": "query", "node": "helmet_front" }
// art_review: open notes, one line each; the marked region as an image; resolve it
{ "action": "list" }
{ "action": "get", "id": "r1" }
{ "action": "update", "id": "r1", "status": "fixed", "reply": "closed the outline gap" }More screenshots are in docs/ui.
Cleanup: retiring superseded art
When a set of assets is replaced (baked-gun sprites by rigged characters, old FP strips by separate arm and gun sweeps), art_cleanup removes the old set without leaving junk and without breaking what still uses it.
{ "action": "replace", "pairs": [{ "old": "marine_old_idle", "new": "marine_idle" }] } // old is now superseded
{ "action": "plan", "select": { "superseded": true }, "include_exports": true } // dry run: counts, sizes, first 10, blockers
{ "action": "apply", "plan_id": "p…", "confirm": true } // soft delete into workspace/trash/<batch>
{ "action": "restore", "ids": ["t…"] } // or {"ids":["canvas:marine_old_idle"]}, {"all":true}
{ "action": "empty", "older_than": "30d", "confirm": true } // the only hard deleteDependency-aware. An asset that a kept asset needs blocks the plan: the parts of a kept rig (a rig is one asset with its parts canvas), the texture of a kept model, the project palette of kept canvases (
depends_on,uses_palette,part_ofa project).include_dependents: trueremoves those dependents too ("all"also takes soft users such asrendered_fromsprites), andforce: trueignores them. Soft users are listed as warnings.Protected. Assets approved in review need
allow_approved: true. Pinned assets produce a warning.Export files are only moved when they are inside
workspace/exportsor a folder the user allowlists incleanup.allow_paths(in<workspace>/config.json, or the web UI under Cleanup › Settings; the agent cannot change it). Symlinks, paths that resolve outside the allowed folder, path traversal and workspace internals are refused. Moved files keep their original paths in the trash, so restore returns them.prune_manifest: {file, keys?}also removes entries from a JSON manifest (with no keys, the entries that name a removed file are removed); the original is kept in the trash.Logged. Every apply or restore is a
removed/restoredentry in the activity timeline and in the graph's event log, with the plan id and the trash batch id.Compact. Plans and results return counts and the first 10 items (
limit, orfull: truefor everything).Single deletes (
art_canvas,art_model,art_audio,art_projectdelete) keep their old graph behaviour but now move the files to the trash too, so they can be restored.cleanup.trash_deletes: falseinconfig.jsonrestores hard deletes.
In the web UI, ☐ Select in the asset browser adds checkboxes (Shift+click selects a range; All shown takes the filtered rows; by tag adds a tag), and Plan cleanup… shows the dry run with blockers and sizes before you apply it. The 🗑 Cleanup button opens the Trash (restore or empty, with sizes), Disk usage (by kind, project and group, biggest first) and Settings. Superseded and orphan assets carry badges in the browser and in the graph.
Select | Plan (blocked) | Plan |
|
|
|
Trash | Disk usage | Graph badges |
|
|
|
Adding an asset kind (registration hook)
A new kind of asset (planets, ships, animation sets, ...) takes part in the asset graph and in cleanup through one call in its own module, registerAssetType from src/assettypes.js. No other file needs a per-kind case:
import { registerAssetType } from './assettypes.js';
registerAssetType('planet', {
list: (engine) => [...planets(engine).keys()], // required: every name of this kind
files: (engine, name) => [path.join(dir(engine), `${name}.json`)], // required for cleanup: every file it is made of
unload: (engine, name, source) => planets(engine).delete(name), // its files are already in the trash
reload: (engine, name, source) => loadPlanet(engine, name), // its files are back: load it again
info: (engine, name) => ({ project, updated }), // optional: project / older_than / unused_since selectors, usage
edges: (engine, add) => add('planet:mars', 'depends_on', 'canvas:mars_tex'), // optional: derived graph edges (depends_on = hard dependency)
sub: ['planetlayer'], // optional: sub-node kinds (planetlayer:mars/clouds) owned by it
container: false, // true: part_of edges into it are hard dependencies (like a project)
flush: (engine, name) => {}, // optional: write pending in-memory changes before measuring / moving
});Once registered, planet:mars (or the bare name mars) resolves in art_graph, art_cleanup selects it by kind, name, tag, group or project, dependencies through its edges block or cascade, and apply / restore move its files, graph links, notes and pins.
Data and configuration
The workspace is ~/.agent-art (--home or AGENT_ART_HOME changes it). Since 0.3.0 there is no implicit fallback to a ./workspace folder next to a source checkout, because it silently split work between clients started from different folders. If that folder exists, serve and mcp print a note; pass --home ./workspace to keep using it. It holds canvases/*.json (layers stored as PNG), models/*.json (3D model definitions), projects/, exports/ and packs/, plus graph.json (asset graph and review notes), reviews/ (review note crops), ui-state.json (hidden and pinned assets in the UI), trash/ (cleanup batches and single deletes, until emptied) and config.json (cleanup.allow_paths, cleanup.trash_deletes). The port is AGENT_ART_PORT (default 7420). The server binds to 127.0.0.1 and refuses cross-origin writes.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Generate game-ready 3D models, textures, and audio from natural language, over MCP.
Generate authentic pixel art - sprites, animations, and tilesets - from any MCP client
Generate, edit, style-match, turn, and animate game-ready pixel art at exact sizes.
AI game assets for agents: consistent sprites, 2D animations, tiles, maps, music and engine exports.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAn MCP server that lets AI agents draw in a live Aseprite editor or headless mode, featuring one-action Unity sprite-sheet export, art-QA sweeps, and game-dev tools.73 PyPI8MIT
- AlicenseBqualityCmaintenanceEnables users to automate the creation, editing, animation, and export of pixel-art game assets through a multi-agent pipeline that derives style specs and blueprints, generates Lua build scripts, and drives the canvas editor to produce .aseprite files, spritesheets, atlases, GIFs, and previews.12108 npm3MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to create and edit pixel-art projects — sprites, props, palettes, eight-direction animation, and tile-based environments — through a stdio MCP server with discovery, help, and call tools. Agents can build from silhouette to detail, inspect native-scale renders, silhouettes, value maps, and GIF playback, then save every change as versionable source.MIT
- AlicenseNot gradedqualityCmaintenanceEnables generating animated 32x32 character sprite sheets — with walk cycles, frame PNGs, looping GIFs, QA metadata and Godot/Unity exports — from a text prompt or reference image, running locally on an NVIDIA GPU or through a hosted cloud backend. It exposes a single create_pixel_sprite tool that writes the files to disk and returns enlarged previews so an MCP client can inspect and iterate on the result.33 PyPI3AGPL 3.0












