File size: 6,611 Bytes
84f8160 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | # 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.
|