PAGAN: making game art as text
PAGAN turns text into pixel art. You write palette-keyed grids (2D) or voxel shapes (3D) in YAML;
the pagan CLI lints them against the project’s style rules, renders previews you can look at, and
builds engine-ready assets (PNG sheets, manifest.json, Godot SpriteFrames). Full notation:
reference.md. Everything below is the workflow.
0. Setup check
pagan --version(it shows the exact build, e.g.pagan 0.1.0 (v0.1.0, ce24ab4)). If missing, install it the way the project specifies (its requirements file / pinned tag, e.g.pip install -r requirements.txt). Never replace a pinned install with an editable checkout (pip install -e) unless you are developing PAGAN itself; if unsure, stop and ask.- Find the art project:
pagan status(it says so if there’s nopagan.tomlhere or above), or look for one (find . -name pagan.toml -not -path '*/node_modules/*'). That folder is the game’s art home:BRIEF.md- what the game is, its key assets, the wishlist, and a dated feedback logSTYLE.md- how the art must look, with a versioned changelogpagan.toml- palette, camera, light, lint rules, output dirpalettes/,sprites/,tiles/,models/- the sources; every asset has adescription
- Always read BRIEF.md and STYLE.md before making or changing anything, and run
pagan status. They are your memory between sessions: the chosen palette, sizes, perspective, references and every decision the owner made live there, not in your head. - No
pagan.tomlyet -> do First-time setup (section 1) before drawing anything.
PAGAN as MCP tools: if you have PAGAN tools (status, write_asset, preview, …), use them instead of
the CLI; the workflow below is the same (write_asset lints, preview returns the image to look at).
0b. Who you’re working for
You may be talking to a human, or (often) to an orchestrator agent that is building the game and comes to you for art. Either way:
- Batch questions into ONE message, each with your recommended default, and say you’ll use the defaults if there’s no answer. Never block: an orchestrator may answer later or not at all.
- Report results so a program can use them: the asset name, its files (
pagan build --json/manifest.jsonentries: sheet, frame rects, animations, pivot), the preview sheet path, and any decision you made.manifest.jsonin the output dir is the contract with the game (any engine). - Godot export (
.tres) is a convenience when the game uses Godot; PAGAN is engine-agnostic.
0c. Project settings are configurable, saved and shared
The project’s look lives in pagan.toml, not in individual assets. Run pagan config show to see every
setting, its current value, its default and what it means. Every one can change at any time:
| Setting | Default | Change it when the owner says… |
|---|---|---|
camera.elevation |
42 (top-down 3/4) | “the angle is too high/low”, “more top-down”, “more side-on” |
light.direction |
[-0.5, 0.45, 0.75] (top-left) | “light from the right”, “sun overhead” |
light.ambient / light.shadow_strength |
0.2 / 0.65 | “darker shadows”, “softer shadows” |
light.baked + export “normals” |
true / off | “the game has its own lighting” (flat colors + normal maps) |
default_palette |
(chosen at setup) | “try a different palette” |
camera.scale |
1 | “everything 3D should be bigger/smaller” |
supersample |
1 (off) | “smooth edges”, “sprites rotate freely”, “not pixel art” (draw at 2x, build smooths) |
godot.filter / godot.mipmaps |
inherit / false | “blurry/shimmering when rotated or scaled” (linear + mipmaps) |
[camera]/[light] only matter for 3D models; config show marks them unused when there are none.
pagan config set <key> <value> validates and saves the change (invalid values are rejected; nothing is
written). It then tells you what refreshes: 3D models re-render automatically on the next pagan build/preview; palette changes recolor every asset; hand-drawn 2D sprites don’t change, so if they
must match a new angle or light, pagan style bump "..." and redraw them (section 4). Log the change in
BRIEF.md’s feedback log. Use the defaults until someone asks for something else, and say which defaults
you used.
1. First-time setup (a new game)
Ask once, in ONE compact message, each question with a recommended default so the owner can just say “defaults”:
- The game - name, genre, what the player does. (needed for BRIEF.md)
- Rendering style - traditional pixel art (default), or flat / vector-like arcade shapes (neon triangles, glowing lines, no outlines or painted light), or painterly. Ask this; don’t assume pixel art.
- Perspective - top-down 3/4 (default), straight top-down, side-on, or isometric. Sets
[camera]. - Mood / references - e.g. “warm and earthy”, “neon”, “Stardew-like”, “Hyper Light Drifter”.
- Sizes - small things 16x16 (default) or 32x32; big things (vehicles, bosses) e.g. 64-96px. Does the game rotate or scale sprites freely? (then: supersample, below)
- Look rules (pixel art) - dark 1px outline (default) or none; light from the top-left (default).
- Engine - Godot 4 (default; give the path to the Godot project) or plain PNGs.
- 2D or 3D game? - a 2D game (incl. top-down 3/4) uses sprites (default). A game with a free 3D camera
(it flies, goes below things, gets close) needs meshes: see section 3b and settle
voxel_sizenow.
Recipes per rendering style:
- Pixel art - the rest of this skill’s defaults: outline, light from the top-left, ramps dark -> light, no pillow shading. These are pixel-art defaults, not PAGAN requirements.
- Flat / vector arcade -
pagan init --style flat: no outline, no painted light or shading ramps; each object is a filled shape with a paler rim (api.Canvas.polygon(points, lambda d: rim if d < 2 else fill)), lines/lasers withline(..., width=); glow and lighting are done by the engine. If sprites rotate freely:pagan config set supersample 2(draw at 2x, the build writes smooth anti-aliased PNGs) and in Godotgodot.filter linear+godot.mipmaps true. Show the owner one flat shape before detailing anything. - Painterly - pixel-art workflow with more ramp steps, soft dithering and no outline; consider
supersample 2for soft edges.
Then:
pagan init art --perspective <top-down|top-down-3/4|side|iso> --style <pixel|flat>inside the game repo (--godot <godot project dir>to build straight into it;--openonly for throwaway prototypes). The perspective sets[camera] elevationand is written into BRIEF.md. Inside a Godot project, init writes.gdignoreso Godot doesn’t import the art sources.- Palette: propose 2-3 palettes (16-32 colors, grouped into ramps dark -> light; one ramp per material:
skin, wood, foliage, stone, metal, cloth…). Write them to
palettes/, runpagan palette show, look at the swatches, show the owner the paths, let them pick or tweak. Setdefault_palette. pagan.toml:output_dir(e.g.../game/assets/generated),export = ["png", "godot"],[camera] elevation(top-down 3/4 ~ 40-45, straight down 90, side-on 10-15),[light].- Fill in BRIEF.md (game, perspective, mood, key assets) and STYLE.md (ramp intent, scale, outline, light, shading, animation rules). Log the owner’s answers in BRIEF.md’s feedback log.
2. Making an asset
Ask only what you can’t infer (BRIEF/STYLE usually answer size, palette, outline). Typical questions, with your defaults: what it looks like (colors, key features), size, 2D or 3D, how many directions, which animations. Start with ONE asset and get it approved before making related ones.
Size is the requester’s choice. PAGAN makes any size (sprites/tiles up to 1024x1024; models any frame size). Small means less detail; that’s the deal, so say so if a request is small for what it describes (e.g. “a 16px knight with a visible crest” won’t read). To go bigger:
- the same model, chunkier: raise
camera: { scale: 2 }in the model and doublesize/pivot; - more detail: model it with more voxels at scale 1. For characters keep real depth and chunky parts (torso 4-6 voxels deep, limbs 2-3 voxels thick, head ~1/3 of the height), or the side views turn into sticks.
2D or 3D?
- 3D model (
pagan new model) for anything that needs several headings or rotations (ships, vehicles, top-down characters that turn), props seen at angles, buildings. Headings come out consistent automatically and share the project light. Animate with poses (parts move by whole voxels). - 2D sprite (
pagan new sprite) for icons, UI, items, front/side-view characters, effects, and anything flat. Also for small props that need one view (a 16px barrel, crate, potion): at 1 voxel per pixel a tiny model has too few voxels to read, so hand-draw it (orpagan bakea model and touch it up). - Shortcuts only when they don’t cost quality:
mirror: horizontaland derived frames (from/flip/shift) save time, but mirroring lights both sides the same, which is wrong when the light comes from one side. Use them for front-facing symmetric subjects or where the difference can’t be seen; never trade a worse sprite for less work. - Tileset (
pagan new tiles) for terrain; tiles must be seamless. Animated terrain (water, lava) is a tilesetanimations:entry, not a separate sprite. Check transitions between different tiles withpagan preview <tileset> --scene layout.txt. - UI and fonts: a 9-slice panel/button gets
nine_slice: [l, t, r, b](the margins reach the manifest; a hollow centre may needlint: { disable: [W001] }); a pixel font is a sprite withfont:(glyph order, cell, baseline, advance): the build writes its metrics to the manifest and a.fntGodot loads as a FontFile. - 2D sprites that turn (birds, arrows, flat arcade ships) when a model isn’t right: draw the shape once
facing east and let
api.directional_frames+api.rotate_pointsmake every heading (same names as models). - Many similar assets or procedural shapes (effect bursts, 16-tile corner sets, a family of ships,
polygon ships with rims):
write a generator script on
pagan.api(reference.md) - never copy PAGAN’s geometry or lint rules into your own code; they’d drift. Keep the script next to the art and regenerate rather than hand-edit.
The loop (cap at ~4 rounds, then show the owner what you have):
- Scaffold:
pagan new sprite|model|tiles <name> --size WxH --description "..."(description = what it is and what it should look like; future you will rely on it). - Author the grid / parts. Write rows carefully: every row exactly the frame width.
pagan lint <file>-> fix every error; read warnings (orphans, ramps, jitter) and fix real ones. A warning that’s wrong by design for one asset (an expanding burst trips W005/W007) goes in that file’slint: { disable: [...] }orlint: { for: { <frame>: [...] } }- never in pagan.toml, which would hide real problems everywhere. Models:pagan fit <model>gives a size and pivot that fit every heading and pose (after adding animations, run it again).pagan preview <name>-> openpreviews/<name>/sheet.pngwith the Read tool and look at it. (It also writes 1x frames topreviews/<name>/native/for scripted montages.)- Self-review against STYLE.md and the reference assets (
pagan galleryshows the whole set): silhouette readable at 1x? light from the right side? ramps used dark->light? no pillow shading? (flat style: clean shapes, even rims, no stray pixels.) Supersampled: judgebuilt_1x.png, not the source. consistent with the other assets’ size and detail? For animations: the “changes” row should only light up the parts that are meant to move - no jumps. - Show the owner: the sheet path (and GIF paths), a one-line summary, and any choice you made.
If the owner isn’t around, leave it
status: draft, note it in BRIEF.md (wishlist: “awaiting review”) with the choices and open questions, and move on. - On approval:
pagan mark <name> --status approved, add it to BRIEF.md “Done” (and log feedback),pagan build(add--verify-godotif GODOT is set). Uncached models and Godot printpagan: ...progress on stderr; a first build of many big models can take minutes, so give the command a long timeout rather than assuming it hung. - Report back what the game side needs to use it (from
manifest.json): the sheet /.trespaths, animation names, frame size, and:- Pivot: resources can’t carry it. In Godot set
centered = trueandoffset = Vector2(godot_offset[0], godot_offset[1])on the AnimatedSprite2D/Sprite2D (manifest value); elsewhere, draw the frame with itspivotpixel at the object’s position. - 3D models: one animation per heading (
e,ne, …) plus<anim>_<heading>; the game picks the heading from its movement direction. - Animation order:
animation_order(manifest keys are sorted alphabetically). - Tilesets: a Godot
TileSet.tres(godot_tileset) with every tile in atlas source 0 at the coordinates in the manifest’stiles(name -> [column, row]); no terrains/collision yet, set those up in the editor if needed.
- Pivot: resources can’t carry it. In Godot set
Trying alternatives: asset names must be unique across sprites, tiles and models, so give
experiments their own names (barrel_a, barrel_b, or pagan bake <model> --name <other>), compare them
in pagan gallery, then delete the losers and keep the winner under the real name.
3. Follow-up requests on existing assets
- “Give it 8 directions / rotations”: if it’s a 2D sprite, rebuild it as a model (
models/) so every heading is consistent (directions: 8), matching the approved sprite’s colors and proportions, then remove/rename the sprite (asset names must be unique). Models already have them: changedirections. - New animation (sword swing, walk, hop): models -> add parts with
id:and ananimations:entry with one pose per frame (parts move by whole voxels;roll/pitchtilt the whole model, e.g. a ship listing as it sinks; a part can rotate about a hinge, e.g. a falling mast). Damage and extras arehidden: trueparts (overlays: they never alter the normal frames; withcarve: truethey cut holes) that posesshow; 2D -> add frames (derive withfrom/shiftwhere possible) and ananimationsentry. Keep everything that shouldn’t move on exactly the same pixels; check the changes row and W007. Typical frame counts: idle 2-4, walk 4-6, attack 3-5. - “Make an enemy / another character”: new asset in the same style - same palette, outline, light,
proportions; compare side by side with
pagan gallery. Variants (variants:ramp_swap/key_swap/ palette) are the cheap way to make recolored versions (elite enemy, night version, team colours);pagan previewwrites variants.png to compare them. When the base colours are one of the set (team cyan + red/orange/…), name them withbase_variant: cyanso the manifest lists all of them uniformly. Many variants of big models multiply sheet sizes: if the game can recolour in a shader, setvariant_sheets = falseand point it at each variant’sswaptable in manifest.json. A look for a whole set (“everything on the map gets an arctic version”): put the variant in the palette file (variants: { arctic: { ramp_swap: ..., tags: [world] } }), not into every asset; tilesets compare variants on their first drawn tile (--frame NAMEfor another, and--scenewrites scene_variants.png). - A family of related assets (character classes, unit types, building tiers): put the shared body in a
template: truemodel andextends:it per member (add parts,remove: [ids], override materials), so a fix to the base reaches all of them. Colours that combine (player colour × rank, team × armour tint):variant_axes, not one variant per combination. - Out of palette keys (62 ASCII letters/digits used): keep going with Unicode letters (à, é, ß, α, Ω…); see reference.md. Don’t alias existing keys or copy the palette.
3b. 3D games (meshes)
When the game’s camera moves freely in 3D, pre-rendered headings can’t serve it: models must also build as
meshes (.glb). Same model files, no edits; sprites still build too unless a model is mesh-only
(directions: 0). Notation and output: reference.md “Meshes”.
Setup (once, with the owner):
pagan config set export png,godot,mesh(drop godot if the engine isn’t Godot).- Pick
voxel_sizewith the owner (pagan config set mesh.voxel_size <metres>); it’s the game’s texel density, the same for every model. Work it out from the closest the camera gets and the render resolution: on-screen pixels per voxel ~screen_height_px * voxel_size / (2 * distance * tan(fov / 2)). At the closest distance a voxel should cover a few pixels (chunky, readable), and anything gameplay hits at close range needs real voxels: an eye that is a hit zone is at least 2x2 voxels, a hand 2-3 thick. Example: 270 px tall, 70 degrees, closest 0.5 m, 0.0625 m voxels -> ~24 px per voxel up close, ~1 px at 12 m. Default 0.0625 (16 voxels per metre: a 1.8 m soldier is ~29 voxels tall). Huge, distant structures may overridevoxel_sizeper model (W009 reminds you they’ll look coarser), or be split into modules. [mesh] shading:ramp(default: palette-locked lighting from the game’s sun, Godot shader),baked(fixed-side props, no shader),flat(the game shades). Godot scenes: Linear tonemapper, a DirectionalLight3D (the sun) with shadows.
Making a 3D asset - the loop from section 2, plus:
- Rig for gameplay: give an
idto everything that moves, detaches or is hit (head, helmet, eyes, shield, a trebuchet arm). Inrig:setparent(children follow: helmet -> head -> torso),pivotat the joint or hinge (shoulder, axle),metafor what game code reads (hitzone: eye,detachable: true,armour: 2),collision: box | convexwhere the game needs a shape. The manifest gives each part’s node path, pivot and AABB (metres) for hitboxes. Only names and meanings the owner agreed; write them in BRIEF.md. - Animations: as poses;
interpolation: linearfor smooth mechanical motion (an arm swinging); keep the default step for pixel-art timing. Show/hide parts per pose for props that appear. - Review in 3D, not just the headings:
pagan preview <model> --orbit(undersides! a flying camera sees them) and--closeup <part>for anything seen up close (faces, hit zones);--engineshows the real shader in Godot when there’s a display. Look at all of them before reporting. - Triangle budget: read
trianglesinpagan build --json’s manifest. Big flat areas of one material merge into few triangles; noise (scattered single voxels, dithering, many materials) multiplies them. Rough budgets unless the owner says otherwise: a prop < 2k, a character < 5k, a big structure < 30k. - Models or textures? Things with a silhouette and depth (characters, siege engines, towers) are models.
Big repeating flat surfaces (ground, wall faces, roofs) are tile textures: a tileset with
export_textures(seamless tiles,groupsto break up repetition,triplanar: truein Godot). - Report back:
godot_scene(orglb), the part node paths andmetakeys, animation names, the triangle count,voxel_size, and the preview paths.
4. Changing the look over time
Look changes go through the palette and the project light whenever possible, because then every asset updates at once and stays consistent:
| Owner says | Do |
|---|---|
| “darker” / “lighter” | pagan palette adjust <pal> --lightness -0.05 (or +) |
| “more contrast” | pagan palette adjust <pal> --contrast 1.15 |
| “more pastel” / “muted” | pagan palette adjust <pal> --saturation 0.6 --lightness 0.04 |
| “more vivid” | --saturation 1.2 |
| “warmer” / “cooler” | --warmth 0.02 / --warmth -0.02 |
| “only the greens darker” | add --ramp <ramp> (or --keys ...) |
| “more / softer shadows” | pagan config set light.ambient 0.1 (3D re-renders); 2D: needs redrawing (below) |
| “light from the right” | pagan config set light.direction "[0.5, 0.45, 0.75]"; 2D: redraw (below) |
| “lower / higher camera angle” | pagan config set camera.elevation 30 (3D re-renders); 2D: redraw (below) |
| “our engine does the lighting” | pagan config set light.baked false and add “normals” to export |
| “try a different palette” | --out <new> to make a copy, or a new palette with the SAME keys, then switch default_palette (grids reference keys, so assets carry over) |
Always use --dry-run or the before/after swatch it writes, then pagan gallery to check the whole
set, and log the change in BRIEF.md’s feedback log.
When existing 2D art must be redrawn (more shading, new outline style, new proportions):
pagan style bump "what changed and why"- bumpsstyle_version, logs it in STYLE.md.- Update STYLE.md’s rules to match.
pagan statuslists every asset flagged “review for style vN” (lint W006).- Go through them one by one: update, preview, look, then
pagan mark <name> --current. pagan galleryat the end;pagan build.
5. Rules of thumb
- Never invent colors: everything comes from the palette (strict mode makes this an error anyway).
- Keep 1px of transparent border when
outline: auto(E007 otherwise). - Previews are for looking;
pagan buildoutput is what the game uses. Don’t hand-edit build output. - Don’t commit or push unless the owner asks.
- If PAGAN itself seems wrong (a crash, a misleading error), say so instead of working around it silently.