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:
co-authored by
Claude Fable 5
parent
099cad998d
commit
b6fe686b22
+22
-10
@@ -86,16 +86,28 @@ them out) → poll `GetAsyncKeyState` per frame, gated on foreground. Virtual co
|
||||
persistent throttle LEVER with a zero detent; A/D a momentary auto-centering stick. [T2]
|
||||
|
||||
## Shadow (default-on, `BT_SHADOW_TILT`)
|
||||
BT 4.10's shadow is the flat `*_tshd.bgf` proxy drawn as blend-pass geometry, posed by `jointshadow`
|
||||
(terrain tilt) + `jointtshadow` (torso twist). Terrain-tilt samples the collision surface gradient;
|
||||
a depth-bias (`D3DRS_DEPTHBIAS`, decal-style) stops it z-fighting/vanishing on inclines. Polar maps
|
||||
have STUB `*_tshd` (shadow authored-off on snow). Also the visual-ground conform (`btvisgnd.cpp`,
|
||||
render-only lift to the visible terrain triangles — presentation only, never touches localOrigin). [T2]
|
||||
⚠ **"Shadow vanishes on inclines" can be a GAIT-DESYNC symptom, not a bias/tilt bug.** The 2026-07-09
|
||||
recurrence CO-RESOLVED with the gait v5 fix (user-confirmed live; no shadow code changed). The bias
|
||||
margin is only ~2-4 world units, so anything that pops the displayed root pose (v4's out-of-phase leg
|
||||
channel showing through) can push the flat quad under the slope → vanish [T2 observation; mechanism
|
||||
T4]. Before touching bias/tilt for this report, check `[sync]`/`BT_SYNC_LOG` for channel drift first.
|
||||
BT 4.10's shadow is the `*_tshd.bgf` proxy, posed by `jointshadow` (terrain tilt — the SKL's own
|
||||
contract: "apply terrain angle to pitch and roll") + `jointtshadow` (torso twist). The working 2026-07-09
|
||||
arrangement (user-verified live: feet layer over it, opaque, survives inclines/declines) has FOUR
|
||||
co-dependent parts — change one, re-check the others:
|
||||
1. **TILT** (mech.cpp `UpdateShadowJoint`): 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 two prior tilt attempts "dug into the hillside"/vanished. ~35° cliff-guard cap.
|
||||
2. **SAMPLER** (mech4.cpp): gradient from the collision probes PLUS `BTVisualGroundLift` per probe
|
||||
(the quad hugs the VISIBLE terrain, whose lift varies across a slope); world→local by the TRUE yaw
|
||||
from `localToWorld` (NOT the `gDriveHeading` scalar mirror — the task-#48 drift bug class).
|
||||
`BT_SHADOW_LOG` traces the normals.
|
||||
3. **DRAW ORDER** (`PASS_SHADOW`, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw between the STATIC terrain
|
||||
and the DYNAMIC opaque bodies (classic decal order), so the mech z-passes over the shadow — the
|
||||
bias can never paint the shadow OVER the feet, structurally.
|
||||
4. **BIAS pairing** (L4D3D.cpp): tilt on (default) → decal epsilon −0.0008; `BT_SHADOW_TILT=0`
|
||||
(flat A/B fallback) → the old −0.004 it needs to survive slope burial. `BT_SHADOW_BIAS` overrides.
|
||||
Polar maps have STUB `*_tshd` (shadow authored-off on snow). The visual-ground conform (`btvisgnd.cpp`)
|
||||
stays render-only (never touches localOrigin). [T2]
|
||||
⚠ **"Shadow vanishes on inclines" can also be a GAIT-DESYNC symptom.** The first 2026-07-09 recurrence
|
||||
CO-RESOLVED with the gait v5 fix (no shadow code changed): a popping root pose (out-of-phase leg
|
||||
channel showing through) can push the quad past the bias margin. Check `[sync]`/`BT_SYNC_LOG` for
|
||||
channel drift before touching the shadow itself. [T2 observation; mechanism T4]
|
||||
|
||||
## Key Relationships
|
||||
- Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
+46
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------//
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user