Files
BT411/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

13 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
cockpit-view First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint living 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
rendering
bgf-format
gauges-hud
subsystems
decomp-reference
material-ramp
siteeyepoint
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