--- 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 tail: gait jolt/rumble [T3 gates], sway model, torso-pitch eye writer, damageForce fill", "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: `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) "the punch patch IS the visible frame / drop-punch removes the cockpit" — an artifact of testing with the broken pre-inverse eye; with the authentic eye the punch patches are plainly the WINDOWS (see §resolved below) and dropping them is correct; (c) a view-direction face-cut heuristic — not data-derivable, superseded by the lattice + winding + punch-drop 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. - **RESOLVED (2026-07-11): the PUNCH patches are the transparent WINDOWS.** Rendering every shell OFFLINE from its mech's AUTHORED eye (scratchpad/cop_eyeview.py, faces coloured by punch flag) showed the punch panes sitting exactly over the window apertures in all 8 mechs, with the visible frame = ONLY the non-punch geometry. The loader now DROPS the punch batches of `_cop` meshes (bgfload finish(), `BT_COP_KEEPPUNCH=1` keeps them for diag). In-game sweep on pure defaults, all 8 mechs [T2]: **madcat** = the dark dome frame (matches footage); **thor** = floating brace remnants (authentic; its frame bits are the small non-punch patches); **owens** = dash strip; **sunder** = two side pillars; **bhk1/loki/vulture/avatar** = authentically FRAMELESS (all-punch canopies — the pod showed no frame for those mechs). NOTE: an early-session in-game test "disproving" drop-punch ran with the broken pre-inverse eye — its conclusion was wrong and cost a detour; with the authentic eye the drop is plainly right. - **Hit-bounce: LIVE and verified [T2] (2026-07-11).** The damage→gyro fan-out **FUN_004b2980** was re-disassembled byte-exact from the raw image (capstone; scratchpad/dis_4b2980.py) and reconstructed as `Gyroscope::ApplyDamageResponse` (gyro.cpp): zero-damage + Collision(0) no-op; hit direction = `Damage::damageForce` or a RANDOM horizontal fallback when ~zero (our senders don't fill damageForce yet — the fallback is binary-legal; directional fidelity = fill it later); rotated by the yaw-only torso-twist frame; per-type scaling `amount / damageMultiplier[type] * damageResponse[type].{trans,pitchRoll,yaw,vibration}` (Explosive alone multiplies burstCount); clamp 1.3; four kicks — impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration, **vibrationDirection@0x390 = (0,1,0)** — the "animationOffset/Scale/Phase scalars" were this unit vector all along), verticalImpulse(yaw, dir). Call sites wired: the take-damage hub (mech.cpp TakeDamageMessageHandler, FIRST action, binary @0x4a0264), the crushable-icon crunch (mech4.cpp, torque 0.4 along Δv + up-impulse 0.2, @4aa81e/@4aa86c), the firing recoil (projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,−1.5) × damage/16, @4bc136-4bc19c). Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed. - **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE (@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without re-verifying the gate semantics); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0); (c) the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the mapper glance-look states (FUN_004afd10); (d) senders filling `Damage::damageForce` (beam: impact−muzzle; projectile: velocity) for directional bounce fidelity; (e) FireWeapon's heat add (pre-existing separate gap). `gyroRumbleTimer@0x5c4` was re-typed from the mis-named `int clipLoadGuard` (binary uses it as a float rumble countdown). ## Offset cockpits — the placement-model verification [T1/T2] Some mechs have laterally OFFSET cockpits, and the data + our port agree exactly (2026-07-11): `[jointeye]` rest translation per SKL vs the `_COP` canopy X-center — **thor: tranx=+1.13, canopy center +1.13 (exact)**; loki −0.25/−0.15; owens +0.19/+0.16; the other five ≈0/≈0. Our in-game measured eye (`[EYECHK]`) reproduces the Thor offset to the hundredth (lateral 1.13 from the mech origin). The eye chain (baseOffset + parent-segment DCS composition) is therefore CORRECT per-mech; the still-blocked cockpit views are a canopy-RENDER question, not a camera one. ## The playable roster is 8, not 12 [T1] `BTL4.RES` contains exactly 8 mech model resources (avatar, bhk1/blkhawk, loki, madcat, owens, sunder, thor, vulture — each with ``, variants `1/2`, and a `dead`). Strider / Raptor / Firestarter / Blackjack have `_COP` meshes, SKLs and gauge configs but NO model resource (cut/other-version leftovers); forcing them via `BT_FORCE_MODEL` feeds NULL to `ResourceDescription::Lock` → AV in `CreatePlayerVehicle` (cdb-verified). Valid `BT_FORCE_MODEL` names: `avatar bhk1 loki madcat owens sunder thor vulture` (+ variants ava1/lok1/lok2/mad1/mad2/ own1/snd1/thr1/vul1/blkhawk). ## Diagnostics / env gates `BT_FORCE_MODEL=` (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=` (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)