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:
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.
Cada entrada de la tabla contiene un offset de inicio. El final de una entrada se obtiene del siguiente offset, de manera que:
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:
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:
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:
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:
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:
Archivos de animaciones (4.bin, 5.bin, 6.bin y 7.bin)
Los archivos de animación contienen entradas identificadas mediante:
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:
Cada descriptor de sección ocupa 12 bytes:
Los tipos reconocidos por el parser son:
Para acceder a una sección:
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:
2. Modelos HighPoly y LowPoly
Una de las particularidades más interesantes es que el lector de modelos toma dos entradas diferentes del paquete:
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.
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
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.
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:
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
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)
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:
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
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.
El lector reconoce las siguientes secciones:
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:
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:
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:
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.
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!
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
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
Código:
entryStart = offset[i]
entryEnd = offset[i + 1]
entryLength = entryEnd - entryStart
En el código de GFPackageExtensions, el offset de una entrada se calcula como:
Código:
entryAddress = packageStart + offset[i]
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]
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
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
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
Los archivos de animación contienen entradas identificadas mediante:
Código:
0x00060000
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
Código:
+0x00 u32 Tipo de sección
+0x04 u32 Longitud
+0x08 u32 Offset relativo
Código:
0 -> Animation Header
1 -> Skeletal Animation
3 -> Material Animation
6 -> Visibility Animation
Código:
sectionAddress = animationStart
+ sectionOffset
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
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
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[])
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
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
Código:
GPUREG_ATTRIBBUFFERS_FORMAT_LOW
GPUREG_ATTRIBBUFFERS_FORMAT_HIGH
GPUREG_ATTRIBBUFFER0_CONFIG1
GPUREG_ATTRIBBUFFER0_CONFIG2
GPUREG_FIXEDATTRIB_*
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
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
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
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
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
Código:
0 = SubHeader
1 = SkeletalAnim
3 = MaterialAnim
6 = VisibilityAnim
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
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
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:
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
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!