Buenas, os dejo por aquí otro write up de ingeniería inversa. En este caso, de modelos y animaciones de Pokémon Colosseum y Pokémon XD (GC) que he ido utilizando para crear mi importer para Unity "GameCube2Unity". Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin StarsMmd y todo el que ha contribuído al repositorio del addon para Blender. En este caso NO está limitado a una versión específica ni del motor ni de la ISO, ya que funciona para los archivos de todas las regiones. Se ha usado para esta investigación los archivos de la versión PAL de Pokémon Colosseum. Aunque se hable específicamente de Renderers de Unity Engine, es válido para toda representación visual de mallas 3D.
1. Resumen ejecutivo
Pokémon Colosseum y Pokémon XD empaquetan muchos de sus recursos de modelo dentro de contenedores FSYS. Para los modelos de Pokémon, el modelo conceptual útil no es "FSYS = un modelo", sino:
El resultado de implementación más importante es que el binding de las animaciones debe seguir el grafo de objetos HSD, no el orden en el que se termina creando las mallas o los renderers.
Esto es especialmente importante para la animación de materiales:
Un único "DObj"/"MObj" de HSD puede producir más de una malla (en nuestro caso de Unity). Vincular la animación de materiales por índice de renderer hace que las pistas posteriores de ojos, boca o animación de textura terminen desplazándose hacia mallas incorrectas.
La parte esquelética sigue una regla similar: hay que conservar la jerarquía original de joints de HSD y evaluar los canales de animación HSD contra los transforms a los que realmente apuntan. Una vez que la bind pose, la conversión de coordenadas y la semántica de los canales de animación se manejan de forma consistente, el modelo original puede reconstruirse directamente en Unity Engine.
2. Qué es FSYS dentro de este pipeline
A efectos de extracción de modelos, un archivo ".fsys" debe tratarse como un contenedor de recursos, no como el propio formato de modelo.
Conceptualmente:
El importador debe resolver primero el recurso embebido deseado y después analiza las estructuras HSD contenidas en ese payload. Esta distinción es importante durante la ingeniería inversa:
- FSYS responde dónde están los recursos y cómo están empaquetados.
- HSD responde cómo se representan el modelo, el esqueleto, los materiales, las texturas y las animaciones.
- La política de exportación/importación responde cómo se reconstruyen esas estructuras como assets de Unity.
No se debe inferir la semántica de HSD únicamente a partir del orden de las entradas FSYS.
3. Grafo de objetos HSD de alto nivel
La jerarquía del lado del modelo reconstruida por el importador puede resumirse como:
Los roles importantes son:
Aquí no se documentan completamente los layouts binarios exactos ni todos los flags; el objetivo son las relaciones necesarias para construir un importador funcional.
4. Esqueleto y bind pose
El árbol "JObj" se reconstruye como una jerarquía de "Transform" de Unity.
Cada joint aporta un transform local de bind aproximadamente equivalente a:
El importador debe utilizar la misma conversión de coordenadas tanto para la bind pose como para los valores de animación. Mezclar convenciones es una de las formas más sencillas de obtener una animación que parece razonable en el frame cero pero que se deforma progresivamente.
Durante esta investigación, varios problemas que parecían ser "animaciones incorrectas" eran en realidad problemas de coordenadas/binding:
- Quaternions de Unity inválidos o no normalizados;
- Valores Euler que llegaban a Unity con valores inválidos/no finitos;
- Una animación evaluada respecto al bind transform incorrecto;
- Un eje/signo del transform convertido de forma diferente entre la importación estática del modelo y la importación de animaciones.
Una regla útil de validación es:
excepto cuando la semántica del canal HSD requiera explícitamente una operación diferente.
5. Animación esquelética
La animación esquelética se representa mediante objetos de animación HSD y canales por propiedad. El flujo práctico de decodificación es:
No todos los joints o animaciones contienen todas las propiedades. Por tanto, la estrategia segura de reconstrucción es:
No se deben inicializar a cero los canales de escala ausentes.
Una animación de escala ausente normalmente significa "conservar la escala existente", no CODE[/CODE].
6. Tratamiento de las rotaciones
La rotación fue una de las partes más sensibles del importador nativo. Las conversiones incorrectas producían errores de Unity como:
Estos errores deben tratarse como fallos del decoder, no simplemente como warnings del editor de Unity.
Antes de crear un quaternion de Unity:
1. Rechazar o sanear NaN/Infinity.
2. Aplicar la conversión angular HSD correcta.
3. Aplicar la convención de ejes GameCube -> Unity.
4. Normalizar el quaternion.
5. Evitar convertir de nuevo un quaternion malformado mediante "Quaternion.Euler".
Un modelo que empieza cerca de la pose esperada pero "explota" durante la reproducción suele indicar con más fuerza una interpretación incorrecta de los canales o una conversión incorrecta del espacio de transforms que pesos de skinning incorrectos.
7. Duración de la animación y frames adicionales
Una versión temprana del importador generaba keys adicionales más allá de la duración real de la animación HSD.
Esto provocaba dos problemas visibles:
La duración correcta del clip debe proceder de la temporización de la propia animación HSD. Añadir keys para implementar animación de material stepped no debe extender el clip. Para un clip que termina en el frame "N", las keys auxiliares deben permanecer dentro del intervalo real de la animación.
8. La animación de materiales es independiente de la animación esquelética
La animación facial de los Pokémon depende frecuentemente de animación de material/textura en lugar de geometría adicional.
Ejemplos típicos:
- Estados de ojos;
- Estados de boca;
- Selección de atlas de texturas;
- Traslación UV;
- Sustitución de textura.
Por tanto, una animación esquelética puede ser geométricamente perfecta y aun así el Pokémon verse incorrecto porque sus pistas "MatAnim" faltan o están vinculadas al material equivocado. La animación de materiales debe decodificarse como un sistema independiente y asociarse con el mismo modelo importado.
9. La regla crítica de binding de MatAnim
Este fue uno de los fixes de ingeniería inversa más importantes. Una implementación incorrecta asociaba las entradas de animación de materiales con renderers de Unity según el orden de creación:
Eso es incorrecto.
Un objeto de modelo HSD puede producir múltiples mallas/renderers de Unity. En cuanto ocurre esto, todos los objetivos de animación posteriores quedan desplazados. La relación correcta es:
El importador debe conservar suficientes metadatos de origen mientras construye el modelo para poder resolver posteriormente esta relación. Un mapa útil en el lado del importador sería conceptualmente:
La key exacta puede ser un offset, identidad de objeto, índice parseado u otro identificador estable del origen. Lo importante es que proceda de la relación HSD, no del orden de enumeración de Unity.
10. Por qué falla el orden de renderers
Consideremos:
Unity puede generar:
Si el importador asume un renderer por slot de material:
La animación de la boca se aplica ahora al segundo renderer relacionado con los ojos y todos los bindings posteriores también pueden desplazarse. En su lugar:
Este fix es independiente del modelo y no debe hardcodearse utilizando nombres concretos de malla.
11. Traslación de texturas HSD y "_MainTex_ST" de Unity
Unity representa la transformación estándar de textura como:
Este mapping debe permanecer explícito en el importador. Un bug anterior mezclaba valores de escala base y offset base, haciendo que valores como "1" apareciesen como offset. Otro bug animaba únicamente los componentes de offset dejando implícitamente a cero los componentes de escala, dando como resultado Tiling = (0, 0). La regla segura es: Cuando se genere cualquier animación UV: conservar/escribir la escala base para X e Y animar únicamente los componentes controlados realmente por HSD. Para materiales normales esto suele significar conservar Tiling = (1, 1), salvo que el "TObj" de origen especifique otra escala base.
12. Las traslaciones U y V no son intercambiables
La convención de coordenadas de textura de GameCube/HSD no se mapea de forma idéntica a Unity en ambos ejes. Para los modelos validados durante esta investigación, la conversión funcional es:
Esto fue necesario para soportar ambos tipos de modelo observados, los que tienen texturas que cambian con Offset en Y hacia abajo (E.g: Zangoose) y en X hacia la derecha (Eg. Venusaur). Esta regla debe seguir considerándose parte del comportamiento actualmente validado del importador y no una especificación completa de todos los posibles modos de matriz de textura HSD.
13. No cuantizar globalmente los offsets UV
Durante el debugging, valores como "0 -0.166687 -0.333252 -0.666687 -0.833252" se interpretaron inicialmente como representación de un atlas universal de tres filas. Esa suposición era incorrecta. Diferentes materiales pueden utilizar diferentes layouts de atlas y diferentes valores de traslación HSD. Los materiales de ojos y boca de un mismo Pokémon no tienen por qué compartir el mismo conjunto de estados UV.
14. Confirmado / fuertemente validado
El siguiente comportamiento está fuertemente validado por el importador nativo funcional y la comparación visual realizada durante el desarrollo:
- FSYS es un contenedor de recursos; la semántica del modelo procede de las estructuras HSD embebidas.
- La jerarquía del modelo se basa en relaciones HSD "JObj/DObj/PObj/MObj/TObj".
- La animación esquelética nativa puede reconstruirse directamente en cualquier motor de render.
- Los canales esqueléticos deben evaluarse contra sus objetivos joint HSD reales.
- Los canales de transform ausentes deben conservar los componentes bind/default.
- Una conversión de rotación inválida/no normalizada puede provocar deformaciones graves en Unity.
- La animación de materiales es independiente de la animación esquelética.
- La animación de materiales debe vincularse mediante las relaciones HSD de origen, no mediante el orden de renderers de Unity.
- Un objeto material/modelo de origen puede generar múltiples renderers de Unity.
- "_MainTex_ST" utiliza escala en X/Y y offset en Z/W.
- El tiling base debe conservarse cuando únicamente se animan offsets.
- Para los casos actualmente validados, la traslación U requiere signo opuesto mientras que V conserva el signo HSD.
- Los valores UV no deben cuantizarse globalmente a un layout de atlas supuesto.
- Los cambios de atlas facial/material requieren comportamiento stepped en lugar de interpolación lineal.
- Las hold keys no deben extender la duración del clip de origen.
- El orden/significado de las acciones de animación difiere entre Pokémon; es más seguro utilizar nombres genéricos "Animation_N".
- La convención de coordenadas actual de Unity requiere una rotación de 90 grados en X en la raíz.
- Zangoose y Venusaur proporcionaron casos de validación complementarios: Zangoose reveló problemas de binding de objetivos de material y del atlas V, mientras que Venusaur reveló problemas del signo del eje U y de conservación del tiling.
15. Provisional / incompleto
Las siguientes áreas todavía requieren más ingeniería inversa antes de poder tratarlas como una especificación completa del formato:
- Semántica completa del header binario FSYS y de los campos de la tabla de entradas;
- Variantes de compresión/alineación entre todos los tipos FSYS de Colosseum/XD;
- significado semántico exacto de todos los flags HSD encontrados;
- Especificación completa de opcodes/interpolación "AObj/FObj";
- Todos los modos de matriz de textura / generación de coordenadas;
- Mapping exacto original del juego entre nombre de acción/índice por especie;
- Semántica de animación de visibilidad en todos los tipos de modelo;
- Rol exacto de todos los objetos geométricos sin textura/con apariencia de helper;
- Si todas las clases de modelos de Colosseum y XD utilizan las mismas suposiciones de conversión U/V;
- Relación completa de recursos de texturas shiny/alternativas;
- Familias de modelos FSYS que no sean Pokémon y objetos de casos especiales.
16. Casos de referencia actuales
Zangoose
Útil para validar:
Venusaur
Útil para validar:
En conjunto, estos dos modelos son significativamente más útiles que tratar cualquiera de ellos como una descripción universal del formato.
17. Arquitectura de implementación de referencia
Para un importador limpio y orientado a ingeniería inversa, hay que mantener las capas separadas:
Esto evita contaminar el parseo binario con suposiciones específicas de Unity y facilita considerablemente los diagnósticos. Una representación normalizada debería conservar explícitamente los identificadores de origen aunque esos valores nunca se expongan en el build runtime final:
18. Lectura binaria de FSYS y extracción de entradas
El importador actual lee FSYS directamente desde el stream de bytes original de GameCube. Esta capa es intencionadamente pequeña: valida el contenedor, resuelve la tabla de entradas, extrae cada payload soportado y descomprime las entradas marcadas como LZSS antes de entregar los bytes resultantes a la capa PKX o HSD/DAT. Todos los valores multibyte de esta ruta se leen como Big-Endian, coincidiendo con la representación de GameCube utilizada por estos archivos. El helper binario implementa explícitamente las lecturas primitivas:
Cada lectura primitiva y cada extracción de bytes comprueba sus límites. Si el rango solicitado queda fuera del buffer de origen, la lectura se rechaza en lugar de consumir silenciosamente datos inválidos.
18.1 Identificación de FSYS
Un contenedor FSYS se identifica mediante sus primeros cuatro bytes:
Conceptualmente:
El importador también utiliza la extensión ".fsys" como pista para enrutar el archivo, pero "ExtractFSYS" exige que el magic real "FSYS" esté presente.
18.2 Campos del header utilizados actualmente
El importador no necesita comprender el header FSYS completo para extraer los recursos de modelo requeridos por el pipeline actual. Los dos campos del header que se consumen actualmente son:
Estos valores se interpretan como "U32" big-endian. El segundo valor no apunta directamente a un payload. Apunta a una lista de offsets de 32 bits:
Por tanto, el importador resuelve la estructura de una entrada mediante:
Esta indirección es importante. La tabla es una lista de punteros/offsets hacia metadatos de entradas, no un array empaquetado de descriptores de payload con un stride fijo asumido.
18.3 Campos de una entrada utilizados por el importador
Para cada estructura de entrada resuelta, el reader actual consume:
Los valores de tipo reconocidos actualmente son:
Los tipos desconocidos se omiten en el importador actual en lugar de asignarles una semántica especulativa. La ruta de extracción puede resumirse así:
18.4 Nombres de las entradas
El importador intenta recuperar el nombre original de la entrada utilizando dos punteros de sus metadatos. Primero comprueba:
Si el puntero es válido y el string ASCII terminado en null resultante no está vacío, se utiliza directamente ese nombre. En caso contrario comprueba:
Si existe un nombre corto pero no tiene extensión, se añade la extensión inferida a partir del tipo de recurso. Si ninguno de los dos punteros produce un nombre válido, el importador genera un fallback determinista:
Por ejemplo:
Este comportamiento de naming es política del importador. La observación relevante de ingeniería inversa es que, para los archivos validados por la implementación actual, las estructuras de entrada contienen punteros de string utilizables en los offsets anteriores.
18.5 Extracción de los bytes del payload
Una vez resueltos la dirección y el tamaño del payload, la extracción es deliberadamente sencilla:
"Slice" comprueba primero que el intervalo completo esté dentro del buffer FSYS y después realiza una copia directa de los bytes. Conceptualmente:
En esta fase no se realiza ninguna interpretación HSD. El resultado sigue siendo el stream exacto de bytes del recurso embebido, salvo cuando la entrada FSYS declara compresión. Esta separación es útil durante el debugging:
Por tanto, un modelo malformado no debe atribuirse inmediatamente al parser HSD. Primero puede volcarse y compararse de forma independiente el payload extraído.
18.6 Flag de compresión
La implementación actual trata el bit alto de los flags de la entrada como indicador de compresión:
Cuando este bit está activo, el payload extraído se pasa al decoder LZSS de FSYS. El decoder exige que el propio payload comprimido comience con:
o:
Si la entrada FSYS está marcada como comprimida pero el payload extraído no contiene este magic, el importador la rechaza en lugar de intentar una descompresión heurística.
18.7 Header LZSS y rango comprimido
El reader LZSS actual comienza a decodificar en el byte "0x10". Lee:
Si el tamaño leído en "0x08" es inválido, menor que el header o queda fuera del payload disponible, el importador utiliza de forma conservadora el tamaño completo de la entrada extraída. El decoder implementado utiliza un ring buffer deslizante de 4096 bytes:
Inicialmente el ring está relleno con ceros.
18.8 Decodificación de los flag bytes de LZSS
Los comandos se controlan mediante flag bytes. Se carga un nuevo flag byte cuando se ha consumido el grupo actual:
Después los comandos se procesan desde el bit menos significativo hacia arriba. Para cada comando:
Después de procesar un comando:
Un literal consume un byte del stream comprimido:
El byte se escribe tanto en la salida como en la posición actual del ring buffer de 4096 bytes.
18.9 Back-references de LZSS
Una back-reference consume dos bytes:
Esto produce:
La implementación copia después mediante:
por lo que el número efectivo de bytes copiados es:
es decir, un rango de:
Cada byte copiado se lee aplicando la máscara del ring:
y se vuelve a escribir inmediatamente en el ring en la posición actual de salida. Esto permite matches solapados, como corresponde a este estilo de stream LZSS.
18.10 Enrutado de los recursos extraídos
Después de la descompresión opcional, la capa de contenedor actual enruta los recursos según su extensión inferida. Para entradas ".pkx":
Para recursos ".dat", ".fdat", ".rdat" y ".cam" aceptados por la capa de contenedor, los bytes extraídos se devuelven directamente como entradas del contenedor. El mapping de tipos FSYS actual únicamente produce ".dat", ".cam" y ".pkx" a partir de IDs de tipo FSYS; ".fdat" y ".rdat" siguen estando aceptados por la ruta común posterior para inputs directos/soportados.
18.11 Extracción PKX -> DAT
Como los recursos de Pokémon pasan frecuentemente por PKX, esta es la siguiente capa relevante a nivel de bytes después de la extracción FSYS. El importador actual distingue los dos layouts utilizados por sus archivos validados de Colosseum/XD comparando:
La implementación actual considera que valores diferentes indican el layout de tipo XD.
Para la ruta más sencilla no-XD:
Para la ruta de tipo XD lee:
y calcula:
donde:
El primer "U32" del PKX se trata inicialmente como tamaño del DAT. Si ese tamaño declarado no cabe dentro del PKX, el importador utiliza como fallback el primer "U32" del propio DAT embebido como tamaño. Este fallback es intencionadamente conservador y existe porque algunos archivos validados declaran un tamaño externo padded o que no resulta utilizable directamente.
18.12 Mapa FSYS actual a nivel de bytes
El subconjunto del formato requerido por el importador actual puede documentarse así:
Este mapa debe entenderse como el subconjunto confirmado y consumido por el importador nativo actual, no como una especificación completa de FSYS. Otros campos del header, campos de entrada, flags y valores de tipo de recurso quedan fuera de lo que necesita actualmente esta implementación.
18.13 Reglas de parseo defensivo
Conviene conservar varias reglas defensivas del reader:
1. Cada lectura numérica comprueba sus límites.
2. Cada slice de payload debe caber completamente dentro del buffer de origen.
3. Los punteros a strings deben estar dentro del FSYS antes de desreferenciarse.
4. Los strings se leen como C strings ASCII y terminan en el primer byte null.
5. Los tipos de recurso no soportados se omiten en lugar de adivinarse.
6. Una entrada comprimida debe contener un magic "LZSS" válido.
7. Los tamaños DAT externos inválidos de PKX se contrastan con el rango real de bytes disponible.
8. Los cálculos de alineamiento son explícitos y no se infieren a partir de la posición actual del stream.
19. Resultado
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:
Créditos / contexto de la investigación
El addon de Blender Blender-Addon-Gamecube-Models se utilizó como un importante punto de referencia de comportamiento durante la investigación, y un pipeline anterior Blender -> FBX -> Unity proporcionó una salida visual conocida como correcta para realizar comparaciones. Sin embargo, el importador final descrito aquí reconstruye los datos relevantes de modelo y animación de forma nativa en lugar de depender de Blender ni de ningún otro motor de render.
1. Resumen ejecutivo
Pokémon Colosseum y Pokémon XD empaquetan muchos de sus recursos de modelo dentro de contenedores FSYS. Para los modelos de Pokémon, el modelo conceptual útil no es "FSYS = un modelo", sino:
Código:
FSYS
|
+-- one or more embedded resources
|
+-- HSD model data
| +-- JObj hierarchy / skeleton
| +-- DObj geometry ownership
| +-- PObj mesh primitives
| +-- MObj materials
| +-- TObj textures / UV state
|
+-- skeletal animations
| +-- AObj
| +-- FObj channels
|
+-- material / texture animations
+-- MatAnimJoint
+-- MatAnim
+-- texture/UV animation channels
Esto es especialmente importante para la animación de materiales:
Código:
MatAnimJoint
-> MatAnim slot
-> DObj
-> MObj
-> PObj / generated Unity Renderer(s)
La parte esquelética sigue una regla similar: hay que conservar la jerarquía original de joints de HSD y evaluar los canales de animación HSD contra los transforms a los que realmente apuntan. Una vez que la bind pose, la conversión de coordenadas y la semántica de los canales de animación se manejan de forma consistente, el modelo original puede reconstruirse directamente en Unity Engine.
2. Qué es FSYS dentro de este pipeline
A efectos de extracción de modelos, un archivo ".fsys" debe tratarse como un contenedor de recursos, no como el propio formato de modelo.
Conceptualmente:
Código:
.fsys / FSYS container
|
+-- file/resource table
|
+-- embedded payload 0
+-- embedded payload 1
+-- ...
- FSYS responde dónde están los recursos y cómo están empaquetados.
- HSD responde cómo se representan el modelo, el esqueleto, los materiales, las texturas y las animaciones.
- La política de exportación/importación responde cómo se reconstruyen esas estructuras como assets de Unity.
No se debe inferir la semántica de HSD únicamente a partir del orden de las entradas FSYS.
3. Grafo de objetos HSD de alto nivel
La jerarquía del lado del modelo reconstruida por el importador puede resumirse como:
Código:
JObj
|
+-- child JObj
| |
| +-- ...
|
+-- DObj
|
+-- MObj
| |
| +-- TObj
|
+-- PObj
+-- PObj
+-- ...
Código:
| Objeto HSD | Rol práctico |
|---|---|
| `JObj` | Jerarquía de joints/nodos y transform local. Se utiliza como jerarquía esquelética.|
| `DObj` | Objeto de visualización/modelo unido a un joint. Contiene el estado dibujable/del modelo. |
| `PObj` | Objeto de polígonos/primitivas. Proporciona geometría y datos relacionados con skinning. |
| `MObj` | Objeto de material utilizado por un "DObj". |
| `TObj` | Objeto de textura y estado/transformación de textura.|
| `AObj` | Objeto de animación / contenedor de temporización de animación. |
| `FObj` | Propiedad/canal animado individual. |
| `MatAnimJoint` | Jerarquía de animación de materiales asociada con los joints del modelo. |
| `MatAnim` | Entrada de animación de material/textura asociada con slots de material del modelo. |
4. Esqueleto y bind pose
El árbol "JObj" se reconstruye como una jerarquía de "Transform" de Unity.
Cada joint aporta un transform local de bind aproximadamente equivalente a:
Código:
local translation
local rotation
local scale
Durante esta investigación, varios problemas que parecían ser "animaciones incorrectas" eran en realidad problemas de coordenadas/binding:
- Quaternions de Unity inválidos o no normalizados;
- Valores Euler que llegaban a Unity con valores inválidos/no finitos;
- Una animación evaluada respecto al bind transform incorrecto;
- Un eje/signo del transform convertido de forma diferente entre la importación estática del modelo y la importación de animaciones.
Una regla útil de validación es:
Código:
conversión del transform estático de bind == conversión del transform de animación
5. Animación esquelética
La animación esquelética se representa mediante objetos de animación HSD y canales por propiedad. El flujo práctico de decodificación es:
Código:
Animation
|
+-- target JObj
|
+-- AObj
|
+-- FObj: translation X
+-- FObj: translation Y
+-- FObj: translation Z
+-- FObj: rotation X
+-- FObj: rotation Y
+-- FObj: rotation Z
+-- FObj: scale X
+-- FObj: scale Y
+-- FObj: scale Z
Código:
Para cada joint animado:
Comenzar con su transform local de bind/default
Evaluar únicamente los canales realmente presentes
Conservar el valor por defecto para los canales ausentes
Convertir el transform HSD resultante al espacio de Unity
Generar AnimationCurves de Unity
Una animación de escala ausente normalmente significa "conservar la escala existente", no CODE[/CODE].
6. Tratamiento de las rotaciones
La rotación fue una de las partes más sensibles del importador nativo. Las conversiones incorrectas producían errores de Unity como:
Código:
QuaternionToEuler: Input quaternion was not normalized
Assertion failed on expression: 'IsFinite(rot)'
Assertion failed on expression:
'CompareApproximately(SqrMagnitude(result), 1.0F)'
Antes de crear un quaternion de Unity:
1. Rechazar o sanear NaN/Infinity.
2. Aplicar la conversión angular HSD correcta.
3. Aplicar la convención de ejes GameCube -> Unity.
4. Normalizar el quaternion.
5. Evitar convertir de nuevo un quaternion malformado mediante "Quaternion.Euler".
Un modelo que empieza cerca de la pose esperada pero "explota" durante la reproducción suele indicar con más fuerza una interpretación incorrecta de los canales o una conversión incorrecta del espacio de transforms que pesos de skinning incorrectos.
7. Duración de la animación y frames adicionales
Una versión temprana del importador generaba keys adicionales más allá de la duración real de la animación HSD.
Esto provocaba dos problemas visibles:
Código:
skeletal clip:
real animation
+ unintended tail frames
material animation:
previous state
-> interpolation through unintended tail
-> next state
8. La animación de materiales es independiente de la animación esquelética
La animación facial de los Pokémon depende frecuentemente de animación de material/textura en lugar de geometría adicional.
Ejemplos típicos:
- Estados de ojos;
- Estados de boca;
- Selección de atlas de texturas;
- Traslación UV;
- Sustitución de textura.
Por tanto, una animación esquelética puede ser geométricamente perfecta y aun así el Pokémon verse incorrecto porque sus pistas "MatAnim" faltan o están vinculadas al material equivocado. La animación de materiales debe decodificarse como un sistema independiente y asociarse con el mismo modelo importado.
9. La regla crítica de binding de MatAnim
Este fue uno de los fixes de ingeniería inversa más importantes. Una implementación incorrecta asociaba las entradas de animación de materiales con renderers de Unity según el orden de creación:
Código:
MatAnim 0 -> Renderer 0
MatAnim 1 -> Renderer 1
MatAnim 2 -> Renderer 2
Un objeto de modelo HSD puede producir múltiples mallas/renderers de Unity. En cuanto ocurre esto, todos los objetivos de animación posteriores quedan desplazados. La relación correcta es:
Código:
MatAnimJoint
|
+-- MatAnim
|
+-- slot de material/modelo HSD
|
+-- DObj
|
+-- MObj
|
+-- todos los PObj/renderers generados a partir de él
Código:
HsdMObjAddress -> List<Renderer>
HsdDObjAddress -> List<Renderer>
HsdJObjAddress -> Transform
10. Por qué falla el orden de renderers
Consideremos:
Código:
DObj A
MObj Eyes
PObj 0
PObj 1
DObj B
MObj Mouth
PObj 0
Código:
Mesh_0
Mesh_0_2
Mesh_1
Código:
Eyes -> Mesh_0
Mouth -> Mesh_0_2 // INCORRECTO
Código:
Eyes -> todos los renderers generados desde MObj
Eyes Mouth -> todos los renderers generados desde MObj Mouth
11. Traslación de texturas HSD y "_MainTex_ST" de Unity
Unity representa la transformación estándar de textura como:
Código:
_MainTex_ST.x = Scale / Tiling X
_MainTex_ST.y = Scale / Tiling Y
_MainTex_ST.z = Offset X
_MainTex_ST.w = Offset Y
12. Las traslaciones U y V no son intercambiables
La convención de coordenadas de textura de GameCube/HSD no se mapea de forma idéntica a Unity en ambos ejes. Para los modelos validados durante esta investigación, la conversión funcional es:
Código:
Unity Offset X = -HSD TRANS_U
Unity Offset Y = HSD TRANS_V"
13. No cuantizar globalmente los offsets UV
Durante el debugging, valores como "0 -0.166687 -0.333252 -0.666687 -0.833252" se interpretaron inicialmente como representación de un atlas universal de tres filas. Esa suposición era incorrecta. Diferentes materiales pueden utilizar diferentes layouts de atlas y diferentes valores de traslación HSD. Los materiales de ojos y boca de un mismo Pokémon no tienen por qué compartir el mismo conjunto de estados UV.
14. Confirmado / fuertemente validado
El siguiente comportamiento está fuertemente validado por el importador nativo funcional y la comparación visual realizada durante el desarrollo:
- FSYS es un contenedor de recursos; la semántica del modelo procede de las estructuras HSD embebidas.
- La jerarquía del modelo se basa en relaciones HSD "JObj/DObj/PObj/MObj/TObj".
- La animación esquelética nativa puede reconstruirse directamente en cualquier motor de render.
- Los canales esqueléticos deben evaluarse contra sus objetivos joint HSD reales.
- Los canales de transform ausentes deben conservar los componentes bind/default.
- Una conversión de rotación inválida/no normalizada puede provocar deformaciones graves en Unity.
- La animación de materiales es independiente de la animación esquelética.
- La animación de materiales debe vincularse mediante las relaciones HSD de origen, no mediante el orden de renderers de Unity.
- Un objeto material/modelo de origen puede generar múltiples renderers de Unity.
- "_MainTex_ST" utiliza escala en X/Y y offset en Z/W.
- El tiling base debe conservarse cuando únicamente se animan offsets.
- Para los casos actualmente validados, la traslación U requiere signo opuesto mientras que V conserva el signo HSD.
- Los valores UV no deben cuantizarse globalmente a un layout de atlas supuesto.
- Los cambios de atlas facial/material requieren comportamiento stepped en lugar de interpolación lineal.
- Las hold keys no deben extender la duración del clip de origen.
- El orden/significado de las acciones de animación difiere entre Pokémon; es más seguro utilizar nombres genéricos "Animation_N".
- La convención de coordenadas actual de Unity requiere una rotación de 90 grados en X en la raíz.
- Zangoose y Venusaur proporcionaron casos de validación complementarios: Zangoose reveló problemas de binding de objetivos de material y del atlas V, mientras que Venusaur reveló problemas del signo del eje U y de conservación del tiling.
15. Provisional / incompleto
Las siguientes áreas todavía requieren más ingeniería inversa antes de poder tratarlas como una especificación completa del formato:
- Semántica completa del header binario FSYS y de los campos de la tabla de entradas;
- Variantes de compresión/alineación entre todos los tipos FSYS de Colosseum/XD;
- significado semántico exacto de todos los flags HSD encontrados;
- Especificación completa de opcodes/interpolación "AObj/FObj";
- Todos los modos de matriz de textura / generación de coordenadas;
- Mapping exacto original del juego entre nombre de acción/índice por especie;
- Semántica de animación de visibilidad en todos los tipos de modelo;
- Rol exacto de todos los objetos geométricos sin textura/con apariencia de helper;
- Si todas las clases de modelos de Colosseum y XD utilizan las mismas suposiciones de conversión U/V;
- Relación completa de recursos de texturas shiny/alternativas;
- Familias de modelos FSYS que no sean Pokémon y objetos de casos especiales.
16. Casos de referencia actuales
Zangoose
Útil para validar:
Código:
Deformación esquelética / conversión de rotación duración de la animación binding del objetivo de material animación vertical de textura facial cambios stepped de estado UV múltiples mallas generadas desde relaciones material/modelo de origen.
Útil para validar:
Código:
Animación UV horizontal conversión de signo TRANS_U conservación de Tiling (1,1) offsets X positivos de Unity producidos a partir de traslaciones U HSD negativas.
17. Arquitectura de implementación de referencia
Para un importador limpio y orientado a ingeniería inversa, hay que mantener las capas separadas:
Código:
FSYS reader
|
v
HSD binary/object parser
|
+-- model graph
+-- texture decoder
+-- skeletal animation decoder
+-- material animation decoder
|
v
normalized intermediate representation
|
v
Unity exporter
+-- meshes
+-- materials
+-- textures
+-- AnimationClips
+-- callbacks
+-- Animator/Animation
+-- prefab
Código:
SourceJointId
SourceDObjId
SourceMObjId
SourcePObjId
SourceTextureId
El importador actual lee FSYS directamente desde el stream de bytes original de GameCube. Esta capa es intencionadamente pequeña: valida el contenedor, resuelve la tabla de entradas, extrae cada payload soportado y descomprime las entradas marcadas como LZSS antes de entregar los bytes resultantes a la capa PKX o HSD/DAT. Todos los valores multibyte de esta ruta se leen como Big-Endian, coincidiendo con la representación de GameCube utilizada por estos archivos. El helper binario implementa explícitamente las lecturas primitivas:
Código:
U16(offset):
(data[offset] << 8) |
data[offset + 1]
U32(offset):
(data[offset] << 24) |
(data[offset + 1] << 16) |
(data[offset + 2] << 8) |
data[offset + 3]
18.1 Identificación de FSYS
Un contenedor FSYS se identifica mediante sus primeros cuatro bytes:
Código:
offset 0x00:
46 53 59 53
F S Y S
Código:
data[0] == 'F'
data[1] == 'S'
data[2] == 'Y'
data[3] == 'S'
18.2 Campos del header utilizados actualmente
El importador no necesita comprender el header FSYS completo para extraer los recursos de modelo requeridos por el pipeline actual. Los dos campos del header que se consumen actualmente son:
Código:
FSYS + 0x0C : U32 -> número de entradas
FSYS + 0x40 : U32 -> offset de la lista de punteros a entradas
Código:
FSYS
|
+-- header
| |
| +-- 0x0C -> entry count
| +-- 0x40 -> entry pointer list offset
|
+-- entry pointer list
|
+-- U32 -> entry 0 structure
+-- U32 -> entry 1 structure
+-- U32 -> entry 2 structure
+-- ...
Código:
entry = U32(entryListOffset + index * 4)
18.3 Campos de una entrada utilizados por el importador
Para cada estructura de entrada resuelta, el reader actual consume:
Código:
entry + 0x02 : U8 -> tipo de recurso
entry + 0x04 : U32 -> dirección del payload
entry + 0x0C : U32 -> flags
entry + 0x14 : U32 -> tamaño almacenado del payload
entry + 0x1C : U32 -> puntero al string del nombre completo
entry + 0x24 : U32 -> puntero al string del nombre corto
Código:
0x02 -> .dat
0x04 -> .dat
0x18 -> .cam
0x1E -> .pkx
Código:
para cada entrada FSYS:
resolver la dirección de sus metadatos
leer type
si el type no está soportado actualmente:
omitir la entrada
leer dirección del payload
leer flags
leer tamaño del payload
copiar los bytes del payload desde:
[payload address, payload address + payload size)
si está presente el flag de compresión:
descomprimir el payload LZSS
enrutar los bytes resultantes según el tipo de recurso
El importador intenta recuperar el nombre original de la entrada utilizando dos punteros de sus metadatos. Primero comprueba:
Código:
entry + 0x1C -> full-name C string
Código:
entry + 0x24 -> short-name C string
Código:
<archive-name>_entry_<index><extension>
Código:
pkx_001_entry_3.dat
18.5 Extracción de los bytes del payload
Una vez resueltos la dirección y el tamaño del payload, la extracción es deliberadamente sencilla:
Código:
file = Slice(address, size)
Código:
FSYS byte array
0x00000000
...
payload address
|
+-- byte 0
+-- byte 1
+-- ...
+-- byte size-1
|
payload address + size
Código:
FSYS reader
-> extrae/descomprime los bytes del recurso
PKX reader
-> localiza el DAT embebido cuando corresponde
HSD/DAT reader
-> interpreta las estructuras del modelo
18.6 Flag de compresión
La implementación actual trata el bit alto de los flags de la entrada como indicador de compresión:
Código:
(flags & 0x80000000) != 0
Código:
4C 5A 53 53
L Z S S
Código:
"LZSS"
18.7 Header LZSS y rango comprimido
El reader LZSS actual comienza a decodificar en el byte "0x10". Lee:
Código:
LZSS + 0x08 : U32 -> tamaño del stream comprimido utilizado por el decoder
LZSS + 0x10 : -> comienzo de los comandos/datos codificados
Código:
ring size = 4096
ring mask = 0x0FFF
initial write r = 4078
length mask = 0x0F
18.8 Decodificación de los flag bytes de LZSS
Los comandos se controlan mediante flag bytes. Se carga un nuevo flag byte cuando se ha consumido el grupo actual:
Código:
flags = input[p++] | 0xFF00
Código:
flag bit = 1:
byte literal
flag bit = 0:
back-reference
Código:
flags >>= 1
Código:
input:
[literal]
output:
literal
18.9 Back-references de LZSS
Una back-reference consume dos bytes:
Código:
i = input[p++]
j = input[p++]
i |= (j >> 4) << 8
j = (j & 0x0F) + 2
Código:
posición de origen:
12 bits
componente de longitud codificado:
4 bits bajos del segundo byte
Código:
for (k = 0; k <= j; k++)
Código:
(lowNibble + 2) + 1
= lowNibble + 3
Código:
3..18 bytes
Código:
ring[(source + k) & 0x0FFF]
18.10 Enrutado de los recursos extraídos
Después de la descompresión opcional, la capa de contenedor actual enruta los recursos según su extensión inferida. Para entradas ".pkx":
Código:
FSYS
-> PKX payload
-> ExtractPKX
-> embedded DAT bytes
-> model parser
18.11 Extracción PKX -> DAT
Como los recursos de Pokémon pasan frecuentemente por PKX, esta es la siguiente capa relevante a nivel de bytes después de la extracción FSYS. El importador actual distingue los dos layouts utilizados por sus archivos validados de Colosseum/XD comparando:
Código:
U32(0x00)
U32(0x40)
Para la ruta más sencilla no-XD:
Código:
DAT offset = 0x40
Código:
0x08 -> GPT1 length
0x10 -> animation count
Código:
DAT offset =
Align32(0x84 + animationCount * 0xD0)
+ Align32(gpt1Length)
Código:
Align32(value) = (value + 0x1F) & ~0x1F
18.12 Mapa FSYS actual a nivel de bytes
El subconjunto del formato requerido por el importador actual puede documentarse así:
Código:
FSYS HEADER
-----------
0x00 4 bytes "FSYS"
0x0C U32 BE entry count
0x40 U32 BE entry pointer list offset
ENTRY POINTER LIST
------------------
+0x00 U32 BE entry metadata offset
+0x04 U32 BE next entry metadata offset
+0x08 U32 BE next entry metadata offset
...
ENTRY METADATA — fields currently used
--------------------------------------
+0x02 U8 resource type
+0x04 U32 BE payload address
+0x0C U32 BE flags
+0x14 U32 BE stored payload size
+0x1C U32 BE full-name string pointer
+0x24 U32 BE short-name string pointer
KNOWN TYPE VALUES IN CURRENT IMPORTER
-------------------------------------
0x02 .dat
0x04 .dat
0x18 .cam
0x1E .pkx
KNOWN FLAG BEHAVIOR IN CURRENT IMPORTER
---------------------------------------
0x80000000 payload is LZSS-compressed
18.13 Reglas de parseo defensivo
Conviene conservar varias reglas defensivas del reader:
1. Cada lectura numérica comprueba sus límites.
2. Cada slice de payload debe caber completamente dentro del buffer de origen.
3. Los punteros a strings deben estar dentro del FSYS antes de desreferenciarse.
4. Los strings se leen como C strings ASCII y terminan en el primer byte null.
5. Los tipos de recurso no soportados se omiten en lugar de adivinarse.
6. Una entrada comprimida debe contener un magic "LZSS" válido.
7. Los tamaños DAT externos inválidos de PKX se contrastan con el rango real de bytes disponible.
8. Los cálculos de alineamiento son explícitos y no se infieren a partir de la posición actual del stream.
19. Resultado
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:
Créditos / contexto de la investigación
El addon de Blender Blender-Addon-Gamecube-Models se utilizó como un importante punto de referencia de comportamiento durante la investigación, y un pipeline anterior Blender -> FBX -> Unity proporcionó una salida visual conocida como correcta para realizar comparaciones. Sin embargo, el importador final descrito aquí reconstruye los datos relevantes de modelo y animación de forma nativa en lugar de depender de Blender ni de ningún otro motor de render.
Última edición: