Files
BT412/engine/MUNGA_L4/l4d3d.h
T
arcattackandClaude Fable 5 b6fe686b22 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>
2026-07-09 16:59:48 -05:00

212 lines
7.4 KiB
C++

#pragma once
#include <D3DX9.h>
#include <hash_map>
#include <string>
#define L4VERTEX_FVF (D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE | D3DFVF_NORMAL)
#define L4POINTVERTEX_FVF (D3DFVF_XYZ)
#define L4VERTEX_2D_FVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
#define L4VERTEX_2D_TEX_FVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1)
#define PASS_OPAQUE 0
#define PASS_DECAL 1
#define PASS_ALPHABLEND 2
#define PASS_SPHERE 3
#define PASS_SKY 4
// 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
{
float x, y, z;
float nx, ny, nz;
DWORD color;
float u, v;
};
struct L4POINTVERTEX
{
float x, y, z;
};
struct L4VERTEX_2D
{
float x, y, z, rhw;
DWORD color;
};
struct L4VERTEX_2D_TEX
{
float x, y, z, rhw;
DWORD color;
float u, v;
};
struct L4TEXOP
{
enum WrapType
{
REPEAT,
CLAMP,
SELECT
} wrap_u, wrap_v;
LPDIRECT3DTEXTURE9 texture;
bool doScroll;
float scrollUDelta;
float scrollVDelta;
};
struct L4RAMP
{
float r0, g0, b0;
float r1, g1, b1;
};
struct L4DRAWOP
{
D3DMATERIAL9 material;
L4TEXOP texture;
bool alphaTest;
bool drawAsDecal;
bool drawAsSky;
// PORT (draw-cost fix): explicit index range for BGF-built meshes. D3DX
// DrawSubset needs an attribute TABLE; our double-sided BGF indices make
// GenerateAdjacency/OptimizeInplace fail silently, so DrawSubset fell back to
// scanning the whole attribute buffer PER CALL (~44us x ~1400 calls = ~60ms/
// frame). The loader knows each batch's exact range -- draw it directly.
int bgfStartIndex; // first index of the batch in the mesh IB
int bgfPrimCount; // triangle count (0 = not a BGF batch -> DrawSubset)
// PORT (authentic LODs): this batch's LOD viewing band [lodNear..lodFar) from
// the BGF's 0x2046 headers. DrawMesh draws only the ops whose band contains
// the camera->object distance (the IG board's distance LOD selection). A
// zero-init op (lodFar==0) is treated as always-drawn (non-BGF fallback).
float lodNear;
float lodFar;
// DIAG (BT_LOD_LOG): last band-gate visibility (0=unknown 1=visible 2=hidden)
// so DrawMesh can log per-op FLIP events during movement (which piece blinks).
int lastBandVisible;
// PUNCH (dpl_Punchize, PATCH SV_SPECIAL): cutout punch-through -- the texture's
// black texels were loaded with alpha 0; draw with alpha TEST in the opaque pass
// (z-write preserved), rejecting hole texels.
bool punchThrough;
// LAYER DEPTH BIAS (additive-LOD coplanar resolution; see bgfload.h): finer
// layers get a slightly more negative normalized-depth bias so EXACTLY-
// coplanar duplicated surfaces resolve to the detail layer instead of
// venetian-blind z-fighting (the board's submission-order rule, in D3D terms).
float lodDepthBias;
};
class d3d_OBJECT
{
public:
d3d_OBJECT(LPDIRECT3DDEVICE9 device, int vertCount);
d3d_OBJECT(LPDIRECT3DDEVICE9 device, LPD3DXMESH mesh, DWORD *adjacencyBuffer, int drawOpCount);
~d3d_OBJECT();
inline LPDIRECT3DDEVICE9 GetDevice() { return mDevice; }
inline int GetVertCount() { return mVertCount; }
inline int GetDrawOpCount() { return mDrawOpCount; }
inline L4DRAWOP* GetDrawOp(int index) { return &(mDrawOps[index]); }
void Draw(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame);
inline void SetRadius(float radius) { mRadius = radius; }
inline void SetOrigin(L4POINTVERTEX origin) { mOrigin = origin; }
inline void SetImmune(bool immune) { mImmune = immune; }
// BT (task #20): mark the mech's *_tshd SHADOW-PROXY geometry -- the binary's
// shadow is this flat silhouette posed by jointshadow/jointtshadow; it must
// draw TRANSLUCENT dark in the blend pass (the port drew it opaque black).
inline void SetIsShadow(int v) { mIsShadow = v; }
inline int GetIsShadow() { return mIsShadow; }
int mIsShadow;
// DIAG (BT_LOD_LOG): model basename for flip telemetry (set by LoadObjectBGF;
// MUST be initialized in BOTH ctors -- debug heap 0xCD fill reads as garbage).
char mDbgName[24];
inline void SetLocalToWorld(D3DXMATRIX localToWorld) { mLocalToWorld = localToWorld; }
inline float GetRadius() { return mRadius; }
inline L4POINTVERTEX GetOrigin() { return mOrigin; }
inline bool GetImmune() { return mImmune; }
inline D3DXMATRIX GetLocalToWorld() { return mLocalToWorld; }
inline LPD3DXMESH GetMesh() { return mMesh; }
inline d3d_OBJECT* GetNext(int pass) { return mNext[pass + 1]; }
inline d3d_OBJECT* GetPrevious( int pass) { return mPrev[pass + 1]; }
inline void SetNext(d3d_OBJECT *next, int pass) { mNext[pass + 1] = next; }
inline void SetPrevious(d3d_OBJECT *previous, int pass) { mPrev[pass + 1] = previous; }
static void ResetState(LPDIRECT3DDEVICE9 device);
static d3d_OBJECT* LoadObject(LPDIRECT3DDEVICE9 device, char *fileName);
static L4TEXOP LoadTexture(LPDIRECT3DDEVICE9 device, const char *fileName);
// PORT (turn-hitch fix): bounding-sphere frustum culling. The 1995 renderer
// drew EVERYTHING every frame (the IG board clipped in hardware); on D3D9 that
// is ~2700 draw calls/frame = ~107ms CPU. The renderer sets the frame's view
// frustum once (SetCullFrustum); Draw() then skips objects fully outside it.
static void SetCullFrustum(const D3DXMATRIX *viewProj); // NULL disables for the frame
static void SetCameraPosition(float x, float y, float z); // for LOD distance selection
D3DXVECTOR3 mCullCenter; // model-space bounding sphere (computed at load)
float mCullRadius; // <= 0 -> unknown, never culled
// PORT (draw-cost fix): direct-draw path for BGF meshes (see L4DRAWOP). The
// mesh's own VB/IB, cached AddRef'd at load; drawn with DrawIndexedPrimitive
// per batch range instead of D3DX DrawSubset. NULL -> DrawSubset fallback.
LPDIRECT3DVERTEXBUFFER9 mBgfVB;
LPDIRECT3DINDEXBUFFER9 mBgfIB;
UINT mBgfStride;
UINT mBgfNumVerts;
private:
static d3d_OBJECT* LoadSpheres(LPDIRECT3DDEVICE9 device, char *fileName);
// Builds a mesh-backed d3d_OBJECT directly from a VWE .BGF (pod ships .bgf, not .x).
static d3d_OBJECT* LoadObjectBGF(LPDIRECT3DDEVICE9 device, char *fileName);
LPDIRECT3DDEVICE9 mDevice;
inline void SetVB(LPDIRECT3DVERTEXBUFFER9 buffer) { mVB = buffer; }
void DrawMesh(int pass, const D3DXMATRIX *viewTransform, Time targetRenderFrame);
void DrawSpheres(int pass, const D3DXMATRIX *viewTransform);
long mID;
// vertex based
int mVertCount;
LPDIRECT3DVERTEXBUFFER9 mVB;
// mesh based
LPD3DXMESH mMesh;
int mDrawOpCount;
L4DRAWOP* mDrawOps;
bool mImmune;
D3DXMATRIX mLocalToWorld;
L4POINTVERTEX mOrigin;
float mRadius;
d3d_OBJECT *mNext[PASS_TOTAL_COUNT + 1];
d3d_OBJECT *mPrev[PASS_TOTAL_COUNT + 1];
void SetTextureScrolling(const L4TEXOP *texture, Time targetRenderFrame);
void SetTextureAddressing(L4TEXOP::WrapType wrap_u, L4TEXOP::WrapType wrap_v);
void SetTexture(LPDIRECT3DTEXTURE9 texture);
// state information for optimizing renderstate changes
static bool mLastTextureScrollState;
static L4TEXOP::WrapType mLastWrapU;
static L4TEXOP::WrapType mLastWrapV;
static bool mLastTexturingState;
static long mNextID;
static stdext::hash_map< std::string , L4TEXOP > mTextureCache;
};
extern int gNumBatches;