Files
BT412/context/cockpit-view.md
T
arcattackandClaude Fable 5 946323b696 Gyro: damage fan-out FUN_004b2980 -- the cockpit hit-BOUNCE is live (task #56)
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
  or a RANDOM horizontal fallback when ~zero (per-component sign roll then
  value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
  -> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
  .{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
  each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
  vibrationDirection@0x390) and verticalImpulse(yaw, dir)

Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.

Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
  in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
  impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
  x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor

DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.

Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.

Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).

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

150 lines
11 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: 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:
impactmuzzle; 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 `<name>`, variants `<pfx>1/2`, and a `<pfx>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=<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)