Files
2026-08-04 14:18:11 -04:00

182 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pokemon Stadium (US) — battle model export
> **Built on [pret/pokestadium](https://github.com/pret/pokestadium).** This
> pipeline is original code, but it could not have been written without that
> project's decompilation: the bone matrix chain and the fact that scale is
> kept out of it (`func_800143C0`), the rotation basis (`func_8000F730`), the
> animation and texture-animation samplers (`func_80016FBC`, `func_80017540`),
> the battle context slots, and the move-id constants all came from reading
> it. **No code or data from that project is vendored here or required to run
> this** — see the mod's [README](../README.md#acknowledgements--pretpokestadium),
> and get anything you want to reuse from upstream under its own terms.
>
> No ROM data is committed either: everything below is generated from a
> cartridge you supply.
All 151 battle Pokemon plus 64 other models from the same segment, in standard
formats. Regenerate straight from the ROM — stdlib only, no `make init`, no
splat, no crunch64:
```bash
model_extract/pipeline/build.py
```
Put a US 1.0 ROM in [baseroms/](baseroms/) (`.z64`, `.n64` or `.v64`), or pass
`--rom=PATH`. See [pipeline/README.md](pipeline/README.md) for the module layout,
how the ROM is unpacked, and the generated-effects notes.
```
viewer.html browse everything in the browser — open it directly
manifest.json every model + what each of its animations is used for
moves.json all 165 moves + the animation each species plays for them
glb/025_pikachu.glb glTF 2.0 binary: mesh, skeleton, skin, animations, textures
glb/x152_model.glb non-Pokemon models from the same segment (props, trophies…)
textures/025_pikachu/ the same textures as loose PNGs, named <n>_<w>x<h>.png
js/ viewer payloads, one per model, plus index.js and moves.js
```
`viewer.html` has a filterable picker for every model plus prev/next/random, a
**move picker** that jumps to whichever animation the current Pokemon plays for
that move, per-animation playback with a frame scrubber, an eye/texture-animation
selector, and texture/lighting/wireframe/skeleton toggles. It reads `js/`, not
`glb/`, because browsers block `fetch` of local files from `file://` — script
injection is what lets the page work when you just double-click it.
Sizes: 73 MB `glb/`, 54 MB `js/`, 7.4 MB `textures/`. Two flags if you want less:
`--no-js` skips the viewer payloads and `viewer.html` (leaving the glTF export
alone), and `--manifest-only` rebuilds just `manifest.json`.
`.glb` files are self-contained and open directly in Blender, Maya, Unity, Unreal,
three.js, Godot, Windows 3D Viewer, macOS Quick Look, and https://gltf-viewer.donmccurdy.com.
Source file `N.bin` holds species `N + 1`.
## Conventions
- Y up, +Z front, right-handed — glTF standard.
- Animations are authored at **30 fps**; keyframe times are `frame / 30`.
- Units are game units. Models are authored 10x and scaled down by the
`model_root` node, matching the geo layout's scale command.
- Textures are `CLAMP_TO_EDGE`, materials are `alphaMode: MASK` with cutoff 0.5
(N64 RGBA5551 has one bit of alpha). `doubleSided` follows the display list's
cull mode.
## Skeleton
The game keeps bone scale *out* of the matrix chain (`func_800143C0` in
[src/12D80.c](../src/12D80.c)): scale accumulates in its own stack, a bone's local
translation is pre-multiplied by the parent's accumulated scale, and the
accumulated scale is applied to the rows of the finished world matrix only at
draw time. glTF node TRS instead propagates scale multiplicatively to children,
so a 1:1 node mapping would be wrong wherever a non-uniformly scaled bone has
descendants.
Each game bone is therefore exported as two nodes:
| node | role |
| --- | --- |
| `boneNN` | pivot: `translation = t * accScale(parent)`, `rotation = R`, scale 1 |
| `boneNN_scale` | leaf child holding `scale = accScale(bone)`, so it cannot propagate |
The skin binds to the `boneNN_scale` nodes, whose world matrices then equal the
game's draw matrices exactly. Vertices are already in bone-local space and each is
rigidly bound to one bone, so all inverse bind matrices are identity.
This was verified by parsing each exported `.glb` back and diffing every joint
matrix against a reference implementation of the game's own math, over the bind
pose and four sampled frames of every animation. Worst-case disagreement is
~1e-2 game units on models spanning 2040 units, entirely from storing rotations
as spec-normalised `SHORT` quaternions.
## Animation semantics
Per-species battle data lives in `assets/us/70D3A0.bin`, 0xB90 bytes per species,
DMA'd into the battle system by `func_84302658` via the `D_80075BD0[species-1]`
pointer table. It is an array of 0x10-byte entries; byte 0 of each entry is an
index into that Pokemon's animation list and byte 1 indexes the auxiliary list:
- **entries 0164** — one per move, in the move ID order of
[oldnotes/stadium1/constants/move_constants.s](../oldnotes/stadium1/constants/move_constants.s).
Entry *n* gives the animation played when the Pokemon uses move *n + 1*.
- **entries 165+** — fixed battle contexts (idle, hit, faint, …).
Every one of the 151 species' tables indexes only animations that exist in that
species' list, which is what confirms the layout.
`manifest.json` reports, for each animation, the exact list of moves that trigger
it plus which context slots reference it. `moves.json` inverts that: every move,
and the animation each of the 151 species plays for it.
One caveat when reading move data: the table is **dense**. Every species has a
row for every move, including moves it can never learn, and those unreachable
rows overwhelmingly point at the species' generic reaction animation — the same
one the `hit` slot uses. So "118 species play Thunderbolt" is an artifact, not a
fact about the game. `moves.json` marks each row with `differsFromDefault` and
gives a `speciesWithOwnAnimation` count per move; treat those as the signal. The `animationSlots` section carries an
`evidence` field per slot:
- `code` — the battle code in `src/fragments/62` names the slot outright.
- `data` — inferred from what the referenced animation actually does, measured
across all 151 species. For example slot 167 is labelled `faint` because its
animation always ends far from the standing pose (the model drops to
0.030.84x idle height, or leaves the frame entirely for fliers), while slot
168's animation always ends at exactly idle height.
`endBehavior` reports what the animation player does past the last frame
(`func_80016FBC`): every animation in the game wraps back to `loopStartFrame`, so
one-shots like `faint` are ended by the battle state machine switching animation,
not by the player clamping. glTF has no loop flag, so importers will loop clips by
default.
The common layout, consistent across nearly every species:
| animation | role |
| --- | --- |
| 0 | idle / standby loop (all 151 species) |
| 1 | second idle-length animation, rarely referenced by the context slots |
| 2 | hit / damage reaction (149151 species across slots 166, 178181) |
| 3 … n-3 | attack animations, selected per move |
| n-2 | faint (slots 167, 177) |
| n-1 | entrance / return-to-idle cycle (slots 168, 183) |
## Texture animations (blinking, dizzy eyes)
The second animation list in the model root is a *texture* animation, not a
skeletal one (`src/18140.c`). Geo command `0x23` carries a channel index at
offset `0x02`; when it is `>= 0`, `func_800176DC` replaces that material's
texture every frame from a per-frame stream of texture-table indices.
Charmander's eyes are the clearest example — texture 2 is the open eye, 3 and 4
are blink frames, and 57 are the dizzy swirl:
| aux animation | frames | texture stream |
| --- | --- | --- |
| 0 | 10 | `2 2 3 3 4 4 4 3 3 2` — a blink |
| 2 | 122 | cycles `5 6 7` — confusion swirl |
| 4 | 18 | a slower blink |
The viewer plays these. Each skeletal animation is paired with the texture
animation the battle table most often sets alongside it, and the `eyes` dropdown
overrides that. glTF 2.0 has no texture-swap animation channel, so this data
lives in `js/` and `moves.json` rather than the `.glb` — the `.glb` files carry
the first frame's texture on each material.
## Known gaps
- **Move effect visuals are not here.** Geo command `0x24` does not draw
anything: `func_80014CB8` just records an attachment point (an id plus a world
position) on the Pokemon, and the battle system spawns particles there. Ids
114 are generic and used by nearly every species. So Charmander's tail flame,
beams, explosions and the like are drawn by the effect system in the battle
fragments and are not present in these model files — Charmander's texture set
contains eyes, claws, teeth and skin, and no flame.
- **The Poke Ball throw/open model was not found.** It is not in this segment,
no other `assets/us/**.bin` contains a model fragment, and scanning the ROM
ranges of fragments 6264 for embedded model headers found none. What does
exist is Poke Ball *2D* artwork in fragment 29
(`fragments/29/fragment29_unk_bin_*`, flagged in the splat yaml). The throw is
most likely built from raw display lists rather than a geo-layout model.
- Files 151214 are exported as `x<file>_model` with generic names. They are
props, trophies, minigame pieces and similar; only Surfing Pikachu (file 152,
the same 37-bone / 723-triangle rig as Pikachu) is named with confidence. They
carry no battle table, so their animations are left unnamed.