From abaa145b6ffabf627bc4ae352c5b8e29ac335c60 Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 11 Jul 2026 06:46:54 -0500 Subject: [PATCH] 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= 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 --- CLAUDE.md | 1 + context/bgf-format.md | 14 +++- context/cockpit-view.md | 113 ++++++++++++++++++++++++++++ context/decomp-reference.md | 15 ++++ context/reconstruction-gotchas.md | 29 ++++++++ engine/MUNGA_L4/L4VIDRND.cpp | 57 +++++++++++--- engine/MUNGA_L4/bgfload.cpp | 119 ++++++++++++++++++++++++++---- game/reconstructed/btl4mssn.cpp | 6 ++ game/reconstructed/btl4vid.cpp | 48 ++++++------ 9 files changed, 349 insertions(+), 53 deletions(-) create mode 100644 context/cockpit-view.md diff --git a/CLAUDE.md b/CLAUDE.md index 9ed5afe..b6d2585 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,7 @@ precise than anything you can infer. | Damage zones, targeting, firing, death | `context/combat-damage.md` | | Rendering, LODs, materials, sky, shadows, beams | `context/rendering.md` | | The death/respawn translocation warp (tsphere vortex) | `context/translocation-warp.md` | +| First-person cockpit canopy (*_cop) + the eyepoint camera | `context/cockpit-view.md` | | Cockpit gauges / MFD HUD | `context/gauges-hud.md` | | Multiplayer, replication, netcode | `context/multiplayer.md` | | Pod hardware, monitors, RIO, MFD surfaces | `context/pod-hardware.md` | diff --git a/context/bgf-format.md b/context/bgf-format.md index 98c4300..08a55e2 100644 --- a/context/bgf-format.md +++ b/context/bgf-format.md @@ -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: diff --git a/context/cockpit-view.md b/context/cockpit-view.md new file mode 100644 index 0000000..3d9c707 --- /dev/null +++ b/context/cockpit-view.md @@ -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: `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=` (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) diff --git a/context/decomp-reference.md b/context/decomp-reference.md index d4c296f..9fc69d3 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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=` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) | | `BT_START_INSIDE` | begin in the cockpit view (V toggles) | +| `BT_FORCE_MODEL=` | 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=` | cockpit canopy + eye diagnostics (`DOUBLE`/`FLIP`/`RAMP_L`/`DEBUG`/`DUMP`; eye forward-push probe) — [[cockpit-view]] | +| `BT_SHOT=` | 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=` | 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]]. diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 75884e9..0263319 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -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 + diff --git a/engine/MUNGA_L4/L4VIDRND.cpp b/engine/MUNGA_L4/L4VIDRND.cpp index 2001e2b..5e51747 100644 --- a/engine/MUNGA_L4/L4VIDRND.cpp +++ b/engine/MUNGA_L4/L4VIDRND.cpp @@ -5451,8 +5451,16 @@ DPLEyeRenderable::DPLEyeRenderable( //D3DXVec3Scale(&dir, &dir, 50); //D3DXVec3Subtract(&pos, &pos, &dir); + // AUTHENTIC VIEW = inverse(eyeWorld) (decomp FUN_004c22c4 @part_013.c:11788, + // FUN_0040b244 = affine inverse) -- NOT a LookAt with a hand-picked forward axis. + // The look/up axes fall out of the eye's OWN basis via the inverse, so every mech + // aims out the windshield regardless of its siteeyepoint axis convention (the old + // +Z-forward/+Y-up LookAt guess is why the view aimed into the canopy for some + // mechs). mat4 = eyeLocal * parentWorld = the eye WORLD matrix; pos/at/up are + // retained only for the aim-boresight feed below. + D3DXMATRIX eyeW = mat4.ToD3DMatrix(); D3DXMATRIX view; - D3DXMatrixLookAtRH(&view,&pos,&at,&up); + D3DXMatrixInverse(&view, NULL, &eyeW); //myDevice->SetTransform(D3DTS_VIEW, &view); @@ -5528,14 +5536,16 @@ void s_oldEyeSway = gBTEyeSwayX; oldLocalToWorld = myEntity->localToWorld; + // AUTHENTIC combine order (decomp FUN_004579a8 @part_007.c:9305-9316): + // eyeLocal = baseOffset(myOrientationMatrix) * R(EyepointRotation), R applied + // FIRST in the eye's own local space. (Was R * Orient -- inverted.) + camMatrix = myOrientationMatrix; if (myEyepointRotation) { - oldEyepointRotation = *myEyepointRotation; - camMatrix = *myEyepointRotation; + oldEyepointRotation = *myEyepointRotation; + Matrix4x4 rotation; rotation = *myEyepointRotation; + camMatrix *= rotation; } - else - camMatrix = Matrix4x4::Identity; - camMatrix *= myOrientationMatrix; myRenderer->GetMatrixStack()->Push(); Matrix4x4 mat4; @@ -5589,14 +5599,31 @@ void pos.x += drot._11 * gBTEyeSwayX; pos.z += drot._13 * gBTEyeSwayX; at.x += drot._11 * gBTEyeSwayX; at.z += drot._13 * gBTEyeSwayX; + // AUTHENTIC VIEW = inverse(eyeWorld) (decomp FUN_004c22c4 @part_013.c:11788, affine + // inverse FUN_0040b244) -- the live per-frame path. Replaces the hand-rolled LookAt + // that assumed +Z=forward/+Y=up (which mis-framed the cockpit); the look/up axes now + // fall out of the eye's own basis. pos/at/up above are kept only for the aim-boresight. + D3DXMATRIX eyeW = mat4.ToD3DMatrix(); D3DXMATRIX view; - D3DXMatrixLookAtRH(&view,&pos,&at,&up); + D3DXMatrixInverse(&view, NULL, &eyeW); + // EYE POSITION CHECK + 'too far back' diagnostic (task #55). + D3DXVECTOR3 eyeFwdW(-view._13, -view._23, -view._33); // D3D view -Z = world look dir + { + const char *ef = getenv("BT_EYE_FWD"); // push eye forward k units along its look dir + if (ef && *ef && myRenderer->mCamera == this) + { + float k = (float)atof(ef); + eyeW._41 += k*eyeFwdW.x; eyeW._42 += k*eyeFwdW.y; eyeW._43 += k*eyeFwdW.z; + D3DXMatrixInverse(&view, NULL, &eyeW); + } + } if (dbgEyeExec < 8) { - DEBUG_STREAM << "[EYE] view set: pos=(" << pos.x << "," << pos.y << "," << pos.z - << ") at=(" << at.x << "," << at.y << "," << at.z - << ") up=(" << up.x << "," << up.y << "," << up.z << ")\n" << std::flush; + DEBUG_STREAM << "[EYECHK] eyeW=(" << eyeW._41 << "," << eyeW._42 << "," << eyeW._43 + << ") fwd=(" << eyeFwdW.x << "," << eyeFwdW.y << "," << eyeFwdW.z + << ") ent=(" << myEntity->localToWorld(3,0) << "," << myEntity->localToWorld(3,1) + << "," << myEntity->localToWorld(3,2) << ")\n" << std::flush; ++dbgEyeExec; } @@ -5627,11 +5654,17 @@ void // X still carries the torso twist (BTTwistToReticleX); the reticle // Y carries any aim elevation. Falls back to the raw basis only // if the view is (degenerately) near-vertical. - D3DXVECTOR3 zax = pos - at; + // Derive the boresight from the AUTHORITATIVE view matrix + // (eyeFwdW = the world look direction from the inverse view), + // not the old LookAt at/pos rows -- those assumed row2=look, + // which the authentic inverse-view convention inverted (task + // #56 follow-through; the stale rows aimed the pick ray at + // the sky = no target, no discharge). + D3DXVECTOR3 zax = -eyeFwdW; // back = -look D3DXVec3Normalize(&zax, &zax); D3DXVECTOR3 xax; D3DXVECTOR3 yax; - D3DXVECTOR3 fwdLevel(at.x - pos.x, 0.0f, at.z - pos.z); + D3DXVECTOR3 fwdLevel(eyeFwdW.x, 0.0f, eyeFwdW.z); if (D3DXVec3LengthSq(&fwdLevel) > 1e-6f) { D3DXVec3Normalize(&fwdLevel, &fwdLevel); diff --git a/engine/MUNGA_L4/bgfload.cpp b/engine/MUNGA_L4/bgfload.cpp index 257928f..d3c9b54 100644 --- a/engine/MUNGA_L4/bgfload.cpp +++ b/engine/MUNGA_L4/bgfload.cpp @@ -502,6 +502,7 @@ struct Builder { bool currentShadowMat = false; // material name contains "shadow" (baked ground-shadow quads) bool currentTSphere = false; // material is tsphere_mtl (translocation warp): ramp it despite normals bool meshIsTSphere = false; // this OBJECT is the translocation warp -> smooth-tessellate the cone + bool meshIsCop = false; // this OBJECT is a *_cop cockpit canopy shell (task #55) bool currentHasEmissive = false; float currentEmissive[3] = {0,0,0}; @@ -510,21 +511,34 @@ struct Builder { std::vector col; void emitTri(uint32_t a, uint32_t b, uint32_t c) { - // forward winding (+ accumulate face normal) - mesh->indices.push_back(a); - mesh->indices.push_back(b); - mesh->indices.push_back(c); - // back winding too -> double sided (engine culls CW; this guarantees visibility) - mesh->indices.push_back(a); - mesh->indices.push_back(c); - mesh->indices.push_back(b); - mesh->tris++; - float ux = px[b]-px[a], uy = py[b]-py[a], uz = pz[b]-pz[a]; - float wx = px[c]-px[a], wy = py[c]-py[a], wz = pz[c]-pz[a]; - float fx = uy*wz - uz*wy, fy = uz*wx - ux*wz, fz = ux*wy - uy*wx; - nx[a]+=fx; ny[a]+=fy; nz[a]+=fz; - nx[b]+=fx; ny[b]+=fy; nz[b]+=fz; - nx[c]+=fx; ny[c]+=fy; nz[c]+=fz; + // Face normal first (lighting accumulation below). + float ux = px[b]-px[a], uy = py[b]-py[a], uz = pz[b]-pz[a]; + float wx = px[c]-px[a], wy = py[c]-py[a], wz = pz[c]-pz[a]; + float fx = uy*wz - uz*wy, fy = uz*wx - ux*wz, fz = ux*wy - uy*wx; + // COCKPIT CANOPY SHELL (*_cop.bgf, the first-person cockpit -- task #55): the canopy + // geogroups carry the SV_SPECIAL "PUNCH" tag (dpl_Punchize, VPX board cmd 0x20). The + // visible frame-with-window is produced by the canopy's OWN geometry: it is an OPEN + // STRUT LATTICE (MAX_COP punch patch: 34/74 boundary edges), so rendered SINGLE-SIDED + // from the authentic cockpit eye the struts are the dark frame and the openings show + // the world. Double-siding draws the FAR-side struts through the near openings -> + // the solid "black box" bug. Verified in-game against gameplay footage (Madcat). + // Scoped to *_cop meshes so other texture-less ramp geometry keeps engine behavior. + // BT_COP_DOUBLE=1 restores double-siding; BT_COP_FLIP=1 flips the kept winding (diag). + const bool cop = meshIsCop && currentHasRamp && currentTex.empty(); + static int s_copSingle=-1, s_copFlip=-1; + if (s_copSingle < 0) { const char* e=getenv("BT_COP_DOUBLE"); s_copSingle=(e && e[0]!='0')?0:1; } + if (s_copFlip < 0) { const char* e=getenv("BT_COP_FLIP"); s_copFlip =(e && e[0]!='0')?1:0; } + const bool single = s_copSingle && cop; + if (single && s_copFlip) { + mesh->indices.push_back(a); mesh->indices.push_back(c); mesh->indices.push_back(b); + } else { + mesh->indices.push_back(a); mesh->indices.push_back(b); mesh->indices.push_back(c); + if (!single) { mesh->indices.push_back(a); mesh->indices.push_back(c); mesh->indices.push_back(b); } + } + mesh->tris++; + nx[a]+=fx; ny[a]+=fy; nz[a]+=fz; + nx[b]+=fx; ny[b]+=fy; nz[b]+=fz; + nx[c]+=fx; ny[c]+=fy; nz[c]+=fz; } void addFace(const std::vector& idx, size_t start, int ppf, @@ -594,8 +608,31 @@ struct Builder { (s_rampTint && currentHasDiffuse && rampNeutral) ? currentColor : 0xFFFFFFFFu; const bool pureEmissive = currentHasEmissive && (currentColor & 0x00FFFFFFu) == 0; - const uint32_t vcol = pureEmissive ? 0xFF000000u + uint32_t vcol = pureEmissive ? 0xFF000000u : (useRamp ? rampTint : currentColor); + // DIAG (task #55): BT_COP_DEBUG paints texture-less ramp batches (the cockpit + // canopy shell blx_cop -- ramp 'softer', no texture) bright green, to SEE what + // the shell actually covers from the real cockpit eyepoint (frame vs open). + // COCKPIT FRAME COLOUR (*_cop shells only): ramped NO-NORMAL geometry with NO + // texture (blakskn_dz + 'softer'). Per the engine material-ramp model the RAMP is + // the surface colour and the (0,0,0) DIFFUSE is IGNORED -- the old tint rule + // collapsed it to black. With no texture there is no per-texel luminance, so + // evaluate the ramp at a flat index. Default 0.08 = near the ramp's DARK end: + // the canopy frame is unlit interior structure, and the dark value matches the + // near-black frame in the cabinet gameplay footage ([T3] tuned-to-footage; the + // board's exact unlit ramp index is not recoverable from the code). + // BT_COP_RAMP_L overrides (diagnostic). + if (useRamp && currentTex.empty() && meshIsCop) { + static float s_copL = -1.0f; + if (s_copL < 0.0f) { const char* e = getenv("BT_COP_RAMP_L"); s_copL = e ? (float)atof(e) : 0.08f; } + float cr = currentRampLo[0] + (currentRampHi[0]-currentRampLo[0])*s_copL; + float cg = currentRampLo[1] + (currentRampHi[1]-currentRampLo[1])*s_copL; + float cb = currentRampLo[2] + (currentRampHi[2]-currentRampLo[2])*s_copL; + auto CB = [](float ff){ int v=(int)(ff*255.0f+0.5f); return (uint32_t)(v<0?0:v>255?255:v); }; + vcol = 0xFF000000u | (CB(cr)<<16) | (CB(cg)<<8) | CB(cb); + } + if (useRamp && currentTex.empty() && meshIsCop && getenv("BT_COP_DEBUG")) + vcol = 0xFF00FF00u; // TRANSLOCATION WARP: tsphere's UVs run U=angle around the Z throat, V=axial // along the tunnel -- i.e. cylindrical. Viewed down the throat axis (as the // effect is), those ARE log-polar coordinates (angle, log-radius), the exact @@ -724,6 +761,18 @@ struct Builder { else batch.lodBias = 0.0f; mesh->batches.push_back(batch); + if (getenv("BT_COP_DUMP") && currentHasRamp && currentTex.empty()) { + float lo[3]={1e9f,1e9f,1e9f}, hi[3]={-1e9f,-1e9f,-1e9f}, cen[3]={0,0,0}; int nv=0; + for (uint32_t i = idxStart; i < idxStart + idxCount; ++i) { + uint32_t v = mesh->indices[i]; + float p[3]={px[v],py[v],pz[v]}; + for (int k=0;k<3;k++){ if(p[k]hi[k])hi[k]=p[k]; cen[k]+=p[k]; } + nv++; + } + if (nv) for(int k=0;k<3;k++) cen[k]/=nv; + fprintf(stderr,"[COP_DUMP] punch=%d tris=%u cen=(%.2f,%.2f,%.2f) bbox=[%.2f,%.2f,%.2f]..[%.2f,%.2f,%.2f]\n", + (int)batch.punch, idxCount/3, cen[0],cen[1],cen[2], lo[0],lo[1],lo[2], hi[0],hi[1],hi[2]); + } } } @@ -1026,6 +1075,40 @@ struct Builder { } void finish() { + // COCKPIT CANOPY WINDING ORIENTATION (task #55): the *_cop shells are OPEN strut + // lattices (38-59% boundary edges, all 12 mechs) drawn SINGLE-SIDED (emitTri) so + // the openings show the world. But the authored winding is NOT globally + // consistent: the left/right torso patches are MIRRORED copies, and mirroring + // flips winding handedness -- so one global winding choice shows one half's + // struts and the other half's BACKS (the solid-box look on BLX/OWX/VUX...). + // Data-driven fix: orient every canopy face INWARD, toward the mesh interior + // where the pilot's eye sits, so the whole shell reads correctly from inside + // regardless of per-patch mirroring. (An earlier pass DROPPED the PUNCH-tagged + // batches believing them the windshield glass -- verified wrong in-game: the + // punch patch IS the visible frame.) BT_COP_FLIP=1 flips the final orientation + // (diagnostic); BT_COP_DOUBLE=1 disables single-siding entirely (emitTri). + if (meshIsCop) { + static int s_flip = -1, s_dbl = -1; + if (s_flip < 0) { const char* e = getenv("BT_COP_FLIP"); s_flip = (e && e[0] != '0') ? 1 : 0; } + if (s_dbl < 0) { const char* e = getenv("BT_COP_DOUBLE"); s_dbl = (e && e[0] != '0') ? 1 : 0; } + if (!s_dbl && !px.empty()) { + float cx = 0, cy = 0, cz = 0; + for (size_t i = 0; i < px.size(); ++i) { cx += px[i]; cy += py[i]; cz += pz[i]; } + cx /= px.size(); cy /= px.size(); cz /= px.size(); + for (size_t i = 0; i + 3 <= mesh->indices.size(); i += 3) { + uint32_t a = mesh->indices[i], b = mesh->indices[i+1], c = mesh->indices[i+2]; + float ux = px[b]-px[a], uy = py[b]-py[a], uz = pz[b]-pz[a]; + float wx = px[c]-px[a], wy = py[c]-py[a], wz = pz[c]-pz[a]; + float fnx = uy*wz - uz*wy, fny = uz*wx - ux*wz, fnz = ux*wy - uy*wx; + float fcx = (px[a]+px[b]+px[c])/3.0f - cx; + float fcy = (py[a]+py[b]+py[c])/3.0f - cy; + float fcz = (pz[a]+pz[b]+pz[c])/3.0f - cz; + bool inward = (fnx*fcx + fny*fcy + fnz*fcz) < 0.0f; // normal points toward interior + if (inward == (s_flip != 0)) // flip to the chosen orientation + { mesh->indices[i+1] = c; mesh->indices[i+2] = b; } + } + } + } if (meshIsTSphere) { tessellateWarpCone(); // tessellateWarpCone REBUILT mesh->indices -> the per-batch indexStart/ @@ -1090,6 +1173,10 @@ bool LoadBgfFile(const std::string& name, BgfData& out) { } MaterialResolver res; Builder b{&out}; + // Cockpit canopy shells are keyed by filename (blx_cop.bgf, max_cop.bgf, ...): the + // single-siding + dark-frame ramp reconstruction in emitTri/buildPmesh applies to + // these meshes only. + b.meshIsCop = stemLower(name).find("_cop") != std::string::npos; b.res = &res; for (const Chunk& c : roots) b.collect(c); b.finish(); diff --git a/game/reconstructed/btl4mssn.cpp b/game/reconstructed/btl4mssn.cpp index 99355e4..1294e17 100644 --- a/game/reconstructed/btl4mssn.cpp +++ b/game/reconstructed/btl4mssn.cpp @@ -179,6 +179,12 @@ void gameModel = NULL; // @+0x2c const char *player_model = NULL; notation_file->GetEntry(player_node, "vehicle", &player_model); + // DIAGNOSTIC: force a specific player mech model (e.g. BT_FORCE_MODEL=madcat) so we can + // bring up / verify a given cockpit shell in-game with its own authentic eyepoint. + { + const char *force = getenv("BT_FORCE_MODEL"); + if (force && force[0]) { player_model = force; } + } if (!player_model) { DEBUG_STREAM << "Error: vehicle not specified!\n" << std::flush; diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index ea61dfe..b6060ac 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -401,25 +401,18 @@ HierarchicalDrawComponent* EulerAngles *eye_rot = (EulerAngles *)entity->GetAttributePointer("EyepointRotation"); // @0051d29d // - // Mount the eye at the eyepoint's REST position but with a clean - // UPRIGHT forward basis, parented on the tree ROOT: riding the - // live joint chain fed the camera the site's authored tilt + the - // torso pose (a permanently canted horizon). The authentic pitch - // /yaw came from the gyro-driven eye-joint chain (deferred); until - // that lands, the cockpit view = eyepoint position + mech yaw. + // AUTHENTIC eye (decomp FUN_004579a8 @part_007.c:9274-9325; caller part_014.c:5525-5566): + // the eye offset matrix is the siteeyepoint segment's LOCAL rest transform (GetBaseOffset, + // segment+0x74 -- already in offset_matrix above), and the eye is parented on its PARENT + // segment's draw component (parent_DCS) -- NOT the root, NOT the full GetSegmentToEntity, + // NOT an upright hack. World orientation + all live motion (torso twist, gait) come from + // the parent-chain composition, exactly as the decomp's dpl_AddDCSToDCS hierarchy does; + // EyepointRotation is combined in Execute as baseOffset * R, and VIEW = inverse(eyeWorld) + // (FUN_004c22c4 @part_013.c:11788) -- so the look/up axes fall out of the eye's own basis + // with no per-mech forward-axis assumption. // - AffineMatrix restToEntity = segment->GetSegmentToEntity(); - Point3D eyePos; - eyePos = restToEntity; // rest translation (W row) - LinearMatrix eyeBasis(True); - eyeBasis(0,0) = -1.0f; eyeBasis(0,1) = 0.0f; eyeBasis(0,2) = 0.0f; // X row - eyeBasis(1,0) = 0.0f; eyeBasis(1,1) = 1.0f; eyeBasis(1,2) = 0.0f; // Y row (up) - eyeBasis(2,0) = 0.0f; eyeBasis(2,1) = 0.0f; eyeBasis(2,2) = -1.0f; // Z row (look = mech forward) - eyeBasis(3,0) = (Scalar)eyePos.x; - eyeBasis(3,1) = (Scalar)eyePos.y; - eyeBasis(3,2) = (Scalar)eyePos.z; mEyeCockpit = new DPLEyeRenderable( - entity, eyeBasis, this_root, eye_rot); + entity, offset_matrix, parent_DCS, eye_rot); if (type == insideEntity) // true inside build: it IS the camera mCamera = mEyeCockpit; dbg_eye = 1; @@ -633,10 +626,16 @@ HierarchicalDrawComponent* // recomputed up (local +Y) = forward x right float yx = zy*xz - zz*xy, yy = zz*xx - zx*xz, yz = zx*xy - zy*xx; + // CAMERA-BASIS CONVENTION (task #56 follow-through): the view is now the + // authentic VIEW = inverse(eyeWorld) (DPLEyeRenderable::Execute), under + // which the camera looks along -Z of its own basis -- so the basis rows + // are right/up/BACK, not right/up/look. This chase matrix was authored + // for the old LookAt (row2 = look); convert by a 180-degree turn about + // local Y (negate the X and Z rows -- determinant stays +1). LinearMatrix debugOffset; // identity - debugOffset(0,0) = xx; debugOffset(0,1) = xy; debugOffset(0,2) = xz; // X row + debugOffset(0,0) = -xx; debugOffset(0,1) = -xy; debugOffset(0,2) = -xz; // X row (right, flipped) debugOffset(1,0) = yx; debugOffset(1,1) = yy; debugOffset(1,2) = yz; // Y row (up) - debugOffset(2,0) = zx; debugOffset(2,1) = zy; debugOffset(2,2) = zz; // Z row (look) + debugOffset(2,0) = -zx; debugOffset(2,1) = -zy; debugOffset(2,2) = -zz; // Z row (BACK = -look) debugOffset(3,0) = camPx; debugOffset(3,1) = camPy; debugOffset(3,2) = camPz; // W row (pos) mEyeChase = new DPLEyeRenderable(entity, debugOffset, this_root, NULL); @@ -2037,6 +2036,7 @@ BTL4VideoRenderer::BTL4VideoRenderer( mViewInside = 0; } +// // // ApplyViewSkeleton -- select + load each body segment's displayed mesh for the // given view (inside = SkeletonType_A / outside = the mech's own skeletonType), @@ -2083,11 +2083,13 @@ int gstate = viewpoint->damageZones[zone_index]->GetGraphicState(); CString *nm = segment->GetVideoObjectName( (EntitySegment::SkeletonType)render_tree.viewSkeleton, gstate); - // The cockpit interior shell (blx_cop -- the canopy frame around the - // eyepoint) currently renders as a black enclosure; keep it hidden until - // its interior rendering is sorted (BT_INSIDE_COCKPIT=1 shows it). + // The cockpit canopy shell (*_cop -- the frame around the eyepoint) SHOWS by + // default: with the authentic eye (baseOffset + parent-segment DCS + inverse + // view) and the loader's single-sided dark-ramp treatment it renders as the + // dark frame with the world through the openings (task #55, verified vs + // gameplay footage). BT_HIDE_COCKPIT=1 hides it (diagnostic). if (inside && nm != NULL && strstr((const char *)*nm, "_cop") != NULL - && !getenv("BT_INSIDE_COCKPIT")) + && getenv("BT_HIDE_COCKPIT")) nm = NULL; d3d_OBJECT *obj = NULL; if (nm != NULL)