#124: the aimed pick now intersects the DRAWN GEOMETRY -- sphere

approximation retired; shared-hull hits route through the cylinder

The zone-walk matrix caught the pick red-handed: with the servo
verifiably holding the reticle on the dtorso segment, the per-segment
BOUNDING-SPHERE pick returned rgun/ruleg -- the gun pods and legs
thread the ray before the torso from most angles (its own comments
admitted a foot could be unhittable behind its own knee). The 1995
pick was a dpl scene intersection against the drawn geometry.

Restored that semantic:
* BTGetPickMesh caches each segment d3d_OBJECT's triangles CPU-side
  once (its own BGF VB/IB, managed-pool locks); MechSegmentPick now
  runs sphere PRE-FILTER -> Moller-Trumbore nearest-hit across the
  threaded segments' posed meshes; the old smallest-sphere selection
  survives only as the no-triangle fallback.
* Segments claimed as CARRIER by 2+ zones (the shared hull: madcat
  seg 4 carries dtorso+ltorso+rtorso+utorso+rears) cannot resolve one
  zone from geometry -- those hits return zone -1 WITH the accurate
  triangle point, and the victim's bit-verified (frame-fixed) cylinder
  assigns the panel by band/wedge. Unique carriers (legs, feet, arms,
  gun pods) keep the direct zone.
* ZoneAimPoint: the walker aims at a zone's VISUAL center (largest
  pick object's cull-center) instead of the segment origin -- joint
  origins made feet/lower legs strike the part above.
* Walker upgrades from live operation: 3-column truth (aim/pick/land),
  engage gate, damped servo with polarity watchdog, approach port.

Zone-walk verdict (full cycles, spinning target, real MP): limbs 5-6/6
direct in-zone; every hull panel routes CYL; the victim's landed zones
now include ltorso/rtorso/reardtorso/rearutorso/rearrtorso -- the
panels night-10 reported unhittable. Residuals tracked on #124:
rtorso/utorso aim-anchor placement, doors/searchlight small-zone
sample, twisted-torso twist-sign verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-03 10:38:24 -05:00
co-authored by Claude Fable 5
parent 16e75d6e1a
commit 1a3c268278
4 changed files with 253 additions and 11 deletions
+228 -6
View File
@@ -1545,10 +1545,106 @@ int BTWreckSinkTick(Entity *victim, float dt)
//
// #73 -- the aimed PER-PART pick (see the header note). Ray-vs-sphere over
// the per-segment draw objects recorded at tree build; world centers come
// through the draw-cached mLocalToWorld (updated every drawn frame -- the
// target being aimed at is on screen, so at most one frame stale).
// #124 -- the pick's TRIANGLE cache. The 1995 pick was a dpl scene
// intersection against the DRAWN GEOMETRY (the division card cast from the
// view); the port's sphere approximation measurably mis-picked (the zone-walk
// matrix: aim dead-on dtorso -> picked rgun/ruleg -- gun/limb spheres thread
// the ray before the torso from most angles, and its own comments admitted "a
// foot can be unhittable behind its own knee"). Restore the authentic
// semantic: nearest RAY-TRIANGLE hit across the candidate segments' posed
// meshes. Positions are read ONCE per d3d_OBJECT from its own BGF buffers
// (managed pool, lockable) and cached CPU-side; the per-frame cost is a
// sphere pre-filter + Moller-Trumbore over the few threaded segments.
//
struct BTPickMesh
{
std::vector<float> pos; // xyz per vertex
std::vector<unsigned int> idx; // triangle list
int ok;
};
static std::map<d3d_OBJECT *, BTPickMesh> gBTPickMeshes;
static BTPickMesh *
BTGetPickMesh(d3d_OBJECT *obj)
{
std::map<d3d_OBJECT *, BTPickMesh>::iterator mi = gBTPickMeshes.find(obj);
if (mi != gBTPickMeshes.end())
return mi->second.ok ? &mi->second : 0;
BTPickMesh &pm = gBTPickMeshes[obj];
pm.ok = 0;
if (obj->mBgfVB == 0 || obj->mBgfIB == 0 || obj->mBgfStride < 12)
return 0;
D3DINDEXBUFFER_DESC ibd;
if (FAILED(obj->mBgfIB->GetDesc(&ibd)))
return 0;
int idx32 = (ibd.Format == D3DFMT_INDEX32);
unsigned int nIdx = ibd.Size / (idx32 ? 4 : 2);
void *vp = 0, *ip = 0;
if (FAILED(obj->mBgfVB->Lock(0, 0, &vp, D3DLOCK_READONLY)))
return 0;
if (FAILED(obj->mBgfIB->Lock(0, 0, &ip, D3DLOCK_READONLY)))
{
obj->mBgfVB->Unlock();
return 0;
}
pm.pos.resize((size_t)obj->mBgfNumVerts * 3);
const unsigned char *vb = (const unsigned char *)vp;
for (UINT v = 0; v < obj->mBgfNumVerts; ++v)
{
const float *p = (const float *)(vb + (size_t)v * obj->mBgfStride);
pm.pos[v*3+0] = p[0]; // position-first vertex layout (the BGF
pm.pos[v*3+1] = p[1]; // loader's own decl; the cull sphere was
pm.pos[v*3+2] = p[2]; // computed from these same floats at load)
}
pm.idx.resize(nIdx);
if (idx32)
{
const unsigned int *s = (const unsigned int *)ip;
for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k];
}
else
{
const unsigned short *s = (const unsigned short *)ip;
for (unsigned int k = 0; k < nIdx; ++k) pm.idx[k] = s[k];
}
obj->mBgfIB->Unlock();
obj->mBgfVB->Unlock();
pm.ok = (pm.idx.size() >= 3 && pm.pos.size() >= 9);
return pm.ok ? &pm : 0;
}
// Moller-Trumbore, both-sided (the pod's dpl geometry has no consistent
// winding guarantee across ported BGF pieces). Returns t >= 0 or -1.
static float
BTRayTri(const float o[3], const float d[3],
const float *a, const float *b, const float *c)
{
float e1[3] = { b[0]-a[0], b[1]-a[1], b[2]-a[2] };
float e2[3] = { c[0]-a[0], c[1]-a[1], c[2]-a[2] };
float pv[3] = { d[1]*e2[2]-d[2]*e2[1], d[2]*e2[0]-d[0]*e2[2], d[0]*e2[1]-d[1]*e2[0] };
float det = e1[0]*pv[0] + e1[1]*pv[1] + e1[2]*pv[2];
if (det > -1e-8f && det < 1e-8f) return -1.0f;
float inv = 1.0f / det;
float tv[3] = { o[0]-a[0], o[1]-a[1], o[2]-a[2] };
float u = (tv[0]*pv[0] + tv[1]*pv[1] + tv[2]*pv[2]) * inv;
if (u < 0.0f || u > 1.0f) return -1.0f;
float qv[3] = { tv[1]*e1[2]-tv[2]*e1[1], tv[2]*e1[0]-tv[0]*e1[2], tv[0]*e1[1]-tv[1]*e1[0] };
float v = (d[0]*qv[0] + d[1]*qv[1] + d[2]*qv[2]) * inv;
if (v < 0.0f || u + v > 1.0f) return -1.0f;
float t = (e2[0]*qv[0] + e2[1]*qv[1] + e2[2]*qv[2]) * inv;
return (t >= 0.0f) ? t : -1.0f;
}
//
// #73 -- the aimed PER-PART pick (see the header note). #124: now a true
// DRAWN-GEOMETRY intersection -- sphere pre-filter, then nearest ray-triangle
// hit across the threaded segments' posed meshes (the 1995 division-card
// semantic). The old smallest-sphere selection survives only as the fallback
// when no triangle anywhere is struck (grazing edge shots). World transforms
// come through the draw-cached mLocalToWorld (at most one frame stale).
//
int
BTL4VideoRenderer::MechSegmentPick(
@@ -1573,12 +1669,42 @@ int
// envelope; smallest-pierced picks the most specific part on the aim line,
// and the torso wins only when no limb is threaded -- which is the per-part
// semantic the 1995 mesh intersection produced.
float bestR = 1e30f; // primary key: sphere radius (ascending)
float bestScore = 1.0f; // tie-break: normalized perpendicular d2/r2
float bestR = 1e30f; // sphere-FALLBACK key: radius (ascending)
float bestScore = 1.0f; // sphere tie-break: normalized d2/r2
float bestT = max_range;
int bestZone = -1;
int hitAny = 0;
// triangle-accurate primary: nearest surface hit across all segments
float triBestT = max_range;
int triBestZone = -1;
int triBestSeg = -1;
int triHit = 0;
// #124: segments claimed as CARRIER by TWO OR MORE zones (the shared hull
// -- e.g. madcat seg 4 carries dtorso AND ltorso AND rtorso AND utorso AND
// the rears) cannot resolve a single zone from geometry. The authentic
// route for their hits is the CYLINDER: return zone -1 with the accurate
// triangle hit point, and the victim's point resolver assigns the panel
// by band/wedge. Unique-carrier segments (legs, feet, arms, gun pods)
// keep their direct zone.
unsigned char segClaims[192];
memset(segClaims, 0, sizeof(segClaims));
{
extern int BTMechZoneSegAndName(void *mech_v, int zone_idx,
int *seg_out, const char **name_out);
int zsi;
const char *znm;
for (int zi = 0; zi < 64; ++zi)
{
if (!BTMechZoneSegAndName((void *)mech, zi, &zsi, &znm))
break;
if (zsi >= 0 && zsi < (int)sizeof(segClaims)
&& segClaims[zsi] < 255)
++segClaims[zsi];
}
}
std::map<int, MechRenderTree::SegPick>::iterator sp;
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
{
@@ -1609,6 +1735,53 @@ int
if (t < 0.0f || t >= max_range)
continue;
// ---- TRIANGLE TEST (#124): the sphere only nominates ----
BTPickMesh *pm = BTGetPickMesh(obj);
if (pm != 0)
{
// ray into object-local space (affine inverse; segment poses are
// rigid, so local t == world t after direction normalization is
// preserved by construction below)
D3DXMATRIX w2l;
if (D3DXMatrixInverse(&w2l, 0, &l2w) != 0)
{
D3DXVECTOR3 lo, ld;
D3DXVECTOR3 wo(ray_start[0], ray_start[1], ray_start[2]);
D3DXVECTOR3 wd(ray_dir[0], ray_dir[1], ray_dir[2]);
D3DXVec3TransformCoord(&lo, &wo, &w2l);
D3DXVec3TransformNormal(&ld, &wd, &w2l);
float o3[3] = { lo.x, lo.y, lo.z };
float d3[3] = { ld.x, ld.y, ld.z };
const float *P = &pm->pos[0];
size_t nv = pm->pos.size() / 3;
for (size_t k = 0; k + 2 < pm->idx.size(); k += 3)
{
unsigned int i0 = pm->idx[k], i1 = pm->idx[k+1], i2 = pm->idx[k+2];
if (i0 >= nv || i1 >= nv || i2 >= nv)
continue;
float tt = BTRayTri(o3, d3, P + i0*3, P + i1*3, P + i2*3);
if (tt >= 0.0f && tt < triBestT)
{
// world-space t of the local hit (handles any scale)
D3DXVECTOR3 lh(o3[0]+d3[0]*tt, o3[1]+d3[1]*tt, o3[2]+d3[2]*tt);
D3DXVECTOR3 wh;
D3DXVec3TransformCoord(&wh, &lh, &l2w);
float wt = (wh.x - ray_start[0]) * ray_dir[0]
+ (wh.y - ray_start[1]) * ray_dir[1]
+ (wh.z - ray_start[2]) * ray_dir[2];
if (wt >= 0.0f && wt < triBestT)
{
triBestT = wt;
triBestZone = sp->second.zone;
triBestSeg = sp->first;
triHit = 1;
}
}
}
}
}
// ---- sphere FALLBACK bookkeeping (unchanged selection) ----
float score = d2 / r2; // 0 = dead-center thread
if (r > bestR
|| (r == bestR && score >= bestScore))
@@ -1621,6 +1794,17 @@ int
hitAny = 1;
}
if (triHit)
{
bestT = triBestT;
bestZone = triBestZone;
hitAny = 1;
// shared-carrier segment -> the cylinder decides (accurate point kept)
if (triBestSeg >= 0 && triBestSeg < (int)sizeof(segClaims)
&& segClaims[triBestSeg] > 1)
bestZone = -1;
}
// #92 probe: which spheres did the ray actually THREAD, and which won?
// "smallest radius wins" means a big sphere can never beat a small one that
// the ray also grazes -- so a foot can be unhittable behind its own knee.
@@ -1691,6 +1875,44 @@ int
return 1;
}
//
// #124 zone walker: the zone's visual aim anchor (see the hpp note).
//
int
BTL4VideoRenderer::ZoneAimPoint(Entity *mech, int zone, float out3[3])
{
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
if (it == mMechRenderTrees.end() || it->second.wrecked)
return 0;
d3d_OBJECT *best = 0;
std::map<int, MechRenderTree::SegPick>::iterator sp;
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
{
if (sp->second.zone != zone || sp->second.obj == NULL)
continue;
if (best == 0 || sp->second.obj->mCullRadius > best->mCullRadius)
best = sp->second.obj;
}
if (best == 0 || best->mCullRadius <= 0.0f)
return 0;
D3DXMATRIX l2w = best->GetLocalToWorld();
D3DXVECTOR3 cw;
D3DXVec3TransformCoord(&cw, &best->mCullCenter, &l2w);
out3[0] = cw.x; out3[1] = cw.y; out3[2] = cw.z;
return 1;
}
int BTMechZoneAimPoint(void *mech, int zone, float out3[3])
{
if (mech == NULL || application == NULL)
return 0;
BTL4VideoRenderer *renderer =
(BTL4VideoRenderer *)application->GetVideoRenderer();
if (renderer == NULL)
return 0;
return renderer->ZoneAimPoint((Entity *)mech, zone, out3);
}
//
// Game-side bridge (mech4.cpp's per-frame target pick; same access pattern as
// the wreck swap below).