257 lines
11 KiB
Markdown
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.
|