Files
BT411/context/cockpit-view.md
T
arcattackandClaude Fable 5 abaa145b6f Cockpit: authentic eyepoint + canopy lattice render (task #55)
The first-person cockpit now shows by default and matches gameplay footage
(verified on the Madcat). Two decomp-verified reconstructions:

EYEPOINT (FUN_004579a8 + caller part_014.c:5525, FUN_004c22c4):
- eye offset = siteeyepoint GetBaseOffset (not GetSegmentToEntity, not the
  upright hack), parented on the PARENT segment DCS (btl4vid.cpp)
- live view = affine INVERSE of the eye world matrix (L4VIDRND Execute) --
  replaces the hand-rolled LookAtRH whose +Z-forward/+Y-up row guess aimed
  some mechs into the canopy; combine order fixed to baseOffset * R
- chase camera basis + aim boresight re-expressed for the inverse convention
  (row2 = back); boresight now derives from the view matrix (the stale rows
  aimed the pick ray at the sky = no target, no discharge)

CANOPY (*_COP.BGF, dpl_Punchize geogroups):
- every shell is an open strut lattice (38-59% boundary edges, all 12 mechs);
  rendered single-sided with per-face INWARD winding (l/r torso patches are
  mirrored, so no global winding works) the struts are the dark frame and the
  openings show the world
- frame colour = the 'softer' ramp near its dark end (texture-less unlit ramp;
  BLXSKIN.BMF has no texture -- the old "punch texels" theory was wrong)
- scoped by filename (meshIsCop); wrong drop-punch + view-cut paths removed
- BT_FORCE_MODEL=<name> forces the player mech for per-mech bring-up

KB: new context/cockpit-view.md topic; punch split corrected in bgf-format;
gotchas 14 (LookAt axis guess) + 15 (per-patch edge namespaces); env gates +
eye/punch addresses in decomp-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:46:54 -05:00

114 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: cockpit-view
title: "First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint"
status: living
source_sections: "task #55 (2026-07-10); reference/decomp part_007.c/part_013.c/part_014.c/part_011.c; engine/MUNGA_L4/bgfload.cpp, L4VIDRND.cpp; game/reconstructed/btl4vid.cpp"
related_topics: [rendering, bgf-format, gauges-hud, subsystems, decomp-reference]
key_terms: [material-ramp, siteeyepoint]
open_questions: ["gyro eye-joint steady offset + hit-bounce (task #56)", "non-Madcat mechs blocked at rest eye position", "exact unlit ramp index for the frame colour (BT_COP_RAMP_L=0.08 is T3 tuned-to-footage)"]
---
# First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint
The pod's only view was the cockpit eyepoint looking out through a per-mech canopy shell.
Task #55 reconstructed both halves — the CANOPY rendering and the EYE camera — from the binary.
Verified in-game on the Madcat against gameplay footage. [T2]
## The canopy shell (*_COP.BGF)
- One per mech (12: AVX BLX FIX JAX LOX MAX OWX RAX SNX STX THX VUX; model→skeleton table in
BTL4.RES @~3232850: `madcat=mad`, `blkhawk=blh`, … — the INSIDE skeleton is the X-variant, so
Madcat = `MAX_COP`). It is the torso segment's SkeletonType_A mesh; the inside view loads
EXACTLY ONE segment mesh (the `_cop`) — 25 others hidden. [T2]
- Material: `<pfx>skin:blakskn_dz_{u,r,l}torso_mtl` — DIFFUSE/AMBIENT (0,0,0) + RAMP_REF
`softer`, **NO texture** (BLXSKIN.BMF is 238 bytes total; byte-verified). The 56 nonzero-UV
verts in BLX_COP sample nothing (planar-projection authoring leftover). [T1]
- Geogroups carry the `SV_SPECIAL " dz_*torso PUNCH "` token → `dpl_Punchize` (see
[[bgf-format]] PUNCH). The board opcode is a constant — the frame/window discriminator is the
GEOMETRY, not a key. [T1]
- **Every `_cop` is an OPEN STRUT LATTICE** (boundary-edge ratios, per-patch edge namespacing:
38-59% across all 12). The struts ARE the frame; the openings ARE the windows. Beware the
edge-counting trap: counting edges across patches (patch-local indices collide) undercounts
boundaries catastrophically (BLX "7%" wrong → 59% right). [T1]
- **Rendering (bgfload.cpp):** keyed by filename (`meshIsCop`, stem contains `_cop`):
1. **Single-sided** (emitTri): double-siding draws far struts through the near openings → the
"black box". `BT_COP_DOUBLE=1` restores (diag).
2. **Per-face INWARD winding orientation** (finish()): the l/r torso patches are MIRRORED
copies, so no global winding works — orient each face's kept winding toward the mesh
interior (where the pilot sits). `BT_COP_FLIP=1` flips (diag).
3. **Dark `softer`-ramp frame colour**: texture-less ramp + no normals = unlit; evaluate the
ramp at a flat index, default `BT_COP_RAMP_L=0.08` (near the dark end — [T3] tuned to the
near-black frame in footage; the board's exact unlit index is not in the code).
- The shell SHOWS by default in the inside view (btl4vid.cpp ApplyViewSkeleton);
`BT_HIDE_COCKPIT=1` hides it (diag).
- **Wrong turns, recorded so they stay dead:** (a) "windows are punch texels" — no texel exists;
(b) dropping the PUNCH-tagged batches as "windshield glass" — the punch patch IS the visible
frame (dropping it removed the whole cockpit); (c) a view-direction face-cut heuristic — not
data-derivable, superseded by the lattice + winding treatment.
## The authentic eyepoint (decomp-verified [T1])
Pipeline: `eyeWorld = parentSegmentWorld · baseOffset · R(EyepointRotation)`; `VIEW =
inverse(eyeWorld)`. No LookAt anywhere.
- **Eye ctor = FUN_004579a8** (part_007.c:9274, = DPLEyeRenderable). Caller (part_014.c:5525-66,
BTL4VID.CPP) gates on segment name `"siteeyepoint"`, passes:
- offset matrix = the segment's **GetBaseOffset()** (segment+0x74, the LOCAL rest transform) —
NOT GetSegmentToEntity, NOT an upright basis;
- parent = the eyepoint segment's **parent draw component** (`dcs_array[GetParentIndex()]`) —
liveness (torso pose, gait, gyro) reaches the eye ONLY through this parent chain;
- the `EyepointRotation` entity attribute (EulerAngles; Mech member, init 0).
- **Combine order:** `FUN_0040b104(dst,A,B)` computes `dst = B × A` (translation from B) →
the eye local = `baseOffset × R`, R applied FIRST in eye-local space. Recomputed only when the
euler CHANGES (FUN_00457b48 @part_007.c:9360, epsilon gate).
- **Per-frame view = FUN_004c22c4** (part_013.c:11742): rebuilds eyeWorld from the view-DCS's
live accumulated transform, then `FUN_0040b244` = TRUE AFFINE INVERSE → the view matrix, then a
UNIFORM zoom scale. The forward/up axes fall out of the eye's own basis — the renderer never
picks an axis. (Our old hand-rolled `D3DXMatrixLookAtRH` with forward=+Z/up=+Y row extraction
was exactly why some mechs aimed into the canopy — see [[reconstruction-gotchas]].)
- Port implementation: btl4vid.cpp (eye build: offset_matrix + parent_DCS), L4VIDRND.cpp
DPLEyeRenderable::Execute (order `Orient × R`, `D3DXMatrixInverse` on the live path — note the
ctor also holds a copy of this code but its view write is DEAD; patch the Execute).
## Status per mech + the gyro (task #56 — gyro now LIVE)
- **Madcat (MAX_COP): verified good on pure defaults** — dark domed frame (top arch +
cross-braces + A-pillars + dash), world through the openings. Un-regressed with the gyro live.
- **The gyro is RE-ENABLED and clean** (task #56): ctor field-map + integrators + writers
reconstructed byte-exact from @004b3778/@004b2ec0/@004b30ec/@004b34ec, layout
static_assert-locked (sizeof==0x3D0), joint-write dispatch moved to the MECH performance tail
(`GyroFrameJointWrite`, matching the binary calls @0x4aaf74/83), gyro↔torso pitch link wired
(`gyro+0x258 = &torso currentTwist`). Runtime: both joints resolve type 5, Performance
installed (flags 0x100), `WriteMechJoint eyePos=(0,0,0) body=(0,0,0)` finite every frame — the
old NaN is gone. Mechanism: WriteMechJoint drives ONE node (`jointeye`, which parents
`siteeyepoint`): TRANSLATION = the eye position spring, ROTATION = the body tip spring.
WriteEyeJoint is a MULTIPLICATIVE sway attenuator on `jointlocal`'s animated rotation
(gated legAnimState!=0; must run after the animation pass). [T2 runtime-verified]
- **Steady-offset hypothesis DISPROVEN [T2]:** the .MDL spring targets are SYMMETRIC
(pos=(0.1,0.15,0.1), neg=(0.1,0.15,0.1) — same for madcat AND bhk1) → the eye equilibrium
is (0,0,0) and the clamps limit gyro motion to ±0.1..0.15u. The gyro is a BOUNCE/sway
mechanism, NOT a large steady offset. The authentic resting eye = raw baseOffset for every mech.
- **bhk1/owens/sunder/vulture (+ loki partial): STILL blocked at rest** — cause now known NOT to
be the gyro. `BT_EYE_FWD=0.6` still reveals the correct bhk1 view, so the eye-vs-canopy
relationship is off by ~0.6u for that group by some OTHER mechanism (candidates: a joint in the
siteeyepoint parent chain we don't pose, per-mech skeleton rest differences, or the canopy
authored for a view state we don't replicate). OPEN.
- **Hit-bounce: plumbing live, trigger missing.** The integrators + ApplyDamageImpulse/Torque/
Vertical hooks are faithful and ready, but the binary's damage→gyro fan-out **FUN_004b2980**
(0x4b2980-0x4b2d8b, called from the damage hub @0x4a02fb; also direct crunch kicks
@0x4aa254/0x4aa288/…) is an unexported gap not yet reconstructed — nothing kicks the gyro yet.
Also pending: the mech+0x3F0 overspeed sway model (bytes 0x4aade8-0x4aaf14; we feed swayBias=0),
and the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the
mapper glance-look states (FUN_004afd10) — separate fidelity items.
## Diagnostics / env gates
`BT_FORCE_MODEL=<name>` (force the player mech: madcat/owens/sunder/…; btl4mssn.cpp),
`BT_HIDE_COCKPIT`, `BT_COP_DOUBLE`, `BT_COP_FLIP`, `BT_COP_RAMP_L`, `BT_COP_DEBUG` (paint the
shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=<f>` (push the eye forward — the
position probe), `[EYECHK]` log (eye world pos + look dir + entity pos, first frames).
## Key Relationships
- Uses: [[bgf-format]] (PUNCH, vertex tags) · [[rendering]] (ramp model, skeleton branch)
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)