SA Model Viewer base

This commit is contained in:
jack
2026-08-29 20:31:31 +01:00
commit 4eab14d4d6
20 changed files with 57452 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# 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>` is the only thing 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.
## 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
## 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.