Files
model-viewer/README.md
T
2026-09-01 01:04:57 +01:00

257 lines
11 KiB
Markdown

# GTA:SA model viewer
Drop a `.dff` and `.txd` into `models/`, open `/samp/skin/<id>`, and it renders.
The RenderWare files are parsed in the browser — no conversion step, no Blender,
no build tooling.
`/samp/skin/<id>` and `/samp/vehicle/<id>` 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 <http://localhost:3000/samp/skin/1>.
`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/<id>.dff` / `models/<id>.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/<id>` renders a vehicle, where `<id>` 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/<id>.dff` / `<id>.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 `<model>1.txd` upwards, extracted zero based to `<id>_pj<n>.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: `<id>.dff` and `<id>.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.