Cultural rubble RENDERS: additional objects were drawing at the world origin (issue #3)

The playtest falsified the first revival: the truck vanished with no wreck.
Cause: the rubble (and any additional cultural object) was built as a
DPLStaticChildRenderable / DCSInstanceRenderable added at TOP LEVEL of
mRenderables -- those classes read the bare matrix stack (identity at top
level), so the rubble drew at the WORLD ORIGIN, not at the icon (and before
the consolidation exclusion it was consolidated there permanently).

Fix: every cultural video object (intact AND rubble) is a RootRenderable
(Static) -- placed from myEntity->localToWorld, registered into the frame
pass lists by the same per-frame Execute path the intact model draws
through.  The revived state switch then genuinely swaps them.

PIXEL-VERIFIED via BT_SHOT screenshot timeline (full-speed BT_GOTO ram,
ARENA1 MECHMOVR truck): intact truck visible -> ram/death -> the MECHMD
wreck debris renders scattered at the truck's exact spot, intact model
gone, mech walks the footprint.  ARENA1 census: 57 rubble-typed objects
across the 124 icons (MECHMD/bpip1d/ab07d_FR...).

NOT yet closed (open on #3): (a) the wreck's scorch base quad draws as an
opaque BLACK rectangle (13 plain ops on MECHMD -- material/ramp resolve,
not a blend flag); (b) the death explosion (psfx 1008) still not visibly
confirmed; (c) the authored burning-fire state visual unreconstructed.
Diag added: [cultobj] census (BT_CULT_LOG) -- per-object file/type/op flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 09:56:15 -05:00
co-authored by Claude Fable 5
parent 28b01e3529
commit 5e7b4e8232
+42 -34
View File
@@ -7139,6 +7139,15 @@ void
continue; // next object
}
// BT_CULT_LOG (issue #3): per-object census -- name/type + draw-op
// flags (hunting the opaque-black scorch quad on rubble models).
if (getenv("BT_CULT_LOG"))
{
DEBUG_STREAM << "[cultobj] entity=" << (long)entity->GetEntityID()
<< " file=" << object_filename
<< " type=" << (int)resource_type << std::flush;
}
SET_VIDEO_LOAD_OBJECT();
// HACK !!! load the object as uncached so entity identification will work right
// this_object = dpl_LoadObject(object_filename, dpl_load_normal);
@@ -7153,6 +7162,21 @@ void
continue; // next object
}
}
else if (getenv("BT_CULT_LOG"))
{
// [cultobj] tail: the loaded object's per-op flags
DEBUG_STREAM << " ops=" << this_object->GetDrawOpCount();
for (int opn = 0; opn < this_object->GetDrawOpCount(); opn++)
{
const L4DRAWOP *op = this_object->GetDrawOp(opn);
DEBUG_STREAM << " [" << opn
<< " aT=" << (int)op->alphaTest
<< " dcl=" << (int)op->drawAsDecal
<< " sky=" << (int)op->drawAsSky << "]";
}
DEBUG_STREAM << " shadow=" << (int)this_object->GetIsShadow()
<< "\n" << std::flush;
}
if (first_object)
{
first_object = False;
@@ -7177,40 +7201,24 @@ void
}
else
{
if (renderer_modes | L4VideoObject::BillboardObject)
{
//------------------------------------------
// attach additional object to separate DCS
// because it is billboarded
//------------------------------------------
component = new DPLStaticChildRenderable(
entity,
false,
this_object,
intersect_mode,
intersect_mask,
offset_matrix,
NULL);
// this_DCS = this_child->GetDCS();
// this_instance = this_child->GetInstance();
}
else
{
//----------------------------------------
// attach additional objects to root_DCS
// (HACK) temporary implementation (HACK)
//----------------------------------------
component = new DCSInstanceRenderable(
entity, // Entity to attach the renderable to
DCSInstanceRenderable::Static, // How/when to execute the renderable
this_object, // object to connect to the instance
NULL, // the DCS to add the instance to
intersect_mode, // type of intersections to do on this object
intersect_mask, // intersection mask for the object
True); // initial visibility setting
//this_DCS = NULL;
// this_instance = another_instance->GetInstance();
}
//------------------------------------------------------------
// Issue #3 (cultural destruction): additional cultural objects
// (the RUBBLE model above all) used to go to
// DPLStaticChildRenderable / DCSInstanceRenderable -- but as
// TOP-LEVEL mRenderables entries those classes read the bare
// matrix stack (identity), so the rubble drew at the WORLD
// ORIGIN, not at the icon (and pre-revival it consolidated
// there permanently). RootRenderable places from
// myEntity->localToWorld -- the same path the intact (first)
// object draws through -- so every cultural object is one.
//------------------------------------------------------------
component = new RootRenderable(
entity, // Entity to attach the renderable to
RootRenderable::Static, // How/when to execute the renderable
this_object, // the additional object (rubble etc.)
false, // DPL Zone (for culling)
intersect_mode, // type of intersections to do on this object
intersect_mask); // intersection mask for the object
}
// add the new object to our renderables list. Keep the draw
// component in hand -- the revived state switch (issue #3)