SA Model Viewer base
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user