# GTA:SA model viewer Drop a `.dff` and `.txd` into `models/`, open `/samp/skin/`, and it renders. The RenderWare files are parsed in the browser — no conversion step, no Blender, no build tooling. `/samp/skin/` and `/samp/vehicle/` are the only things the server exposes. The root, `/index.html` and any other path return 404. ## Running ```bash npm install npm start ``` Then open . `PORT` and `MODEL_ROOT` are read from the environment, so you can point it at a folder outside the project: ```bash MODEL_ROOT=C:/samp/skins npm start ``` See [.env.example](.env.example) for the full set, including the S3 options. ## Getting the default SA-MP skins The 312 SA-MP skin ids are just GTA:SA ped model ids, so they come out of your own install rather than off the web: ```bash node tools/extract-peds.js --game "D:/SteamLibrary/steamapps/common/Grand Theft Auto San Andreas" ``` It reads `data/peds.ide` for the id -> model mapping, pulls each `.dff` and `.txd` out of `models/gta3.img` and `models/player.img`, and writes them to `models/.dff` / `models/.txd`. `--game` is optional if the install is in one of the usual places. That yields **265 skins**. Three groups are absent for real reasons: - The gaps (1-6, 8, 42, 65, 74, 86, 119, 149, 208, 265-273, 289+) are the ids SA-MP treats as invalid. They are simply not in peds.ide. - Ids 290-299 (`special01`-`special10`) are empty placeholder slots that servers fill with `AddCharModel`, so there is nothing to extract. - Id 0 (CJ) has no single model. `player.dff` is an 8KB stub and the game assembles him from the clothing components in player.img at runtime, so the extractor skips him rather than writing a placeholder. Extracted models are gitignored; they're Rockstar's assets, not yours to ship. ## Vehicles `/samp/vehicle/` renders a vehicle, where `` is the SA-MP model id (400-611). They come out of your own install the same way skins do: ```bash node tools/extract-vehicles.js --game "D:/SteamLibrary/steamapps/common/Grand Theft Auto San Andreas" ``` That reads `data/vehicles.ide` for the id -> model mapping, pulls each `.dff` and `.txd` out of `models/gta3.img`, and writes them to `models/vehicles/.dff` / `.txd`. All 212 come across, along with the 194 upgrade parts from `data/maps/veh_mods/veh_mods.ide` - vehicles are 400-611 and parts 1000-1193, so one flat folder holds both. It also copies two things the viewer needs: - `vehicle.txd`, from `models/generic/`. The grunge, glass, light and tyre textures every vehicle's materials name live there rather than in the per-vehicle txd, so without it most of a car renders untextured. - `vehicles.json`, built from `data/carcols.dat` and `data/carmods.dat`: the 127 entry colour palette, each vehicle's factory colour combinations, how many paintjobs it has, and which upgrade parts it accepts. A vehicle clump is not just "every atomic, drawn at its frame". Three rules come with it, all in [public/js/vehicle.js](public/js/vehicle.js): - **Damage variants.** `bump_front_ok` and `bump_front_dam` are both in the clump. The `_dam` ones are hidden, or they z-fight the intact panels. - **`chassis_vlo`.** The low detail chassis sits inside the real one, and is hidden for the same reason. - **Wheels.** A car carries exactly *one* `wheel` atomic, parented to whichever `wheel_XX_dummy` the modeller happened to pick. The game clones it to every dummy, so the viewer does too - mirrored on the left, because all the dummies share one orientation and differ only in position. Six wheelers work out of the box (`wheel_lm_dummy`), and a vehicle that models a corner itself keeps its own geometry. ### Colours SA encodes paint in the material colour: the game looks for a reserved value and swaps in the carcols entry. Checked against the retail models - every car uses `60,255,0` for the primary, two thirds also use `255,0,175` for the secondary, and the `car4` vehicles (camper, cement, squalo) add `0,255,255` for the third. Those materials keep their colour when textured rather than being forced to white, so the grunge map multiplies over the body colour the way it does in game. Colours are carcols palette indices - the same numbers `ChangeVehicleColor` takes - and default to the vehicle's first factory combination: ``` /samp/vehicle/596?c1=3&c2=1 ``` Repainting does not need a reload, so the console gets `setColors(3, 1)`, plus `carcols` for the palette and `viewer` for everything else. ### Upgrade parts Parts are fitted by component id - the same numbers `AddVehicleComponent` takes - and stack with the colour parameters: ``` /samp/vehicle/401?u=1005,1006,1001,1007,1020,1013,1008,1025&c1=3&c2=1 ``` An upgrade dff is a single atomic on a single root frame and carries no attachment information at all. What holds it on is a dummy frame in the vehicle, picked by the prefix of the part's model name: | prefix | frame | | | --- | --- | --- | | `spl_` `rf_` `nto_` `lgt_` | `ug_spoiler` `ug_roof` `ug_nitro` `ug_lights` | | | `bnt_` `bntl_` `bntr_` | `ug_bonnet` `ug_bonnet_left` `ug_bonnet_right` | | | `wg_l_` `wg_r_` | `ug_wing_left` `ug_wing_right` | | | `misc_a_` `misc_c_` | `misc_a` `misc_c` | | | `fbb_` `bbb_` | `ug_frontbullbar` `ug_backbullbar` | slamvan only | | `fbmp_` `rbmp_` | `bump_front_dummy` `bump_rear_dummy` | replaces the stock bumper | | `exh_` | `exhaust_ok` | replaces the stock exhaust | | `wheel_` | every wheel dummy | | Two of those took measuring rather than guessing: - **Exhausts hang off `exhaust_ok`, not `exhaust`.** The `exhaust` frame is the smoke emitter and sits about a metre behind the car. Across the 31 vehicles that accept `exh_b_l`, mounting on the stock exhaust component puts the pipe end within a few cm of the rear of the body every time; mounting on `exhaust` overshoots by a metre every time. - **A part's own root transform is dropped.** Fitting one re-frames its atomic onto the vehicle's dummy, which discards the frame it came with. Only the bumpers have a non-identity root - `fbmp_a_l`'s is a metre off centre and its geometry cancels that out, exactly like the stock bumper does against `bump_front_dummy` - so they are the only parts where it shows. Parts carry their own `_dam` twins, skipped like the vehicle's own. Fitting a left hand body kit part fits its right hand twin too, which is what carmods' `link` section is for. Wheels are the one part that needs scaling: a stock wheel already comes at the vehicle's own size, an upgrade wheel is modelled at unit diameter and the ide's wheel scale is what sizes it - measured, infernus stock 0.70 against a scale of 0.70, linerun 1.10 against 1.10, every `wheel_*` upgrade 0.99. `?u=` accepts anything; `mods` in the console lists what the vehicle is actually offered, id to part name. ### Paintjobs Thirteen vehicles take one - the six Transfender tuners, the six lowriders and the camper. Three each, except broadway with two and camper with one: ``` /samp/vehicle/562?pj=0 ``` A paintjob is a one texture dictionary stored beside the vehicle in the archive as `1.txd` upwards, extracted zero based to `_pj.txd` because that is what `ChangeVehiclePaintjob` counts from. It is not a name for name texture swap: the vehicle's own txd marks the panels a paintjob covers by giving them a texture called `remap*`, and fitting one replaces that texture on every material using it - seven of them on the elegy, across the chassis, doors, bonnet, boot and both bumpers. The artwork carries the colour, so those panels stop taking the primary paint while a paintjob is on and go back to it when it comes off. `setPaintjob(0)` in the console switches without a reload, and anything out of range takes it back off the way 3 does in game. Not yet handled: `stereo` and `hydralics` have component ids but no geometry, the light corona materials, dirt level, and the `_hi`/`_lo` LOD split some planes use. ## Naming Files are named by id: `.dff` and `.txd`, in `models/` locally or under the bucket prefix on S3. The `.txd` is optional; without it the model renders untextured, and a `.txd` with no matching `.dff` is ignored. You can drag a `.dff` and `.txd` straight onto the page to preview them without filing them first, which is handy for checking a custom skin. ## Deployment The server reads its model list from a local folder by default, or from S3 when `S3_BUCKET` is set. Either way the list is built once at boot and refreshed on a timer (`INDEX_REFRESH_MS`, default 15 minutes), so uploading a new skin does not need a restart. `MODELS_BASE_URL` controls where the browser fetches model files from: - **unset** — files are requested from `/models` on this origin, and nginx proxies that to S3 or CloudFront. Same-origin, so no bucket CORS rule. - **set to a CDN origin** — the browser fetches S3 directly and the server stays out of the data path. Needs the CORS rule in [deploy/bucket-cors.json](deploy/bucket-cors.json). Ready-to-edit configs live in [deploy/](deploy/): an nginx site that exposes only `/samp/skin/`, a hardened systemd unit, the bucket CORS rule and a read-only IAM policy. If you would rather not put AWS credentials on the box at all, drop the `@aws-sdk/client-s3` dependency and have `/api/skin/:id` return the URLs without checking the index. You lose clean 404s for unknown ids — the viewer reports the failed fetch instead. ## API - `GET /api/skin/:id` — the URLs for one id, or 404 - `GET /api/vehicle/:id` — the same, plus the shared txd and catalogue URLs ## What's implemented `public/js/rw/` is a standalone RenderWare reader with no dependencies: - `stream.js` — chunk walker. Every RW chunk is a 12 byte header (`type`, `size`, `libraryID`) followed by its body; containers just hold more chunks. Both formats below are built on this. - `dff.js` — clump, frame hierarchy, geometry (positions, normals, UVs, prelit colours, triangles), material list and texture names. - `txd.js` — texture dictionary, D3D8 and D3D9 rasters. - `dxt.js` — DXT1/3/5 decoder, everything ends up as RGBA8. Verified against 179 dffs and 127 txds (1383 textures) from a SA-MP cache: all parsed, with every triangle index and material id in range. Things deliberately left out, in rough order of how likely they are to matter: - **Animation.** Ped dffs store vertices in bind pose, so they render standing still, correctly. Posing them would need the Skin PLG (`0x116`) and HAnim PLG (`0x11E`) chunks turned into a `THREE.Skeleton` plus an IFP reader. Bind pose has one non-obvious rule: a *skinned* geometry's vertices live in the space of its atomic frame's **parent**, not the atomic frame itself, so that one frame is skipped when placing the mesh. The atomic hangs off `Pelvis` in most peds but off `Root` in a handful, which is why neither "always apply the frame" nor "always use identity" works. Checked against all 265 extracted peds: every one stands upright at human height under this rule. - **Native geometry.** PS2/Xbox dffs use a different vertex layout and are rejected with a message rather than parsed. PC files are fine. - **Bin Mesh PLG.** SA stores per-material tristrips in `0x50E`, but the plain triangle list is always present too, so the viewer groups that by material instead. - Environment/specular maps, UV animation, vehicle paint colours, IMG archives.