elya-snes / docs /ART_SPEC.md
elyanlabs's picture
Source snapshot of github.com/Scottcjn/elya-snes@544ccea
84f8160 verified
|
Raw History Blame Contribute Delete
6.61 kB

Art spec β€” draw against these numbers

Everything here is a hardware constraint, not a style preference. Art that matches these drops straight into the pipeline; art that does not has to be resampled, and resampling is what made the logo need 704 tiles instead of 109.

Elya, canon β€” this section is normative

From patch-and-veil-elya-design-lock.md. Written here so it cannot drift again: the first sprite batch drifted on all four counts below, and drift in a character is not something a palette check or a tile count can catch.

hair long uncut AUBURN-RED, past the waist. Not brown. Not dark.
dress BROWN Victorian β€” high collar, long sleeves, floor-length skirt
NOT no apron. No maid gear. She is not a servant and is not dressed as one
build petite adult woman, mid-twenties β€” never childlike
NOT no earrings, no jewellery, no lipstick, no makeup. Image models add all four by default and every one of them has to be explicitly negated
facing RIGHT. OAM's H-flip is free, but only if the source faces the way the game runs
read 16-bit, not 8-bit. She should fill her 32x32 cell and carry visible detail β€” hair mass, collar, sleeve, hem β€” not read as a 16x16 character scaled up

The last one is a size statement, not a style one: a figure that occupies 13 of 32 pixels of width has three pixels of face and no room for the dress to be a dress. Fill the cell.

tools/mkart.py will not build a sprite sheet from art that has not been checked against this list. art_spec_check() is that check, expressed as numbers, and it runs on every build against whatever is about to be baked:

check threshold which drift it catches
width >= 24 of 32 "fill the cell" β€” a narrow figure has no room for a face
fill >= 45% same, by area
skin >= 28 px a face needs pixels; the placeholder had 16 and had none
white <= 12 px apron / maid's collar. A wide flat white collar is maid gear by another name
hair >= dress/3 "long uncut, past the waist"
facing skin centroid right of hair centroid faces RIGHT

The white budget is there because the palette trick was not enough. The object palette was deliberately scrubbed of apron white so a maid outfit would have nowhere to live β€” and the drift came back anyway, through the shape, as a broad flat collar drawn in the one white entry the collar legitimately needs. A palette check cannot catch that. A pixel budget can.

Elya herself is hand-placed at native size in mkart.py, in the same idiom as the @ block and the nabla, because two rounds of generated art failed this list and downscaling a 512 px render into a 32 px cell spends most of the cell on mush. Generated art may still override her β€” but only if it passes the gate. Keying on "the file exists" is how the apron got in the first time; the filename never knew what was in the picture.

Canvas

256 x 224. That is the whole NTSC screen. Design at 1x β€” not at 4x and downscaled. Downscaling invents intermediate colours at every edge, and on a 4bpp target those either get quantised away (mush) or eat palette entries (expensive). Hand-placed pixels at native size beat any downscale.

If you must work large, work at an integer multiple (2x = 512x448, 4x = 1024x896) with hard edges and no antialiasing, then nearest-neighbour down.

Sprites

Object size is set globally, one pair for the whole screen (OBSEL). The useful pair for a platformer is 16x16 and 32x32:

object slot notes
Elya 32x32 gives room for a jump pose and a foot-tap without swapping size
@ block 16x16
coin 16x16
nabla 16x16 or 32x32 whichever the design needs β€” pick before drawing
  • 16 colours per sprite palette, entry 0 transparent β€” so 15 usable.
  • Sprites use palettes 8-15; backgrounds use 0-7. They are separate budgets, so sprite colour does not compete with the sky.
  • 128 sprites on screen; 32 per scanline. A 32x32 Elya costs four 16x16 slots' worth of scanline budget. Not a concern for this design, but it is why you do not get an army of nablas.
  • Horizontal and vertical flip are free in OAM. Draw Elya facing one way only; walking left costs nothing extra.

Background

  • 8x8 tiles, 4bpp, 15 colours + transparent.
  • Flat areas deduplicate to a single tile, so large fields of one colour are effectively free. Texture is what costs VRAM, not size.
  • H/V flip works in the tilemap too β€” symmetric scenery is half price.
  • The sky gradient is HDMA and costs no tiles at all. Do not draw a gradient into the background art; it will be generated.

Palette discipline β€” the thing that actually bites

The logo taught this the hard way: the source red was not flat (11,610 unique colours), and every extra palette entry fragmented the background into hundreds of near-identical tiles.

So: use exact, repeated colour values. If two areas are meant to be the same red, they must be the same RGB triple, not two reds that look alike. In your editor, work from a fixed swatch set and turn antialiasing OFF.

A good starting budget for a platformer scene:

use entries
Elya skin/hair/dress 6-8
@ block + coin (gold ramp) 3-4
nabla 3
ground/scenery 6-8
sky 0 β€” HDMA

Animation

Frames cost VRAM linearly, so budget them:

animation frames why
idle bob 2
foot tap 4 the loop that runs while she waits on you
jump 1
land 1
coin spin 4 classic rotation reads at any size
block struck 1 squash frame

That is ~13 sprite frames. At 32x32 4bpp a frame is 512 bytes, at 16x16 it is 128. Call it 8 KB of VRAM for everything, against 64 KB total and 5.2 KB already spent on the logo.

What to send me

PNG, native resolution, no antialiasing, hard edges, transparent background for sprites (real alpha, not a magenta key). I will run it through tools/png2snes.py and send back a preview of exactly what the PPU renders, plus the tile count, before anything gets baked into the ROM.

One boundary

Original art in the familiar idiom, not the familiar art. The mechanic β€” jump, strike block, collectible pops out β€” is not protectable and never has been. The specific Mario sprite, the coin sprite, the ? glyph and the sounds are. That is why the block is stamped @, and it is a better joke anyway.