Skins (not part of the original)
A skin is a folder of replacement images layered over the original art by the hires renderer. --param skin=<name>[,<name2>...] stacks them: later skins win, and any tile or sprite no skin provides is drawn from the original art. Skins work at every hires scale, including hires=1 (the hires renderer then runs at 640 x 480 just to show the skins). Without skin= nothing changes.
# the sample skin of this repository (assets/skins/sample, generated by tools/make-sample-skin.py)
gasm-run opengta.wasm --asset-dir GTA --asset-dir skins=assets/skins \
--param intro=0 --param mission=1 --param hires=2 --param skin=sample
# two skins: mine over the sample
gasm-run opengta.wasm --asset-dir GTA --asset-dir skins=my-skins --param hires=4 --param skin=sample,mineSkins are read as files skins/<name>/.... With gasm, a second --asset-dir with the prefix skins= (gasm's --asset-dir [prefix=]dir, see its ABI "Folders") makes a folder of skins appear there; a skins/ folder inside the game folder works too. The gasm backend reads skins straight from the assets, so they also work when the game comes from the installer's cabinets. Natively they go through the file layer (hires_skin_reader, default vfs_read_all). The browser player has a skin picker: skins are imported into the browser's storage and passed to the game the same way.
Making one, step by step: Create a skin. The skin_template tool lists what your data has to replace and checks a skin (below).
Layout
skins/<name>/skin.ini
skins/<name>/style<NNN>/side/<n>.png side tile n
skins/<name>/style<NNN>/lid/<n>.png lid tile n
skins/<name>/style<NNN>/aux/<n>.png aux tile n (the frames tile animations show)
skins/<name>/style<NNN>/sprite/<n>.png sprite n
skins/<name>/style<NNN>/sprite/<n>_delta<k>.png delta k of sprite n (damage, doors), optional
skins/<name>/style<NNN>/sprite/<n>_index.png remap index map of sprite n (ped clothes, car paint), optional
skins/<name>/font/..., skins/<name>/pictures/... fonts and frontend pictures (see below)skin.ini:name = ...,author = ...,scale = N(the hires scale the art is made for; logged, informative). Lines starting with;or#are comments. A skin withoutskin.iniis skipped with a message.style<NNN>: the style number,style001(Liberty City),style002(San Andreas),style003(Vice City).- Numbers are the style's own: side tiles 0..sides-1, lids 0..lids-1, aux tiles 0..aux-1, as the map and
Style_UpdateAnimsuse them (an animated lid shows aux tiles: replace those too, or the animation shows the original frames); sprite numbers are the game's (the index into the sprite info table, asSprite.frameholds them; car info records add their group base, see Sprites). - Images: PNG, 8 bits per channel, RGBA, RGB, palette (with
tRNStransparency), grey or grey + alpha, not interlaced. Other PNGs are refused with a message naming the file. Any size: an image is stretched over the tile (tiles should be square) or the sprite's footprint. Alpha is transparency where the original has its transparent colour 0: on flat (see-through) faces, sprites and slope sides; opaque faces draw the colour regardless. - Images much larger than the scale can show are box-filtered down at load: tiles to 128 N texels, sprites to 2 N times their original size (so a 256 x 256 tile is drawn from 128 x 128 at
hires=1).
Per-resolution variants: @Nx
Any image of a skin (tiles, their _r variants, sprites, masks, deltas, glyphs, pictures) can come in versions drawn by hand for one scale: <name>@1x.png .. <name>@4x.png next to (or instead of) <name>.png, e.g. lid/7@2x.png, sprite/72_mask@4x.png, font/BIG1/65@1x.png. At hires=N the first that exists is used:
<name>@Nx.png, the image for this scale;- the nearest larger level,
@(N+1)x..@4x(scaled down: losing detail is safe); - the master
<name>.png, any size; - the nearest smaller level,
@(N-1)x..@1x(scaled up).
Every level is scaled to the footprint like any skin image, so the sizes are a recommendation (tiles 128 N, sprites 2 N times the original); a level larger than the cap is box-filtered down. The variant suffixes go before the level: 7_r1@2x.png, not 7@2x_r1.png. A skin can mix freely: a master for most files and levels only where a scale needs its own detail (thinner lines at 1x, small text that only reads at 4x). skin_template --validate checks a level's name like its master's and reports any other @ suffix.
Variants: remaps and directions
The original draws one tile with different CLUTs: lids with the block's remap (ext bits 3-4, 0..3; mostly shading, e.g. the shadowed pavement), sides with a CLUT per direction (0 top, 1 bottom, 2 left, 3 right). Sprites have a remap byte (cars: the colour of the car, peds: the clothes).
lid/<n>_r<r>.png,side/<n>_r<d>.png,aux/<n>_r<r>.png: the image for that variant. Without it the plain image stands for variant 0 and the others are derived: each colour channel scaled the way the variant's CLUT scales the original tile on average.sprite/<n>_r<r>.png: the image for remap r.sprite/<n>_mask.png, the paint mask convention for remapped sprites (cars): white where the image is paint, black elsewhere (grey blends). For remap r the masked pixels take the paint colour the remap gives the original (the average remapped colour of the texels the remap changes), shaded by the skin pixel's brightness relative to the original paint's average. Paint is a texel the remap recolours by more than 60 (|dR| + |dG| + |dB|; the remaps also tint glass and dark trim slightly).sprite/<n>_index.png, a remap index map, for sprites whose remaps recolour several parts at once (ped clothes: shirt, trousers, skin and hair are ramps of the CLUT that a remap swaps). Each pixel's red value names the original CLUT index the skin pixel stands for (0: none). For remap r a pixel takes the remap's colour of its index, shaded by the skin pixel's brightness against the index's own colour; index 0 and the indices the remap leaves alone keep the skin's colour. Draw it at the plain image's size, opaque and without anti-aliasing (an edge pixel names one index). It takes precedence over a paint mask.- Without a mask, an index map or an
_rimage, every remap shows the plain image.
Deltas: damage and doors
Car sprites (and a few others) have deltas: patches the original lays over the graphic for damage (by corner and side), the door animation steps and the siren lights (Sprites, Cars); a sprite shows any combination of them (its delta mask). On a skin sprite:
sprite/<n>_delta<k>.png, when the skin has it: an image laid over the sprite's image (alpha blended, stretched over it like the sprite image: draw it at the same size, transparent where delta k changes nothing) when the sprite shows delta k. It isn't recoloured by remaps: draw damage in neutral colours (dents, scratches, glass) or leave the paint out.- Otherwise the delta is derived from the original: the texels the original delta writes are stretched over the skin image (bilinear, so they soften at the skin's resolution). Where the delta recolours a texel of the graphic, the skin pixel is shaded by the brightness change it makes to the original (new / old luminance, at most 2x), so dents and scratches show in the skin's own colours; where it draws outside the original outline (an open door), its colours are pasted through the sprite's CLUT (with the remap); where it makes a texel transparent, the skin image is cut.
Deltas apply lowest first, as Sprite_GetComposite 0x4143a0 builds a composite; each combination of sprite, CLUT and delta mask is made once and cached. The derived damage is visible without any work by the skin's author; the result of both kinds is in out/skins/damage_2x.png (skin_test).
Fonts and pictures
Skins can also replace the glyphs of the game's fonts and the frontend's pictures. They are drawn by the hires HUD and frontend (hires renderer, "HUD and menus"), so they show with hires=2..4, or with hires=1 and a skin (the hires layer then runs at 640 x 480 just to show them). Fonts and pictures are found in the same skin stack as the tiles: the last skin of skin=a,b that has the file wins, and anything no skin has is drawn from the original.
skins/<name>/font/<FONT>/<code>.png glyph <code> of the font GTADATA/<FONT>.FON
skins/<name>/pictures/<NAME>.png the frontend picture GTADATA/<NAME>.RAWFonts
<FONT> is the font file's name without folder and extension, in capitals. At 640 x 480 the game uses the 2 variants of the HUD fonts (the 1 files are its low-resolution set and are never drawn):
| Font | Used for | First code |
|---|---|---|
BIG2 | the big messages ("MISSION COMPLETE!", "WASTED!") | 33 |
SUB2 | subtitles, zone texts, ammo and timers, pause and quit texts, the video mode menu | 33 |
STREET2 | street and area names on the zone signs | 33 |
PAGER2 | the pager's scrolling text | 33 |
SCORE2 | the score digits (top right) | 0 |
MISSMUL2 | the lives and the multiplier (glyph 10 is the "x") | 0 |
EXPSCOR2 | the score popups over the city | 0 |
F_MHEAD | frontend menu items and titles | 33 |
F_MTEXT | frontend texts, key prompts, the credits | 33 |
F_MMISS | frontend mission names and score tables | 33 |
CUTTEXT | the cutscene stills' subtitles | 33 |
F_KEY | the menu marker (codes 1..13, its animation frames) | 1 |
F_CITY1..F_CITY4 | the animated city pictures of the city select (one glyph per frame) and the network results | 1 |
CUT00..CUT53, CUT00T.. | the animations over the cutscene stills (one glyph per frame; which fonts a level loads is the exe's table at 0x4af540) | 1 |
<code> is the glyph's code in the font, in decimal: the first code (table) plus the glyph's index in the .FON file. For the text fonts (first code 33) that is the ASCII code: 65.png is "A", 97.png "a", 48.png "0"; spaces are never drawn (they only advance). Letters beyond ASCII (accented letters of the French, German, Italian and Spanish texts) are mapped by the original's table (read from the exe at 0x4b0980) to codes 128 and up of the font (glyphs 95 and up of a text font): font/F_MTEXT/130.png is the font's glyph 97, whichever accented letter the font has there. The digit fonts (first code 0) are numbered by digit: font/SCORE2/7.png is "7". The icon fonts (first code 1) start at 1.png.
- Size: an image is stretched over the glyph's box, which is the original glyph's width times the font's height (each glyph of a font has its own width; the height is the font's), times N at
hires=N. Make it that box times any factor, e.g. 4 x forhires=4; other aspect ratios are stretched. The advance (where the next glyph starts) stays the original glyph's width: a skin can't change the layout. Images much larger than 4 times the box are box-filtered down at load, then resampled to the box with a bicubic filter. - Transparency: the alpha channel. Fully transparent pixels show what is below, partial alpha is blended. Draw the glyph's outline or drop shadow into the image if it should have one (the original glyphs carry theirs in their pixels).
- Colours: the original draws one glyph with different palettes (the HUD: the score in the player's colour, the armour count, the selected and other video modes; the frontend: each font's own palette). A colour image is drawn as it is, whatever the palette. A grey image (every pixel with red = green = blue) is tinted: each pixel's grey is multiplied by the average colour the original glyph has in the palette it is drawn with, divided by the image's average grey, so a white-to-grey glyph takes the colour of each use.
- Score popups (
EXPSCOR2) are stretched further over the growing popup rectangle, as the original stretches its digits.
Pictures
<NAME> is the picture's file name without folder and extension (GTADATA/F_UPPER.RAW -> pictures/F_UPPER.png). Each is stretched over the picture's area at N x; the size to make it is the original size times any factor:
| Picture | Original size | What |
|---|---|---|
F_UPPER | 640 x 168 | the top strip of the backdrop without the logo animation (results screen) |
F_LOGO0..F_LOGO7 | 640 x 168 | the animated logo strip (8 frames, 83 ms each) |
F_LOWER0, F_LOWER1 | 640 x 312 | the lower part of the backdrop (1: the city select map) |
CUT0..CUT5 | 640 x 480 | the cutscene stills (one per level) |
F_PLAY1..F_PLAY8 | 102 x 141 | the player portraits |
F_PLAYN | 180 x 50 | the name plate under a portrait |
F_RSTAR, F_RSTARN | 64 x 59 | the two Rockstar logos of the start menu |
- The backdrop is two pictures: rows 0-167 (
F_UPPERor the currentF_LOGOframe) over rows 168-479 (F_LOWER0/F_LOWER1); the screens draw their texts over it. - Pictures are opaque, as the original copies them: transparent parts of the image come out black.
- Where the original leaves a picture's last column out (
Gfx_BlitImagestops at column 639: an image reaching the right edge loses it; rows from 479 are never drawn), the skin's is cut the same way. - Resampling: bicubic from the image to the picture's area at N x, once per picture (images larger than 4 times that area are box-filtered first).
The intro movie (MOVIE.SMK) is not skinnable. tests/hires_ui_test.c generates a skin with a glyph of F_MHEAD, a grey digit of SUB2 and F_UPPER (out/hires_ui/skins/uitest) and checks all three. Implementation: src/render/hires/skin_ui.c (the names the art is known by, registered when the original loads it; the lookups, on the skin stack's hires_skin_image).
The sample skin
assets/skins/sample is generated by tools/make-sample-skin.py (Python stdlib) from code alone: no game file is read, and its skin.ini says so. It replaces, in style 1, lid 1 (road), lid 8 (a pavement), lid 41 and aux 25-35 (water and its animation frames), side 138 (a wall) and sprite 72 (the car parked at mission 1's start, with a paint mask), at 256 x 256 (the car 248 x 256), and leaves everything else to the original art. It writes its PNGs as RGB, palette + tRNS and RGBA, with all five row filters, to exercise the decoder.
python3 tools/make-sample-skin.py # regenerates assets/skins/sample
./build/skin_test # decoder, stack and fallback checks, renders in out/skins/The skin_template tool
tools/skin-template.c, built as build/skin_template with the tests. It reads your game data (the installed game or the unzipped installer: --data <folder>, else $OPENGTA_DATA, else ./game) and writes metadata only, never a pixel of the game: the game's art is not ours to redistribute, and a skin derived from it couldn't be shared either. Skins are new art; the tool says what there is to draw and at what size.
./build/skin_template my-skin # the template: skin.json, CHECKLIST.md, skin.ini, folders
./build/skin_template --validate my-skin # check a skin (exit status 1 on errors)
./build/skin_template --at nyc 105 119 4 # the tiles of a map column, as skin file names- Template.
skin.json(for scripts) andCHECKLIST.md(for people) list per style: every side, lid and aux tile, with how many faces of the maps show it and with which remaps / directions; the tile animations and their aux frames; every sprite with its group, index in the group, size, deltas (texels each one changes) and the remaps it is drawn with (car records with 12 colours each, peds' clothes); the car info records (model, vtype, sprite, doors); the fonts (.FON: first code, height, each glyph's code and width) and the frontend pictures (.RAWwith their sizes). The checklist marks the files the folder already has, so running it again on a skin in progress shows what's left. It createsstyle001-style003/{side,lid,aux,sprite},font/andpictures/, and askin.iniif there is none; existing images are never touched. - Validate. Every file of the folder against the same manifest: folder and file names (style, kind, number in range,
_rvariants 0..3 for tiles and the sprite's remap range,_delta<k>below the sprite's delta count, no leading zeros,.pngin lower case), PNG decoding with the game's own decoder, square tiles and sprite aspect ratios, sizes below the original's or above what is kept, paint masks against their sprite image (same size, grey), delta images against the sprite (an image for it, transparency), glyphs and pictures against the font / picture,skin.ini(keys, scale 1..4), the folder name (skin=is a comma-separated list), and files the game never reads. Errors, warnings and notes, then a summary of what the skin replaces. --at. The faces of one map column (mapnyc,sanbormiami, x, y and optionally z in blocks, as the city viewer'sfront=0 map= x= y= z=): each face's tile number, its variant and the file that replaces it, and the animation frames if the tile is animated.
Implementation
src/render/hires/hires_skin.c (loader, overlay layer), hires_png.c (decoder, on src/inflate.c), hires_tex.c (the overlay stack the city pass asks: skin n ... skin 1, then the original art). Each skin caches what it found and what it didn't, so a missing file is probed once. A layer answers a HiresAsset (style, kind, number, variant, the CLUT the original would use and its variant-0 CLUT, the original texels, for sprites the delta mask), so other kinds of layer (an upscaler over the original art, a pack in another format) fit the same hook.
Other kinds of art use hires_skin_image(path, cap, &which) (hires_skin.h): any image of the loaded skins by its path inside a skin folder (font/BIG2/65.png, pictures/F_UPPER.png), the topmost skin that has it, decoded once and cached (missing files too), box-filtered down to cap texels if larger. The fonts and pictures layer (skin_ui.c) is built on it.