Shadow: authentic terrain tilt + decal draw order (feet layer correctly)

User report: the ground shadow painted OVER the mech's feet.  Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.

Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):

- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
  jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
  the surface normal via an orthonormal basis fed to the ENGINE's own
  LinearMatrix->EulerAngles conversion.  Hand-derived Euler signs are exactly
  how the prior tilt attempts "dug into the hillside"; the engine conversion
  is convention-proof.  ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
  BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
  lift varies across a slope; world->local rotation by the TRUE yaw from
  localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
  class).  BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
  between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
  decal order.  The mech, drawn after, z-passes over the shadow: the bias
  can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
  BT_SHADOW_TILT=0 flat fallback -> the old -0.004.  BT_SHADOW_BIAS overrides.

KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 16:59:48 -05:00
co-authored by Claude Fable 5
parent 099cad998d
commit b6fe686b22
7 changed files with 145 additions and 30 deletions
+31 -5
View File
@@ -2210,17 +2210,43 @@ void
BoundingBox *bZ = gnode->FindBoundingBoxUnder(qZ, &hZ);
if (bC != 0 && bX != 0 && bZ != 0)
{
// surfaceY = probeY - downDistance; gradient -> world normal (-dH/dx, 1, -dH/dz)
const Scalar yC = baseY - hC, yX = baseY - hX, yZ = baseY - hZ;
// surfaceY = probeY - downDistance; then add the render-only
// VISUAL lift at each probe (task #49b): the quad is drawn on
// the VISIBLE terrain (btvisgnd conform), which runs 0..~2u
// above the collision solid by DIFFERENT amounts across a
// slope -- the VISUAL gradient, not the collision gradient,
// is the surface the quad must hug. Lift 0 (no data/gated
// off) degrades to the collision gradient.
extern float BTVisualGroundLift(float x, float y, float z);
Scalar yC = baseY - hC, yX = baseY - hX, yZ = baseY - hZ;
yC += BTVisualGroundLift((float)cx, (float)yC, (float)cz);
yX += BTVisualGroundLift((float)(cx+D), (float)yX, (float)cz);
yZ += BTVisualGroundLift((float)cx, (float)yZ, (float)(cz+D));
Vector3D wn(-(yX - yC) / D, 1.0f, -(yZ - yC) / D);
Scalar len = (Scalar)sqrtf(wn.x * wn.x + wn.y * wn.y + wn.z * wn.z);
if (len > 1e-6f) { wn.x /= len; wn.y /= len; wn.z /= len; }
// world -> mech-local: rotate by -gDriveHeading about Y (mech upright, yaw only)
const Scalar cth = (Scalar)cosf((float)gDriveHeading);
const Scalar sth = (Scalar)sinf((float)gDriveHeading);
// world -> mech-local: rotate by -yaw about Y. Use the TRUE
// heading from localToWorld (engine convention MATRIX.cpp:196:
// Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z)) -- NOT
// the gDriveHeading scalar mirror, which is seeded to 0 and
// drifts from the real pose (the task-#48 goto-steering bug
// class; a wrong yaw here swings the tilt as the mech turns).
UnitVector zAxisS;
localToWorld.GetFromAxis(Z_Axis, &zAxisS);
const Scalar shYaw = (Scalar)atan2f((float)zAxisS.x, (float)zAxisS.z);
const Scalar cth = (Scalar)cosf((float)shYaw);
const Scalar sth = (Scalar)sinf((float)shYaw);
shadowNormal.x = wn.x * cth - wn.z * sth;
shadowNormal.y = wn.y;
shadowNormal.z = wn.x * sth + wn.z * cth;
if (getenv("BT_SHADOW_LOG"))
{
static float s_shLog = 0.0f; s_shLog += dt;
if (s_shLog >= 1.0f) { s_shLog = 0.0f;
DEBUG_STREAM << "[shtilt] wn=(" << wn.x << "," << wn.y << "," << wn.z
<< ") local=(" << shadowNormal.x << "," << shadowNormal.y
<< "," << shadowNormal.z << ") yaw=" << shYaw << "\n" << std::flush; }
}
}
}
UpdateShadowJoint(shadowNormal);