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
+15 -2
View File
@@ -948,10 +948,23 @@ void d3d_OBJECT::DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time target
if (s_bias == -1e9f)
{
const char *bv = getenv("BT_SHADOW_BIAS");
s_bias = bv ? (float)atof(bv) : -0.004f;
if (bv)
s_bias = (float)atof(bv);
else
{
// Default pairs with the tilt (task #49b): with the quad TILTED
// onto the slope (BT_SHADOW_TILT on, the default) only a decal
// epsilon is needed -- the old big bias (2-4 world units) also
// beat the mech's FEET in the depth test, painting the shadow
// OVER them. The flat-quad A/B fallback (BT_SHADOW_TILT=0)
// still needs the big bias to survive slope burial.
const char *tv = getenv("BT_SHADOW_TILT");
const int tiltOn = (tv == 0 || tv[0] != '0');
s_bias = tiltOn ? -0.0008f : -0.004f;
}
}
mDevice->SetRenderState(D3DRS_DEPTHBIAS, *(DWORD *)&s_bias);
float slope = s_bias == 0.0f ? 0.0f : -1.0f; // steep-slope pixels get extra pull
float slope = s_bias == 0.0f ? 0.0f : -1.0f; // coplanar decal term
mDevice->SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, *(DWORD *)&slope);
}
mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
+10
View File
@@ -7583,6 +7583,16 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
}
}
// GROUND SHADOWS (task #49b): the classic decal order -- terrain (statics,
// above) -> shadow decals (here) -> dynamic opaque bodies (below). The
// shadow's depth-bias only has to beat the TERRAIN already in the z-buffer;
// the mech drawn AFTER simply z-passes over the shadow, so the bias can
// never paint the shadow over the feet. Drawn with pass id PASS_ALPHABLEND
// so DrawMesh's mIsShadow state-block branch (blend + bias + restore) runs
// unchanged.
for (d3d_OBJECT *obj = mRenderLists[PASS_SHADOW]; obj != NULL; obj = obj->GetNext(PASS_SHADOW))
obj->Draw(PASS_ALPHABLEND, &viewTransform, mTargetRenderTime);
for (d3d_OBJECT *obj = mRenderLists[PASS_OPAQUE]; obj != NULL; obj = obj->GetNext(PASS_OPAQUE))
obj->Draw(PASS_OPAQUE, &viewTransform, mTargetRenderTime);
+13
View File
@@ -143,6 +143,18 @@ void HierarchicalDrawComponent::Execute()
{
if (isDeathDraw || !l4_application->IsDead())
{
// GROUND SHADOW (task #49b): tshd proxies go to the dedicated
// PASS_SHADOW list ONLY -- drawn after the static terrain but
// before the dynamic opaque bodies, so the mech's feet z-pass
// over the shadow instead of the biased shadow painting over
// the feet. (Their drawOps carry alphaTest=1, which would
// otherwise route them to the late alpha-blend pass.)
if (graphicalObject->GetIsShadow())
{
myRenderer->AddToPassList(graphicalObject, PASS_SHADOW);
}
else
{
bool addedToOpaqueList = false, addedToAlphaList = false, addedToDecalList = false, addedToSphereList = false, addedToSkyList = false;
for (int i=0; i<graphicalObject->GetDrawOpCount(); i++)
{
@@ -187,6 +199,7 @@ void HierarchicalDrawComponent::Execute()
}
}
}
} // close the non-shadow classification branch (task #49b)
}
}
}
+8 -1
View File
@@ -14,7 +14,14 @@
#define PASS_ALPHABLEND 2
#define PASS_SPHERE 3
#define PASS_SKY 4
#define PASS_TOTAL_COUNT 5
// GROUND-SHADOW list (task #49b): tshd proxies draw between the STATIC opaque
// geometry (terrain) and the DYNAMIC opaque list (mech bodies) -- the classic
// decal order. The feet then z-pass over the already-drawn shadow, so the
// shadow's big terrain depth-bias can never paint OVER the mech. (The list is
// drawn with pass id PASS_ALPHABLEND so d3d_OBJECT::DrawMesh's mIsShadow
// state-block branch runs unchanged.)
#define PASS_SHADOW 5
#define PASS_TOTAL_COUNT 6
struct L4VERTEX
{