Files
BT412/context/cockpit-view.md
T
arcattackandClaude Fable 5 27959990ea View: the INI viewangle is the HORIZONTAL field -- authentic 60x47 frustum (task #55)
Community reports said the camera looked too far back and the cockpit frames
were never this revealed; the user's hypothesis (our projection widens the
view) was exactly right.  The original Tesla projection call, preserved in a
comment at DPLRenderer::SetView, is

  dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))

i.e. BTDPL.INI's viewangle=60.0 is the HORIZONTAL field of view with the
vertical derived by aspect: the pod's frustum was 60 x 46.8 deg at 4:3.  The
port fed the 60 to D3DXMatrixPerspectiveFovRH as the VERTICAL fov -> 75 x 60
at 4:3 (worse on widescreen): the world rendered ~25% farther away and far
more canopy was visible than the pod ever showed.

Fix: BTFovYFromHorizontal(viewangle, aspect) at all five projection sites
(main, per-frame resize, sky pass, WM_SIZE rebuild, fallback) -- the
horizontal field stays the authentic 60 deg at any window shape.  The aim
boresight reads the live projection (gBTAimP22) and adapts; combat
re-verified (kill chain, no NaN).  Cockpits now show the frames at the
pod's framing: viewports dominant, frames at the periphery.

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

174 lines
13 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 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: `<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) "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.
- **FINAL (2026-07-11, i860-firmware-decoded): PUNCH is a three-chunk STENCIL-CUT KIT.** The
VREND.MNG board firmware's 'damageize' handler (@0xf040f6f8, disassembled with a custom i860
tool) writes the dpl_Punchize token triple {dmg_set, undmg2enbl|texture, dmg_clear} onto the
punch geogroup's first three geometry chunks IN FILE ORDER: chunk1 = the detailed
window-aperture shapes = an invisible per-pixel MASK; chunk2 = the coarse HULL = the ONLY
colour-drawn geometry, drawn where NOT masked; chunk3 = a byte-identical hull twin = the
damage-reset record, never visible. The visible canopy = hull-minus-apertures. Port:
bgfload identifies the within-patch twin pair (hull + skip) and tags the non-paired pmeshes as
masks; L4D3D draws mask (stencil 1, no colour/z-write), hull (stencil-reject), mask (stencil 0)
on a D24S8 device; the cut never touches z, so later-drawn entities show through apertures.
BT_COP_PLATES=1 disables the kit (diag). In-game, all 8 mechs on pure defaults [T2]:
connected dark frames with clear viewports; thor (the footage mech) shows the big central
viewport + surrounding frame exactly as filmed. TWO earlier readings are superseded and were
WRONG: "punch patches = windows, drop them" (left floating frame fragments) and "duplicate
pairs = plates, hide both" (hid the HULL and drew the MASK = the inversion the user caught).
- **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).
## The authentic view field (FOV) — viewangle is HORIZONTAL [T1]
`BTDPL.INI [dpl_defaults] viewangle=60.0` is the **horizontal** field of view: the original Tesla
call (preserved in the DPLRenderer::SetView comment, L4VIDEO.cpp) is
`dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))` — horizontal
half-extent 1.0, vertical scaled by aspect. Authentic frustum at 4:3 = **60° × 46.8°**. The
port had fed the 60 to D3DX as the VERTICAL fov (75° × 60° at 4:3, worse on widescreen) — the
world read ~25% farther away ("camera too far back" reports) and the canopy frames were
massively over-revealed. Fixed (`BTFovYFromHorizontal`, all 5 projection sites incl. sky +
resize): the horizontal field stays 60° at any window shape. The aim/pick feed reads the live
projection (`gBTAimP22`) so targeting adapts automatically (combat re-verified). Credit: a
user-community report ("frames were never this revealed") + the resolution/FOV hypothesis.
## 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)