# 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.