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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user