Buenas, os dejo por aquí un write up de ingeniería inversa de modelos y animaciones de Pokémon Stadium (N64) que he añadido en mi importer para Unity "Stadium2Unity" como .md. Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin la ayuda del usuario DramaticShape (y su mod para PokémonGen1Recomp), así como todo el repositorio de Pret y aquellos que lo contribuyen, así que muchísimas gracias por la ayuda.
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 pokestadium, con md-5: 3a7324ce816d5891dea074055690750a.
1. Estructura general
El pipeline de modelos Pokémon de Pokémon Stadium es bastante autocontenido. El recorrido principal es:
Una característica importante de Stadium 1 es que el propio root del FRAGMENT permite acceder tanto a la lista de animaciones esqueléticas como a la lista de animaciones auxiliares/de materiales. Existe además información de Batalla independiente en la ROM que resulta útil para determinar qué animación auxiliar/material está asociada normalmente con cada animación esquelética.
2. Normalización de la ROM y revisión utilizada
El importer admite los tres byte orders habituales de Nintendo 64 y los normaliza al orden big-endian de la ROM.
Magic values reconocidos:
La implementación se desarrolló utilizando la ROM USA .n64 con MD5:
Un MD5 diferente se trata actualmente como una advertencia. Por tanto, todos los offsets fijos de ROM documentados a continuación deben considerarse específicos de esta revisión mientras no se comprueben en otras versiones.
3. Archivo de modelos Pokémon
El archivo de modelos Pokémon comienza en:
La cabecera contiene el número de entradas en:
Los descriptores empiezan en:
y tienen un stride de:
El parser actual utiliza los dos primeros words de cada descriptor:
Por tanto, el payload se obtiene desde:
y ocupa:
bytes.
3.1. Compresión
Una entrada puede estar:
En `PERS-SZP`, el offset hacia el stream Yay0 real se encuentra en:
El flujo utilizado es:
Es conveniente mantener separadas la extracción del archivo y el parseo del FRAGMENT. No se debe asumir que cada entrada del archivo comienza directamente por `FRAGMENT`.
4. Módulos FRAGMENT
Después de la descompresión, los recursos Pokémon son módulos FRAGMENT. El parser comprueba:
Los punteros internos utilizan direcciones virtuales relativas a:
Por tanto:
Un puntero con valor cero se interpreta como null.
4.1. Localización del root
El root no se obtiene mediante un offset local hardcodeado. El parser busca aproximadamente entre:
una secuencia MIPS formada por un `LUI` seguido de un `ADDIU` que utilice el mismo registro.
Conceptualmente:
Combinando ambos inmediatos se obtiene la dirección virtual del root.
Después:
Esto evita asumir que la estructura root siempre se encuentra en la misma posición local dentro del FRAGMENT.
5. Estructura root
Los campos utilizados por el parser son:
Las listas terminan al encontrar:
El parser de modelos actual solo necesita el primer stream de layout.
Una de las observaciones más útiles de Stadium 1 es, por tanto:
Todo ello puede alcanzarse partiendo del mismo root.
6. Stream de comandos de Layout
El modelo/layout no es simplemente un array convencional de bones y meshes. Se trata de un stream/árbol de comandos que debe interpretarse recursivamente.
Durante el recorrido, el parser mantiene:
6.1. Comandos estructurales
Entre los comandos observados y utilizados están:
Para reconstruir el modelo, lo fundamental es respetar la jerarquía mientras se recorre el stream.
6.2. Model Header — comando 0x17
El comando `0x17` expone las tablas de texturas y TLUT.
Campos relevantes:
Los registros de textura tienen un stride de:
con:
Los registros TLUT también utilizan:
y contienen:
La display list de la TLUT puede modificar/sobrescribir el puntero o el número de colores de la paleta. Por ello, seguir esa display list es importante para reconstruir correctamente las texturas CI.
6.3. Root Scale — comando 0x1C
El parser lee tres valores signed 16.16 fixed-point:
que forman la escala root del modelo.
6.4. Bone — comando 0x1D
La estructura utilizada por el parser es:
El parent del bone es el bone que se encuentre en la parte superior del stack en ese momento.
6.5. Bone.Channel: asociación con las animaciones
Un detalle especialmente importante es que no se debe asumir:
Cada bone contiene explícitamente:
en:
Por tanto:
Un valor negativo significa que el bone no tiene un track de animación muestreado y debe mantener su transformación de bind pose.
Para los bones animados, `Bone.Channel` es el índice utilizado por el decoder.
6.6. Estado de material/textura — comando 0x23
El parser extrae:
Después sigue la material display list para recuperar:
El `TextureAnimation` channel es el que posteriormente permite conectar una primitive con una entrada de:
7. Display Lists de N64 y geometría
La geometría se reconstruye interpretando las display lists referenciadas desde el layout. El parser implementa el subconjunto necesario para los modelos Pokémon, incluyendo:
7.1. Formato de vértice
Para el formato observado, cada vértice ocupa:
Layout:
Las coordenadas de textura se interpretan como:
La interpretación de `0x0C..0x0F` depende del geometry mode de iluminación de N64.
Con lighting activo:
Sin lighting:
Cada vértice cargado se asocia con el bone activo en el momento en que se procesa su display list.
7.2. Tile state
El estado equivalente a `G_SETTILE` permite recuperar:
Estos estados deben conservarse.
Una textura que visualmente parece estar mal decodificada puede en realidad tener una decodificación correcta y estar utilizando incorrectamente mirror/clamp. Si el motor de destino no admite directamente algún modo de wrap, puede ser necesario modificar/bakear la textura.
Eso es una solución del exporter y no forma parte del formato de Stadium.
8. Estructura de las animaciones esqueléticas
La cabecera de una animación esquelética se interpreta como:
Cada descriptor de bajo nivel ocupa:
y contiene:
8.1. Tres descriptores por canal lógico
El `Channel` de un bone se convierte en:
A continuación se leen tres descriptores consecutivos:
Cada descriptor describe de forma independiente los datos de:
para ese eje.
La asociación completa es:
Por tanto, la asociación correcta no es:
sino:
9. Modos de sampling de animación
Los flags de la animación seleccionan al menos dos familias de encoding en el decoder actual.
9.1. Modo Hermite / keyframes
Cuando:
está activo, Scale/Rotation/Translation pueden representarse mediante keyframes.
Una key contiene:
El interpolation flag de cada componente determina si existe un `outTangent` independiente.
El mapping utilizado es:
La interpolación se evalúa mediante Hermite.
Conversiones observadas:
Después, la rotación se convierte a las unidades angulares de 16 bits utilizadas por Stadium.
9.2. Modo packed / sampled
Sin el flag Hermite, un componente puede ser:
Los tamaños observados incluyen:
Es especialmente importante inicializar cada componente con el valor de bind pose antes de aplicar el sampling.
Si un componente no está presente:
10. Representación de las rotaciones
Los bones y tracks decodificados conservan las rotaciones utilizando una unidad basada en una vuelta de 16 bits:
Para las rotaciones Hermite, el valor almacenado en la key se interpreta primero como décimas de grado antes de convertirlo. Al exportar a otro motor conviene hacer esta conversión de forma explícita y consistente.
Errores pequeños en esta parte pueden provocar:
11. Animaciones auxiliares / de materiales
Stadium 1 contiene una lista independiente de animaciones auxiliares en el root del FRAGMENT.
La cabecera parseada es:
Cada descriptor de canal auxiliar ocupa cuatro bytes:
Los datos son valores de un byte.
Para cada frame:
Si:
el parser devuelve:
Estos valores funcionan como selecciones de textura/material para primitives cuyo comando de layout tenga asociado el mismo canal `TextureAnimation`. Así se representan, entre otras cosas, cambios animados como:
12. Asociación entre animaciones esqueléticas y de materiales
Aunque las dos listas de animaciones se encuentran dentro del FRAGMENT del modelo, la asociación preferente:
se obtiene de los datos de Batalla de la ROM.
Las localizaciones fijas utilizadas para la ROM USA son:
La tabla de punteros contiene una entrada de 32 bits por especie.
Para especies:
el offset del puntero se calcula como:
El importer conserva únicamente los 24 bits inferiores:
y obtiene la tabla de Batalla mediante:
La tabla de Batalla de cada Pokémon inspeccionada por el importer tiene:
Para asociar las animaciones se utilizan los dos primeros bytes:
Conceptualmente:
El importer cuenta cuántas veces aparece cada pareja:
y asigna a cada animación esquelética la animación auxiliar que aparece asociada con mayor frecuencia.
Hay que distinguir aquí dos cosas:
El FRAGMENT indica qué animaciones existen.
Los datos de Batalla indican qué animaciones de material se utilizan junto a determinadas animaciones esqueléticas en contextos de Batalla.
Elegir únicamente la pareja más frecuente es una heurística del importer. Una reimplementación más estricta del juego podría conservar toda la tabla de Batalla en lugar de reducirla a una única `AuxAnimation` preferida.
13. Estrategias para exportar las animaciones de materiales
Los datos reconstruidos pueden representarse de distintas formas en el motor de destino.
El importer de Unity utilizado durante esta investigación permite dos estrategias.
13.1. Animation Curves
Se pueden generar object-reference keys dentro del AnimationClip que sustituyan materiales/texturas en los frames correspondientes. Esto representa de forma bastante directa el `AuxAnimationData` decodificado.
13.2. Callbacks / Animation Events
Otra posibilidad es generar eventos de animación y utilizar un componente runtime que seleccione la textura/material correspondiente de un conjunto previamente generado. Esto puede resultar útil en motores antiguos o plataformas con restricciones adicionales. Ambos métodos son decisiones del exporter/runtime. No representan dos formatos diferentes dentro de Pokémon Stadium.
14. Pokémon Shiny
La exportación de Pokémon Shiny debe considerarse una capa adicional sobre el parser principal del modelo.
El FRAGMENT proporciona los recursos normales de:
La implementación utilizada en esta investigación añade conocimiento específico sobre recoloreado/LUT para reconstruir las variantes Shiny.
La distinción importante es:
No se debe asumir que junto a cada Pokémon normal exista un segundo modelo completo almacenado específicamente para su versión Shiny.
El exporter puede generar:
reutilizando:
del modelo normal.
15. Variantes de color HUE / Entrenador
Las variantes de color dependientes del entrenador son también una capa adicional sobre el modelo normal.
El importer utilizado permite:
La variante reutiliza:
y sustituye los recursos correspondientes de:
15.1. Animaciones de materiales en las variantes
Al exportar una variante hay un detalle importante.
No basta con:
si los clips de animación siguen haciendo referencia a los materiales normales.
En ese caso el Pokémon puede verse correctamente hasta que se reproduzca una animación de ojos, boca u otro cambio de material.
Entonces reaparecería la textura normal.
El orden correcto en el motor de destino es:
Esto no constituye una regla nueva del formato de ROM. Es una consecuencia de conservar correctamente las referencias representadas por los canales de animación auxiliar.
16. Arquitectura recomendada para un parser
Una implementación puede separarse en:
Mantener el exporter del motor separado del código de lectura/parser de ROM permite diferenciar claramente:
17. Errores comunes durante la reconstrucción
17.1. Asociar las animaciones mediante el índice del bone
Incorrecto:
Correcto:
Los bones con:
no se muestrean.
17.2. Suponer un offset fijo para el root del FRAGMENT
El parser funcional obtiene el root desde el bootstrap MIPS del propio módulo.
17.3. Ignorar la bind pose como fallback
Los componentes ausentes conservan su valor de bind pose. Utilizar cero como fallback corrompe la pose.
17.4. Decodificar CI sin el estado de la display list de TLUT
La display list del registro TLUT puede resolver o modificar la información de la paleta.
17.5. Ignorar mirror/clamp
Una textura puede estar correctamente decodificada y aun así visualizarse mal si se ignora el tile state.
17.6. Tratar material animation como skeletal animation
Son estructuras distintas:
17.7. Cambiar los materiales de una variante sin cambiar sus referencias animadas
Esto puede hacer que aparezcan ojos/bocas normales sobre un Pokémon cuya variante HUE o Shiny está correctamente aplicada.
17.8. Suponer que todas las entradas están sin comprimir
Hay que comprobar:
18. Qué está sólidamente establecido por la implementación
El importer funcional proporciona evidencia de trabajo para:
19. Aspectos que todavía deben tratarse con cautela
El importer actual es suficiente para reconstruir los modelos, pero no todos los bytes del formato tienen un significado semántico confirmado.
En particular:
Para un trabajo orientado a decompilación es preferible conservar los campos desconocidos y los registros raw, aunque un exporter de assets concreto no los necesite.
20. Stadium 1 frente a Stadium 2: diferencia principal
Stadium 1 resultó especialmente útil como referencia al estudiar Stadium 2 porque buena parte del modelo conceptual se mantiene:
La diferencia importante de almacenamiento es:
Por tanto, un decoder derivado de Stadium 1 puede interpretar correctamente los canales S/R/T y, aun así, no encontrar animaciones en Stadium 2 si está buscando los recursos en el lugar equivocado. Una vez localizado el animation bank separado de Stadium 2, la semántica de canales observada en Stadium 1 sirve como referencia para interpretar las animaciones de Batalla. Si quieres ver el write-up completo de Stadium 2, lo tienes en este mismo foro: https://whackahack.com/foro/threads/n64-ingenieria-inversa-de-los-modelos-de-pokemon-stadium-2-stadiumgs2unity-importer-para-unity-engine.69322/
21. Pseudocódigo mínimo
Una importación simplificada de un Pokémon de Stadium 1 sería:
Ese es el modelo mental mínimo del formato que implementa actualmente el parser.
22. Base utilizada para este write-up
La intención es documentar las estructuras y comportamientos soportados por esa implementación sin inventar nombres para campos cuyo significado todavía no está confirmado. Los descubrimientos más reutilizables para ingeniería inversa son:
23. Resultado
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:

24. 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.
25. Créditos
La investigación se realizó de forma independiente durante el desarrollo de Stadium2Unity, comparando con la implementación de DramaticShape para Pokemon Stadium 1 para PokemonGen1Recomp y con la decompilación "pret/pokestadium". 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.
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 pokestadium, con md-5: 3a7324ce816d5891dea074055690750a.
1. Estructura general
El pipeline de modelos Pokémon de Pokémon Stadium es bastante autocontenido. El recorrido principal es:
Código:
ROM
|
+-- Archivo de modelos Pokémon @ 0x00920000
|
+-- entrada del archivo
|
+-- descompresión cuando sea necesaria
| +-- PERS-SZP
| +-- Yay0
|
+-- módulo FRAGMENT
|
+-- Species ID
|
+-- stream de comandos de modelo/layout
| +-- skeleton
| +-- materiales
| +-- texturas / TLUT
| +-- display lists / geometría
|
+-- lista de punteros a animaciones esqueléticas
|
+-- lista de punteros a animaciones
auxiliares/de materiales
2. Normalización de la ROM y revisión utilizada
El importer admite los tres byte orders habituales de Nintendo 64 y los normaliza al orden big-endian de la ROM.
Magic values reconocidos:
Código:
0x80371240 z64 / big-endian nativo
0x37804012 v64 / byte-swapped
0x40123780 n64 / word-swapped
Código:
3a7324ce816d5891dea074055690750a
3. Archivo de modelos Pokémon
El archivo de modelos Pokémon comienza en:
Código:
ROM 0x00920000
Código:
archive + 0x0C
Código:
archive + 0x10
Código:
0x10 bytes
Código:
struct ArchiveEntry
{
u32 relativeOffset;
u32 compressedSize;
// El resto de campos del descriptor
// no son necesarios para el parser actual.
};
Código:
0x00920000 + relativeOffset
Código:
compressedSize
3.1. Compresión
Una entrada puede estar:
Código:
sin comprimir
Yay0
PERS-SZP
-> contiene un stream Yay0 interno
Código:
+0x08
Código:
PERS-SZP
-> leer offset del stream interno
-> decodificar Yay0
Yay0
-> decodificar Yay0
cualquier otro caso
-> utilizar directamente el payload
4. Módulos FRAGMENT
Después de la descompresión, los recursos Pokémon son módulos FRAGMENT. El parser comprueba:
Código:
offset +0x08: "FRAGMENT"
Código:
FRAGMENT base = 0x8FF00000
Código:
localOffset = pointer - 0x8FF00000
4.1. Localización del root
El root no se obtiene mediante un offset local hardcodeado. El parser busca aproximadamente entre:
Código:
0x20 .. 0x80
Conceptualmente:
Código:
LUI rX, hi(root)
ADDIU rX, rX, lo(root)
Después:
Código:
rootLocalOffset =
rootVirtualAddress - 0x8FF00000
5. Estructura root
Los campos utilizados por el parser son:
Código:
root +0x00 : u16 species ID
root +0x08 : ptr
-> lista de punteros a streams
de modelo/layout
root +0x0C : ptr
-> lista de punteros a
animaciones esqueléticas
root +0x10 : ptr
-> lista de punteros a
animaciones auxiliares/materiales
Código:
0x00000000
Una de las observaciones más útiles de Stadium 1 es, por tanto:
Código:
FRAGMENT
|
+-- geometría
+-- skeleton
+-- animaciones esqueléticas
+-- animaciones auxiliares/materiales
6. Stream de comandos de Layout
El modelo/layout no es simplemente un array convencional de bones y meshes. Se trata de un stream/árbol de comandos que debe interpretarse recursivamente.
Durante el recorrido, el parser mantiene:
Código:
bone stack
textura actual
TLUT actual
material display list actual
canal de texture animation actual
estado de tiles de N64
vertex cache de 64 entradas
6.1. Comandos estructurales
Entre los comandos observados y utilizados están:
Código:
0x00 / 0x03
recurse hacia un stream hijo
0x02
jump / continuar en otro stream
0x05
push del bone actual
0x06
pop del bone
6.2. Model Header — comando 0x17
El comando `0x17` expone las tablas de texturas y TLUT.
Campos relevantes:
Código:
+0x02 : s16 textureCount
+0x04 : s16 tlutCount
+0x08 : ptr textureTable
+0x0C : ptr tlutTable
Código:
0x0C bytes
Código:
+0x00 : u8 format
+0x01 : u8 size
+0x02 : s16 width
+0x04 : u16 height
+0x08 : ptr texel data
Código:
0x0C bytes
Código:
+0x02 : u16 count
+0x04 : ptr palette data
+0x08 : ptr palette display list
6.3. Root Scale — comando 0x1C
El parser lee tres valores signed 16.16 fixed-point:
Código:
+0x04 : X
+0x08 : Y
+0x0C : Z
6.4. Bone — comando 0x1D
La estructura utilizada por el parser es:
Código:
+0x01 : u8 boneId
+0x02 : u8 flags
+0x03 : s8 animation channel
+0x04 : s16 translation X
+0x06 : s16 translation Y
+0x08 : s16 translation Z
+0x0A : s16 rotation X
+0x0C : s16 rotation Y
+0x0E : s16 rotation Z
+0x10 : s32 scale X, 16.16
+0x14 : s32 scale Y, 16.16
+0x18 : s32 scale Z, 16.16
6.5. Bone.Channel: asociación con las animaciones
Un detalle especialmente importante es que no se debe asumir:
Código:
bone N = animation channel N
Código:
s8 Channel
Código:
bone command +0x03
Código:
Bone
|
+-- Channel
|
+-- canal que debe utilizar
el decoder de animación
Para los bones animados, `Bone.Channel` es el índice utilizado por el decoder.
6.6. Estado de material/textura — comando 0x23
El parser extrae:
Código:
+0x02 : texture-animation channel
+0x04 : material display-list pointer
+0x08 : texture index
+0x0A : TLUT index
Código:
palette state
tile addressing
mirror S/T
clamp S/T
Código:
AuxAnimationData.Channels[]
7. Display Lists de N64 y geometría
La geometría se reconstruye interpretando las display lists referenciadas desde el layout. El parser implementa el subconjunto necesario para los modelos Pokémon, incluyendo:
Código:
display lists anidadas
carga de vértices
emisión de triángulos
estado de render
estado de tiles
7.1. Formato de vértice
Para el formato observado, cada vértice ocupa:
Código:
0x10 bytes
Código:
+0x00 : s16 position X
+0x02 : s16 position Y
+0x04 : s16 position Z
+0x08 : s16 texture S
+0x0A : s16 texture T
+0x0C..0x0F :
normal + alpha
o
RGBA
Código:
S / 32.0
T / 32.0
Con lighting activo:
Código:
s8 nx
s8 ny
s8 nz
u8 alpha
Código:
u8 red
u8 green
u8 blue
u8 alpha
7.2. Tile state
El estado equivalente a `G_SETTILE` permite recuperar:
Código:
palette index
mirror S
mirror T
clamp S
clamp T
Una textura que visualmente parece estar mal decodificada puede en realidad tener una decodificación correcta y estar utilizando incorrectamente mirror/clamp. Si el motor de destino no admite directamente algún modo de wrap, puede ser necesario modificar/bakear la textura.
Eso es una solución del exporter y no forma parte del formato de Stadium.
8. Estructura de las animaciones esqueléticas
La cabecera de una animación esquelética se interpreta como:
Código:
+0x00 : u8 flags
+0x06 : u16 loopStart
+0x08 : u16 channelCount
+0x0A : u16 frameCount
+0x0C : ptr channel descriptor table
+0x10 : ptr scale data
+0x14 : ptr rotation data
+0x18 : ptr translation data
Código:
0x0A bytes
Código:
+0x00 : u8 número de valores/keys de scale
+0x01 : u8 número de valores/keys de rotation
+0x02 : u8 número de valores/keys de translation
+0x03 : u8 interpolation flags
+0x04 : u16 scale offset/index
+0x06 : u16 rotation offset/index
+0x08 : u16 translation offset/index
8.1. Tres descriptores por canal lógico
El `Channel` de un bone se convierte en:
Código:
baseIndex = Bone.Channel * 3
Código:
baseIndex + 0 -> X
baseIndex + 1 -> Y
baseIndex + 2 -> Z
Código:
Scale
Rotation
Translation
La asociación completa es:
Código:
Bone
|
+-- Bone.Channel
|
+-- Channel * 3
|
+-- descriptor X
+-- descriptor Y
+-- descriptor Z
Código:
bone index -> track index
Código:
Bone.Channel -> grupo de tres descriptores
9. Modos de sampling de animación
Los flags de la animación seleccionan al menos dos familias de encoding en el decoder actual.
9.1. Modo Hermite / keyframes
Cuando:
Código:
flags & 0x08
Una key contiene:
Código:
s16 frame
s16 value
s16 inTangent
[s16 outTangent]
El mapping utilizado es:
Código:
interpolation & 0x01
translation wide key
interpolation & 0x02
rotation wide key
interpolation & 0x04
scale wide key
Conversiones observadas:
Código:
rotation value / 10.0
-> grados
scale value / 100.0
9.2. Modo packed / sampled
Sin el flag Hermite, un componente puede ser:
Código:
ausente
-> conservar componente de bind pose
constante
-> valor almacenado directamente
en el descriptor
sampled
-> valores leídos de los arrays
de datos del componente
Código:
translation
12-bit o 16-bit según flags
rotation
samples packed de 12 bits
promovidos a unidades angulares de 16 bits
scale
s16 / 1000
Si un componente no está presente:
Código:
fallback correcto = bind pose
fallback incorrecto = 0
10. Representación de las rotaciones
Los bones y tracks decodificados conservan las rotaciones utilizando una unidad basada en una vuelta de 16 bits:
Código:
65536 unidades = 360 grados
Errores pequeños en esta parte pueden provocar:
Código:
jitter
saltos al cruzar el wrap angular
rotaciones por el camino largo
11. Animaciones auxiliares / de materiales
Stadium 1 contiene una lista independiente de animaciones auxiliares en el root del FRAGMENT.
La cabecera parseada es:
Código:
+0x00 : u8 flags
+0x06 : u16 loopStart
+0x08 : u16 channelCount
+0x0A : u16 frameCount
+0x0C : ptr channel table
+0x10 : ptr data
Código:
+0x00 : u16 count
+0x02 : u16 baseIndex
Para cada frame:
Código:
value =
data[
baseIndex
+ min(frame, count - 1)
]
Código:
count == 0
Código:
-1
Código:
ojos
bocas
otras variaciones de material/textura
12. Asociación entre animaciones esqueléticas y de materiales
Aunque las dos listas de animaciones se encuentran dentro del FRAGMENT del modelo, la asociación preferente:
Código:
animación esquelética
<->
animación auxiliar/material
Las localizaciones fijas utilizadas para la ROM USA son:
Código:
Main ROM code/data base : 0x00001000
Main VRAM base : 0x80000400
Pointer table VRAM : 0x80075BD0
Battle data base : 0x0070D3A0
Para especies:
Código:
1 .. 151
Código:
pointerOffset =
MainRomOffset
+ (PointerTableVram - MainVram)
+ (species - 1) * 4
Código:
relative = pointer & 0x00FFFFFF
Código:
battleTable =
0x0070D3A0 + relative
Código:
stride/size = 0xB90
entry size = 0x10
Código:
entry +0x00
-> skeletal animation index
entry +0x01
-> auxiliary animation index
0xFF
-> sin animación auxiliar
Código:
Datos de Batalla
|
+-- entrada
|
+-- animation index
|
+-- auxiliary animation index
Código:
(animationIndex, auxIndex)
Hay que distinguir aquí dos cosas:
El FRAGMENT indica qué animaciones existen.
Los datos de Batalla indican qué animaciones de material se utilizan junto a determinadas animaciones esqueléticas en contextos de Batalla.
Elegir únicamente la pareja más frecuente es una heurística del importer. Una reimplementación más estricta del juego podría conservar toda la tabla de Batalla en lugar de reducirla a una única `AuxAnimation` preferida.
13. Estrategias para exportar las animaciones de materiales
Los datos reconstruidos pueden representarse de distintas formas en el motor de destino.
El importer de Unity utilizado durante esta investigación permite dos estrategias.
13.1. Animation Curves
Se pueden generar object-reference keys dentro del AnimationClip que sustituyan materiales/texturas en los frames correspondientes. Esto representa de forma bastante directa el `AuxAnimationData` decodificado.
13.2. Callbacks / Animation Events
Otra posibilidad es generar eventos de animación y utilizar un componente runtime que seleccione la textura/material correspondiente de un conjunto previamente generado. Esto puede resultar útil en motores antiguos o plataformas con restricciones adicionales. Ambos métodos son decisiones del exporter/runtime. No representan dos formatos diferentes dentro de Pokémon Stadium.
14. Pokémon Shiny
La exportación de Pokémon Shiny debe considerarse una capa adicional sobre el parser principal del modelo.
El FRAGMENT proporciona los recursos normales de:
Código:
texturas
TLUT
geometría
skeleton
animaciones
La distinción importante es:
Código:
Parsing del FRAGMENT
= ingeniería inversa del formato del modelo
Reconstrucción Shiny
= interpretación de color/LUT
aplicada sobre el modelo ya parseado
El exporter puede generar:
Código:
Shiny textures
Shiny materials
Shiny prefab
Código:
geometría
skeleton
animaciones
15. Variantes de color HUE / Entrenador
Las variantes de color dependientes del entrenador son también una capa adicional sobre el modelo normal.
El importer utilizado permite:
Código:
HUE obtenido de datos del entrenador
HUE aleatorio
HUE manual
Código:
geometría
skeleton
timing de las animaciones esqueléticas
Código:
texturas
materiales
15.1. Animaciones de materiales en las variantes
Al exportar una variante hay un detalle importante.
No basta con:
Código:
1. crear prefab variante
2. reemplazar materiales estáticos
En ese caso el Pokémon puede verse correctamente hasta que se reproduzca una animación de ojos, boca u otro cambio de material.
Entonces reaparecería la textura normal.
El orden correcto en el motor de destino es:
Código:
1. Crear/copiar texturas y materiales de la variante.
2. Clonar/remapear los clips de animación
de materiales o los texture-swap sets.
3. Hacer que esas referencias apunten
a los materiales/texturas de la variante.
4. Asignar los materiales estáticos
de la variante a los renderers.
5. Guardar el prefab variante.
16. Arquitectura recomendada para un parser
Una implementación puede separarse en:
Código:
RomReader
|
+-- normalizar byte order de N64
+-- localizar archivo en la revisión conocida
+-- extraer entradas
ArchiveDecoder
|
+-- raw
+-- PERS-SZP
+-- Yay0
FragmentReader
|
+-- virtual pointer -> local pointer
+-- localizar root mediante bootstrap MIPS
+-- lecturas endian
FragmentParser
|
+-- metadata del root
+-- layout command walker
+-- skeleton
+-- registros texture/TLUT
+-- display lists de N64
+-- animaciones esqueléticas
+-- animaciones auxiliares
BattleDataParser
|
+-- uso/asociación:
skeletal animation
<->
auxiliary animation
EngineExporter
|
+-- meshes
+-- skeleton
+-- textures/materials
+-- skeletal clips
+-- material animation
+-- Shiny assets
+-- variantes HUE
Código:
dato original
interpretación del formato
decisión de exportación
17. Errores comunes durante la reconstrucción
17.1. Asociar las animaciones mediante el índice del bone
Incorrecto:
Código:
bone 0 -> animation channel 0
bone 1 -> animation channel 1
...
Código:
bone
-> Bone.Channel
-> channel descriptors
Código:
Channel < 0
17.2. Suponer un offset fijo para el root del FRAGMENT
El parser funcional obtiene el root desde el bootstrap MIPS del propio módulo.
17.3. Ignorar la bind pose como fallback
Los componentes ausentes conservan su valor de bind pose. Utilizar cero como fallback corrompe la pose.
17.4. Decodificar CI sin el estado de la display list de TLUT
La display list del registro TLUT puede resolver o modificar la información de la paleta.
17.5. Ignorar mirror/clamp
Una textura puede estar correctamente decodificada y aun así visualizarse mal si se ignora el tile state.
17.6. Tratar material animation como skeletal animation
Son estructuras distintas:
Código:
AnimationData
-> Scale / Rotation / Translation de bones
AuxAnimationData
-> selección de material/textura
17.7. Cambiar los materiales de una variante sin cambiar sus referencias animadas
Esto puede hacer que aparezcan ojos/bocas normales sobre un Pokémon cuya variante HUE o Shiny está correctamente aplicada.
17.8. Suponer que todas las entradas están sin comprimir
Hay que comprobar:
Código:
PERS-SZP
Yay0
18. Qué está sólidamente establecido por la implementación
El importer funcional proporciona evidencia de trabajo para:
Código:
Archivo de modelos Pokémon:
ROM 0x00920000
Número de entradas:
archive +0x0C
Stride de descriptor:
0x10
Descompresión:
PERS-SZP / Yay0
Base virtual FRAGMENT:
0x8FF00000
Root:
obtenido mediante código MIPS
Root:
Species ID
model/layout pointer list
skeletal animation pointer list
auxiliary animation pointer list
Skeleton:
command stream
Bone.Channel explícito
Texturas:
texture tables
TLUT tables
palette state
mirror/clamp
Geometría:
display lists de N64
Animaciones:
Scale / Rotation / Translation
Hermite
packed/sample
Material animation:
auxiliary channels
Datos de Batalla:
asociación skeletal/Aux
Exportación:
normal
Shiny
HUE variants
19. Aspectos que todavía deben tratarse con cautela
El importer actual es suficiente para reconstruir los modelos, pero no todos los bytes del formato tienen un significado semántico confirmado.
En particular:
- Los campos no utilizados de los descriptores de archivo de `0x10` bytes no están documentados aquí.
- Muchos IDs de comandos de layout solo se implementan hasta el nivel necesario para reconstruir los modelos utilizados.
- No es necesario interpretar todos los opcodes posibles de las display lists de N64.
- Varios bits de flags de animación se conocen por su comportamiento operativo, pero no necesariamente por sus nombres simbólicos originales.
- Reducir las referencias de Batalla a una única `AuxAnimation` preferida es una decisión del importer.
- La reconstrucción Shiny/HUE incluye conocimiento de comportamiento/color adicional al parser puro del FRAGMENT.
- Los offsets fijos de ROM están confirmados para la revisión USA utilizada y no deben asumirse universales.
Para un trabajo orientado a decompilación es preferible conservar los campos desconocidos y los registros raw, aunque un exporter de assets concreto no los necesite.
20. Stadium 1 frente a Stadium 2: diferencia principal
Stadium 1 resultó especialmente útil como referencia al estudiar Stadium 2 porque buena parte del modelo conceptual se mantiene:
Código:
FRAGMENT
Bone.Channel
descriptores S/R/T
canales Aux/material
estado de materiales/display lists de N64
Código:
Stadium 1:
model FRAGMENT
-> lista de animaciones esqueléticas
accesible directamente
Stadium 2:
entrada N del archivo de modelos de Batalla
+
entrada N del archivo separado
de animaciones de Batalla
->
animation bank
21. Pseudocódigo mínimo
Una importación simplificada de un Pokémon de Stadium 1 sería:
Código:
rom = normalize_n64_rom(file);
archive = rom + 0x00920000;
count = be32(archive + 0x0C);
entry = archive + 0x10 + index * 0x10;
payload =
archive + be32(entry + 0x00);
size =
be32(entry + 0x04);
fragmentBytes =
decompress_if_needed(payload, size);
fragment =
FragmentReader(
fragmentBytes,
0x8FF00000);
root =
find_root_from_mips_bootstrap(fragment);
model.species =
be16(root + 0x00);
layouts =
read_ptr_list(
ptr(root + 0x08));
animations =
read_ptr_list(
ptr(root + 0x0C));
auxAnims =
read_ptr_list(
ptr(root + 0x10));
walk_layout(
layouts[0],
model);
for each animation:
for each bone:
if bone.channel >= 0:
sample animation channel
bone.channel
into S/R/T track;
for each aux animation:
decode byte-valued
material channels;
read battle table
for model.species;
associate skeletal animation indices
with auxiliary animation indices;
22. Base utilizada para este write-up
La intención es documentar las estructuras y comportamientos soportados por esa implementación sin inventar nombres para campos cuyo significado todavía no está confirmado. Los descubrimientos más reutilizables para ingeniería inversa son:
Código:
estructura root del FRAGMENT
binding mediante Bone.Channel
decodificación de los
descriptores de animación
representación de las animaciones
auxiliares/de materiales
asociación independiente mediante
los datos de Batalla
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:

24. 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.
25. Créditos
La investigación se realizó de forma independiente durante el desarrollo de Stadium2Unity, comparando con la implementación de DramaticShape para Pokemon Stadium 1 para PokemonGen1Recomp y con la decompilación "pret/pokestadium". 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:
