Buenas, os dejo por aquí un write up de ingeniería inversa de modelos y animaciones de Pokémon Snap (N64) de cómo he hecho mis tools para mi fangame "Pokémon Snap 3DS", ya que los modelos 3D los he sacado de la propia ROM original. Este post separa las observaciones confirmadas de las hipótesis de trabajo y mencionar que no habría sido posible sin la ayuda del usuario Pfedak, de NoClip Website, SubDrag de N64Vault y todo el repositorio de Pret del Decomp de Pokémon Snap (Ethteck y todo el que ha contribuído). Así que muchísimas gracias por la ayuda.
Nota: Los offsets hacen referencia a la ROM utilizada durante esta investigación, siendo ésta la versión USA 1.0 recomendada por Pret para el proyecto de decompilación pokesnap, con md-5: 9b10b9d70dac67ae40953148fbe9cb96.
0. Estructura organizada
En Pokémon Snap, una Room puede entenderse como una unidad o bloque lógico del escenario. Cada Room agrupa una porción de geometría del nivel y puede llevar asociados datos adicionales como objetos, actores o información de movimiento. No debe entenderse necesariamente como una “habitación” en sentido literal. Es más bien una subdivisión interna del "stage" (Ruta) utilizada por el juego para organizar y cargar distintas partes del escenario.
A nivel práctico, una Room puede contener:
Las Rooms se dividen principalmente en dos grupos:
Por tanto, un nivel de Pokémon Snap no es un único modelo monolítico. A nivel de runtime es más útil entenderlo como un conjunto de Rooms y sistemas globales relacionados:
1. Estructura general de un nivel
Un nivel de Pokémon Snap no es un único modelo monolítico. A nivel de runtime es más útil entenderlo como varios sistemas relacionados:
La cabecera del nivel contiene la primera división importante:
Las dos listas de rooms terminan con un puntero nulo.
2. Modelos del escenario estático
Las Rooms son la unidad básica de geometría estática
La geometría del escenario se reconstruye room por room.
Una Room normalizada contiene:
Para el escenario estático, la parte principal es `Node`.
Ese nodo contiene tanto la transformación de la room como el modelo generado a partir de sus display lists de Nintendo 64.
La malla y la posición de la room son datos distintos. Los vértices se reconstruyen en espacio local y después el nodo de la room coloca ese bloque en el nivel.
2.1. Cómo reconstruir la malla
Los datos gráficos de N64 son stateful. No se obtiene directamente un `Mesh` indexado moderno.
Hay que ejecutar la display list manteniendo el estado de render:
Un resultado de alto nivel puede quedar así:
En Snap2Unity se reutiliza inicialmente el builder general de modelos y después, para el escenario estático, los `SkinnedMeshRenderer` generados se convierten a `MeshFilter + MeshRenderer`, eliminando el armature y el Animator que ya no son necesarios.
2.2. Ejemplo en Unity
Después se aplica la posición nativa de la room:
La conversión de escala N64 -> Unity ya se ha aplicado durante la construcción de las posiciones/mallas, por lo que no se debe volver a escalar el objeto padre.
2.3. Combinar las mallas
Una vez reconstruida la geometría, puede optimizarse de varias formas.
Por room:
O para todo el nivel:
Y dentro de ese scope:
Esto ya es una decisión del importer/motor de destino, no una propiedad del formato original.
3. Modelos dinámicos: Pokémon
Un Pokémon dinámico no es únicamente su modelo.
El actor de runtime combina dos partes:
Esto es importante porque dos apariciones de la misma especie pueden utilizar el mismo aspecto visual pero tener definiciones de actor distintas según el nivel.
3.1. Reconstrucción de un Pokémon
El flujo general es:
En Unity puede representarse como:
3.2. Matrix palette y bones
Los modelos de Pokémon Snap dependen de las matrices activas del pipeline de N64, no de una tabla de pesos como la de un formato moderno.
Al convertirlo a un SkinnedMesh:
Si una draw call utiliza varias matrices, la malla reconstruida puede conservar esa palette de bones.
Es mejor derivar la influencia del bone del estado de matrices original que intentar adivinarla por proximidad geométrica.
4. Zero-One
El Zero-One es especial porque combina dos sistemas:
El modelo del vehículo y la ruta global no son la misma cosa.
4.1. Modelo
El Zero-One contiene:
Su reconstrucción preferente utiliza directamente la matrix palette de N64 para generar bones y bind poses.
El resultado se coloca bajo un `TrackRoot`:
4.2. Tipos de animación
Es útil separar:
La ruta por el nivel no es simplemente otra animación de un bone del vehículo.
4.3. Combinación de meshes
Solo deberían combinarse renderers cuando no se destruye información animada.
Deben mantenerse separados si intervienen en:
Renderers compatibles con el mismo material y la misma palette de bones sí pueden combinarse.
5. Skybox
El Skybox está referenciado directamente desde la cabecera del nivel y no forma parte de las Rooms normales.
Su descriptor contiene:
Su reconstrucción es:
El nodo base tiene transformación neutra:
Si existe animación, se crea un track de material por cada material del Skybox.
Esto permite que el cielo tenga geometría estática pero texturas o parámetros que cambien durante la animación.
6. Other Pokémon: modelos almacenados aparte
Algunos Pokémon tienen recursos visuales que no se encuentran en la misma zona que los datos visuales ordinarios del nivel.
Entre los casos tratados por Snap2Unity están:
Aquí hay que separar dos conceptos:
6.1. Ejemplo: Pikachu
Un nivel puede contener un spawn de Pikachu y tener perfectamente definidos:
Pero el modelo visual de Pikachu puede estar almacenado en otra zona distinta de los recursos visuales habituales del stage.
En ese caso se puede tomar solo la parte visual desde el recurso dedicado de Pikachu.
La combinación correcta sería:
El recurso de Other actúa por tanto como fallback visual, no como fallback de comportamiento.
6.2. Mantener el ActorDef del nivel
Si el ActorDef del stage se ha podido parsear, no debe sustituirse por el ActorDef genérico asociado al modelo de Other.
Hay que conservar:
Y reemplazar únicamente la parte visual.
Conceptualmente:
Solo si tampoco es posible recuperar el ActorDef del nivel tendría sentido utilizar la definición genérica asociada al recurso dedicado.
Esto es especialmente importante con Pikachu, porque diferentes apariciones pueden tener máquinas de estados muy distintas aunque visualmente sigan siendo Pikachu.
7. Main Track
El Main Track es la ruta global que recorre el Zero-One.
No debe confundirse con los Paths individuales de Pokémon u objetos.
Se reconstruye a partir de la lista de Path Rooms del nivel.
7.1. Bloques de Path Rooms
La cabecera del nivel contiene:
Cada entrada apunta a un descriptor de bloque.
Ese descriptor contiene:
El `WorldBlockGFX` contiene información relacionada con la ruta:
Por tanto una Path Room tiene dos funciones:
7.2. Datos del movement animation
La estructura de movimiento contiene:
Al final del stream existe un trailer de 24 bytes que permite localizar los streams reales del track:
Para el comando de posiciones utilizado por el track:
El stream de posiciones contiene:
puntos `vec3f`.
7.3. B-spline cúbica uniforme
Los puntos se evalúan usando ventanas consecutivas de cuatro puntos:
Para `u` entre 0 y 1:
La derivada de la spline permite obtener la tangente y orientar el Zero-One siguiendo la ruta.
7.4. De coordenadas locales del bloque al mundo
Cada bloque tiene su propia posición global y yaw.
Por tanto:
En Unity:
Los bloques pueden mantenerse separados:
o concatenarse en una única ruta:
El orden de concatenación es el mismo orden de las Path Rooms.
8. Paths
Los Paths normales son independientes del Main Track.
Están asociados a `ObjectSpawn` concretos dentro de las Rooms.
Un `TrackPath` contiene:
Estos paths pueden ser utilizados por Pokémon u otros objetos móviles.
8.1. Sampling
La ruta se puede muestrear en su intervalo temporal nativo:
Conversión a Unity:
Una representación útil para conservar metadata es:
Así se mantiene la relación entre el path importado y el spawn que lo utiliza.
8.2. Visualización
Para debugging/editor se puede añadir un `LineRenderer`.
También pueden generarse waypoints:
Cada waypoint puede orientarse según la tangente de la ruta.
Esto es únicamente una representación del editor. El dato nativo sigue siendo la estructura de path con sus tiempos, puntos y coeficientes.
9. Cómo se divide el nivel en Rooms
La cabecera contiene dos listas terminadas en null:
El nivel puede reconstruirse concatenando ambas:
Durante el parseo puede compartirse una misma texture cache entre todas las Rooms para reutilizar correctamente recursos comunes.
9.1. Path Rooms
Las Path Rooms son los bloques asociados al avance del Zero-One.
Además de su geometría normal, sus descriptores contienen la información necesaria para reconstruir el bloque correspondiente del Main Track.
9.2. Non-Path Rooms
Contienen geometría adicional que no forma parte de la secuencia principal de bloques del Zero-One.
Se renderizan exactamente con el mismo pipeline:
pero no aportan un bloque consecutivo al Main Track.
9.3. Organización en Unity
Se puede ordenar primero por tipo:
O primero por Room:
Los elementos globales permanecen fuera de esas Rooms:
La jerarquía de Unity es una decisión del importer, pero refleja bastante bien la separación nativa entre datos ligados a una Room y datos globales del stage.
10. Pipeline completo de reconstrucción
11. Distinción esencial
Mantener estos sistemas separados pero conectados permite reconstruir el nivel de una forma mucho más próxima a cómo Pokémon Snap organiza realmente sus datos en runtime.
12. Mapa de offsets, overlays y recorrido de estructuras
Antes de entrar en los offsets internos de cada estructura conviene explicar cómo se obtiene el bloque de datos de cada nivel desde la ROM.
En Snap2Unity no se trata cada stage como un único rango continuo. Para poder resolver correctamente los punteros virtuales del juego se reconstruye un pequeño archivo de trabajo con varios chunks u overlays, conservando además la dirección RAM en la que cada uno estaba cargado originalmente.
El archivo de nivel utilizado por las tools contiene conceptualmente:
Hay una distinción importante:
Por tanto, `Collision` no es un quinto bloque binario separado. Es una dirección que posteriormente se resuelve contra los chunks cargados en el `CRGDataMap`.
De la misma forma, el chunk llamado `Photo` no debe confundirse con la estructura runtime `PhotoData` utilizada por el juego al capturar y puntuar una fotografía. Aquí `Photo` es el nombre del overlay binario adicional del stage.
12.0.1. Tabla de overlays del stage
Los rangos principales `Data` y `Code` no están hardcodeados individualmente para cada nivel. Se obtienen de una tabla de la ROM.
La entrada se calcula como:
Desde esa posición se leen los datos del overlay principal:
Por tanto:
El overlay de código se obtiene mediante:
y:
Esto permite copiar directamente desde la ROM:
manteniendo después sus direcciones virtuales originales para poder resolver punteros N64.
12.0.2. Offsets específicos utilizados por cada nivel
Además de `Data` y `Code`, cada stage utiliza un overlay `Photo` conocido y tres direcciones de entrada: `Header`, `Objects` y `Collision`.
Los valores utilizados por Snap2Unity para la ROM USA 1.0 son:
Rainbow Cloud es el único de estos stages para el que la configuración utilizada por Snap2Unity no define una raíz de colisión.
12.0.3. ParticleData
`ParticleData` se extrae como otro rango independiente de ROM.
Los límites conocidos utilizados son:
El índice se calcula a partir del Scene ID:
y el rango es:
Por stage:
12.0.4. Cómo se vuelven a unir los chunks
Una vez extraídos, los tres overlays con espacio de direcciones propio se registran en un mapa virtual:
De esta forma un puntero N64 no se interpreta como un offset relativo al archivo generado, sino como la dirección RAM original que era en runtime.
Por ejemplo:
busca qué chunk contiene esa dirección y convierte el puntero virtual en una posición dentro del bloque correspondiente.
Esto es lo que permite que punteros internos que cruzan entre overlays sigan teniendo sentido después de haber extraído los datos de la ROM.
`ParticleData` se conserva como un chunk aparte, pero no forma parte de este mapa de punteros utilizado para reconstruir Rooms, actores y modelos.
12.0.5. De los chunks a los datos importados
Los campos `Header`, `Objects` y `Collision` tienen funciones diferentes:
Por eso en las tools puede terminar apareciendo un asset llamado, por ejemplo:
pero ese `CollisionData` ya es una representación importada creada a partir de:
No corresponde a un chunk adicional copiado directamente desde la ROM.
De forma resumida:
A partir de aquí sí se pueden estudiar los offsets internos de cada una de esas estructuras.
12.1. Cabecera del stage
La cabecera del stage proporciona los punteros principales hacia las Rooms y el Skybox:
`PathRooms` y `NonPathRooms` son listas de punteros terminadas en null:
Por tanto, el recorrido inicial es:
12.2. Descriptor de un bloque de Path Room
Cada entrada de `PathRooms` apunta a un descriptor que contiene tanto la referencia al bloque gráfico como su transformación dentro del stage:
La posición y el yaw permiten transformar los datos locales del bloque a coordenadas globales del nivel.
12.3. WorldBlockGFX y datos del Main Track
Dentro de `WorldBlockGFX` aparecen los datos relacionados con la ruta del Zero-One:
Para reconstruir el Main Track:
12.4. MovementAnimation
La estructura de movimiento contiene un rango de comandos:
Los datos utilizados para resolver el track se encuentran en un trailer de 24 bytes situado inmediatamente antes de `CommandEnd`:
Su estructura es:
Para el stream de posiciones:
El opcode utilizado es:
y el número de puntos de control es:
`Positions` apunta a un array consecutivo de `vec3f`:
12.5. Graph de un modelo
La jerarquía de un modelo se almacena como una secuencia plana de nodos con un stride fijo de:
Cada entrada tiene:
El campo situado en `+0x02` contiene tanto el tipo de billboard como la profundidad jerárquica:
Un `depth` de:
marca el final del graph.
El parent no se almacena como un puntero. Se reconstruye mediante el depth:
De esta forma una lista plana puede reconstruirse como una jerarquía de transforms/bones.
12.6. Descriptor de modelo/objeto
El descriptor de un objeto con modelo contiene los punteros principales de la representación visual:
El recorrido principal es:
12.7. Interpretación de las Display Lists
`RenderFunction` determina cómo debe interpretarse el `DisplayList` de cada nodo.
Los casos principales son:
Esto significa que:
no debe interpretarse siempre como un puntero directo a comandos gráficos. Primero hay que resolver la `RenderFunction` asociada al modelo.
12.8. Cabecera de animación
Cada animación utiliza una cabecera de:
El framerate se obtiene mediante:
`NodeTrackTable` contiene un puntero por nodo siguiendo exactamente el orden del graph:
Si el puntero es `0x00000000`, ese nodo no tiene track para esa animación.
12.9. Comandos de AnimationTrack
Los tracks son streams de comandos de longitud variable.
Cada comando comienza con una cabecera de 4 bytes.
El tipo se obtiene mediante:
Los flags/canales:
Y el incremento:
Los tipos de comando utilizados por el parser son:
Después de la cabecera, el tamaño y la interpretación del payload dependen del `kind` y del número de bits activos en `flags`.
Un comando `Loop` incluye a continuación un puntero de 32 bits hacia una dirección anterior del propio track.
Un comando `Path` incluye un puntero a una estructura de path.
12.10. Tabla de animación de materiales
`AnimationHeader + 0x0C` apunta a una tabla organizada primero por nodo:
La lista de cada nodo contiene un puntero por material:
Por tanto:
12.11. Estructura genérica de Path
Los Paths referenciados por actores o comandos de animación utilizan:
`TimeList` contiene:
valores `float`.
La cantidad de floats de `PointList` depende del tipo de path:
Si `QuarticList` es distinto de null:
12.12. Descriptor del Skybox
El puntero:
lleva al descriptor:
Estos cuatro campos permiten reconstruir el Skybox independientemente de las Rooms.
12.13. Recorrido completo de punteros
Para las Rooms y el Main Track:
Para un modelo:
Y para un Path:
Este mapa permite pasar de la explicación de alto nivel de las secciones anteriores a la lectura directa de las estructuras utilizadas por el juego.
14. Resultado
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:

15. Créditos
La investigación se realizó de forma independiente durante el desarrollo de Pokémon Snap 3DS, comparando con la implementación de Pokemon Snap en NoClip Website y con la decompilación incompleta "Ethteck/pokemonsnap". SubDrag, de N64 Vault ayudó mucho en la búsqueda de offsets con su Golden Eye Editor. Una vez más, mil gracias a todo el equipo que se dedica horas y horas a hacer ingeniería inversa de todas nuestras ROMs favoritas.
Nota: Los offsets hacen referencia a la ROM utilizada durante esta investigación, siendo ésta la versión USA 1.0 recomendada por Pret para el proyecto de decompilación pokesnap, con md-5: 9b10b9d70dac67ae40953148fbe9cb96.
0. Estructura organizada
En Pokémon Snap, una Room puede entenderse como una unidad o bloque lógico del escenario. Cada Room agrupa una porción de geometría del nivel y puede llevar asociados datos adicionales como objetos, actores o información de movimiento. No debe entenderse necesariamente como una “habitación” en sentido literal. Es más bien una subdivisión interna del "stage" (Ruta) utilizada por el juego para organizar y cargar distintas partes del escenario.
A nivel práctico, una Room puede contener:
Código:
Room
|
+-- GFXNode
| +-- transformación del bloque
| +-- geometría
| +-- display lists
| +-- materiales
| +-- texturas
|
+-- ObjectSpawn[]
| +-- actores estáticos
| +-- actores dinámicos
| +-- paths asociados
|
+-- datos adicionales de animación/movimiento
Código:
Path Rooms
bloques del escenario asociados al recorrido principal del Zero-One
Non-Path Rooms
bloques adicionales de escenario que no forman parte de esa secuencia principal
1. Estructura general de un nivel
Un nivel de Pokémon Snap no es un único modelo monolítico. A nivel de runtime es más útil entenderlo como varios sistemas relacionados:
Código:
Stage
|
+-- Listas de Rooms
| +-- Path Rooms
| +-- Non-Path Rooms
|
+-- Geometría estática
|
+-- Actores estáticos
|
+-- Actores dinámicos
| +-- modelo Pokémon
| +-- animaciones
| +-- animación de materiales/visibilidad
| +-- ActorDef / StateGraph / comportamiento
|
+-- Skybox
|
+-- Zero-One
| +-- modelo animado
| +-- animación local
| +-- animación del track
| +-- animación completa
|
+-- Main Track del Zero-One
| +-- un bloque por Path Room
| +-- posición global
| +-- stream de puntos de una B-spline
|
+-- Paths de objetos
+-- asociados a ObjectSpawn
Código:
StageHeader
+0x00 -> lista de Path Rooms
+0x04 -> lista de Non-Path Rooms
+0x08 -> descriptor del Skybox
2. Modelos del escenario estático
Las Rooms son la unidad básica de geometría estática
La geometría del escenario se reconstruye room por room.
Una Room normalizada contiene:
Código:
Room
{
GFXNode Node;
List<ObjectSpawn> Objects;
AnimationData Animation;
}
Ese nodo contiene tanto la transformación de la room como el modelo generado a partir de sus display lists de Nintendo 64.
Código:
Room
|
+-- GFXNode
|
+-- Translation
+-- Model
|
+-- ejecución de display lists
+-- vértices
+-- triángulos
+-- materiales
+-- texturas
+-- vertex colors
2.1. Cómo reconstruir la malla
Los datos gráficos de N64 son stateful. No se obtiene directamente un `Mesh` indexado moderno.
Hay que ejecutar la display list manteniendo el estado de render:
Código:
inicializar estado RSP/render
ejecutar display list:
cargar vértices
actualizar textura
actualizar tile/TLUT
actualizar estado/material
emitir triángulos
separar draw calls cuando cambie un estado relevante
por cada draw call:
crear mesh/submesh
asignar material
Código:
Room
MeshRenderer 0
Mesh
Material 0
MeshRenderer 1
Mesh
Material 1
...
2.2. Ejemplo en Unity
Código:
static void ConvertToStaticRenderer(SkinnedMeshRenderer skinned)
{
GameObject go = skinned.gameObject;
Mesh mesh = skinned.sharedMesh;
Material[] materials = skinned.sharedMaterials;
bool visible = skinned.enabled;
UnityEngine.Object.DestroyImmediate(skinned);
MeshFilter filter = go.GetComponent<MeshFilter>();
if (filter == null)
filter = go.AddComponent<MeshFilter>();
filter.sharedMesh = mesh;
MeshRenderer renderer = go.GetComponent<MeshRenderer>();
if (renderer == null)
renderer = go.AddComponent<MeshRenderer>();
renderer.sharedMaterials = materials;
renderer.enabled = visible;
}
Código:
roomRoot.localPosition = new Vector3(
nativePosition.x * 0.01f,
nativePosition.y * 0.01f,
-nativePosition.z * 0.01f);
roomRoot.localRotation = Quaternion.identity;
roomRoot.localScale = Vector3.one;
2.3. Combinar las mallas
Una vez reconstruida la geometría, puede optimizarse de varias formas.
Por room:
Código:
Room_00 -> Combined Meshes
Room_01 -> Combined Meshes
Room_02 -> Combined Meshes
Código:
All Rooms -> Combined Meshes
Código:
By Material
un renderer por grupo de material equivalente
All
una malla con múltiples submeshes/materiales
3. Modelos dinámicos: Pokémon
Un Pokémon dinámico no es únicamente su modelo.
El actor de runtime combina dos partes:
Código:
Dynamic Actor
|
+-- ActorDef
| +-- Object ID
| +-- init function
| +-- escala
| +-- StateGraph
| +-- motions
| +-- interacciones
| +-- signals
| +-- callbacks auxiliares
|
+-- modelo visual
+-- nodos / bones
+-- geometría de display lists
+-- materiales
+-- texturas
+-- animación esquelética
+-- visibility animation
+-- material animation
3.1. Reconstrucción de un Pokémon
El flujo general es:
Código:
1. Resolver el ActorDef usado por el nivel actual.
2. Parsear sus nodos del modelo.
3. Ejecutar las display lists de cada nodo.
4. Crear jerarquía/bones.
5. Reconstruir meshes y materiales.
6. Parsear animaciones nativas.
7. Asociar cada track con su nodo.
8. Importar keys de materiales.
9. Importar keys de visibilidad.
10. Conservar por separado el ActorDef/StateGraph del nivel.
Código:
Actor Root
|
+-- Armature
| +-- Bone 0
| +-- Bone 1
| +-- ...
|
+-- SkinnedMeshRenderer(s)
|
+-- Animator
|
+-- datos runtime del actor
Los modelos de Pokémon Snap dependen de las matrices activas del pipeline de N64, no de una tabla de pesos como la de un formato moderno.
Al convertirlo a un SkinnedMesh:
Código:
matriz N64 activa
-> bone de Unity
vértice emitido con matriz N
-> peso 1.0 al bone N
Es mejor derivar la influencia del bone del estado de matrices original que intentar adivinarla por proximidad geométrica.
4. Zero-One
El Zero-One es especial porque combina dos sistemas:
Código:
Modelo animado del Zero-One
+
Main Track del nivel
4.1. Modelo
El Zero-One contiene:
Código:
ZeroOne
Nodes[]
Animations[]
SharedOutput
El resultado se coloca bajo un `TrackRoot`:
Código:
ZeroOne
|
+-- TrackRoot
|
+-- jerarquía animada del vehículo
Es útil separar:
Código:
Local Animation
animación interna del modelo del Zero-One
Track Animation
movimiento de TrackRoot a lo largo del Main Track
Full Animation
Local Animation + Track Animation
4.3. Combinación de meshes
Solo deberían combinarse renderers cuando no se destruye información animada.
Deben mantenerse separados si intervienen en:
Código:
visibility animation
material animation
bone palettes diferentes
5. Skybox
El Skybox está referenciado directamente desde la cabecera del nivel y no forma parte de las Rooms normales.
Su descriptor contiene:
Código:
+0x00 -> display list
+0x04 -> función de render
+0x08 -> lista de materiales
+0x0C -> datos de animación de materiales
Código:
descriptor
|
+-- parsear materiales
+-- inicializar estado RSP
+-- ejecutar display list
+-- crear un GFXNode
+-- parsear material animation si existe
+-- construir modelo
Código:
Translation = 0,0,0
Rotation = 0,0,0
Scale = 1,1,1
Parent = none
Esto permite que el cielo tenga geometría estática pero texturas o parámetros que cambien durante la animación.
6. Other Pokémon: modelos almacenados aparte
Algunos Pokémon tienen recursos visuales que no se encuentran en la misma zona que los datos visuales ordinarios del nivel.
Entre los casos tratados por Snap2Unity están:
Código:
Pikachu
Magikarp
Bulbasaur
Zubat
Código:
ActorDef utilizado por el nivel
```
y
[code]
lugar del que se obtiene el modelo visual
Un nivel puede contener un spawn de Pikachu y tener perfectamente definidos:
Código:
Object ID
init function
ActorDef
StateGraph
states / blocks
motions
signals
callbacks
spawn behavior
scale
En ese caso se puede tomar solo la parte visual desde el recurso dedicado de Pikachu.
La combinación correcta sería:
Código:
DATOS DEL STAGE RECURSO POKÉMON DEDICADO
--------------- ------------------------
ActorDef nodos del modelo
StateGraph display lists
states / blocks texturas
motions materiales
signals animaciones
callbacks visibility/material keys
spawn type
scale
\ /
\ /
+---- actor final importado -+
6.2. Mantener el ActorDef del nivel
Si el ActorDef del stage se ha podido parsear, no debe sustituirse por el ActorDef genérico asociado al modelo de Other.
Hay que conservar:
Código:
StateGraph del stage
motions del stage
signals del stage
callbacks auxiliares del stage
spawn type del stage
comportamiento específico del stage
init function del stage
Conceptualmente:
Código:
ActorDef stageDefinition = ParseStageActor(...);
GameObject visual = TryBuildStageVisual(stageDefinition);
if (visual == null)
{
visual = BuildDedicatedPokemonVisual(objectId);
// Se conserva stageDefinition.
// No se reemplaza por la definición genérica
// asociada al modelo visual dedicado.
}
Esto es especialmente importante con Pikachu, porque diferentes apariciones pueden tener máquinas de estados muy distintas aunque visualmente sigan siendo Pikachu.
7. Main Track
El Main Track es la ruta global que recorre el Zero-One.
No debe confundirse con los Paths individuales de Pokémon u objetos.
Se reconstruye a partir de la lista de Path Rooms del nivel.
7.1. Bloques de Path Rooms
La cabecera del nivel contiene:
Código:
+0x00 -> pathRooms
Ese descriptor contiene:
Código:
+0x00 -> WorldBlockGFX
+0x04 -> world X
+0x08 -> world Y
+0x0C -> world Z
+0x10 -> yaw
+0x14 -> reversed
Código:
+0x10 -> road
+0x14 -> numControlLines
+0x18 -> movement animation
+0x1C -> movement animation duration
Código:
1. Es un bloque de geometría del escenario.
2. Define un bloque consecutivo del Main Track.
La estructura de movimiento contiene:
Código:
+0x00 -> commandStart
+0x04 -> commandEnd
Código:
trailer +0x00 -> opcode + cantidad codificada
trailer +0x08 -> PositionsPtr
trailer +0x0C -> segment rate
trailer +0x10 -> timing pointer
trailer +0x14 -> payload pointer adicional
Código:
opcode = 0x02
encodedCount = 24 bits inferiores de word0
Código:
encodedCount + 2
7.3. B-spline cúbica uniforme
Los puntos se evalúan usando ventanas consecutivas de cuatro puntos:
Código:
P0 P1 P2 P3 -> segmento 0
P1 P2 P3 P4 -> segmento 1
P2 P3 P4 P5 -> segmento 2
...
Código:
B0 = (1 - 3u + 3u² - u³) / 6
B1 = (4 - 6u² + 3u³) / 6
B2 = (1 + 3u + 3u² - 3u³) / 6
B3 = u³ / 6
P(u) = B0*P0 + B1*P1 + B2*P2 + B3*P3
7.4. De coordenadas locales del bloque al mundo
Cada bloque tiene su propia posición global y yaw.
Por tanto:
Código:
punto local de la B-spline
-> rotar por yaw del bloque
-> sumar world position del bloque
-> convertir ejes N64 -> Unity
Código:
Unity = (x, y, -z) * 0.01
Código:
ZeroOnePath
ZeroOne_Block_00_Path
ZeroOne_Block_01_Path
ZeroOne_Block_02_Path
Código:
ZeroOnePath
ZeroOne_Path
8. Paths
Los Paths normales son independientes del Main Track.
Están asociados a `ObjectSpawn` concretos dentro de las Rooms.
Código:
ObjectSpawn
ID
Behaviour
Position
Euler
Scale
Path -> TrackPath
Código:
TrackPath
{
Kind
Length
Duration
SegmentRate
Times[]
Points[]
Quartics[]
}
8.1. Sampling
La ruta se puede muestrear en su intervalo temporal nativo:
Código:
minTime = Times[0]
maxTime = Times[Length - 1]
para N muestras:
t = lerp(minTime, maxTime, normalized)
position = GetPathPoint(path, t)
tangent = GetPathTangent(path, t)
Código:
unityPoint = new Vector3(
nativePoint.x * 0.01f,
nativePoint.y * 0.01f,
-nativePoint.z * 0.01f);
unityTangent = new Vector3(
nativeTangent.x,
nativeTangent.y,
-nativeTangent.z);
Código:
SnapImportedPath
RoomIndex
ObjectIndex
ObjectId
Behaviour
Kind
Length
Duration
SegmentRate
Times[]
Points[]
Tangents[]
8.2. Visualización
Para debugging/editor se puede añadir un `LineRenderer`.
También pueden generarse waypoints:
Código:
Path
|
+-- WP_000
+-- WP_001
+-- WP_002
+-- ...
Esto es únicamente una representación del editor. El dato nativo sigue siendo la estructura de path con sus tiempos, puntos y coeficientes.
9. Cómo se divide el nivel en Rooms
La cabecera contiene dos listas terminadas en null:
Código:
+0x00 -> Path Rooms
+0x04 -> Non-Path Rooms
Código:
rooms = []
añadir todas las Path Rooms
añadir todas las Non-Path Rooms
9.1. Path Rooms
Las Path Rooms son los bloques asociados al avance del Zero-One.
Además de su geometría normal, sus descriptores contienen la información necesaria para reconstruir el bloque correspondiente del Main Track.
Código:
Path Room 0
geometría
world placement
bloque de track 0
Path Room 1
geometría
world placement
bloque de track 1
Path Room 2
geometría
world placement
bloque de track 2
Contienen geometría adicional que no forma parte de la secuencia principal de bloques del Zero-One.
Se renderizan exactamente con el mismo pipeline:
Código:
descriptor
-> GFXNode
-> display lists
-> meshes
-> materiales
-> texturas
9.3. Organización en Unity
Se puede ordenar primero por tipo:
Código:
Level
|
+-- StaticGeometry
| +-- Room_00
| +-- Room_01
|
+-- DynamicActors
| +-- Room_00
| +-- Room_01
|
+-- Paths
+-- Room_00
+-- Room_01
Código:
Level
|
+-- Room_00
| +-- StaticGeometry
| +-- StaticActors
| +-- DynamicActors
| +-- Paths
|
+-- Room_01
+-- StaticGeometry
+-- StaticActors
+-- DynamicActors
+-- Paths
Código:
Skybox
Zero-One
ZeroOnePath
sistemas de partículas globales
10. Pipeline completo de reconstrucción
Código:
1. Resolver la cabecera del stage.
2. Leer:
Path Rooms
Non-Path Rooms
Skybox
3. Parsear todas las Rooms.
4. Para cada Room:
ejecutar display lists
reconstruir meshes/materiales/texturas
aplicar world position
5. Parsear sus ObjectSpawn.
6. Para cada actor dinámico:
resolver ActorDef del stage
reconstruir visual del stage cuando sea posible
usar modelo Pokémon dedicado cuando sea necesario
conservar siempre el ActorDef del stage si existe
7. Reconstruir Skybox.
8. Reconstruir modelo del Zero-One.
9. Reconstruir Main Track:
bloques de Path Rooms
world placement
movement animation trailer
control points
B-spline cúbica uniforme
concatenación de bloques
10. Reconstruir independientemente los ObjectSpawn.TrackPath.
11. Organizar contenido por Rooms o por grupos.
12. Exportar assets/prefabs al motor de destino.
Código:
ROOM GEOMETRY
bloques estáticos del mundo
DYNAMIC ACTOR MODEL
parte visual del Pokémon/actor
ACTOR DEFINITION
comportamiento específico del stage
ZERO-ONE MODEL
vehículo animado
MAIN TRACK
rail global reconstruido desde las Path Rooms
OBJECT PATH
trayectoria perteneciente a un ObjectSpawn
SKYBOX
fondo global del stage
12. Mapa de offsets, overlays y recorrido de estructuras
Antes de entrar en los offsets internos de cada estructura conviene explicar cómo se obtiene el bloque de datos de cada nivel desde la ROM.
En Snap2Unity no se trata cada stage como un único rango continuo. Para poder resolver correctamente los punteros virtuales del juego se reconstruye un pequeño archivo de trabajo con varios chunks u overlays, conservando además la dirección RAM en la que cada uno estaba cargado originalmente.
El archivo de nivel utilizado por las tools contiene conceptualmente:
Código:
VP_CRGLevelArchive
|
+-- Data
| +-- StartAddress
|
+-- Code
| +-- CodeStartAddress
|
+-- Photo
| +-- PhotoStartAddress
|
+-- ParticleData
|
+-- Header
+-- Objects
+-- Collision
Código:
Data / Code / Photo / ParticleData
= bloques binarios copiados desde la ROM
StartAddress / CodeStartAddress / PhotoStartAddress
= dirección RAM base original de cada bloque
Header / Objects / Collision
= direcciones virtuales RAM que actúan como
puntos de entrada a estructuras concretas
De la misma forma, el chunk llamado `Photo` no debe confundirse con la estructura runtime `PhotoData` utilizada por el juego al capturar y puntuar una fotografía. Aquí `Photo` es el nombre del overlay binario adicional del stage.
12.0.1. Tabla de overlays del stage
Los rangos principales `Data` y `Code` no están hardcodeados individualmente para cada nivel. Se obtienen de una tabla de la ROM.
La entrada se calcula como:
Código:
tableOffset = 0x57580 + sceneId * 0x24
Código:
tableOffset + 0x00 -> Data ROM start
tableOffset + 0x04 -> Data ROM end
tableOffset + 0x08 -> Data RAM start
Código:
Data.length = DataRomEnd - DataRomStart
StartAddress = DataRamStart
Código:
tableOffset + 0x24 -> Code ROM start
tableOffset + 0x28 -> Code ROM end
tableOffset + 0x2C -> Code RAM start
Código:
Code.length = CodeRomEnd - CodeRomStart
CodeStartAddress = CodeRamStart
Código:
ROM[DataRomStart .. DataRomEnd]
-> Data
ROM[CodeRomStart .. CodeRomEnd]
-> Code
12.0.2. Offsets específicos utilizados por cada nivel
Además de `Data` y `Code`, cada stage utiliza un overlay `Photo` conocido y tres direcciones de entrada: `Header`, `Objects` y `Collision`.
Los valores utilizados por Snap2Unity para la ROM USA 1.0 son:
Código:
BEACH
Scene ID: 0x10
Photo ROM: 0x0013C780
Photo RAM: 0x801B0310
Photo Length: 0x00026530
Header: 0x8011B914
Objects: 0x802CBEE4
Collision: 0x80318F00
TUNNEL
Scene ID: 0x12
Photo ROM: 0x001D1D90
Photo RAM: 0x8018BC50
Photo Length: 0x000240E0
Header: 0x8011E6CC
Objects: 0x802EDFAC
Collision: 0x80326EE0
CAVE
Scene ID: 0x14
Photo ROM: 0x0027AB80
Photo RAM: 0x801AEDF0
Photo Length: 0x0001F610
Header: 0x8012A0E8
Objects: 0x802C6234
Collision: 0x80317610
RIVER
Scene ID: 0x16
Photo ROM: 0x0030AF90
Photo RAM: 0x8019AEE0
Photo Length: 0x0001BC80
Header: 0x8012AC90
Objects: 0x802E271C
Collision: 0x80321560
VOLCANO
Scene ID: 0x18
Photo ROM: 0x003D0560
Photo RAM: 0x801A9900
Photo Length: 0x00025E70
Header: 0x800FFFB8
Objects: 0x802E0D44
Collision: 0x8031D4D0
VALLEY
Scene ID: 0x1A
Photo ROM: 0x0047CF30
Photo RAM: 0x80186B10
Photo Length: 0x0002B230
Header: 0x80100720
Objects: 0x802D282C
Collision: 0x8031F9C0
RAINBOW CLOUD
Scene ID: 0x1C
Photo ROM: 0x004EC000
Photo RAM: 0x80139C50
Photo Length: 0x00004610
Header: 0x800F5DA0
Objects: 0x8034AB34
Collision: 0x00000000
12.0.3. ParticleData
`ParticleData` se extrae como otro rango independiente de ROM.
Los límites conocidos utilizados son:
Código:
0x00AB5860
0x00AB85E0
0x00ABE7A0
0x00AC6890
0x00AC8510
0x00ACF6F0
0x00AD0E00
0x00ADD310
0x00ADEC60
Código:
particleIndex = (sceneId - 0x0E) >> 1
Código:
ParticleData =
ROM[
ParticleAddresses[particleIndex]
..
ParticleAddresses[particleIndex + 1]
]
Código:
COMMON / 0x0E
0x00AB5860 .. 0x00AB85E0
BEACH / 0x10
0x00AB85E0 .. 0x00ABE7A0
TUNNEL / 0x12
0x00ABE7A0 .. 0x00AC6890
CAVE / 0x14
0x00AC6890 .. 0x00AC8510
RIVER / 0x16
0x00AC8510 .. 0x00ACF6F0
VOLCANO / 0x18
0x00ACF6F0 .. 0x00AD0E00
VALLEY / 0x1A
0x00AD0E00 .. 0x00ADD310
RAINBOW CLOUD / 0x1C
0x00ADD310 .. 0x00ADEC60
Una vez extraídos, los tres overlays con espacio de direcciones propio se registran en un mapa virtual:
Código:
CRGDataMap
|
+-- Data
| Start = StartAddress
|
+-- Code
| Start = CodeStartAddress
|
+-- Photo
Start = PhotoStartAddress
Por ejemplo:
Código:
dataMap.Deref(level.Header)
dataMap.Deref(level.Objects)
dataMap.Deref(level.Collision)
Esto es lo que permite que punteros internos que cruzan entre overlays sigan teniendo sentido después de haber extraído los datos de la ROM.
`ParticleData` se conserva como un chunk aparte, pero no forma parte de este mapa de punteros utilizado para reconstruir Rooms, actores y modelos.
12.0.5. De los chunks a los datos importados
Los campos `Header`, `Objects` y `Collision` tienen funciones diferentes:
Código:
Header
-> estructura general del stage
-> Path Rooms
-> Non-Path Rooms
-> Skybox
Objects
-> tablas/definiciones de actores del stage
-> ActorDef utilizados por los ObjectSpawn
Collision
-> raíz del CollisionTree nativo
-> nodos de partición
-> GroundPlanes
Código:
CollisionData
Código:
level.Collision
-> CRGDataMap
-> ParseCollisionTree(...)
-> estructura de colisión importada
De forma resumida:
Código:
ROM
|
+-- tabla de overlays
| |
| +-- Data
| +-- Code
|
+-- rango Photo específico del stage
| |
| +-- Photo
|
+-- tabla/rangos de partículas
|
+-- ParticleData
+
|
+-- Header address
+-- Objects address
+-- Collision address
|
v
VP_CRGLevelArchive
|
v
CRGDataMap
|
+----------+----------+
| | |
Rooms Actors CollisionTree
12.1. Cabecera del stage
La cabecera del stage proporciona los punteros principales hacia las Rooms y el Skybox:
Código:
StageHeader
+0x00 u32* PathRooms
+0x04 u32* NonPathRooms
+0x08 u32* SkyboxDescriptor
Código:
RoomList
+0x00 -> Room/Block 0
+0x04 -> Room/Block 1
+0x08 -> Room/Block 2
...
+0xNN -> 0x00000000
Código:
StageHeader
|
+-- +0x00 -> PathRooms[]
|
+-- +0x04 -> NonPathRooms[]
|
+-- +0x08 -> SkyboxDescriptor
Cada entrada de `PathRooms` apunta a un descriptor que contiene tanto la referencia al bloque gráfico como su transformación dentro del stage:
Código:
PathRoomBlock
+0x00 u32* WorldBlockGFX
+0x04 f32 WorldX
+0x08 f32 WorldY
+0x0C f32 WorldZ
+0x10 f32 Yaw
+0x14 u32 Reversed / flags
12.3. WorldBlockGFX y datos del Main Track
Dentro de `WorldBlockGFX` aparecen los datos relacionados con la ruta del Zero-One:
Código:
WorldBlockGFX
+0x10 u32* Road / datos relacionados con la ruta
+0x14 u32 NumControlLines
+0x18 u32* MovementAnimation
+0x1C f32 MovementAnimationDuration
Código:
WorldBlockGFX + 0x18
-> MovementAnimation
La estructura de movimiento contiene un rango de comandos:
Código:
MovementAnimation
+0x00 u32* CommandStart
+0x04 u32* CommandEnd
Código:
trailer = CommandEnd - 0x18
Código:
MovementTrailer
+0x00 u32 OpcodeAndCount
+0x04 u32 campo auxiliar
+0x08 u32* Positions
+0x0C f32 SegmentRate
+0x10 u32* Times / timing
+0x14 u32* AdditionalPayload
Código:
opcode = OpcodeAndCount >> 24
encodedCount = OpcodeAndCount & 0x00FFFFFF
Código:
0x02
Código:
pointCount = encodedCount + 2
Código:
Positions
+0x00 f32 X0
+0x04 f32 Y0
+0x08 f32 Z0
+0x0C f32 X1
+0x10 f32 Y1
+0x14 f32 Z1
...
La jerarquía de un modelo se almacena como una secuencia plana de nodos con un stride fijo de:
Código:
0x2C bytes por nodo
Código:
GFXNodeEntry
+0x00 metadata / flags
+0x02 u16 BillboardAndDepth
+0x04 u32* DisplayList
+0x08 f32 TranslationX
+0x0C f32 TranslationY
+0x10 f32 TranslationZ
+0x14 f32 EulerX
+0x18 f32 EulerY
+0x1C f32 EulerZ
+0x20 f32 ScaleX
+0x24 f32 ScaleY
+0x28 f32 ScaleZ
Código:
billboard = byte(+0x02) >> 4
depth = be16(+0x02) & 0x0FFF
Código:
0x12
El parent no se almacena como un puntero. Se reconstruye mediante el depth:
Código:
depth 0 -> no tiene parent
depth N -> su parent es el último nodo
registrado en depth N-1
12.6. Descriptor de modelo/objeto
El descriptor de un objeto con modelo contiene los punteros principales de la representación visual:
Código:
ObjectModelDescriptor
+0x00 u32* GraphStart
+0x04 u32* MaterialList
+0x08 u32* RenderFunction
+0x0C u32* AnimationTable
+0x10 f32 ScaleX
+0x14 f32 ScaleY
+0x18 f32 ScaleZ
+0x1C f32 CenterX
+0x20 f32 CenterY
+0x24 f32 CenterZ
+0x28 f32 Radius
+0x2C u16 Flags
Código:
ObjectModelDescriptor
|
+-- +0x00 -> GraphStart
+-- +0x04 -> MaterialList
+-- +0x08 -> RenderFunction
+-- +0x0C -> AnimationTable
`RenderFunction` determina cómo debe interpretarse el `DisplayList` de cada nodo.
Los casos principales son:
Código:
Direct
DisplayList -> stream F3DEX2
Split
DisplayList -> pareja de punteros a display lists
Multi
DisplayList -> secuencia de:
state index
display-list pointer
MultiSplit
combinación de indexed states
y split display lists
Código:
GFXNodeEntry + 0x04
12.8. Cabecera de animación
Cada animación utiliza una cabecera de:
Código:
AnimationHeader
+0x00 f32 Rate
+0x04 f32 FrameCount
+0x08 u32* NodeTrackTable
+0x0C u32* MaterialTrackTable
Código:
fps = 30.0 * Rate
Código:
NodeTrackTable
+0x00 -> track del nodo 0
+0x04 -> track del nodo 1
+0x08 -> track del nodo 2
...
12.9. Comandos de AnimationTrack
Los tracks son streams de comandos de longitud variable.
Cada comando comienza con una cabecera de 4 bytes.
El tipo se obtiene mediante:
Código:
kind = byte(+0x00) >> 1
Código:
flags = (be32(command) >> 15) & 0x03FF
Código:
increment = be16(+0x02) & 0x7FFF
Código:
0x00 Exit
0x01 InitFunc
0x02 Block
0x03 LerpBlock
0x04 Lerp
0x05 SplineVelBlock
0x06 SplineVel
0x07 SplineEnd
0x08 SplineBlock
0x09 Spline
0x0A StepBlock
0x0B Step
0x0C Skip
0x0D Path
0x0E Loop
0x0F SetFlags
0x10 Func
0x11 MultiFunc
0x12 ColorStepBlock
0x13 ColorStep
0x14 ColorLerpBlock
0x15 ColorLerp
0x16 SetColor
Un comando `Loop` incluye a continuación un puntero de 32 bits hacia una dirección anterior del propio track.
Un comando `Path` incluye un puntero a una estructura de path.
12.10. Tabla de animación de materiales
`AnimationHeader + 0x0C` apunta a una tabla organizada primero por nodo:
Código:
MaterialTrackTable
+ nodeIndex * 4
-> MaterialListForNode
Código:
MaterialListForNode
+ materialIndex * 4
-> AnimationTrack
Código:
AnimationHeader
|
+-- +0x0C -> tabla de nodos
|
+-- nodeIndex * 4
|
+-- lista de materiales
|
+-- materialIndex * 4
|
+-- AnimationTrack
Los Paths referenciados por actores o comandos de animación utilizan:
Código:
Path
+0x00 u8 PathKind
+0x02 u16 Length
+0x04 f32 SegmentRate
+0x08 u32* PointList
+0x0C f32 Duration
+0x10 u32* TimeList
+0x14 u32* QuarticList
Código:
Length
La cantidad de floats de `PointList` depende del tipo de path:
Código:
default:
(Length + 2) * 3
Bezier:
Length * 9
Linear:
Length * 3
Código:
(Length - 1) * 5 floats
El puntero:
Código:
StageHeader + 0x08
Código:
SkyboxDescriptor
+0x00 u32* DisplayList
+0x04 u32* RenderFunction
+0x08 u32* MaterialList
+0x0C u32* MaterialAnimation
12.13. Recorrido completo de punteros
Para las Rooms y el Main Track:
Código:
StageHeader
|
+-- +0x00 PathRooms[]
| |
| +-- PathRoomBlock
| |
| +-- +0x00 WorldBlockGFX
| |
| +-- +0x18 MovementAnimation
| |
| +-- CommandEnd - 0x18
| |
| +-- +0x08 Positions
|
+-- +0x04 NonPathRooms[]
|
+-- +0x08 SkyboxDescriptor
Código:
ObjectModelDescriptor
|
+-- +0x00 GraphStart
| |
| +-- nodos cada 0x2C bytes
|
+-- +0x04 MaterialList
|
+-- +0x08 RenderFunction
|
+-- +0x0C AnimationTable
|
+-- AnimationHeader
|
+-- +0x08 NodeTrackTable
|
+-- +0x0C MaterialTrackTable
Código:
Path pointer
|
+-- +0x00 PathKind
+-- +0x02 Length
+-- +0x04 SegmentRate
+-- +0x08 PointList
+-- +0x0C Duration
+-- +0x10 TimeList
+-- +0x14 QuarticList
14. Resultado
Como una imagen vale más que mil palabras, aquí está el resultado de toda esta investigación:

15. Créditos
La investigación se realizó de forma independiente durante el desarrollo de Pokémon Snap 3DS, comparando con la implementación de Pokemon Snap en NoClip Website y con la decompilación incompleta "Ethteck/pokemonsnap". SubDrag, de N64 Vault ayudó mucho en la búsqueda de offsets con su Golden Eye Editor. Una vez más, mil gracias a todo el equipo que se dedica horas y horas a hacer ingeniería inversa de todas nuestras ROMs favoritas.