Registrarse

[3DS] Ingeniería inversa de los modelos de Pokémon Ultra Sol/Ultra Luna - 3DS2Unity

Manurocker95

Doctorando en Ingeniería Biomédica & Game Dev
Miembro insignia
Buenas, os traigo otro write-up de ingeniería inversa, esta vez sobre los modelos de Pokémon Ultra Sol y Ultra Luna para Nintendo 3DS. Como en el caso de Pokédex 3D Pro, la investigación parte de un importador para Unity, Pokemon3DStoUnity. Este análisis se apoya en el código y en los trabajos previos de GDKChan y compañía en sus aplicaciones SPICA y Ohana3DS. Por lo tanto, el mérito real es suyo y se les reconoce todo el mérito por su trabajo previo.

1. Estructura general

A diferencia de los .z5 de Pokédex 3D Pro, aquí tenemos recursos que Game Freak empaquetó mediante su biblioteca gráfica GameFreakLibrary 2 (GFLib2) en archivos .bin. Estos utilizan estructuras propias de Game Freak, relacionadas con el sistema gráfico de Nintendo 3DS y con formatos como H3D/BCH (Binary CTR H3D), usados para representar modelos 3D, esqueletos, materiales, texturas y animaciones.

En Pokémon Ultra Sol y Ultra Luna, los recursos de cada Pokémon se distribuyen en un conjunto de nueve archivos .bin, organizados de la siguiente manera:

Código:
0.bin -> Models (High Poly / Low Poly)
1.bin -> Regular Textures
2.bin -> Shiny Textures
3.bin -> Grayscale / Emissive Textures
4.bin -> Battle Animations
5.bin -> Overworld / Pet Animations
6.bin -> Idle / Walk / Run Animations
7.bin -> Mouth Animations
8.bin -> Empty / Extra Data
Estos números representan el orden de los archivos dentro del conjunto, independientemente de sus nombres reales.

Cabecera y offsets del contenedor

El importador utiliza GFPackageExtensions para interpretar los paquetes. Su cabecera comienza con dos caracteres ASCII que identifican el tipo de paquete, seguidos de un contador de entradas y una tabla de offsets.

Código:
0x00  char[2]  Identificador del paquete
0x02  u16      Número de entradas
0x04  u32[]    Tabla de offsets
Cada entrada de la tabla contiene un offset de inicio. El final de una entrada se obtiene del siguiente offset, de manera que:

Código:
entryStart = offset[i]
entryEnd = offset[i + 1]
entryLength = entryEnd - entryStart
La tabla contiene, por tanto, los límites de los bloques almacenados en el paquete.

En el código de GFPackageExtensions, el offset de una entrada se calcula como:

Código:
entryAddress = packageStart + offset[i]
Esto permite acceder a cada recurso sin asumir que tenga una posición fija dentro del archivo.

Aclaración sobre los offsets del paquete

Hay un matiz importante en GFPackageExtensions: la tabla empieza en el offset 0x04 del paquete, pero el código calcula las direcciones de las entradas con la base del INICIO del paquete. Es decir, cuando el paquete comienza en packageStart:

Código:
entryAddress = packageStart + offset[i]
entryLength  = offset[i+1] - offset[i]
El parser obtiene cada par leyendo dos u32 consecutivos de la tabla, avanzando cuatro bytes por entrada. Esto no debe confundirse con una tabla de pares independientes de ocho bytes por entrada.

Identificación de los bloques

Una vez localizada cada entrada, el importador comprueba su firma binaria para determinar cómo debe interpretarla:

Código:
0x15122117 -> Modelo GF
0x15041213 -> Textura GF
0x00060000 -> Animación GF
0x00484342 -> BCH
Los valores se interpretan como enteros de 32 bits en little-endian.

Estas firmas no tienen por qué aparecer en el offset 0 del archivo .bin, ya que normalmente corresponden al comienzo de los bloques internos.

Ejemplo: organización del archivo de modelos (0.bin)

Cuando el importador detecta un paquete de modelos, utiliza las primeras entradas para reconstruir las distintas representaciones del Pokémon:

Código:
Entry[0] -> PM_HighPoly
Entry[1] -> PM_LowPoly
Entry[2] -> Shader Package
Entry[3] -> Additional Shaders
Las dos primeras entradas contienen las representaciones de alta y baja resolución. Las siguientes proporcionan los shaders utilizados por sus materiales.

Archivos de texturas (1.bin, 2.bin y 3.bin)

Los tres grupos utilizan el mismo lector de texturas GF, diferenciándose por el tipo de recurso que contienen: texturas normales, variantes shiny y texturas auxiliares como las de escala de grises o emisión.

Cada entrada de textura se identifica mediante la firma:

Código:
0x15041213
Archivos de animaciones (4.bin, 5.bin, 6.bin y 7.bin)

Los archivos de animación contienen entradas identificadas mediante:

Código:
0x00060000
Cada entrada puede contener diferentes secciones de animación, incluyendo transformaciones esqueléticas, materiales y visibilidad.

La cabecera de una animación utiliza la siguiente estructura:

Código:
0x00     u32    Firma de animación
0x04 u32 Número de secciones
0x08 ... Tabla de secciones
Cada descriptor de sección ocupa 12 bytes:

Código:
+0x00 u32 Tipo de sección
+0x04 u32 Longitud
+0x08 u32 Offset relativo
Los tipos reconocidos por el parser son:

Código:
0 -> Animation Header
1 -> Skeletal Animation
3 -> Material Animation
6 -> Visibility Animation
Para acceder a una sección:

Código:
sectionAddress = animationStart
+ sectionOffset
El importador utiliza estos datos para reconstruir las animaciones y convertirlas a estructuras H3D, que posteriormente se transforman en recursos compatibles con Unity.

Por tanto, el binario completo queda de la siguiente forma:

Código:
Archivos .bin
  |
  +-- GFPackageExtensions / paquete PC
       |
       +-- Modelo GF (0x15122117)
       |    +-- PM_HighPoly
       |    +-- PM_LowPoly
       |    +-- Materiales / mallas / esqueleto
       |    +-- Paquetes de shaders
       |
       +-- Texturas GF (0x15041213)
       |
       +-- Animaciones GF (0x00060000)
            +-- Skeletal
            +-- Material / UV
            +-- Visibility
                 |
                 +-- Conversión a H3D
                      |
                      +-- Assets de Unity
2. Modelos HighPoly y LowPoly

Una de las particularidades más interesantes es que el lector de modelos toma dos entradas diferentes del paquete:

Código:
Entry[0] -> PM_HighPoly
Entry[1] -> PM_LowPoly
Entry[2] -> Paquete de shaders
Entry[3] -> Paquete adicional de shaders, si existe
Estos nombres son asignados por el importador. Dentro de GFModel se leen tablas de nombres y hashes para shaders, texturas, materiales y mallas; también se recuperan la bounding box, una matriz de transformación, la jerarquía de huesos y las LUT.

3. Estructura interna del modelo GF

Una vez extraídas las entradas del paquete de modelos (0.bin), el importador utiliza GFModel para interpretar cada representación del Pokémon. El modelo no es una única lista de vértices: agrupa referencias a shaders, texturas, materiales, mallas y huesos.

El lector comienza con una firma de 32 bits, un contador de secciones y un descriptor GFSection. A continuación resuelve tablas de nombres y hashes de recursos, lee los límites del modelo (bounding box), una matriz de transformación, el esqueleto y los registros de materiales y mallas.

Código:
GFModel
  +-- Magic / SectionCount / GFSection
  +-- Shader name/hash table
  +-- Texture name/hash table
  +-- Material name/hash table
  +-- Mesh name/hash table
  +-- Bounding box (min/max)
  +-- Transform matrix
  +-- Skeleton (GFBone[])
  +-- LUTs / Materials (GFMaterial[])
  +-- Meshes (GFMesh[])
Las direcciones de las tablas internas deben interpretarse con la base que utilice cada lector. No conviene confundirlas con los offsets de las entradas del contenedor exterior.

4. Geometría y comandos de la GPU PICA200

Campos identificados en GFMesh / SubMesh


Código:
Campo                  Tipo             Interpretación
-----------------------------------------------------------------
Name / Hash            u32 + string     Identificación de submalla
BoneIndicesCount       u8               Entradas de paleta usadas
BoneIndices            byte[31]         Paleta local de huesos
VerticesCount          i32              Cantidad de vértices
IndicesCount           i32              Cantidad de índices
VerticesLength         i32              Tamaño del buffer de vértices
IndicesLength          i32              Tamaño del buffer de índices
Los campos anteriores reflejan los tipos y el orden lógico interpretados por GFMesh.cs. No se indican offsets absolutos donde el código utiliza secciones y comandos variables.



Cada GFMesh puede contener varias submallas. Estas tienen nombre, paleta local de huesos, número de vértices e índices y tamaños de sus buffers.

Código:
SubMesh:
  BoneIndicesCount : u8
  BoneIndices      : 31 bytes
  VerticesCount    : i32
  IndicesCount     : i32
  VerticesLength   : i32
  IndicesLength    : i32
Una particularidad es que el layout de los vértices no tiene por qué ser fijo. SPICA interpreta comandos de configuración de la GPU PICA200, entre ellos:

Código:
GPUREG_ATTRIBBUFFERS_FORMAT_LOW
GPUREG_ATTRIBBUFFERS_FORMAT_HIGH
GPUREG_ATTRIBBUFFER0_CONFIG1
GPUREG_ATTRIBBUFFER0_CONFIG2
GPUREG_FIXEDATTRIB_*
A partir de esa configuración reconstruye los atributos que correspondan: posición, normal, UV, color e índices/pesos de huesos. Por tanto, no se debe asumir un único stride universal para todos los Pokémon.

5. Esqueleto y skinning

Relación entre índices locales y huesos


Código:
Campo / estructura     Tipo / función       Interpretación
------------------------------------------------------------------
GFBone                 registro             Hueso del esqueleto
Skeleton               GFBone[]             Lista global de huesos
BoneIndicesCount       u8                   Tamaño de paleta usada
BoneIndices            byte[31]             Tabla de índices locales
PICAVertex             atributos            Índices y pesos de vértice
El modelo contiene registros GFBone que describen los huesos y sus relaciones jerárquicas. Las submallas disponen además de una paleta local de índices de huesos. Esto significa que un índice presente en los datos de un vértice puede necesitar traducirse mediante la paleta de su submalla para identificar el hueso global correcto.

Durante la conversión, estos datos se trasladan a las estructuras H3D y después a los componentes de skinning de Unity. Es necesario conservar correctamente la jerarquía, las transformaciones y las matrices de referencia para que las animaciones deformen la malla como en el juego.

6. Texturas y materiales (1.bin, 2.bin y 3.bin)

Campos de GFTexture (orden de lectura)


Código:
Campo             Tipo          Interpretación
-----------------------------------------------------------------
Magic             u32           Firma 0x15041213
HeaderValue       u32           Campo de cabecera leído
GFSection         descriptor    Datos de sección
TextureLength     u32           Longitud de datos de imagen
Reserved          byte[12]      Área omitida por el lector
Name              byte[0x40]    Nombre de textura
Width             u16           Anchura
Height            u16           Altura
Format            u16           Formato GFTextureFormat
MipmapSize        u16           Información de mipmaps
Reserved          byte[16]      Área omitida por el lector
RawBuffer         byte[N]       N = TextureLength
Esta tabla muestra el orden de lectura, no offsets absolutos posteriores a GFSection.

Los paquetes de texturas pueden contener entradas GFTexture, reconocidas mediante la firma 0x15041213. La estructura que interpreta el lector es:

Código:
+0x00  u32       Magic
+0x04  u32       Valor de cabecera / recuento
+0x08  ...       GFSection (tamaño según lector)
        u32       TextureLength
        12 bytes  Reservados
        0x40      Name
        u16       Width
        u16       Height
        u16       Format
        u16       MipmapSize
        16 bytes  Reservados
        N bytes   RawBuffer
Los offsets de los campos posteriores a GFSection dependen del tamaño efectivo de ese descriptor: el listado anterior es un ORDEN DE LECTURA, no una tabla de offsets absolutos verificada contra un .bin original.

El formato de los píxeles se convierte mediante GFTextureFormat y los decodificadores de texturas PICA200. No todas las texturas deben interpretarse como RGBA8 sin consultar su identificador de formato.

Los materiales hacen referencia a texturas y shaders. El conversor también recupera etapas de combinación de texturas (TexEnv) y parámetros de fragment shader, necesarios para aproximar el aspecto original de la 3DS. Las texturas regulares, shiny y auxiliares comparten mecanismos de lectura, aunque cumplen funciones diferentes dentro del conjunto de nueve archivos.

7. Animaciones (4.bin, 5.bin, 6.bin y 7.bin)

Descriptor de sección GFMotion


Código:
Offset   Tipo    Campo           Descripción
-----------------------------------------------------------
+0x00    u32     SectionType     Identificador de sección
+0x04    u32     SectionLength   Longitud de la sección
+0x08    u32     SectionOffset   Offset relativo del bloque
Los bloques GFMotion se reconocen por la firma 0x00060000. Cada bloque comienza con una cabecera seguida de una tabla de secciones de 12 bytes por entrada.

Código:
GFMotion, relativo al inicio del bloque:
+0x00  u32  Magic
+0x04  u32  SectionCount
+0x08  ...  Section[SectionCount]
Section, 12 bytes:
+0x00  u32  SectionType
+0x04  u32  SectionLength
+0x08  u32  SectionOffset
sectionAddress = motionStart + SectionOffset
El lector reconoce las siguientes secciones:

Código:
0 = SubHeader
1 = SkeletalAnim
3 = MaterialAnim
6 = VisibilityAnim
7.1. SubHeader

La primera sección contiene los datos generales de la animación. Los siguientes offsets son relativos al comienzo de ESA SECCIÓN, no al comienzo del GFMotion:

Código:
+0x00  u32     FramesCount
+0x04  u16     LoopFlags (bit 0 = loop)
+0x06  u16     BlendFlags (bit 0 = blended)
+0x08  float3  AnimRegionMin
+0x14  float3  AnimRegionMax
+0x20  u32     AnimHash
7.2. SkeletalAnim

Contiene las curvas de transformación asociadas a los huesos del modelo. El parser GFSkeletonMot interpreta estos datos y los convierte en una animación esquelética H3D utilizando el esqueleto del Pokémon como referencia.

7.3. MaterialAnim / UV

Además del movimiento de los huesos, existe animación de materiales. GFMaterialMot identifica materiales y unidades de textura, mientras que GFMotUVTransform recupera curvas de:

Código:
ScaleX / ScaleY
Rotation
TranslationX / TranslationY
La conversión contempla interpolación Hermite y propiedades de coordenadas UV. Esto puede ser relevante para expresiones y efectos, pero la relación exacta con el parpadeo de los ojos requiere comprobar animaciones originales concretas.

7.4. VisibilityAnim

Existe también una sección independiente para animar la visibilidad de partes del modelo. No hay que confundirla con la animación esquelética ni con el desplazamiento de UV: son tres mecanismos diferentes que pueden coexistir dentro del mismo recurso.

8. Resultados

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

1794573753899.png

9. ¿Qué es H3D y cómo llega a Unity?

En este proyecto, H3D es el conjunto de clases y estructuras gráficas intermedias que utiliza SPICA para representar modelos, mallas, materiales, texturas, huesos y animaciones. Está relacionado con el ecosistema BCH/H3D de Nintendo 3DS, pero NO significa que los archivos originales de Ultra Sol/Ultra Luna sean necesariamente archivos BCH.

Código:
.bin / GFPackage
  -> GFModel / GFTexture / GFMotion
  -> SPICA: H3DModel / H3DMesh / H3DTexture
  -> Unity: Mesh / Material / Texture2D
            SkinnedMeshRenderer / AnimationClip
Esta separación permite decodificar los recursos propios de Game Freak antes de construir objetos Unity. También facilita compartir parte del código de conversión entre formatos diferentes.

10. Front-end

El proyecto contiene un exportador experimental, orientado principalmente a sustituir determinadas texturas y reconstruir el conjunto de archivos conservando el resto de recursos originales. La serialización de geometría modificada no está implementada, por lo que todavía no puede considerarse una exportación bidireccional completa de modelos, esqueletos y animaciones. La idea de este importador es no tener el proceso tedioso de usar un programa externo (SPICA/Ohana3DS) + exportar uno a uno a Unity Engine. Se puede exportar todo en batch simplemente aportando los binarios correspondientes.

En fin, con esto y un bizcocho, ¡hasta mañana a las ocho!
 
Arriba