|
Download docs/ART_SPEC.md from elyanlabs/elya-snes: direct link, hf CLI and curl.
- Browser
- Download file 6.61 kB
-
https://huggingface.co/elyanlabs/elya-snes/resolve/main/docs/ART_SPEC.md
- Command line
-
hf download hf://elyanlabs/elya-snes/docs/ART_SPEC.md
-
curl -L -o ART_SPEC.md https://huggingface.co/elyanlabs/elya-snes/resolve/main/docs/ART_SPEC.md
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. | |