Registrarse

[N64] Ingeniería inversa de los modelos de Hey, You Pikachu! de Nintendo 64

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 Hey, You Pikachu! (N64) de cómo he hecho mis tools para extraer los modelos 3D directamente de la propia ROM original USA (1.0). Este post separa las observaciones confirmadas de las hipótesis de trabajo y basado un poco en otras investigaciones de esta misma plataforma, cómo he llegado a poder parsearlos. Dicho esto, mencionar que hasta ahora NO hay ninguna tool existente capaz de parsear esto. En este write-up se intenta describir cómo lo organiza todo el juego en datos de la ROM y en runtime, separando lo que está bastante validado de lo que todavía es provisional.

Bueno, ¡al lío!

1. Resumen

Aunque Hey You, Pikachu! comparte hardware con otros títulos de Nintendo 64 que ya he analizado como Pokémon Snap o Pokémon Stadium, su sistema de modelos 3D no debe tratarse como una variante directa de los formatos utilizados por estos juegos. Comparte conceptos propios de la plataforma (display lists F3DEX2, caché de vértices del RSP, matrices, texturas y TLUTs), pero la organización de los recursos, la jerarquía de los modelos, el sistema de animación y la asociación entre geometría y nodos siguen estructuras propias.

En particular, un "actor" (entidad 3D) de Hey You, Pikachu! no se reconstruye simplemente leyendo una malla y un esqueleto almacenados juntos. Su representación se obtiene combinando tablas y recursos nativos: un registro de modelo, recursos comprimidos y relocados, una jerarquía de nodos, display lists que seleccionan matrices durante la ejecución, bancos de animación independientes y, en determinados personajes, un sistema externo para texturas faciales.

Por ello, aunque conocer otros juegos de N64 resulta útil para reconocer componentes comunes del hardware y de F3DEX2, la estructura descrita a continuación corresponde específicamente al pipeline nativo de Hey You, Pikachu!.

Bien, vamos con ello. Comenzando con la representación 3D de un actor, ésta no parece ser un único “archivo de modelo” autocontenido. El esquema útil es:
Código:
entrada de la tabla de modelos
|
+-- descriptor de recurso
|   |
|   +-- recurso de modelo comprimido
|   |
|   +-- metadata de relocation/fixups
|
+-- registro runtime del modelo
|   |
|   +-- puntero a la jerarquía
|   |
|   +-- puntero al selector de texturas faciales
|
+-- jerarquía
|   |
|   +-- escala / rotación / traslación local
|   |
|   +-- child / sibling
|   |
|   +-- node type
|   |
|   +-- display list
|
+-- display lists tipo F3DEX2
|   |
|   +-- carga de vértices
|   |
|   +-- selección de matrices
|   |
|   +-- triángulos
|   |
|   +-- texturas / TLUT
|   |
|   +-- estado de iluminación/render
|
+-- bancos de animación
|   |
|   +-- tracks identificados por node type
|   |
|   +-- escalares constantes / lineales / cúbicos
|
+-- bancos externos de texturas
    |
    +-- ojos / boca / expresiones
    |
    +-- pequeños programas selectores
El punto más importante es que la geometría funciona mediante una paleta de matrices. Las display lists seleccionan matrices del segmento
Código:
0x07
, cargan vértices en la caché de 32 slots del RSP y cada vértice queda asociado a la matriz que estaba activa cuando se procesó.

Por tanto, el esqueleto no se deduce de “trozos” de la malla (mesh): sale de la jerarquía nativa del juego y las display lists indican qué matriz de esa jerarquía transforma cada vértice.

2. Tabla de modelos

La tabla de modelos que podemos reconstruir empieza en:
Código:
 ROM 0x00088C18 stride 0x54
Es importante no confundir:
Código:
 model index resource descriptor ID identidad semántica del personaje/prop
No son necesariamente el mismo índice.

3. Tabla de descriptores de recursos

La tabla confirmada de descriptores de recursos de modelos empieza en:
Código:
 ROM 0x00079EA0 stride 0x24
El layout que tenemos actualmente es:

Código:
struct ModelResourceDescriptor
{
    u32 unknown00;       // +0x00
    u32 compressionType; // +0x04
    u32 romStart;        // +0x08
    u32 romEnd; // +0x0C
    u32 field10; // +0x10
    u32 field14; // +0x14
    u32 field18; // +0x18
    u32 field1C; // +0x1C
    u32 field20; // +0x20
}; // 0x24
+0x04 selecciona el camino RAW o comprimido y +0x08/+0x0C son los límites dentro de la ROM utilizados por el loader.

Nota: Hay todavía campos que no tengo claro que son...

4. Recursos comprimidos

Un bloque comprimido comienza con:
Código:
 +0x00 packed size +0x04 unpacked size +0x08 compressed stream
El payload de los modelos que estamos decodificando encaja con una compresión de la familia LH5. Pero descomprimir no basta. El juego aplica después una serie de fixups/relocations al buffer de salida.

El flujo nativo es conceptualmente:
Código:
 ROM comprimida -> decompression -> fixups -> recurso relocatado
utilizable
Funciones relevantes localizadas:
Código:
 0x800380F4 carga RAW 0x800381D4 carga comprimida 0x80037E14
resource request helper 0x8002D010 wrapper decompression/fixups
0x80049F74 candidato más fuerte al núcleo del codec
5. Jerarquía del modelo

El registro de modelo contiene en
Código:
+0x1C
un puntero a la raíz de la jerarquía.

El nodo que estamos reconstruyendo ocupa
Código:
0x38
bytes:
Código:
 struct ModelNode { float scaleX; // +0x00 float scaleY; // +0x04
float scaleZ; // +0x08

    float rotationX;     // +0x0C
    float rotationY;     // +0x10
    float rotationZ;     // +0x14

    float translationX;  // +0x18
    float translationY;  // +0x1C
    float translationZ;  // +0x20

    u32 unknown24;       // +0x24
    u32 sibling;         // +0x28
    u32 child;           // +0x2C
    u32 displayList;     // +0x30
    u32 type;            // +0x34

}; // 0x38
La relación child/sibling permite recorrer el árbol de forma depth-first. El campo
Código:
+0x34
contiene un node type muy importante porque el sistema de animaciones utiliza el mismo identificador.

Un caso especial observado es:
Código:
 type 100
que en la reconstrucción de matrices se comporta como un nodo de sólo traslación: sin rotación y con escala identidad.

6. Transformaciones nativas

Los nodos guardan S/R/T como floats big-endian.

La rotación del juego utiliza su propia ruta de seno:
Código:
 constante angular: ROM 0x000A7A98 tabla de seno: ROM 0x0009D804
Esto permite reproducir la matriz del motor sin depender de adivinar un Euler order usando las funciones matemáticas del host.

Las matrices locales se acumulan siguiendo la jerarquía y terminan formando la paleta de matrices utilizada por las display lists.

7. Paleta de matrices

Las display lists hacen referencia a matrices mediante el segmento
Código:
0x07
.

Los targets observados están alineados a:
Código:
 0x40 bytes
por lo que:
Código:
 matrixSlot = segment07Offset / 0x40
Durante la ejecución hay que mantener:
Código:
 active matrix active matrix slot matrix stack vertex cache
La conclusión importante es que los modelos funcionan como rigid matrix-palette skinning por vértice.

Cada vértice que hemos podido reconstruir queda ligado a una sola matriz/hueso con peso 1.0. No hay evidencia por ahora de un skinning clásico de cuatro pesos en este path de personajes.

8. Display lists F3DEX2

Entre los comandos importantes observados están:
Código:
 0x01 vertex load
0xDA matrix
0xDE display-list call
0xDF end display list
0xFD set texture image
Hay además comandos de triángulos, geometría, tiles, TLUT, combiner/render mode, etc.

Un detalle crítico: una llamada
Código:
G_DL
no crea una caché de vértices nueva.

Las display lists anidadas comparten el estado del RSP, incluida la caché de 32 vértices. Si se resetea al entrar en una sub-display-list se pierden triángulos válidos.

9. Formato de vértice

El footprint encaja con el Vtx clásico de 16 bytes:
Código:
struct NativeVertex
{
    s16 x; // +0x00
    s16 y; // +0x02
    s16 z; // +0x04 // +0x06 auxiliar/flag
    s16 s; // +0x08
    s16 t; // +0x0A
    s8 nx_or_r; // +0x0C
    s8 ny_or_g; // +0x0D
    s8 nz_or_b; // +0x0E
    u8 a_or_aux; // +0x0F
};
Las posiciones son signed 16-bit.

Las coordenadas S/T son fixed-point y terminan usando escala
Código:
/32
, pero el UV final también depende del estado de textura. Los últimos cuatro bytes no deben interpretarse siempre como RGBA. Con lighting activo, los tres primeros pueden ser normales signed.

10. UVs[/b]

No siempre basta con:
Código:
 u = s / 32 v = t / 32
El juego mantiene estado adicional de transformación de coordenadas y después aplica parámetros del tile:
Código:
 shift S/T tile origin texture width/height
Por eso un extractor que lea sólo S/T puede obtener una malla correcta con las texturas desplazadas o escaladas incorrectamente.

11. Materiales

El “material” no está necesariamente guardado como un objeto independiente equivalente a un material "moderno".

Se reconstruye a partir del estado activo al dibujar:
Código:
 texture image texture format / size TLUT tile state geometry mode
lighting culling combiner alpha/render mode facial texture slot
Los triángulos con el mismo estado efectivo pueden agruparse en el mismo material.

12. Normales, vertex colors y alpha

Los bytes
Código:
+0x0C..+0x0F
del vértice dependen del geometry mode.

Con iluminación:
Código:
 RGB-like bytes -> normal signed XYZ
Sin iluminación pueden representar color de vértice.

Por eso hay que interpretar el vértice junto con el estado del display list.

Dos errores fáciles son:
Código:
 normal signed -> interpretada como RGB alpha/aux -> interpretado
siempre como transparencia
Ambos pueden producir resultados visualmente muy engañosos.

13. Texturas faciales externas

La animación facial no forma parte simplemente del skeletal animation.

El registro de modelo puede tener en:
Código:
 model record +0x24
un puntero a slots de texturas externas.

Cada slot es:
Código:
 struct FacialSlot { s32 bank; s32 selector; }; // 8 bytes
El selector indexa una tabla en:
Código:
 ROM 0x0006BC90
14. Programa selector facial

Cada instrucción ocupa
Código:
0x0C
:

Código:
struct FacialSelectorInstruction
{
    s32 op;
    float duration;
    s16 first;
    s16 last;
};
Comportamientos observados:
Código:
 op 0 mantener/seleccionar frame op 1 recorrer un rango op 2 paso
inmediato/especial op 3 salto de control cuando duration < 0
Así que los ojos/boca no son sólo “una lista de texturas”: hay un pequeño programa nativo que decide qué frame utilizar.

15. Bancos de imágenes faciales

Metadata localizada en:
Código:
 bank pointers: ROM 0x0006C120 bank counts: ROM 0x0006C14C image
base: ROM 0x00D69490
Descriptor:

Código:
struct FacialImage {
    u32 imageOffset;
    u32 paletteOffset;
    u16 width;
    u16 height;
    u8 size;
    u8 format;
    // resto todavía sin nombrar completamente
}; // 0x10
La imagen y la paleta se resuelven respecto a
Código:
0x00D69490
.

16. Bancos de animación

El sistema de skeletal animation que tenemos localizado usa:
Código:
 animation ROM base: 0x00B77EB0 bank count conocido: 230
Tablas:
Código:
 0x00078378 inicio/fin del banco 0x00077C48 posición de la lista
de tracks 0x00077FE0 duración
Para el banco N:
Código:
 start = 0xB77EB0 + BE32(0x78378 + N4) end = 0xB77EB0 +
BE32(0x78378 + (N+1)4)

trackList = 0xB77EB0 + BE32(0x77C48 + N*4)

duration = BEFloat(0x77FE0 + N*4)
17. Tracks de animación

Cada track:
Código:
 struct AnimationTrackEntry { s32 nodeType; u32 vectorOffset; };
// 8 bytes
La lista termina cuando:
Código:
 nodeType <= 0
Y aquí está una de las claves:
Código:
 animation nodeType <-> hierarchy node +0x34
Las animaciones se deben asociar por node type, no por índice de hueso.

18. Vectores de animación

Según el node type:
Código:
 nodeType < 38 -> 6 escalares nodeType >= 38 -> 3 escalares
Cada escalar:
Código:
 struct AnimationScalar { u32 kind; u32 value; }; // 8 bytes
Encodings observados:
Código:
 kind 0 constante kind 1 función cúbica kind 2 función lineal
Para kind 0:
Código:
 constant = (s32)value / 65536.0
19. Stream de escalares

Los escalares animados usan registros de
Código:
0x10
bytes:
Código:
 struct AnimatedScalarRecord { float a; float b; float c; s16
base; u8 fraction; u8 ticks; };
Valor base:
Código:
 base + fraction / 256.0
Duración:
Código:
 ticks * nativeTimeQuantum
El quantum temporal sale de:
Código:
 ROM 0x000A1158
Con
Código:
ticks == 255
se obtiene un valor terminal/hold.

Para kind 2:
Código:
 value(t) = a*t + b
Para kind 1:
Código:
 value(t) = ((at + b)t + c)*t + baseValue
Es decir: el juego guarda funciones por tramos, no necesariamente una key explícita por frame.

20. Compatibilidad modelo/animation bank

No todos los bancos deben aplicarse a todos los modelos.

Una comprobación nativa útil es que el banco tenga un track raíz cuyo node type coincida con el root type de la jerarquía.

Actualmente:
Código:
 0 < nodeType < 38 nodeType == modelRootType
La lógica completa que decide qué acción/game state selecciona cada banco todavía está por reconstruir.

21. Base pose

La jerarquía contiene la pose de referencia:
Código:
 scale rotation translation
Si un nodo no tiene track de animación, debe conservar esos valores.

Poner componentes ausentes a cero destruye la pose nativa.

22. Flujo de reconstrucción
Código:
1. Resolver model record en 0x88C18 + index*0x54. 
2. Resolver resource descriptor. 
3. Leer descriptor de 0x79EA0. 
4. Elegir RAW o compressed. 
5. Si está comprimido: - leer packed/unpacked size - descomprimir LH5 - aplicar fixups 
6. Resolver hierarchy root desde modelRecord+0x1C. 
7. Recorrer nodos de 0x38 bytes. 
8. Construir matrices nativas. 
9. Construir palette segment 0x07. 
10. Ejecutar display lists conservando estado RSP. 
11. Para cada triángulo: - resolver vértices cacheados - aplicar su matrix slot - calcular UVs - interpretar normal/color según geometry mode - guardar el draw state
12. Resolver facial slots desde modelRecord+0x24. 
13. Resolver bancos de animación compatibles. 
14. Asociar tracks por node type. 
15. Samplear escalares constantes/lineales/cúbicos.
23. Fallos típicos


  • Confundir model index con resource ID
  • Desplaza nombres/identidades y puede hacer parecer que faltan modelos que en realidad siguen en ROM.
  • Resetear la vertex cache en cada G_DL
  • Rompe referencias válidas entre display lists anidadas.
  • Asignar una display list completa a un único hueso
  • Incorrecto: el propio command stream cambia la matriz activa.
  • Aplicar dos veces la jerarquía
  • La matrix palette ya contiene la transformación acumulada esperada por el path de geometría.
  • Asociar animaciones por bone index
  • El contrato nativo es node type.
  • Interpretar siempre +0x0C..+0x0F como RGBA
  • Con lighting pueden ser normales.
  • Buscar todas las caras dentro del model resource
  • Ojos/boca pueden venir de bancos externos.

24. Confirmado / fuertemente validado
  • Model table: 0x00088C18, stride 0x54.
  • Resource descriptors: 0x00079EA0 stride +0x24
  • +0x04 distingue RAW/compressed.
  • +0x08/+0x0C contienen los límites dentro de la ROM del recurso.
  • Los modelos comprimidos requieren decompression + fixups.
  • La jerarquía se referencia desde model record +0x1C.
  • Los nodos usados actualmente tienen tamaño 0x38.
  • +0x34 contiene el node type usado también por animación.
  • Segmento 0x07 contiene referencias a matrices alineadas a 0x40.
  • Las display lists anidadas comparten la vertex cache del RSP.
  • El path actual se comporta como rigid matrix-palette skinning por vértice.
  • Los materiales salen del draw state.
  • Las texturas faciales externas se referencian desde model record +0x24.
  • Los selectores faciales usan instrucciones de 0x0C.
  • Los bancos de animación conocidos parten de 0x00B77EB0.
  • Hay 230 bancos en las tablas actualmente identificadas.
  • Los tracks se asocian por native node type.
  • Los escalares soportan constante, lineal y cúbico.

25. Todavía provisional/incompleto

  • Semántica completa de todos los campos del model record.
  • Semántica completa de todos los campos del resource descriptor.
  • Regla runtime exacta que selecciona cada animation bank.
  • Root motion completo.
  • Sincronización completa skeletal/facial/eventos.
  • Todos los modos de combiner/render usados por el juego.
  • Interpretación normal/color/alpha para todos los geometry modes.
  • Semántica de todos los node types.
  • Mapeo completo de characters, props y scenes.
  • Path nativo completo de environments/scenes.
  • Comportamientos dinámicos implementados por código MIPS.


26. Imagen general actual
Código:
ROM model table
|
+-- model/runtime record
| |
| +-- hierarchy root
| |
| +-- facial selector
|
+-- resource descriptor
|
+-- compressed model
|
+-- LH5 decompression
|
+-- fixups
|
+-- F3DEX2 display lists
|
+-- segment 07 matrix palette
|
+-- 32-slot vertex cache
|
+-- texture/TLUT/render state
|
+-- triangles

hierarchy
|
+-- native node types
|
+-- accumulated matrices
|
+-- rigid vertex ownership

animation tables
|
+-- animation bank
|
+-- node-type tracks
|
+-- scalar descriptors
|
+-- constant
|
+-- linear
|
+-- cubic

facial system
|
+-- selector program
|
+-- external image bank
|
+-- texture
|
+-- palette
Con lo que hay ahora ya se puede explicar de forma bastante coherente la relación entre carga de recursos, jerarquía, geometría por matrix palette, skeletal animation y texturas faciales, dejando separados los detalles que todavía no están demostrados. E

27. Resultado

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

1793224627106.png


Queda claro que aún hay mucho trabajo por hacer en la investigación de este juego pero es una primera aproximación para poder importar los modelos 3D de este juego en motores de render y de videojuegos modernos.[/ICODE]
 
Última edición:
Arriba