375 lines
13 KiB
Markdown
375 lines
13 KiB
Markdown
# GC Camera Reverse Notes
|
|
|
|
Status: work in progress. This file records facts extracted from `game471.exe`
|
|
and `docs/stage.pat` so the Vectorail camera port does not keep accumulating
|
|
guesswork.
|
|
|
|
## Stage Camera Records
|
|
|
|
`docs/stage.pat` defines a 59-byte camera key:
|
|
|
|
```c
|
|
struct Camera {
|
|
u32 timeMs;
|
|
u8 aMode;
|
|
u8 fMode;
|
|
float dist;
|
|
float rotationA[2];
|
|
float originOff[3];
|
|
u8 projType;
|
|
float fieldFar[3];
|
|
float fieldNear[3];
|
|
float rotationB[1];
|
|
};
|
|
```
|
|
|
|
Observed across stage files:
|
|
|
|
- `fieldFar[]` and `fieldNear[]` are almost always zero.
|
|
- Therefore `fieldNear[0]` is not the gameplay FOV. The current Vectorail GC
|
|
loader uses it as `fov`; that is almost certainly wrong.
|
|
- Useful camera fields are `dist`, `rotationA[0]`, `rotationA[1]`,
|
|
`originOff[0..2]`, `rotationB`, and the mode bytes.
|
|
- Common defaults seen in real stages:
|
|
- `projType` is usually `1`.
|
|
- `fMode` is usually `1` or `2`, with some `0`.
|
|
- `aMode` is usually `0`, but `1..6` also appear.
|
|
|
|
Example from `ac_10pt8tion_hard.dat`:
|
|
|
|
```text
|
|
time=0 aMode=0 fMode=0 dist=22 rotA=(0,90) originOff=(0,0,9) projType=1 rotB=0
|
|
```
|
|
|
|
## Runtime Matrix Path
|
|
|
|
The apparent gameplay matrix slots in the tune object are not where the camera
|
|
is calculated:
|
|
|
|
- `tune + 0x150`: saved D3D `VIEW` matrix.
|
|
- `tune + 0x110`: saved D3D `PROJECTION` matrix.
|
|
- `tune + 0x0d0`: alternate saved D3D `VIEW` matrix.
|
|
- `tune + 0x090`: alternate saved D3D `PROJECTION` matrix.
|
|
|
|
Functions confirmed:
|
|
|
|
- `FUN_006449f0` calls `GetTransform(2, tune+0x150)` and
|
|
`GetTransform(3, tune+0x110)`.
|
|
- `FUN_00645e00` does the same after setting a 2D ortho projection.
|
|
- `FUN_00648d40` also snapshots `VIEW/PROJECTION` at render start.
|
|
- `FUN_0064da90` snapshots `VIEW/PROJECTION` into `+0xd0/+0x90`.
|
|
|
|
So these offsets are consumers/snapshots, not the camera mechanics.
|
|
|
|
The D3D device calls use the normal Direct3D 9 transform ids:
|
|
|
|
```text
|
|
0xb0 on IDirect3DDevice9 vtable = SetTransform
|
|
+0xe4 on IDirect3DDevice9 vtable = GetRenderState
|
|
+0xb4 on IDirect3DDevice9 vtable = GetTransform
|
|
2 = D3DTS_VIEW
|
|
3 = D3DTS_PROJECTION
|
|
0x100 = D3DTS_WORLD
|
|
```
|
|
|
|
`FUN_004e72c0` / `FUN_004e5af0` are a central D3D state cache, not camera
|
|
math. The cache fields are:
|
|
|
|
```text
|
|
+0x3a8 cached WORLD
|
|
+0x3e8 cached VIEW
|
|
+0x428 cached PROJECTION
|
|
+0x390 cached viewport
|
|
```
|
|
|
|
Wrapper setters identified:
|
|
|
|
```text
|
|
FUN_004e3c10 = set view matrix, copies 16 floats to +0x310 and calls SetTransform(2)
|
|
FUN_004e3bc0 = set projection matrix, copies 16 floats to +0x350 and calls SetTransform(3)
|
|
FUN_004e3c60 = copy 16-float matrix to +0x180, no direct D3D SetTransform
|
|
```
|
|
|
|
These are useful for tracing the render path, but current xrefs show most
|
|
gameplay code uses the global D3D device helpers and snapshots rather than
|
|
directly calling these wrappers.
|
|
|
|
## Common 3D Camera
|
|
|
|
`CCommon3DCamera` is identified around `0063d820..0063d9d0`.
|
|
|
|
Struct layout:
|
|
|
|
```text
|
|
+0x04 eye vec3
|
|
+0x10 target vec3
|
|
+0x1c up vec3
|
|
+0x28 base fov, radians
|
|
+0x2c camera mode
|
|
```
|
|
|
|
Projection:
|
|
|
|
```c
|
|
D3DXMatrixPerspectiveFovLH(
|
|
out,
|
|
camera->fovRad * fovMul,
|
|
viewportWidth / viewportHeight,
|
|
nearZ,
|
|
farZ);
|
|
```
|
|
|
|
Important difference from Vectorail: the original uses the runtime viewport
|
|
aspect, not a hardcoded `1280.0 / 720.0`.
|
|
|
|
View:
|
|
|
|
```c
|
|
D3DXMatrixLookAtLH(out, eye, target, up);
|
|
```
|
|
|
|
when camera mode is `0`.
|
|
|
|
Known refs:
|
|
|
|
```text
|
|
FUN_0063d820 projection, caller FUN_00577840
|
|
FUN_0063d9d0 view/look-at, caller FUN_00577840
|
|
FUN_0063d8a0 constructor, caller FUN_00578570
|
|
```
|
|
|
|
Only one `D3DXMatrixPerspectiveFovLH` call is present in the binary, and it is
|
|
in `FUN_0063d820`. Only two `D3DXMatrixLookAtLH` calls are present:
|
|
`FUN_0063d9d0` and `FUN_0065b1f0`. This makes `CCommon3DCamera` the strongest
|
|
candidate for the gameplay camera path, but its stage-game caller has not been
|
|
found yet.
|
|
|
|
## Stage Object Transform Detour (Corrected)
|
|
|
|
`FUN_00643570` is not a camera or stage-object evaluator. It belongs to the
|
|
temporary rail-strip construction path. The actual scene-object loop is
|
|
`FUN_006445b0`; it iterates `0xdc`-byte runtime objects from
|
|
`stageResource + 0x140` and calls `FUN_00643b20` for their transforms and
|
|
`FUN_00643fd0` for their colors.
|
|
|
|
Inside `FUN_00643b20`:
|
|
|
|
```text
|
|
+0xa4/+0xa8/+0xac count/times/vec3 track-like stream
|
|
+0xb0/+0xb4/+0xb8 count/times/vec3 stream
|
|
+0xbc/+0xc0/+0xc4 count/times/vec3 stream
|
|
```
|
|
|
|
It samples every keyframe array through `FUN_005e9100`. Movement, scale, and
|
|
rotation are linearly interpolated vec3 values. The final Euler rotation is
|
|
then converted to a quaternion using the engine's `(-Y, X, Z)` convention and
|
|
converted to a matrix; there is no quaternion slerp. Base and
|
|
animated translations, rotations and scales are separate matrices which are
|
|
multiplied in this exact sequence:
|
|
|
|
```text
|
|
T(base) * T(move) * R(base) * R(animated) * S(base) * S(animated)
|
|
```
|
|
|
|
The color evaluator selects a keyed RGBA value in
|
|
place of the base color and linearly interpolates it when enabled.
|
|
|
|
The recovered helper operations include:
|
|
|
|
```text
|
|
FUN_005df6a0 = vec3 subtract
|
|
FUN_005df660 = vec3 scale
|
|
FUN_005df6f0 = vec3 add
|
|
```
|
|
|
|
and builds matrices through the local matrix stack:
|
|
|
|
```text
|
|
FUN_005e0650 = translation matrix
|
|
FUN_005e0610 = scale matrix
|
|
FUN_005e0330 = Euler-degrees-to-rotation-matrix entry
|
|
FUN_005e0220 = quaternion from the (-Y, X, Z) half angles
|
|
FUN_005df790 = quaternion-to-column-major-matrix
|
|
FUN_005e0680 = in-place right matrix multiply
|
|
```
|
|
|
|
The first boolean in transform/color key records becomes runtime metadata byte
|
|
`+5` and identifies repeating key ranges. The second becomes byte `+4` and
|
|
enables interpolation from the active key to the next. Visibility uses a
|
|
separate fixed fade window: `_DAT_006fcae8` is `250.0f` milliseconds.
|
|
|
|
The renderer also checks runtime object `+0xd4`, populated from the newer stage
|
|
supplemental parent-index stream. When present, it evaluates the parent and
|
|
child independently, multiplies their matrices, and multiplies their RGBA via
|
|
`FUN_0043a540`. It does not recursively walk more than one parent level.
|
|
|
|
## Stage Camera Evaluator (Confirmed)
|
|
|
|
`FUN_005e9e20(stage, result, timeMs, interpolate)` is the stage-camera
|
|
evaluator. The output is 44 bytes:
|
|
|
|
```text
|
|
+0x00 eye vec3
|
|
+0x0c target vec3
|
|
+0x18 up vec3
|
|
+0x24 projType byte
|
|
+0x28 projBlend float (0 or 1 outside an fMode=1 transition)
|
|
```
|
|
|
|
`FUN_005e0ca0` passes those first three vectors to `FUN_005e0800`, the game's
|
|
look-at matrix builder. This proves the vector order and bypasses the earlier
|
|
`CCommon3DCamera` false lead.
|
|
|
|
The 59-byte wire key becomes this 0x44-byte runtime key:
|
|
|
|
```text
|
|
+0x00 aMode (int)
|
|
+0x04 fMode (int)
|
|
+0x08 dist
|
|
+0x0c rotationA.x
|
|
+0x10 rotationA.y
|
|
+0x14 rotationA.z = 0
|
|
+0x18 originOff vec3
|
|
+0x24 projType byte
|
|
+0x28 fieldFar vec3
|
|
+0x34 fieldNear vec3
|
|
+0x40 rotationB
|
|
```
|
|
|
|
`FUN_005ed4c0` reads every serialized field directly and does not reject
|
|
non-finite floats. This matters for `ac_comet_{easy,normal,hard}.dat`: their
|
|
first key at 0 ms intentionally has NaN in both `rotationA` components, and the
|
|
next key starts at 1316 ms. The Linux loader must retain that first key; dropping
|
|
it activates the second camera too early and changes the intro.
|
|
|
|
Let `T(t)` be the linearly interpolated track position and `R` be the orbit
|
|
vector calculated from `rotationA` and `dist`. The seven `aMode` branches are:
|
|
|
|
```text
|
|
aMode 0: target = T(t) + originOff; eye = target + R
|
|
aMode 1: target = T(keyTime) + originOff; eye = target + R
|
|
aMode 2: for camera index > 1, return camera(keyTime - 1ms, no interpolation);
|
|
otherwise the same as mode 0
|
|
aMode 3: target = T(t) + originOff;
|
|
eye = T(keyTime) + originOff + R
|
|
aMode 4: target = fieldFar; eye = fieldNear
|
|
aMode 5: target = T(t) + originOff; eye = fieldNear
|
|
aMode 6: target = fieldFar; eye = T(t) + R
|
|
```
|
|
|
|
This confirms that `originOff` is a world-space addition, not a rail-local
|
|
offset.
|
|
|
|
### Orbit axis convention
|
|
|
|
`FUN_005dfaa0` creates a quaternion from the Euler tuple
|
|
`(-rotationA.y, rotationA.x, rotationA.z)` in degrees. `FUN_005e01d0` then
|
|
transforms `(0, 0, dist)` by its matrix. In particular, the first 10pt8tion
|
|
key `rotationA=(0,90), dist=22` produces `R=(0,22,0)`: the camera is directly
|
|
above the rail, not behind it.
|
|
|
|
### Interpolation modes
|
|
|
|
Interpolation only happens strictly between the active key and the following
|
|
key:
|
|
|
|
```text
|
|
fMode 0: evaluate the active key directly
|
|
fMode 1: evaluate camera states at both key timestamps, remove endpoint roll,
|
|
linearly interpolate eye/target/up, normalize up, then apply the
|
|
linearly interpolated rotationB
|
|
fMode 2: linearly interpolate all raw float/vector fields, retain the active
|
|
key's aMode/fMode/projType, then evaluate that mixed key
|
|
```
|
|
|
|
There is no shortest-angle interpolation in `fMode=2`; the rotation floats are
|
|
mixed as ordinary scalars.
|
|
|
|
### Up vector and roll
|
|
|
|
`FUN_005e0ad0` rebuilds `up` by projecting world-up `(0,1,0)` onto the plane
|
|
normal to `target-eye`. If the two directions are collinear it falls back to
|
|
`(0,0,1)`. `rotationB` then rotates that up vector around the normalized view
|
|
axis.
|
|
|
|
### Gameplay projection
|
|
|
|
`FUN_0063fd60(cameraState, fovMul)` consumes the complete 44-byte result of
|
|
the stage-camera evaluator. It selects or blends two projections:
|
|
|
|
```text
|
|
projBlend <= 0: orthographic
|
|
projBlend >= 1: perspective
|
|
0 < projBlend < 1:
|
|
ortho + (perspective - ortho) * clamp(projBlend^3, 0, 1)
|
|
```
|
|
|
|
The perspective matrix uses vertical FOV `75 * fovMul`, the live viewport
|
|
aspect, near `1` and far `1000`. The orthographic half-height is
|
|
`distance(eye,target) * tan(FOV/2)` and its half-width is that value times the
|
|
viewport aspect. Consequently objects on the target plane keep the same
|
|
screen size while the projection changes, but depth shrinking disappears in
|
|
orthographic sections.
|
|
|
|
The distance is not clamped to the near plane. A zero-length camera therefore
|
|
also produces zero orthographic extents in the original. Likewise, its vector
|
|
normalizer returns `(0,0,0)` for a zero-length input instead of propagating a
|
|
NaN as `glm::normalize` does. The Linux port mirrors both edge cases and uses
|
|
explicit left-handed view/projection builders; the `NO` depth variant is the
|
|
OpenGL backend adaptation of the original D3D left-handed matrices.
|
|
|
|
The evaluator sets `projBlend` to `0` for `projType=0` and `1` for
|
|
`projType=1`. An `fMode=1` transition linearly interpolates the endpoint
|
|
blend values before the projection builder applies the cubic curve.
|
|
|
|
The gameplay owner initializes its `fovMul` field at `+0x238` to `1.0` and the
|
|
normal update path passes it unchanged to `FUN_0063fd60`; therefore the normal
|
|
stage camera uses a 75-degree vertical FOV. This scalar is runtime state rather
|
|
than part of the 59-byte camera record. The same is true of viewport aspect and
|
|
the fixed near/far planes.
|
|
|
|
### Gameplay item projection modifiers
|
|
|
|
The value checked at global gameplay-state offset `+0xcd8` is the selected item
|
|
ID from `data/boot/item.dat`, not a camera or screen-mode enum:
|
|
|
|
- `4`, `MIRROR`: flip the stage horizontally;
|
|
- `6`, `REVERSE`: flip the stage horizontally and vertically.
|
|
|
|
At the end of `FUN_0063ff90`, both items negate the first column of the 3D
|
|
projection at gameplay-camera offset `+0x90` and the 2D orthographic projection
|
|
at `+0x110`. `REVERSE` also negates their second columns. The view matrix at
|
|
`+0xd0` is not changed. Course geometry switches from `D3DCULL_CCW` to
|
|
`D3DCULL_CW` only for `MIRROR`; the two-axis `REVERSE` transform preserves
|
|
triangle winding.
|
|
|
|
## Vectorail Port Status
|
|
|
|
The player now imports the raw camera keys (including non-finite sentinel
|
|
values) and ports the confirmed `aMode`,
|
|
`fMode`, orbit, up/roll, projection type/blend, FOV and clipping-plane
|
|
behavior. GC cameras are evaluated directly without the legacy follow-camera
|
|
smoothing. Remaining camera work is validation against captured original
|
|
frames.
|
|
|
|
## Switch and Android cross-version validation
|
|
|
|
The base Switch executable retains `CTuneGameData` RTTI and the original GC
|
|
source filenames. Its stripped `CTuneGameData::GetWayPosition` and
|
|
`CTuneGameData::GetCameraData` implementations were matched to the named
|
|
Android ARM64 functions by class layout, control flow, and camera-mode
|
|
behavior.
|
|
|
|
Switch stores camera timestamps separately from a 0x44-byte runtime payload,
|
|
where Android uses an interleaved 0x48-byte record. The evaluator itself still
|
|
implements the same `aMode` branches `0..6`, `fMode` 1/2 interpolation, up and
|
|
roll construction, projection type/blend, and the mode-2 one-millisecond
|
|
hold. This provides an independent confirmation of the arcade reconstruction
|
|
above.
|
|
|
|
FOV is platform policy rather than a stage field. Android chooses between
|
|
`60.0`, `68.5`, and `75.0` degrees for its 3:2, 16:9, and iPhone-X layout
|
|
modes. That mobile-only aspect switch does not override the arcade
|
|
executable's confirmed 75-degree gameplay FOV used by the Linux arcade target.
|
|
See `re_gc_switch.md` for the Switch addresses and asset inventory.
|