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:
arcattack
2026-07-11 06:46:54 -05:00
co-authored by Claude Fable 5
parent 7b0b4f64ab
commit abaa145b6f
9 changed files with 349 additions and 53 deletions
+1
View File
@@ -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` |
+12 -2
View File
@@ -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:
+113
View File
@@ -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)
+15
View File
@@ -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]].
+29
View File
@@ -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 +
+45 -12
View File
@@ -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);
+103 -16
View File
@@ -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<uint32_t> 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<int32_t>& 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]<lo[k])lo[k]=p[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();
+6
View File
@@ -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;
+25 -23
View File
@@ -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)