Files
BT411/game/reconstructed/btvisgnd.cpp
T
arcattackandClaude Fable 5 70eea6c1a4 Boresight parallax FIXED + the #4/#5 verdict instrumentation (Gitea #16, #4, #5)
PARALLAX (#16): the pick/fire ray was anchored at mech.y+5.0 (a port
improvisation) while the sight line ran from the eyepoint (y=6.23) --
two parallel rays whose constant offset grew into the reported low-miss
as range closed (measured ry +0.072 @50u -> +1.54 @2.7u).  The decomp's
sight and pick share the eye origin (HudSimulation @4b7830 chain).  Fix:
the viewpoint mech's cockpit eye owns the aim-camera publish in BOTH
views, origin = its own eye translation; leveling + deliberate elevation
untouched; chase view now converges to cockpit ballistics (V cannot
change where shots land).  After: pick pinned to the crosshair (ry <=
5e-6) from 50u to point-blank; 26 laser + 8 missile center-mass hits at
3/4-screen.  Awaiting the reporters' approach-test.

VERDICT INSTRUMENTATION (#4 closed authentic, #5 verdict posted):
BT_RANGE_LOG per-frame pick tracing + BTGroundRayHitExact analytic
cross-check (0/8000 arena fall-throughs; cavern 6/8400 single-frame
grazes -- the 'crazy sliding' is the authentic world-pick + 500 m/s
slide over depth discontinuities); BT_AUD_TAIL StopNote/fade timing +
BT_FIRE_PULSE single-shot driver (the energy 'buzz' is the AUTHORED
charging loop: crescendo through recharge, 1.309s authored release,
measured within one frame).  CAVERN.EGG: solo cavern test egg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:51:54 -05:00

401 lines
14 KiB
C++

//###########################################################################
//
// btvisgnd.cpp
//
// PORT ADDITION (presentation layer -- NOT a reconstruction): visual-ground
// conform for the external chase view.
//
// WHY: the mech SIM rides the coarse 1995 collision solids (cones/blocks;
// the authentic snap puts origin.y = solid surfaceY exactly -- see
// Mech::AuthenticGroundAndCollide). The VISUAL terrain mesh runs 0..~2.1u
// ABOVE those solids on mound/mesa slopes (LOD0-exact measurement, dbase),
// so an upright mech's feet clip into the visible sand there. In 1995 this
// was INVISIBLE: the game rendered cockpit-only (you can never see your own
// feet; other mechs are 100+m away). Our port adds a close external camera,
// so the port also adds this presentation fix: sample the RENDERED terrain
// triangles under the mech and lift the render matrix (localToWorld ONLY --
// never localOrigin) by the visual-minus-solid gap. Collision, aim, damage,
// network dead-reckoning all stay on the authentic solid model.
//
// Data source: the map's MakeMessage stream in BTL4.RES (class-42 static
// terrain instances) + each instance's LOD0 geometry via the engine BGF
// loader (bgfload.h). No tuning constants -- the exact content geometry.
//
// Gate: BT_VISUAL_GROUND (default ON; =0 restores raw solid-height render).
// Diagnostics under BT_GROUND_LOG.
//
//###########################################################################
#include <bt.hpp>
#include <APP.hpp> // application / GetCurrentMission / GetResourceFile
#include <MISSION.hpp> // Mission::GetMapID
#include <bgfload.h>
#include <vector>
#include <string>
#include <map>
#include <cstring>
#include <cstdlib>
#include <cmath>
//---------------------------------------------------------------------------//
// Geometry cache
//---------------------------------------------------------------------------//
namespace
{
struct VgTri
{
float ax, ay, az, bx, by, bz, cx, cy, cz;
};
struct VgMesh
{
std::vector<VgTri> tris;
float minx, maxx, minz, maxz; // model-local XZ bound
float miny, maxy; // model-local Y bound (dome vs flat-ground test)
};
struct VgInst
{
int mesh; // index into gMeshes
float px, py, pz; // world placement
float minx, maxx, minz, maxz; // world XZ bound (placed)
};
std::vector<VgMesh> gMeshes;
std::vector<VgInst> gInsts;
int gState = 0; // 0 = not tried, 1 = ready, -1 = unavailable
bool SkippedName(const char *n)
{
// backdrop / sky pieces -- vertical scenery, never ground underfoot
if (strstr(n, "sky") || strstr(n, "SKY")) return true;
if (_strnicmp(n, "profile", 7) == 0) return true;
if (_strnicmp(n, "rwin", 4) == 0) return true;
return false;
}
int LoadMeshFor(const char *resource_name)
{
static std::map<std::string, int> cache; // name -> gMeshes idx (-1 = no geometry)
std::string key(resource_name);
std::map<std::string, int>::iterator it = cache.find(key);
if (it != cache.end())
return it->second;
int idx = -1;
BgfData data;
std::string file = key + ".bgf";
if (LoadBgfFile(file, data) && data.ok && !data.indices.empty())
{
VgMesh m;
m.minx = m.minz = m.miny = 1e9f;
m.maxx = m.maxz = m.maxy = -1e9f;
// The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then
// (a,c,b) -- 6 indices per source triangle. Take the forward one.
for (size_t i = 0; i + 5 < data.indices.size(); i += 6)
{
const BgfVtx &A = data.verts[data.indices[i]];
const BgfVtx &B = data.verts[data.indices[i + 1]];
const BgfVtx &C = data.verts[data.indices[i + 2]];
VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z };
m.tris.push_back(t);
float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z }, ys[3] = { A.y, B.y, C.y };
for (int k = 0; k < 3; ++k)
{
if (xs[k] < m.minx) m.minx = xs[k];
if (xs[k] > m.maxx) m.maxx = xs[k];
if (zs[k] < m.minz) m.minz = zs[k];
if (zs[k] > m.maxz) m.maxz = zs[k];
if (ys[k] < m.miny) m.miny = ys[k];
if (ys[k] > m.maxy) m.maxy = ys[k];
}
}
if (!m.tris.empty())
{
if (getenv("BT_GROUND_LOG"))
{
float miny = 1e9f, maxy = -1e9f;
for (size_t j = 0; j < m.tris.size(); ++j)
{
const VgTri &t = m.tris[j];
const float ys[3] = { t.ay, t.by, t.cy };
for (int k = 0; k < 3; ++k)
{ if (ys[k] < miny) miny = ys[k]; if (ys[k] > maxy) maxy = ys[k]; }
}
DEBUG_STREAM << "[visgnd] mesh '" << key << "' tris=" << m.tris.size()
<< " x[" << m.minx << "," << m.maxx << "] z[" << m.minz << "," << m.maxz
<< "] y[" << miny << "," << maxy << "]\n" << std::flush;
}
gMeshes.push_back(m);
idx = (int)gMeshes.size() - 1;
}
}
cache[key] = idx;
return idx;
}
// Build the instance table from the CURRENT mission's map message stream.
// Map stream record layout (BTL4.RES, byte-verified):
// [0] u32 count
// records: u32 mlen | i32 mid | u32 mflags | ... i32 class @+28 |
// i32 resourceID @+40 | float pos[3] @+48 ; advance (mlen+3)&~3
// mflags bit1 = include-marker (skip); class 42 = static terrain/cultural.
bool BuildTables()
{
if (application == 0) return false;
Mission *mission = application->GetCurrentMission();
ResourceFile *rf = application->GetResourceFile();
if (mission == 0 || rf == 0) return false;
ResourceDescription *map = rf->FindResourceDescription(mission->GetMapID());
if (map == 0) return false;
map->Lock();
const unsigned char *base = (const unsigned char *)map->resourceAddress;
size_t size = map->resourceSize;
if (base == 0 || size < 4) return false;
int count = *(const int *)base;
size_t p = 4;
int made = 0, skipped = 0;
for (int i = 0; i < count && p + 52 <= size; ++i)
{
unsigned mlen = *(const unsigned *)(base + p);
unsigned mflags = *(const unsigned *)(base + p + 8);
int cls = *(const int *)(base + p + 28);
int rid = *(const int *)(base + p + 40);
const float *pos = (const float *)(base + p + 48);
p += (mlen + 3) & ~3u;
// CENSUS (BT_GROUND_LOG): log EVERY map entity's class + name, so we can see
// what class the garages/structures are (the class-42 filter may drop them).
if (getenv("BT_GROUND_LOG"))
{
ResourceDescription *crd = rf->FindResourceDescription(rid);
DEBUG_STREAM << "[mapent] cls=" << cls << " mflags=" << mflags
<< " name='" << (crd ? crd->resourceName : "?") << "'\n" << std::flush;
}
if (mflags & 0x2) continue; // include marker
if (cls != 42) continue; // terrain/static cultural only
ResourceDescription *rd = rf->FindResourceDescription(rid);
if (rd == 0) continue;
int mi = LoadMeshFor(rd->resourceName);
if (mi < 0) { ++skipped; continue; }
// GEOMETRY-AWARE skip: the name filter (sky/backdrop) is right for a high
// vertical DOME, but arena1's ground is a 10000x10000 FLAT plane at y=0
// misnamed "sky" -- name-skipping it removed the only groundable geometry
// (task #50: buildings/ground stopped moving the range axis). Keep a
// name-skipped mesh IF it is a wide, near-flat, near-ground plane (the
// arena floor); still skip true domes / tall vertical backdrops.
if (SkippedName(rd->resourceName))
{
const VgMesh &gm = gMeshes[mi];
const float yext = gm.maxy - gm.miny;
const float xext = gm.maxx - gm.minx, zext = gm.maxz - gm.minz;
const bool flatWideGround =
(yext < 200.0f) && (xext > 500.0f) && (zext > 500.0f) && (gm.miny < 200.0f);
if (getenv("BT_GROUND_LOG"))
DEBUG_STREAM << "[visgnd] name-skip '" << rd->resourceName
<< "' yext=" << yext << " xz=" << xext << "x" << zext
<< " miny=" << gm.miny << " -> keepAsGround=" << (flatWideGround?1:0)
<< "\n" << std::flush;
if (!flatWideGround) { ++skipped; continue; }
}
VgInst inst;
inst.mesh = mi;
inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2];
inst.minx = gMeshes[mi].minx + pos[0];
inst.maxx = gMeshes[mi].maxx + pos[0];
inst.minz = gMeshes[mi].minz + pos[2];
inst.maxz = gMeshes[mi].maxz + pos[2];
gInsts.push_back(inst);
++made;
}
if (getenv("BT_GROUND_LOG"))
DEBUG_STREAM << "[visgnd] sampler ready: " << made << " terrain instances ("
<< gMeshes.size() << " meshes, " << skipped << " skipped)\n" << std::flush;
return made > 0;
}
// Highest visual surface at world (x,z) whose height lies in [yLo, yHi].
// The band keeps the query LOCAL: standing at a butte base it picks the skirt
// underfoot, not the mesa top 40u above.
bool SampleBand(float x, float z, float yLo, float yHi, float *out)
{
bool found = false;
float best = 0.0f;
for (size_t ii = 0; ii < gInsts.size(); ++ii)
{
const VgInst &inst = gInsts[ii];
if (x < inst.minx || x > inst.maxx || z < inst.minz || z > inst.maxz)
continue;
const float lx = x - inst.px, lz = z - inst.pz;
const VgMesh &m = gMeshes[inst.mesh];
for (size_t ti = 0; ti < m.tris.size(); ++ti)
{
const VgTri &t = m.tris[ti];
// point-in-triangle (XZ), sign-consistent
const float d1 = (t.bx - t.ax) * (lz - t.az) - (t.bz - t.az) * (lx - t.ax);
const float d2 = (t.cx - t.bx) * (lz - t.bz) - (t.cz - t.bz) * (lx - t.bx);
const float d3 = (t.ax - t.cx) * (lz - t.cz) - (t.az - t.cz) * (lx - t.cx);
if (!((d1 >= 0 && d2 >= 0 && d3 >= 0) || (d1 <= 0 && d2 <= 0 && d3 <= 0)))
continue;
const float area = (t.bx - t.ax) * (t.cz - t.az) - (t.bz - t.az) * (t.cx - t.ax);
if (area > -1e-6f && area < 1e-6f)
continue;
const float w1 = ((t.bx - lx) * (t.cz - lz) - (t.bz - lz) * (t.cx - lx)) / area;
const float w2 = ((t.cx - lx) * (t.az - lz) - (t.cz - lz) * (t.ax - lx)) / area;
const float w3 = 1.0f - w1 - w2;
const float y = w1 * t.ay + w2 * t.by + w3 * t.cy + inst.py;
if (y < yLo || y > yHi)
continue;
if (!found || y > best) { found = true; best = y; }
}
}
if (found) *out = best;
return found;
}
} // namespace
//---------------------------------------------------------------------------//
// BTVisualGroundLift -- render-only Y delta so the mech's feet meet the
// VISIBLE ground. Returns 0 when gated off, unavailable, or no local surface.
//---------------------------------------------------------------------------//
float
BTVisualGroundLift(float x, float y, float z)
{
static int s_on = -1;
if (s_on < 0)
{
const char *v = getenv("BT_VISUAL_GROUND");
s_on = (v == 0 || v[0] != '0') ? 1 : 0;
}
if (!s_on)
return 0.0f;
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return 0.0f;
// Local band: up to 3u of visual rising above the solid (measured max ~2.2),
// up to 1u of visual dipping below it. Outside the band = not the ground
// underfoot (cliff tops, building roofs) -> no conform.
float surf;
if (!SampleBand(x, z, y - 1.0f, y + 3.0f, &surf))
return 0.0f;
float lift = surf - y;
if (lift < -1.0f) lift = -1.0f;
if (lift > 3.0f) lift = 3.0f;
return lift;
}
//---------------------------------------------------------------------------//
// BTGroundRayHit -- march a ray (o + dir*t) against the visible terrain and
// return the first point where it meets the surface (rising ground: butte
// faces, dune fronts). Used by the weapon fire path to end the beam + place
// the impact on the ground. Returns false (no hit) for a level shot over open
// desert -- the caller then flies the beam to max range.
//---------------------------------------------------------------------------//
bool
BTGroundRayHit(float ox, float oy, float oz,
float dx, float dy, float dz,
float maxDist, float *hx, float *hy, float *hz)
{
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return false;
const float step = 4.0f;
for (float t = step; t <= maxDist; t += step)
{
const float x = ox + dx * t;
const float y = oy + dy * t;
const float z = oz + dz * t;
float surf;
// wide downward band: catch terrain rising into a mostly-horizontal ray.
if (SampleBand(x, z, y - 90.0f, y + 6.0f, &surf) && y <= surf)
{
*hx = x; *hy = surf; *hz = z;
return true;
}
}
return false;
}
//---------------------------------------------------------------------------//
// BTGroundRayHitExact -- BT_RANGE_LOG diagnostic (Gitea #4 verdict
// instrumentation, uncommitted): ANALYTIC closest-hit ray intersect
// (Moller-Trumbore, no backface cull, no step size, no height band) over the
// SAME visual triangle set the march samples -- including vertical walls and
// ceilings, which SampleBand's XZ point-in-triangle test cannot see. The
// march-vs-exact range difference on the same ray is direct evidence for (or
// against) the "ray falls through geometry" hypothesis; it is NOT used by any
// gameplay path.
//---------------------------------------------------------------------------//
bool
BTGroundRayHitExact(float ox, float oy, float oz,
float dx, float dy, float dz,
float maxDist, float *hx, float *hy, float *hz)
{
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return false;
float bestT = maxDist;
bool found = false;
for (size_t ii = 0; ii < gInsts.size(); ++ii)
{
const VgInst &inst = gInsts[ii];
// local-space ray (instances are translation-placed only)
const float lox = ox - inst.px, loy = oy - inst.py, loz = oz - inst.pz;
const VgMesh &m = gMeshes[inst.mesh];
for (size_t ti = 0; ti < m.tris.size(); ++ti)
{
const VgTri &t = m.tris[ti];
const float e1x = t.bx - t.ax, e1y = t.by - t.ay, e1z = t.bz - t.az;
const float e2x = t.cx - t.ax, e2y = t.cy - t.ay, e2z = t.cz - t.az;
// p = dir x e2
const float px = dy * e2z - dz * e2y;
const float py = dz * e2x - dx * e2z;
const float pz = dx * e2y - dy * e2x;
const float det = e1x * px + e1y * py + e1z * pz;
if (det > -1e-9f && det < 1e-9f)
continue; // parallel
const float inv = 1.0f / det;
const float tx = lox - t.ax, ty = loy - t.ay, tz = loz - t.az;
const float u = (tx * px + ty * py + tz * pz) * inv;
if (u < 0.0f || u > 1.0f)
continue;
// q = tvec x e1
const float qx = ty * e1z - tz * e1y;
const float qy = tz * e1x - tx * e1z;
const float qz = tx * e1y - ty * e1x;
const float v = (dx * qx + dy * qy + dz * qz) * inv;
if (v < 0.0f || u + v > 1.0f)
continue;
const float tt = (e2x * qx + e2y * qy + e2z * qz) * inv;
if (tt > 0.05f && tt < bestT)
{
bestT = tt;
found = true;
}
}
}
if (found)
{
*hx = ox + dx * bestT;
*hy = oy + dy * bestT;
*hz = oz + dz * bestT;
}
return found;
}