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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7b0b4f64ab
commit
abaa145b6f
+12
-2
@@ -43,8 +43,18 @@ LOD whose band contains the camera distance.
|
||||
- **ADDITIVE_LODS** (object-level `SV_SPECIAL` token, 135 pod BGFs — all arena structures): at eye
|
||||
distance d, EVERY LOD with `d < OutDist` draws (near detail ADDS onto coarser massing). Non-additive
|
||||
keep first-LOD-only (the 2007 engine's own behavior). [T2]
|
||||
- **PUNCH** (patch-level `SV_SPECIAL`): cutout geogroups — black texels = HOLES (alpha-test in the
|
||||
opaque pass). Coplanar shells inside one LOD z-fight → depth-bias by submission ordinal. [T2]
|
||||
- **PUNCH** (patch-level `SV_SPECIAL`, `dpl_Punchize` = FUN_00490308 → VPX board cmd `0x20`, tokens
|
||||
`{0x80000003,0x80000017,0x80000004}` — a CONSTANT geogroup-modifier opcode, no color key; the
|
||||
sibling `dpl_Damagize` FUN_004902b0 uses the same cmd with a different triple). Two cases: [T1]
|
||||
- **Textured** punch (arena walls/catwalks): black texels = HOLES (alpha-test in the opaque
|
||||
pass). Coplanar shells inside one LOD z-fight → depth-bias by submission ordinal. [T2]
|
||||
- **Texture-less** punch (the `*_cop` cockpit canopies: blakskn + `softer` ramp, NO texture tag
|
||||
in the BMF — byte-verified, 238-byte BLXSKIN.BMF): the frame/window split is the GEOMETRY —
|
||||
every `_cop` is an open strut lattice (38-59% boundary edges, all 12 mechs); rendered
|
||||
single-sided with per-face INWARD winding orientation (the l/r torso patches are MIRRORED, so
|
||||
no single global winding works) the struts are the dark frame and the openings show the world.
|
||||
The old "canopy windows are punch texels" claim was WRONG — there is no texel to key. See
|
||||
[[cockpit-view]]. [T2 render-verified in-game on Madcat vs gameplay footage]
|
||||
|
||||
## Shading model (from libDPL DPLTYPES.H)
|
||||
Shading is selected PER-GEOMETRY by vertex type:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
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)
|
||||
@@ -163,6 +163,10 @@ The authentic stack is DEFAULT-ON (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`,
|
||||
| `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) |
|
||||
| `BT_FIRE_ARC=<deg>` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
|
||||
| `BT_START_INSIDE` | begin in the cockpit view (V toggles) |
|
||||
| `BT_FORCE_MODEL=<name>` | force the player mech (`madcat`/`owens`/…; btl4mssn.cpp — per-mech cockpit bring-up) |
|
||||
| `BT_HIDE_COCKPIT` | hide the `*_cop` canopy shell (SHOWS by default — see [[cockpit-view]]) |
|
||||
| `BT_COP_*` / `BT_EYE_FWD=<f>` | cockpit canopy + eye diagnostics (`DOUBLE`/`FLIP`/`RAMP_L`/`DEBUG`/`DUMP`; eye forward-push probe) — [[cockpit-view]] |
|
||||
| `BT_SHOT=<path.png>` | periodic backbuffer dump (every 90 frames) — non-disruptive visual verification |
|
||||
| `BT_TLOC_LOG` | translocation-warp lifecycle log (`[tloc] warp COLLAPSE/EXPAND/phase=…`) |
|
||||
| `BT_WARP_*` | translocation-warp visual tuning — all default to the verified values (see [[translocation-warp]]): `EYE_UP=8.25` (on-axis), `SPIN=4`, `TWIST=0`, `CONTRAST=1.0`, `LO_*/HI_*` (lavender ramp), `BLUR=0`, `TESS=3`, `MIP=1`, `CULL=cw`, `ANISO=0` |
|
||||
| `BT_WARP_SELFTEST` / `BT_WARP_SELFSHOT=<prefix>` | DIAG (off by default): force a held warp in a solo game / dump backbuffer frames to disk (visual-verification harness) |
|
||||
@@ -173,6 +177,17 @@ BT_PUNCH, …) are catalogued in [[rendering]]. Warp visuals: [[translocation-wa
|
||||
**Translocation-warp addresses** [T1]: `POVTranslocateRenderable` = L4VIDRND.cpp:1749 / .h:638; its
|
||||
per-frame Execute (spin about local Z + submit, NO texcoord/ramp work) = **`FUN_00453dc4`**
|
||||
(`reference/decomp/all/part_007.c`), using axis-rotation helper `FUN_0040998c` (axis idx 2 = Z).
|
||||
|
||||
**Cockpit-eye / punch addresses** [T1] (full story: [[cockpit-view]]): eye ctor
|
||||
**`FUN_004579a8`** (part_007.c:9274 = DPLEyeRenderable; offset = segment `GetBaseOffset`
|
||||
segment+0x74, parent = parent-segment DCS; caller part_014.c:5525-66 gates on `"siteeyepoint"`);
|
||||
matrix multiply **`FUN_0040b104(dst,A,B)` = `B × A`** (translation from B — operand-order trap!);
|
||||
euler→rotation `FUN_0040ac6c`; per-frame view **`FUN_004c22c4`** (part_013.c:11742): eyeWorld from
|
||||
the live view-DCS then **`FUN_0040b244` = affine INVERSE** → VIEW (+ uniform zoom) — NO LookAt;
|
||||
eye euler-change refresh `FUN_00457b48` (part_007.c:9360). Punch: **`dpl_Punchize` =
|
||||
`FUN_00490308`** (part_011.c:6219) → `FUN_0048e5b0` → `FUN_00492580(0x20,…)` board send, constant
|
||||
tokens `{0x80000003,0x80000017,0x80000004}` (sibling `dpl_Damagize` `FUN_004902b0` = same cmd,
|
||||
different triple); SV_SPECIAL PUNCH dispatch part_011.c:10209/10252 → callback LAB_004596ac.
|
||||
Respawn SIM path: `DropZoneReply` `FUN_004bffd0` → `Mech::Reset` @0x4009fb74; `CreatePlayerVehicle`
|
||||
`FUN_004bfcac`. See [[translocation-warp]], [[multiplayer]].
|
||||
|
||||
|
||||
@@ -224,6 +224,35 @@ worse the longer the app runs (and are invisible right after launch).
|
||||
|
||||
---
|
||||
|
||||
## 14. Hand-rolled LookAt / axis-convention guess (camera reconstruction)
|
||||
|
||||
When the original forms a view/camera matrix by **inverting a composed world transform**, any port
|
||||
reconstruction that instead extracts "forward/up" ROWS and feeds a LookAt has silently HARD-CODED an
|
||||
axis convention (+Z=forward/+Y=up) the engine never promises. It works for meshes/segments that
|
||||
happen to match and aims into geometry for the rest — a per-asset-random bug that looks like bad
|
||||
data, not bad code.
|
||||
|
||||
- **Archetype (the cockpit eye, task #55):** the binary's per-frame camera is
|
||||
`VIEW = affine_inverse(eyeWorld)` (FUN_004c22c4 → FUN_0040b244); our `DPLEyeRenderable::Execute`
|
||||
hand-built `D3DXMatrixLookAtRH(pos, pos+row2, row1)` — some mechs looked out, others into the
|
||||
canopy. Fix: compose the full eye world matrix and invert it; the axes fall out of the basis. [T1]
|
||||
- **Tell:** the same camera code behaving differently per mech/asset. Also check BOTH copies of
|
||||
duplicated ctor/Execute code — the dead one can mislead you about which path is live (our ctor had
|
||||
the CORRECT multiply order but its view write was commented out; the live Execute had it inverted).
|
||||
|
||||
## 15. Per-patch index namespaces (mesh connectivity analysis)
|
||||
|
||||
BGF face indices are LOCAL to their vertex chunk/patch. Any cross-patch analysis keyed on raw index
|
||||
tuples (edge counting, adjacency, dedup) silently MERGES unrelated edges from different patches and
|
||||
corrupts the metric.
|
||||
|
||||
- **Archetype:** BLX_COP boundary-edge ratio measured 7% ("closed shell") with a global edge Counter
|
||||
→ the whole "closed vs open canopy" theory. Correct per-patch namespacing gives 59% — ALL 12
|
||||
canopies are open lattices. Namespace edge keys by patch identity (and remember l/r patches are
|
||||
MIRRORED — winding handedness flips, so no global winding choice can be right; orient per-face).
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic recipe (the standard loop)
|
||||
1. Read the RAW decomp `reference/decomp/all/part_*.c` for the `FUN_xxxx`.
|
||||
2. Map `FUN_`/`DAT_`/`this+0xNN` to engine symbols via BT headers + WinTesla MUNGA source +
|
||||
|
||||
Reference in New Issue
Block a user