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
+46 -12
View File
@@ -294,18 +294,52 @@ void
return;
}
// The *_tshd proxy is a FLAT blob shadow (no light source, no projection). A flat
// quad cannot bend to a slope, so tilting it to the terrain angle only made its
// uphill edge dig into the hillside (and a nearby cliff wrenched it over). The
// renderer's depth-bias already draws it ON TOP of the terrain so it doesn't get
// buried/culled on elevation -- which was the reason the tilt was added. So keep
// the shadow FLAT (horizontal, riding the depth-bias) and only preserve the yaw
// channel (jointtshadow handles torso-twist yaw separately). ground_normal is now
// unused for pitch/roll; kept in the signature for the torso-yaw path + future use.
(void)ground_normal;
EulerAngles angles = shadowJointNode->GetEulerAngles(); // keep yaw
EulerAngles flat(0.0f, angles.yaw, 0.0f);
shadowJointNode->SetRotation(flat);
// APPLY THE TERRAIN ANGLE (task #49b -- the SKL's own contract for this
// joint: "apply terrain angle to pitch and roll"). ground_normal is the
// surface normal in the MECH-LOCAL frame (the caller rotates the world
// gradient by -yaw); pose the quad so its up (+Y) aligns with it and the
// quad LIES ON the slope. A flat quad + a big depth-bias was the previous
// workaround for slope burial, but a bias big enough to beat the terrain
// also beat the mech's FEET (the shadow painted OVER them); with the tilt
// the bias drops to a decal epsilon and the feet layer correctly.
// Cliff guard: cap the tilt (~35 deg) so a bogus gradient from probing
// across a cliff edge can't wrench the quad vertical.
Vector3D n = ground_normal;
if (n.y < 0.82f) // cap ~35 deg
{
Scalar xz = (Scalar)sqrtf((float)(n.x * n.x + n.z * n.z));
if (xz > 1e-6f)
{
const Scalar s = 0.5735f / xz; // sin(35 deg) / |xz|
n.x *= s; n.z *= s; n.y = 0.8192f; // cos(35 deg)
}
else
{
n.x = 0.0f; n.z = 0.0f; n.y = 1.0f;
}
}
// Convention-proof: build the tilted ORTHONORMAL BASIS (quad up = n) and let
// the ENGINE's own LinearMatrix -> EulerAngles conversion produce the angles
// (hand-derived Euler signs are exactly how the original tilt attempt "dug
// into the hillside"). Basis: Y = n; Z = world Z projected off n (so the
// tilt carries no yaw; jointtshadow handles torso-twist yaw separately);
// X = Y x Z (right-handed, matches the identity basis).
{
Vector3D zx(0.0f - n.x * n.z, 0.0f - n.y * n.z, 1.0f - n.z * n.z);
Scalar zl = (Scalar)sqrtf((float)(zx.x * zx.x + zx.y * zx.y + zx.z * zx.z));
if (zl < 1e-6f) { zx.x = 0.0f; zx.y = 0.0f; zx.z = 1.0f; zl = 1.0f; }
zx.x /= zl; zx.y /= zl; zx.z /= zl;
Vector3D xx(n.y * zx.z - n.z * zx.y,
n.z * zx.x - n.x * zx.z,
n.x * zx.y - n.y * zx.x);
LinearMatrix tiltM(1); // identity ctor
tiltM.SetFromAxis(X_Axis, UnitVector(xx.x, xx.y, xx.z));
tiltM.SetFromAxis(Y_Axis, UnitVector(n.x, n.y, n.z));
tiltM.SetFromAxis(Z_Axis, UnitVector(zx.x, zx.y, zx.z));
EulerAngles tilt;
tilt = tiltM; // the engine's own conversion
shadowJointNode->SetRotation(tilt);
}
}
//---------------------------------------------------------------------------//