#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor panels". We swapped destroyed LIMB meshes but never darkened a panel. The 1995 game darkens armour through the MATERIALS. MakeMechRenderables (FUN_004cef28) built, per (damage zone, material), a watcher FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials
This commit is contained in:
@@ -743,6 +743,11 @@ HierarchicalDrawComponent*
|
||||
<< (mEyeCockpit ? "" : " (COCKPIT EYE MISSING)") << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// ARMOUR DARKENING (issue #87): the segment geometry is loaded and segPick is
|
||||
// populated, so the mech's .DZM zone->material lists can now be resolved onto
|
||||
// real draw ops. The binary does this in the same function.
|
||||
BindArmourDamage(entity, render_tree);
|
||||
|
||||
return this_root;
|
||||
}
|
||||
|
||||
@@ -852,9 +857,30 @@ void
|
||||
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||
r->second->SetDrawObj(NULL);
|
||||
|
||||
// The swap replaced the DRAWN object, so the segment's recorded geometry
|
||||
// is now stale. Re-point it: the armour-damage bindings (issue #87) hold
|
||||
// draw-op addresses inside these objects and would otherwise keep tinting
|
||||
// the mesh that was just swapped out. (#73's aimed pick reads the same
|
||||
// map, so it stops ray-testing a discarded mesh too.)
|
||||
{
|
||||
std::map<int, MechRenderTree::SegPick>::iterator sp =
|
||||
render_tree.segPick.find(segment_slot);
|
||||
if (sp != render_tree.segPick.end())
|
||||
{
|
||||
if (new_object != NULL && new_object->GetIsShadow() == 0)
|
||||
sp->second.obj = new_object;
|
||||
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||
render_tree.segPick.erase(sp); // blown off: no geometry to hit or tint
|
||||
}
|
||||
}
|
||||
|
||||
++swapped;
|
||||
}
|
||||
|
||||
// Rebind the .DZM armour materials onto whatever geometry is now drawn.
|
||||
if (swapped != 0)
|
||||
BindArmourDamage(entity, render_tree);
|
||||
|
||||
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
||||
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
||||
@@ -1319,6 +1345,190 @@ int
|
||||
}
|
||||
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ARMOUR DARKENING -- BindArmourDamage / TickArmourDamage (issue #87)
|
||||
//
|
||||
// The 1995 renderer darkened a mech's armour panels as they took damage, and it
|
||||
// did it through the MATERIALS, not through geometry. In MakeMechRenderables
|
||||
// (FUN_004cef28) it walked the mech's damage zones and, for every material that
|
||||
// zone paints, built a watcher (FUN_004573e4) holding {material, &zone->damageLevel,
|
||||
// 0.1}. The watcher snapshotted the material's sixteen authored colour floats
|
||||
// (ambient 3, emissive 3, diffuse 3, specular+shininess 4, opacity 3), precomputed
|
||||
// a DAMAGED set = pristine x 0.1, and on every change of the level (FUN_00457784)
|
||||
// re-pushed lerp(pristine, damaged, level) -- the constant at 0x4579a4 is 1.0.
|
||||
// (Faithful quirk: opacity is read and lerped but never written back -- only 13 of
|
||||
// the 16 values are pushed. We scale colour terms only, which matches.)
|
||||
//
|
||||
// The zone->material lists are the per-mech .DZM files (VIDEO\<mech>SKIN.DZM,
|
||||
// "[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl"), compiled into BTL4.RES and
|
||||
// already parsed by MUNGA's own DamageZone stream ctor into materialTable, keyed
|
||||
// by skeleton type (DAMAGE.cpp:311-336) -- the data has been loaded and unused all
|
||||
// along. What was missing was purely the consumer.
|
||||
//
|
||||
// Our draw path bakes material colour into each draw op's D3DMATERIAL9 at load, so
|
||||
// instead of holding material pointers we bind (draw object, op index) pairs here
|
||||
// once, and copy the live level onto them each frame; L4D3D applies the same lerp
|
||||
// at SetMaterial time. Binding by op index keeps the per-frame cost to a pointer
|
||||
// write -- no string compare in the frame loop.
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::BindArmourDamage(Entity *entity, MechRenderTree &tree)
|
||||
{
|
||||
tree.dmgBinds.clear();
|
||||
if (entity == NULL)
|
||||
return;
|
||||
|
||||
const int log = (getenv("BT_ARMOR_LOG") != NULL);
|
||||
|
||||
for (int zone_index = 0; zone_index < entity->damageZoneCount; ++zone_index)
|
||||
{
|
||||
DamageZone *zone = entity->damageZones[zone_index];
|
||||
if (zone == NULL)
|
||||
continue;
|
||||
|
||||
// The .DZM list for the skeleton currently DISPLAYED -- not the one the
|
||||
// tree was built with. Each skeleton variant is painted from its own
|
||||
// skin library (the Black Hawk's N set is blhskin:, its X set blxskin:),
|
||||
// and ApplyViewSkeleton reloads every segment mesh when the view changes,
|
||||
// so keying on the build skeleton matches a material list belonging to
|
||||
// geometry that is not on screen -> zero bindings and no darkening.
|
||||
// A zone with no list for this skeleton simply never darkens, as on the pod.
|
||||
MaterialList *material_list =
|
||||
zone->GetMaterialList((Enumeration)tree.viewSkeleton);
|
||||
if (material_list == NULL)
|
||||
continue;
|
||||
|
||||
int zone_binds = 0;
|
||||
material_list->First();
|
||||
CString *material_name;
|
||||
while ((material_name = material_list->ReadAndNext()) != NULL)
|
||||
{
|
||||
const char *want = (const char *)*material_name;
|
||||
if (want == NULL || want[0] == '\0')
|
||||
continue;
|
||||
int name_hits = 0;
|
||||
|
||||
// Every non-shadow segment object of this mech is a candidate: one
|
||||
// zone's materials can span several segments (the torso zones paint
|
||||
// across the torso and door pieces), and one segment can carry
|
||||
// several zones' materials.
|
||||
for (std::map<int, MechRenderTree::SegPick>::iterator si = tree.segPick.begin();
|
||||
si != tree.segPick.end(); ++si)
|
||||
{
|
||||
d3d_OBJECT *obj = si->second.obj;
|
||||
if (obj == NULL)
|
||||
continue;
|
||||
for (int op = 0; op < obj->GetDrawOpCount(); ++op)
|
||||
{
|
||||
L4DRAWOP *draw_op = obj->GetDrawOp(op);
|
||||
if (draw_op->dzMatName[0] == '\0')
|
||||
continue;
|
||||
// Both sides are hand-authored; case does not agree on every mech.
|
||||
if (_stricmp(draw_op->dzMatName, want) != 0)
|
||||
continue;
|
||||
MechRenderTree::DmgBind bind;
|
||||
bind.obj = obj;
|
||||
bind.op = op;
|
||||
bind.zone = zone_index;
|
||||
tree.dmgBinds.push_back(bind);
|
||||
draw_op->dzDamageLevel = 0.0f;
|
||||
++zone_binds;
|
||||
++name_hits;
|
||||
}
|
||||
}
|
||||
// A .DZM name that matches NOTHING on the drawn geometry is the
|
||||
// failure mode worth naming: it means the material list and the mesh
|
||||
// disagree (wrong skeleton key, or a mech re-skinned since authoring),
|
||||
// and that zone silently stops darkening.
|
||||
if (log && name_hits == 0)
|
||||
DEBUG_STREAM << "[armor] unmatched '" << want << "' (zone "
|
||||
<< zone_index << ")\n" << std::flush;
|
||||
}
|
||||
if (log)
|
||||
DEBUG_STREAM << "[armor] zone " << zone_index << " '"
|
||||
<< (const char *)zone->damageZoneName << "' -> "
|
||||
<< zone_binds << " draw op(s)\n" << std::flush;
|
||||
}
|
||||
|
||||
if (log)
|
||||
DEBUG_STREAM << "[armor] entity " << (void *)entity << " skel="
|
||||
<< tree.skeletonType << ": " << tree.dmgBinds.size()
|
||||
<< " material binding(s)\n" << std::flush;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BTL4VideoRenderer::TickArmourDamage(Entity *entity)
|
||||
{
|
||||
std::map<Entity*, MechRenderTree>::iterator tree_it =
|
||||
mMechRenderTrees.find(entity);
|
||||
if (tree_it == mMechRenderTrees.end())
|
||||
return;
|
||||
MechRenderTree &tree = tree_it->second;
|
||||
if (tree.dmgBinds.empty())
|
||||
return;
|
||||
// A wrecked mech has been swapped to the <mech>dbr hulk -- the bound segment
|
||||
// objects are gone from the draw; leave them alone.
|
||||
if (tree.wrecked)
|
||||
return;
|
||||
|
||||
const int log = (getenv("BT_ARMOR_LOG") != NULL);
|
||||
|
||||
// BENCH (BT_ARMOR_FORCE=<0..1>): pin EVERY bound zone to a fixed level, so the
|
||||
// same scene can be rendered undamaged and fully-damaged and diffed pixel-wise
|
||||
// without smoke, motion or death confounding the comparison.
|
||||
static int s_forceInit = 0;
|
||||
static int s_forceOn = 0;
|
||||
static float s_forceLevel = 0.0f;
|
||||
if (!s_forceInit)
|
||||
{
|
||||
s_forceInit = 1;
|
||||
const char *fv = getenv("BT_ARMOR_FORCE");
|
||||
if (fv != NULL && *fv != '\0')
|
||||
{
|
||||
s_forceOn = 1;
|
||||
s_forceLevel = (float)atof(fv);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < tree.dmgBinds.size(); ++i)
|
||||
{
|
||||
MechRenderTree::DmgBind &bind = tree.dmgBinds[i];
|
||||
if (bind.zone < 0 || bind.zone >= entity->damageZoneCount)
|
||||
continue;
|
||||
DamageZone *zone = entity->damageZones[bind.zone];
|
||||
if (zone == NULL || bind.obj == NULL)
|
||||
continue;
|
||||
L4DRAWOP *draw_op = bind.obj->GetDrawOp(bind.op);
|
||||
const float level = s_forceOn ? s_forceLevel : (float)zone->damageLevel;
|
||||
// Change-gated, like the binary's watcher Perform (FUN_00457784 compares the
|
||||
// cached level against the live one and only then re-pushes the colours).
|
||||
if (log && fabs(level - draw_op->dzDamageLevel) > 0.001f)
|
||||
DEBUG_STREAM << "[armor] '" << draw_op->dzMatName << "' zone "
|
||||
<< bind.zone << " level " << draw_op->dzDamageLevel
|
||||
<< " -> " << level << " (brightness "
|
||||
<< (1.0f - (1.0f - kDamagedMaterialScale) * (level > 1.0f ? 1.0f : level))
|
||||
<< "x)\n" << std::flush;
|
||||
draw_op->dzDamageLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sim-side bridge (per-mech, every frame, from Mech::PerformAndWatch).
|
||||
//
|
||||
void BTArmourDamageTick(Entity *mech)
|
||||
{
|
||||
if (mech == NULL || application == NULL)
|
||||
return;
|
||||
BTL4VideoRenderer *renderer =
|
||||
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||
if (renderer == NULL)
|
||||
return;
|
||||
renderer->TickArmourDamage(mech);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sim-side bridge (UpdateDeathState drives the sink each dead frame).
|
||||
//
|
||||
@@ -2673,8 +2883,21 @@ int
|
||||
}
|
||||
r->second->SetDrawObj(obj);
|
||||
render_tree.segGState[slot] = (int)gstate;
|
||||
// Keep the per-segment geometry record in step with what is actually drawn
|
||||
// (the armour-damage bindings and the #73 pick both key off it).
|
||||
if (obj != NULL && obj->GetIsShadow() == 0)
|
||||
{
|
||||
MechRenderTree::SegPick sp;
|
||||
sp.obj = obj;
|
||||
sp.zone = zone_index;
|
||||
render_tree.segPick[slot] = sp;
|
||||
}
|
||||
else
|
||||
render_tree.segPick.erase(slot);
|
||||
if (obj) ++shown; else ++hidden;
|
||||
}
|
||||
// This reloaded every segment mesh, so every armour-damage binding is stale.
|
||||
BindArmourDamage(viewpoint, render_tree);
|
||||
if (reinstall_paint)
|
||||
{
|
||||
TearDownMaterialSubstitutionList();
|
||||
|
||||
Reference in New Issue
Block a user