Registrarse
  • ¡Votaciones abiertas del Concurso de Sprites SP 2026!
    Mirá los sprites de los participantes y elegí tus favoritos.
    Ver sprites y votar →

[N64] Ingeniería Inversa de los modelos de Pokémon Stadium 2 - StadiumGS2Unity - Importer para Unity Engine

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Buenas, os dejo por aquí un write up de ingeniería inversa de modelos y animaciones de Pokémon Stadium 2 (N64) que he añadido en mi importer para Unity "StadiumGS2Unity" como .md. Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin el Scaevolus y todo el repositorio de Pret y aquellos que lo contribuyen, así que muchísimas gracias por la ayuda. A modo de ejercicio se decidió usar a Pikachu por ser el Pokémon más clásico (025 en la PokéDex) pero es funcional para todos los modelos de la ROM.

Nota: Los offsets hacen referencia a la ROM utilizada durante esta investigación, siendo ésta la versión USA recomendada por Pret para el proyecto de decompilación pokestadiumgs, con md-5: fe05fb7a1e76adcf8942b24a97748939.

1. Resumen ejecutivo


Pokemon Stadium 2 / Pokemon Stadium GS no almacena la representación completa de un modelo de batalla dentro de una única entrada de archivo. El modelo conceptual útil es:

Código:
Entrada de Pokémon N
   |
   +-- archivo de modelos 0x027ED000, entrada N
   |      +-- modelo FRAGMENT
   |      +-- esqueleto / mapeo Bone.Channel
   |      +-- meshes / datos de display-list
   |      +-- texturas + TLUTs
   |      +-- AuxAnimations (datos de animación de materiales / texturas)
   |
   +-- archivo de animaciones de batalla 0x02D7D000, entrada N
          +-- banco de animaciones
                 +-- animación 0
                 +-- animación 1
                 +-- ...
El descubrimiento crítico fue que el valor “239” visto durante la investigación inicial del offset “0x02D7D000” no es el número de entradas del archivo. El número real es el valor de 32 bits big-endian situado en “+0x0C” respecto al inicio del archivo; para esta tabla es 282. Una vez entendido esto, la entrada de modelo Pokémon “N” ya pudo emparejarse directamente con la entrada “N” del archivo de animaciones.

Para Pikachu ("entrada 25"), esto acabó produciendo las 12 animaciones de batalla, y el decodificador de animaciones de "estilo Stadium" existente pudo reutilizarse una vez que los canales se enlazaron mediante "Bone.Channel" en lugar de mediante el índice del hueso dentro del array.

2. Archivos de recursos relevantes de la ROM

Una tabla de recursos observada en la ROM en "0x00437620" contiene 19 offsets de ROM. Las entradas relevantes para el trabajo con modelos incluyen:

Código:
| Recurso | Offset ROM | Notas |
|---|---:|---|
| Archivo de modelos de minijuegos / miscelánea | `0x02000000` | 227 entradas en el importer actual; los modelos animados de minijuegos fueron los primeros modelos GS importados correctamente. |
| Archivo de modelos Pokémon de batalla| `0x027ED000` | 282 entradas. Recursos principales FRAGMENT/modelo de Pokémon. |
| Archivo de animaciones de batalla| `0x02D7D000` | 282 entradas; la entrada `N` corresponde a la entrada `N` del modelo de batalla. |
| Archivo relacionado con color / LUT | `0x03FD5000` | Relevante para el trabajo de color/HUE de Stadium; su semántica exacta no está completamente documentada aquí. |
Existen otros slots en la tabla de recursos, pero quedan fuera del alcance de este documento sobre modelos.

3. Estructura genérica de los archivos

Los recursos de modelos de batalla y de animaciones de batalla utilizan una estructura de archivo en la que el número real de entradas se almacena en "+0x0C" y los descriptores comienzan en "+0x10".

Código:
struct ResourceArchive {
    // los campos en +0x00 .. +0x0B no están completamente descritos aquí
    u32 count;              // +0x0C, big endian
    ResourceEntry entries[];// +0x10
};

struct ResourceEntry {
    u32 offset;             // +0x00, relativo a la base del archivo/banco
    u32 paddedSize;         // +0x04
    u32 unknown08;          // +0x08
    u32 unknown0C;          // +0x0C
}; // 0x10 bytes
La rutina del juego responsable de esta búsqueda es:

Código:
GetOrLoadResourceArchiveEntry @ runtime 0x80104A40
4. Archivo de modelos Pokémon de batalla ("0x027ED000")

El archivo principal de modelos de batalla contiene 282 entradas. Las entradas "0..251" incluyen la secuencia de Pokémon/modelos utilizada por el juego, con entradas adicionales no estándar, de depuración o alternativas después del roster normal.

En las herramientas actuales, las etiquetas extra conocidas incluyen:

Código:
- `252` - Substitute Doll (muñeco sustituto estilo Rhydon)
- `253` - Egg
- `254..278` - Unown (formas alternativas del alfabeto)
- `279` - Pikachu (posiblemente debug)
- `280` - Poke Ball
- `281` - Ho-Oh (posiblemente debug)
4.1 Entrada de referencia de Pikachu

Pikachu es el ejemplo canónico validado:

Código:
Archivo de modelos:         0x027ED000
Entrada:                     25
Descriptor de entrada:       0x027ED1A0
Payload del modelo:          0x02893A20
Tamaño padded del modelo:    0x00007FA0
Lo importante no son las direcciones hardcodeadas de Pikachu en sí, sino la fórmula del descriptor:

Código:
descriptor = archive + 0x10 + entryIndex * 0x10
payload    = archive + descriptor.offset
5. Representación de modelo FRAGMENT

El payload del modelo se parsea como un grafo de objetos de estilo "FRAGMENT". El importer reconstruido lo normaliza aproximadamente en esta estructura:

Código:
FragmentModel
{
    int Species;
    string Name;
    Vector3 RootScale;

    List<BoneData> Bones;
    List<TextureRecord> Textures;
    List<TlutRecord> Tluts;
    List<PrimitiveData> Primitives;

    List<AnimationData> Animations;
    List<AuxAnimationData> AuxAnimations;
}
Esta distinción es importante: el FRAGMENT contiene la información del lado del modelo necesaria para interpretar las animaciones, pero el banco de animaciones esqueléticas de batalla es externo y se encuentra en "0x02D7D000".

5.1 Esqueleto

Cada hueso parseado contiene al menos:

Código:
BoneData
{
    int Parent;
    int BoneId;
    int Channel;
    byte Flags;
    Vector3 Translation;
    Vector3 RotationUnits;
    Vector3 Scale;
}
El resultado clave de la ingeniería inversa es "Bone.Channel".

No enlaces los grupos XYZ de animación de batalla mediante el índice del hueso en el array. Un hueso con "Channel = C" consume los canales de animación:

Código:
C*3 + 0
C*3 + 1
C*3 + 2
Esos tres registros corresponden a los ejes X/Y/Z de los datos de transformación de ese hueso. Los huesos con "Channel == -1" no tienen pista de animación y conservan su transformación bind/default.

Este mapeo se validó con Pikachu: el conjunto de canales de su modelo coincidía con los grupos de canales de animación de batalla decodificados, y utilizarlo produjo movimiento esquelético correcto.

6. Archivo de animaciones de batalla ("0x02D7D000")

Este archivo es paralelo al archivo de modelos de batalla :

Código:
entrada N del archivo de modelos  <-->  entrada N del archivo de animaciones
Su número real de entradas es:

Código:
BE32(0x02D7D000 + 0x0C) = 282
Los descriptores utilizan un stride de "0x10" y comienzan en "archive + 0x10".

6.1 Referencia del banco de animaciones de Pikachu

Código:
Archivo de animaciones:             0x02D7D000
Entrada:                             25
Descriptor de entrada:              0x02D7D1A0
Payload del banco de animaciones:   0x02F5A100
Tamaño padded del banco:            0x0001A0B0
La entrada del archivo de animaciones es a su vez un "banco de animaciones", es decir, otra tabla de descriptores.

7. Estructura del banco de animaciones

El parser de trabajo trata el banco como otro archivo de recursos:

Código:
bank + 0x0C                  -> animationCount (BE32)
bank + 0x10 + i * 0x10      -> descriptor de animación i
Para cada descriptor de animación:

Código:
+0x00 -> payloadOffset, relativo a la base del banco
+0x04 -> payloadSize
Por tanto:

Código:
animationPayloadROM = bankROM + payloadOffset
Para Pikachu, el banco contiene 12 animaciones.

8. Payload de animación de batalla

El decodificador actual lee primero un offset de 32 bits en "payload + 0x00". Este apunta al header real de la animación dentro del payload.

Código:
u32 headerOffset = BE32(payload + 0x00);
Los campos del header validados y utilizados por el decodificador son:

Código:
| Offset del header | Tipo | Significado |
|---|---|---|
| `+0x00` | `u8` | flags |
| `+0x06` | `u16` | inicio del loop |
| `+0x08` | `u16` | número de canales |
| `+0x0A` | `u16` | número de frames |
| `+0x0C` | `u32` | offset de la tabla de canales |
| `+0x10` | `u32` | offset de datos de escala |
| `+0x14` | `u32` | offset de datos de rotación |
| `+0x18` | `u32` | offset de datos de traslación |
Todos los valores multibyte son big endian.

Una validación estructural útil es:

Código:
channelCount > 0
channelCount % 3 == 0
frameCount > 0
Por tanto, el número de grupos de transformación XYZ es:

Código:
xyzGroupCount = channelCount / 3
9. Descriptores de canales

Cada registro de canal ocupa "0x0A" bytes:

Código:
struct AnimationChannel {
    u8  nScale;          // +0x00
    u8  nRotation;       // +0x01
    u8  nTranslation;    // +0x02
    u8  interpolation;   // +0x03
    u16 oScale;          // +0x04
    u16 oRotation;       // +0x06
    u16 oTranslation;    // +0x08
};
Los campos "n*" y "o*" controlan el muestreo desde los bloques de datos de escala, rotación y traslación. La implementación actual reutiliza la lógica de sampling de estilo Stadium; el avance específico de GS fue encontrar el recurso correcto y enlazarlo con "Bone.Channel".

Conceptualmente, la decodificación es:

Código:
para cada hueso del modelo:
    si bone.Channel < 0:
        continuar

    firstChannel = bone.Channel * 3

    para cada frame:
        comenzar con translation / rotation / scale bind del hueso

        para axis X,Y,Z:
            descriptor = channelTable + (firstChannel + axis) * 0x0A
            samplear translation
            samplear rotation
            samplear scale

        almacenar TrackData[bone][frame]
Comenzar desde la transformación bind es importante porque no todos los componentes proporcionan necesariamente valores animados en todas las situaciones.

10. Por qué fueron importantes los modelos de minijuegos

Los modelos animados de "0x02000000" ya se decodificaban correctamente antes de encontrar las animaciones de batalla. Este fue un resultado diagnóstico muy importante: demostraba que el decodificador básico de modelos y animaciones Stadium/GS era viable.

Por tanto, el problema de los modelos de batalla era principalmente de "resolución de recursos", y no una prueba de que Stadium 2 utilizara un formato de animación esquelética completamente distinto.

Una vez suministrada la entrada correcta de "0x02D7D000" a los conceptos de decodificación existentes, Pikachu se animó correctamente.

11. Animaciones de materiales / texturas

La animación esquelética es solo la mitad de una animación de Pokémon en Stadium. Ojos, bocas y otras texturas que cambian se representan por separado como "AuxAnimations" asociadas al FRAGMENT del modelo.

La representación normalizada es:

Código:
AuxAnimationData
{
    int Index;
    int FrameCount;
    int LoopStart;
    byte Flags;
    int[][] Channels;
}
Las primitivas referencian canales de animación de textura mediante su campo "TextureAnimation".

"AnimationData" contiene:

Código:
int AuxAnimation = -1;
de modo que un clip esquelético puede emparejarse con la correspondiente animación de materiales.

11.1 Mapeo de Pikachu

Para Pikachu, comparar los números de frames y comprobar que la AuxAnimation realmente afecta a canales de animación de textura referenciados por el modelo produjo los siguientes mapeos inequívocos:

Código:
| Animación de batalla | Frames | AuxAnimation | Frames |
|---:|---:|---:|---:|
| 01 | 122 | 03 | 122 |
| 02 | 80 | 00 | 80 |
| 03 | 107 | 01 | 107 |
| 04 | 71 | 02 | 71 |
| 05 | 103 | 04 | 103 |
| 06 | 42 | 05 | 42 |
| 07 | 83 | 06 | 83 |
| 08 | 94 | 08 | 94 |
| 09 | 200 | 07 | 200 |
| 10 | 120 | 11 | 120 |
| 11 | 90 | 12 | 90 |
La animación de batalla "00" tiene 40 frames y no presentó una coincidencia única exacta de número de frames con ninguna AuxAnimation durante la pasada de diagnóstico. Aux 05 tiene 42 frames y parecía superficialmente plausible, pero ya es la coincidencia exacta para la animación de batalla 06, por lo que NO debe asignarse a la animación 00 sin evidencias más sólidas.

Las AuxAnimations 09, 10 y 13 tampoco tuvieron una correspondencia directa con clips de batalla en la prueba de Pikachu. Esto demuestra que no debe asumirse que las AuxAnimations forman un array estrictamente 1:1 paralelo a las animaciones esqueléticas de batalla.

11.2 Asociación automática segura

La regla conservadora actual es:

1. recopilar los canales de animación de textura realmente referenciados por las primitivas del modelo;
2. considerar únicamente AuxAnimations que contengan datos para al menos uno de esos canales;
3. exigir una coincidencia exacta de "FrameCount" con el clip esquelético;
4. asignar únicamente cuando exactamente una AuxAnimation cumpla esas condiciones;
5. en caso contrario, dejar `AuxAnimation = -1`.

Esto produjo animaciones correctas de ojos/materiales en los clips de Pikachu validados sin realizar suposiciones en los casos ambiguos.

12. Algoritmo práctico del importer

Ahora puede implementarse aproximadamente una importación de Pokémon de batalla del siguiente modo:

Código:
INPUT: entrada de Pokémon N

1. Leer el archivo de modelos 0x027ED000.
2. Leer el descriptor en 0x027ED000 + 0x10 + N*0x10.
3. Decodificar/parsear su payload FRAGMENT.
4. Construir esqueleto, meshes, texturas, TLUTs, primitivas y AuxAnimations.

5. Leer el archivo de animaciones 0x02D7D000.
6. Verificar N < BE32(archive + 0x0C).
7. Leer el descriptor del banco de animaciones en archive + 0x10 + N*0x10.
8. Abrir el banco en archive + descriptor.offset.
9. Leer animationCount en bank + 0x0C.
10. Para cada descriptor de animación del banco:
      a. localizar el payload relativo a la base del banco;
      b. leer el offset interno del header desde payload +0x00;
      c. parsear frame count, loop, tabla de canales y bloques S/R/T;
      d. para cada hueso, utilizar Bone.Channel*3 como su grupo XYZ;
      e. samplear los frames en TrackData.
11. Sustituir/adjuntar la lista de animaciones esqueléticas del modelo.
12. Emparejar de forma conservadora las AuxAnimations útiles con los clips esqueléticos.
13. Exportar meshes/materiales/texturas.
14. Crear clips de animación de Unity y curvas/callbacks de animación de materiales.
15. Crear el prefab.
13. Modos de fallo importantes

- Tratar "239" como el número de entradas del archivo

Esta fue la principal pista falsa. Para "0x02D7D000", utiliza el campo de número de entradas en "+0x0C": 282

Buscar las animaciones de batalla dentro de la entrada del modelo

El FRAGMENT contiene metadatos del lado del modelo para animación/materiales, pero el banco real de animaciones esqueléticas de batalla se encuentra en el archivo paralelo separado "0x02D7D000".

- Enlazar mediante índice de hueso

Esto puede producir una animación que parece plausible pero es incorrecta. Utiliza "Bone.Channel".

Suponer que cada AuxAnimation corresponde 1:1 a una animación de batalla

Pikachu demuestra que esta suposición es falsa. Utiliza evidencias como el número de frames y los canales de textura referenciados, y deja sin asignar los clips ambiguos.

- Recolorear materiales estáticos pero no las referencias animadas a materiales

Los ojos/materiales de la variante volverán a los normales cada vez que la curva de animación cambie de material. Clona/remapea primero las referencias de materiales de la animación.

- Suponer que las tablas HUE de Stadium 1 son completas para Stadium 2

Son útiles como guía, no como prueba para las entradas de Generación II. Mantén separados los datos verificados del juego de los fallbacks del importer.

14. Confirmado frente a todavía provisional

Confirmado / fuertemente validado:

- Archivo de modelos de batalla en "0x027ED000".
- El número de entradas en "+0x0C" es 282.
- Archivo de animaciones de batalla en "0x02D7D000".
- Relación directa por índice de entrada entre modelo de batalla y banco de animaciones, validada con Pikachu y utilizada por el importer genérico.
- Stride de descriptor de "0x10" y direccionamiento relativo de payload utilizado por el parser funcional.
- Descriptor/payload del modelo de Pikachu y descriptor/payload de su banco de animaciones indicados anteriormente.
- El banco de Pikachu se decodifica en las 12 animaciones esqueléticas de batalla .
- "Bone.Channel" es el mecanismo correcto de enlace de canales de transformación.
- Las AuxAnimations controlan cambios de material/textura como los ojos.
- Los 11 mapeos exactos de Pikachu entre animaciones esqueléticas/Aux indicados anteriormente funcionan visualmente.
- El archivo de minijuegos "0x02000000" contiene modelos importables/animables de forma independiente.

Provisional / incompleto

- Nombres semánticos completos para todos los campos genéricos de los descriptores de archivo más allá de offset/size.
- Significado exacto de todos los flags del header de animación y variantes de interpolación.
- Asociación de animación de materiales para la animación de batalla 00 de Pikachu.
- Propósito de cada AuxAnimation sin correspondencia.
- Reglas exactas de generación HUE/color de Stadium 2 para todas las especies de Generación II.
- Semántica completa del archivo de color/LUT "0x03FD5000".
- Etiquetas para muchas entradas de modelo no estándar posteriores al roster normal.

15. Referencia: Pikachu de principio a fin

Código:
Pokémon: Pikachu
Entrada:  25

MODELO
  archivo           0x027ED000
  descriptor        0x027ED1A0
  payload           0x02893A20
  tamaño padded     0x00007FA0

ANIMACIONES DE BATALLA
  archivo           0x02D7D000
  descriptor        0x02D7D1A0
  payload del banco 0x02F5A100
  tamaño padded     0x0001A0B0
  animaciones       12

CARGADOR DE RECURSOS
  runtime           GetOrLoadResourceArchiveEntry @ 0x80104A40
Este ejemplo es útil como objetivo conocido y correcto al implementar un extractor desde cero: si la entrada 25 no resuelve a estos recursos para la misma revisión de ROM, corrige primero el direccionamiento del archivo antes de depurar el sampler de animaciones.

16. Resultado

Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:



17. Front-End

Al utilizar Unity Engine como Front-End para hacer ingeniería inversa, obtenemos una forma directa y completamente user-friendly de importar modelos para Fangames. Por ejemplo:


18. Créditos

La investigación se realizó de forma independiente durante el desarrollo de StadiumGS2Unity, comparando con la implementación de Pokemon Stadium 1 y con la decompilación incompleta "pret/pokestadiumgs". Scaevolus, colaborador del proyecto pokestadiumgs de Pret, proporcionó la información decisiva de que la tabla "0x02D7D000" contiene las animaciones de batalla, que su número real de entradas se encuentra en "+0x0C" (282), las direcciones conocidas y correctas de los recursos de Pikachu y la referencia "GetOrLoadResourceArchiveEntry @ 0x80104A40". Estas pistas se validaron posteriormente contra la ROM y se integraron en el importer funcional. Una vez más, mil gracias a todo el equipo que se dedica horas y horas a hacer ingeniería inversa de todas nuestras ROMs favoritas.
 
Última edición:

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Añado vídeo de la propia tab de Unity, que es el "Front-End" que ve el usuario, mientras que lo explicado es lo que "va detrás".
 
Arriba