The mech rendered with a live root and frozen limbs. Cause, and it is not in
the renderer:
L4VIDRND.CPP:1028 sets SINGLE_AXIS_HINGE True, so HingeRenderable pushes a
joint with dpl_SetDCSXAxis / YAxis / ZAxis -- three separate libDPL entry
points, each handed only (sine, cosine). The AXIS is carried by WHICH
FUNCTION WAS CALLED, and libDPL never puts it on the wire.
Confirmed against a live capture rather than argued: every articulation
record in joints_check.fifodump is 12 bytes, [handle][sin][cos], 26133 of
them, no axis field anywhere. And the renderer cannot recover it from
context either -- that capture contains ZERO action-0x22 name records, so a
DCS handle cannot be matched back to a .SKL node. vrboard was parsing those
records correctly and dropping them on purpose, with a comment saying so.
THE FIX is the archive's own alternative. The #else half of that same #if
(L4VIDRND.CPP:1153) builds a Quaternion from the Hinge and assigns it over
the DCS matrix -- the axis ends up IN the matrix, and it flushes as a
12-float pose the renderer already applies. That is precisely what
BallJointRenderable::Execute does unconditionally, with no #if at all, which
is why ball joints were never affected. So this is the branch the original
authors wrote and did not take, applied to hinges so they behave like the
ball joints beside them.
A DCS REMEMBERS HOW IT WAS LAST SET -- the part that cost a build.
The first attempt subclassed HingeRenderable and overrode only Execute. It
compiled, ran, and did not work: the wire still carried 12-byte records. What
it DID change was their contents -- the pair went from (sin, cos) to
(0.9990, 0.0000), which is m00 and m01 of the matrix being written. That is
the whole diagnosis in one number. The base ctor calls dpl_SetDCS?Axis before
the subclass gets control, which puts the DCS in single-axis mode, and the
flush then serialises two floats out of it no matter what goes in through
dpl_GetDCSMatrix.
So the axis setter must never touch this DCS. BTL4HingeRenderable therefore
derives from ChildOffsetRenderable, whose ctor builds the offset DCS and calls
no axis setter -- making it the exact hinge analogue of BallJointRenderable:
same base, holds its own attribute pointer plus a previous-value copy, writes
a full matrix in Execute. CODE/ untouched; Component::Execute is virtual
(CMPNNT.HPP:40) so the override dispatches normally, and shadowing 1900 lines
of L4VIDRND.CPP to flip one #define was not needed.
VERIFIED on the rig, torso-sweep conf, clean run:
before 26133 records, ALL 2-float, 1 handle, renderer applied NOTHING
after 7626 records, ALL 12-float, ZERO 2-float messages remaining,
22 handles emitted, renderer applied all 22 (anim_abs)
and the swept joint moves: handle 0x684, m00 across 1532 distinct values
from 0.1582 to 1.0000 -- cos of a 0..80 degree sweep, which is exactly what
BT_FORCE_TORSO drives.
LEG GAIT: NOT FIXED, and the earlier note claiming this would fix it was
wrong. The transport was necessary but not sufficient. On a walking run
(new pod_render_joints.conf = the arena mission with BT_JOINTS) all 22 joints
flush as matrices and the renderer applies them, but only handle 0x672 -- the
vehicle ROOT -- animates. The limbs emit their initial pose and never change.
Because nothing drives them. Joint::SetHinge / SetRotation exist, and the
ONLY caller anywhere in the reconstruction is TorsoSimulation
(BT/TORSO.CPP:382, horizontalJointNode->SetRotation). That is why the
torso is the one thing that moves. MAD.SKL declares JointCount=25 --
jointhip, jointlthigh, jointrthigh, jointtorso, jointshakey, jointeye and
the rest -- and the gait that should walk them is simply not reconstructed
yet. Separate piece of work, now unblocked rather than done.
HARNESS CAVEAT: pose_probe frames on "the last-articulated root" in
anim_abs. With 21 joint DCSs now landing there, that heuristic picks a joint
instead of the vehicle root and frames empty arena. The change invalidated
the assumption; the render is not evidence either way until it takes the root
handle explicitly. The counts above are the evidence.
FAULTS SEEN: two crashes during this work, both cr2=7000FA64 at host 66D9 --
the known parked load-window fault, address unmoved across a relink (which is
the standing test for "not ours"). They died at DIFFERENT points ([mer] 217
and [mer] 116), where a defect in new construction code would die
consistently. Third and fourth runs clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1035 lines
34 KiB
C++
1035 lines
34 KiB
C++
//===========================================================================//
|
|
// File: btl4vid.cpp //
|
|
// Project: BattleTech //
|
|
// Contents: Implementation details for the BT video renderer //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
|
// All Rights reserved worldwide //
|
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
|
|
#include <btl4.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(BTL4VID_HPP)
|
|
# include <btl4vid.hpp>
|
|
#endif
|
|
//
|
|
// Matrix4x4 -- the DCS wire layout (see RecurseSKLFile). l4video.hpp pulls
|
|
// rotation.hpp but not matrix.hpp; L4VIDRND.CPP includes it the same way.
|
|
//
|
|
#if !defined(MATRIX_HPP)
|
|
# include <matrix.hpp>
|
|
#endif
|
|
//
|
|
// Mech::ResolveJoint + the Joint types the articulation path switches on.
|
|
//
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
#if !defined(JOINT_HPP)
|
|
# include <joint.hpp>
|
|
#endif
|
|
|
|
//
|
|
// The engine's damage-zone tagging callback reads this while geometry is
|
|
// loading (L4VIDEO.CPP:527). It is a bare global there -- defined at
|
|
// L4VIDEO.CPP:351 with no header declaration -- so declare it here to set
|
|
// it around our own loads, exactly as DPLRenderer does around its.
|
|
//
|
|
extern Entity
|
|
*Entity_Being_Created;
|
|
|
|
//###########################################################################
|
|
//###################### BTL4HingeRenderable ############################
|
|
//###########################################################################
|
|
//
|
|
// A HingeRenderable that flushes the joint as a FULL MATRIX instead of a
|
|
// single-axis sin/cos pair.
|
|
//
|
|
// THE PROBLEM. L4VIDRND.CPP:1028 sets SINGLE_AXIS_HINGE True, so both the
|
|
// ctor and Execute push the hinge with dpl_SetDCSXAxis / YAxis / ZAxis --
|
|
// three distinct libDPL entry points, each handed only (sine, cosine). The
|
|
// AXIS is therefore carried by WHICH function was called, and libDPL does not
|
|
// put it on the wire. Confirmed against a live capture
|
|
// (emulator/render-bridge/joints_check.fifodump): every articulation record
|
|
// is 12 bytes, [dcs handle][sin][cos], 26133 of them, and there is no axis
|
|
// field anywhere in the message. Nor can the renderer recover it from
|
|
// context -- the wire names nothing (zero action-0x22 name records in that
|
|
// capture), so a handle cannot be matched back to a .SKL node.
|
|
//
|
|
// The consequence is visible: vrboard parses those records, cannot apply
|
|
// them, and leaves the flushed matrix standing. The mech renders with a
|
|
// live root but frozen limbs.
|
|
//
|
|
// THE FIX is the archive's own alternative. The #else half of that same
|
|
// #if (L4VIDRND.CPP:1153) builds a Quaternion from the Hinge and assigns it
|
|
// over the DCS matrix, which encodes the axis in the matrix itself and
|
|
// flushes as a 12-float pose. The renderer already applies those -- it is
|
|
// exactly what BallJointRenderable::Execute does unconditionally, with no
|
|
// #if at all, which is why ball joints were never affected by this.
|
|
//
|
|
// So this is not a new mechanism. It is the branch the original authors
|
|
// wrote and did not take, applied to hinges so they behave like the ball
|
|
// joints beside them.
|
|
//
|
|
// Cost: 48 wire bytes per changed hinge per frame instead of 8. With ~22
|
|
// articulated nodes that is under 1KB/frame, against captures that already
|
|
// run to megabytes.
|
|
//
|
|
// Why a subclass and not a shadow of L4VIDRND.CPP: this needs one method,
|
|
// and Component::Execute is virtual (CMPNNT.HPP:40), so an override
|
|
// dispatches normally. Shadowing the engine file to flip one #define would
|
|
// fork 1900 lines of it to change two.
|
|
//
|
|
// WHY THIS DERIVES FROM ChildOffsetRenderable AND NOT FROM HingeRenderable.
|
|
// The first attempt did subclass HingeRenderable and override only Execute.
|
|
// It built, ran, and DID NOT WORK -- the wire still carried 12-byte records.
|
|
// What it did change was their contents: the pair went from (sin, cos) to
|
|
// (0.9990, 0.0000), which is m00 and m01 of the matrix being written.
|
|
//
|
|
// That is the whole answer. The DCS remembers HOW IT WAS LAST SET. The base
|
|
// HingeRenderable ctor calls dpl_SetDCSXAxis/YAxis/ZAxis before we get
|
|
// control, which puts the DCS in single-axis mode, and the flush then
|
|
// serialises two floats out of it no matter what we write through
|
|
// dpl_GetDCSMatrix. Writing a full matrix into an axis-mode DCS just means
|
|
// the first two cells of the matrix get sent.
|
|
//
|
|
// So the axis-mode setter must never run on this DCS. Deriving from
|
|
// ChildOffsetRenderable -- whose ctor builds the offset DCS and touches no
|
|
// axis setter -- is what avoids it. That makes this class the exact hinge
|
|
// analogue of BallJointRenderable: same base, same shape, holds its own
|
|
// attribute pointer plus a previous-value copy, writes a full matrix in
|
|
// Execute. Which is presumably how it would have been written had the
|
|
// single-axis optimisation not been there.
|
|
//
|
|
// NOTE on the archive's stale comment: the ctor there says the quaternion
|
|
// hop exists "because the math library doesn't support direct assignment of
|
|
// hinge to matrix yet". Still true, and worth recording because the header
|
|
// suggests otherwise -- Matrix4x4::operator=(const Hinge&) is DECLARED at
|
|
// MATRIX.HPP:94 but implemented nowhere, so taking the direct route is a
|
|
// link error. Quaternion::operator=(const Hinge&) is real
|
|
// (MUNGA/ROTATION.CPP:707, correct half-angle about axisNumber) and
|
|
// Matrix4x4::operator=(const Quaternion&) is real (MATRIX.CPP:260).
|
|
//
|
|
class BTL4HingeRenderable :
|
|
public ChildOffsetRenderable
|
|
{
|
|
public:
|
|
BTL4HingeRenderable(
|
|
Entity *entity,
|
|
ExecutionType execution_type,
|
|
dpl_OBJECT *graphical_object,
|
|
dpl_ZONE *this_zone,
|
|
dpl_ISECT_MODE intersect_mode,
|
|
uint32 intersect_mask,
|
|
dpl_DCS *parent_DCS,
|
|
LinearMatrix *offset_matrix,
|
|
const Hinge *my_hinge
|
|
):
|
|
ChildOffsetRenderable(
|
|
entity, execution_type, graphical_object, this_zone,
|
|
intersect_mode, intersect_mask, parent_DCS, offset_matrix
|
|
)
|
|
{
|
|
Check(my_hinge);
|
|
|
|
myHinge = my_hinge;
|
|
oldHinge = *my_hinge;
|
|
//
|
|
// Push the initial pose, exactly as both stock joint renderables
|
|
// do in their ctors -- otherwise the DCS holds the identity the
|
|
// base installed until the joint first moves.
|
|
//
|
|
FlushAsMatrix();
|
|
}
|
|
|
|
void
|
|
Execute();
|
|
|
|
protected:
|
|
void
|
|
FlushAsMatrix();
|
|
|
|
const Hinge
|
|
*myHinge; // the joint attribute we watch
|
|
Hinge
|
|
oldHinge; // last value seen, so we only flush on change
|
|
};
|
|
|
|
void
|
|
BTL4HingeRenderable::FlushAsMatrix()
|
|
{
|
|
Quaternion
|
|
temp_quaternion;
|
|
float32
|
|
*temp_matrix;
|
|
|
|
temp_matrix = dpl_GetDCSMatrix(myDCS);
|
|
Check_Pointer(temp_matrix);
|
|
temp_quaternion = oldHinge;
|
|
*(Matrix4x4 *)temp_matrix = temp_quaternion;
|
|
|
|
//
|
|
// This is what DPL_FLUSH_DCS expands to (L4VIDRND.CPP:73). The macro is
|
|
// file-private to L4VIDRND.CPP, so spell it out rather than redefine it
|
|
// here and risk the two drifting. It matters that this is the DELAYED
|
|
// flush and not a bare dpl_FlushDCS: the batch is what the renderer
|
|
// coalesces per frame, and going around it would put every joint on the
|
|
// wire immediately.
|
|
//
|
|
myRenderer->DPLDelayDCSFlush(myDCS);
|
|
}
|
|
|
|
void
|
|
BTL4HingeRenderable::Execute()
|
|
{
|
|
Check(this);
|
|
|
|
if (oldHinge != *myHinge)
|
|
{
|
|
oldHinge = *myHinge;
|
|
FlushAsMatrix();
|
|
}
|
|
//
|
|
// Chain the GRANDPARENT, not HingeRenderable::Execute -- that would run
|
|
// the single-axis write we are replacing and re-flush the same DCS.
|
|
//
|
|
ChildOffsetRenderable::Execute();
|
|
}
|
|
|
|
BTL4VideoRenderer::BTL4VideoRenderer(
|
|
RendererRate calibration_rate,
|
|
RendererComplexity calibration_complexity,
|
|
RendererPriority calibration_priority,
|
|
InterestType interest_type,
|
|
InterestDepth depth_calibration
|
|
):
|
|
DPLRenderer(
|
|
calibration_rate,
|
|
calibration_complexity,
|
|
calibration_priority,
|
|
interest_type,
|
|
depth_calibration
|
|
)
|
|
{
|
|
}
|
|
|
|
BTL4VideoRenderer::~BTL4VideoRenderer()
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// LoadMissionImplementation -- called by Renderer::LoadMission (the authentic
|
|
// engine, CODE/RP/MUNGA/RENDERER.CPP:263) after it has set
|
|
// LoadingRendererStatus, stamped nextRenderTime and started the renderer with
|
|
// the RendererManager. This is where a GAME renderer builds the mission's
|
|
// scene content on the Division board.
|
|
//
|
|
// BRING-UP NO-OP (phase 1 of the btl4vid ladder). A no-op is a LEGAL body
|
|
// here, not a cheat: the engine's own VideoRenderer::LoadMissionImplementation
|
|
// (CODE/RP/MUNGA/VIDREND.CPP:259) is a bare Tell, and our GaugeRenderer
|
|
// (GAUGREND.CPP:3275) ships the same. The renderer therefore comes up and
|
|
// runs its frame loop with an EMPTY scene, which is exactly what we want to
|
|
// measure before writing any content.
|
|
//
|
|
// THE AUTHENTIC SHAPE, pinned by the surviving sibling header for Red
|
|
// Planet's renderer (CODE/RP/RP_L4/RPL4VID.HPP -- same engine, same board,
|
|
// same year; only the .CPP is missing there too):
|
|
//
|
|
// LoadMissionImplementation walks the mission's entities and calls
|
|
// MakeEntityRenderables(entity, model_resource, view_type) for each,
|
|
// which builds a dpl_DCS hierarchy through ReadSKLFile /
|
|
// RecurseSKLFile (the skeleton notation pages), with a material
|
|
// substitution list set up around it.
|
|
//
|
|
// The BT-specific renderables (BTReticleRenderable, BTTranslocationRenderable,
|
|
// the pending-wrecks map) come from the BT411 donor game/reconstructed/
|
|
// btl4vid.cpp -- 3188 lines -- but NOTE that port restructured this hook and
|
|
// has no method under this name, so the CONTRACT above comes from the 1995
|
|
// engine and only the CONTENT comes from the donor.
|
|
//#############################################################################
|
|
//
|
|
//
|
|
//#############################################################################
|
|
// ReadSKLFile -- build one entity's render skeleton from its .SKL file.
|
|
//
|
|
// The .SKL is a plain NotationFile. Each page is one node of the skeleton:
|
|
//
|
|
// [jointhip]
|
|
// parent=jointlocal
|
|
// Type=hingex <- joint kind (animation; unused at build)
|
|
// Object=mad_hip.bgf <- optional geometry for this node
|
|
// dzone=dz_hip <- damage-zone tags (zero or more)
|
|
// tranx=.. trany=.. tranz=..
|
|
// pitch=.. yaw=.. roll=..
|
|
// joint=jointtorso <- child pages (zero or more)
|
|
//
|
|
// so the whole model is a recursive walk from [ROOT].
|
|
//
|
|
// Files live under video\ -- the engine uses the same bare "video\\" prefix
|
|
// for its .pfx loads (L4VIDEO.CPP:1513).
|
|
//
|
|
// STAGE 1 (this version): the tree, the geometry and the parenting are real;
|
|
// every node is given an IDENTITY matrix. The model therefore collapses onto
|
|
// the entity origin and looks wrong, which is deliberate -- the STRUCTURE is
|
|
// what is being proved here, and it is provable without looking at a pixel:
|
|
// MAD.SKL declares JointCount=25 (+1 root) and DZoneCount=22, and the
|
|
// dpl3-revive capture of a REAL pod decodes as 26 DCS bodies and 22 instance
|
|
// bodies. The counts this walk reports must match.
|
|
//
|
|
// STAGE 2 is the transforms. The dpl_MATRIX convention is NOT yet proven
|
|
// (see RENDER-ROADMAP.NOTES.md) -- a DCS flush body carries 16 float32, and a
|
|
// decoded capture suggests row-major with the translation in the last row,
|
|
// but the decoder's offset is suspect by one word. Rather than guess it and
|
|
// ship a mech that renders confidently in the wrong orientation, this stage
|
|
// leaves identity in place.
|
|
//#############################################################################
|
|
//
|
|
dpl_DCS *
|
|
BTL4VideoRenderer::ReadSKLFile(
|
|
Entity *entity,
|
|
dpl_DCS *parent_dcs,
|
|
const char *skeleton_filename,
|
|
ViewFrom view_type,
|
|
int *eye_count,
|
|
int *joint_count)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(skeleton_filename);
|
|
Check_Pointer(eye_count);
|
|
Check_Pointer(joint_count);
|
|
|
|
char
|
|
path[256];
|
|
strcpy(path, "video\\");
|
|
strcat(path, skeleton_filename);
|
|
|
|
NotationFile
|
|
*skeleton = new NotationFile(path);
|
|
Register_Object(skeleton);
|
|
|
|
if (skeleton->PageCount() == 0)
|
|
{
|
|
DEBUG_STREAM << "[skl] could not read " << path << "\n" << flush;
|
|
Unregister_Object(skeleton);
|
|
delete skeleton;
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// Every node of this model shares one zone, switched on and flushed by
|
|
// the engine helper (L4VIDEO.CPP:1338).
|
|
//
|
|
dpl_ZONE
|
|
*zone = MakeNewZone();
|
|
|
|
int
|
|
node_count = 0,
|
|
object_count = 0;
|
|
|
|
//
|
|
// The ROOT page's DCS attaches under the caller's parent (the mech's
|
|
// RootRenderable DCS, so the model rides the entity's localToWorld) or,
|
|
// with no parent, straight to the scene -- the old bring-up behaviour,
|
|
// which parks the model at the world origin.
|
|
//
|
|
dpl_DCS
|
|
*root = RecurseSKLFile(
|
|
entity, parent_dcs, skeleton, "ROOT", 0, view_type,
|
|
zone, &node_count, &object_count, eye_count, joint_count);
|
|
|
|
DEBUG_STREAM << "[skl] " << path
|
|
<< " -> " << node_count << " nodes, "
|
|
<< object_count << " objects, "
|
|
<< *eye_count << " eye, "
|
|
<< *joint_count << " articulated\n" << flush;
|
|
|
|
//
|
|
// The engine guards this the same way (L4VIDEO.CPP, DPLReadEnvironment):
|
|
// ~NotationFile REWRITES the file when dirtyFlag is set, and these are
|
|
// the game's shipped .SKL data files.
|
|
//
|
|
Verify(!skeleton->IsDirty());
|
|
Unregister_Object(skeleton);
|
|
delete skeleton;
|
|
return root;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RecurseSKLFile -- one page of the skeleton, then its children.
|
|
//#############################################################################
|
|
//
|
|
dpl_DCS *
|
|
BTL4VideoRenderer::RecurseSKLFile(
|
|
Entity *entity,
|
|
dpl_DCS *parent_dcs,
|
|
NotationFile *skeleton,
|
|
const char *page_name,
|
|
int recursion_depth,
|
|
ViewFrom view_type,
|
|
dpl_ZONE *zone,
|
|
int *node_count,
|
|
int *object_count,
|
|
int *eye_count,
|
|
int *joint_count)
|
|
{
|
|
Check(this);
|
|
Check(skeleton);
|
|
Check_Pointer(page_name);
|
|
|
|
if (!skeleton->PageExists(page_name))
|
|
{
|
|
DEBUG_STREAM << "[skl] missing page '" << page_name << "'\n" << flush;
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// A guard, not a limit: the file is authored data and a bad parent= chain
|
|
// could otherwise recurse forever. The deepest real chain in MAD.SKL is
|
|
// nowhere near this.
|
|
//
|
|
if (recursion_depth > 32)
|
|
{
|
|
DEBUG_STREAM << "[skl] recursion too deep at '" << page_name
|
|
<< "'\n" << flush;
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
// The node's LOCAL TRANSFORM, in the engine's own terms -- the wire
|
|
// convention is no longer inferred from captures, it is READ from the
|
|
// engine: RootRenderable's ctor (L4VIDRND.CPP:853) writes an entity pose
|
|
// into a DCS with
|
|
//
|
|
// *(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = myEntity->localToWorld;
|
|
//
|
|
// so whatever Matrix4x4::operator=(const AffineMatrix&) produces IS the
|
|
// DCS matrix layout. Reading it (MATRIX.CPP:130): Matrix4x4 is ROW-major
|
|
// (MATRIX.HPP:113, entries[(Row<<2)+Column]) with rotation in rows 0-2,
|
|
// ZEROS in column 3, and the TRANSLATION IN ROW 3 -- entries 12/13/14.
|
|
//
|
|
// (The previous revision put translation at entries 3/7/11, quoting
|
|
// AffineMatrix as precedent. AffineMatrix does keep translation at
|
|
// 3/7/11 -- but because it is COLUMN-major, entries[(column<<2)+row]
|
|
// (AFFNMTRX.HPP:99); its (3,c) translation row lands at 3/7/11 by
|
|
// storage, not by convention. Matrix4x4 transposes that on copy. The
|
|
// 'last row made the maths blow up' claim attached to the old choice
|
|
// came from the contaminated bisect -- the crashes were the RIO fault.)
|
|
//
|
|
// Rotation comes from the page's pitch/yaw/roll through the engine's own
|
|
// Matrix4x4::operator=(const EulerAngles&), so the rotation ORDER is the
|
|
// engine's by construction, not a guess. MAD.SKL base-pose angles are
|
|
// all 0 or ~1e-3, so this is nearly identity today -- but it is the
|
|
// correct compose for any skeleton authored with real angles.
|
|
//
|
|
Scalar
|
|
tran_x = 0.0f,
|
|
tran_y = 0.0f,
|
|
tran_z = 0.0f,
|
|
rot_pitch = 0.0f,
|
|
rot_yaw = 0.0f,
|
|
rot_roll = 0.0f;
|
|
skeleton->GetEntry(page_name, "tranx", &tran_x);
|
|
skeleton->GetEntry(page_name, "trany", &tran_y);
|
|
skeleton->GetEntry(page_name, "tranz", &tran_z);
|
|
skeleton->GetEntry(page_name, "pitch", &rot_pitch);
|
|
skeleton->GetEntry(page_name, "yaw", &rot_yaw);
|
|
skeleton->GetEntry(page_name, "roll", &rot_roll);
|
|
|
|
Matrix4x4
|
|
node_matrix;
|
|
node_matrix = EulerAngles(rot_pitch, rot_yaw, rot_roll);
|
|
node_matrix(3,0) = tran_x;
|
|
node_matrix(3,1) = tran_y;
|
|
node_matrix(3,2) = tran_z;
|
|
|
|
//
|
|
// This node's geometry, if it has any. Loaded BEFORE the node is built,
|
|
// because a joint renderable takes its object as a ctor argument and
|
|
// builds/flushes the instance itself (DCSObjectRenderable, L4VIDRND.CPP:
|
|
// 649). Entity_Being_Created is already set by our caller so the
|
|
// library's C callback can tag the geometry with damage zones
|
|
// (L4VIDEO.CPP:4176).
|
|
//
|
|
dpl_OBJECT
|
|
*object = NULL;
|
|
const char
|
|
*object_name;
|
|
if (skeleton->GetEntry(page_name, "Object", &object_name) && object_name)
|
|
{
|
|
object = dpl_LoadObject((char *)object_name, dpl_load_normal);
|
|
if (object == NULL)
|
|
{
|
|
DEBUG_STREAM << "[skl] couldn't load object " << object_name
|
|
<< " for '" << page_name << "'\n" << flush;
|
|
}
|
|
}
|
|
|
|
dpl_DCS
|
|
*dcs = NULL;
|
|
|
|
//
|
|
// ARTICULATION. A node whose page name resolves to a live skeleton Joint
|
|
// gets a JOINT RENDERABLE instead of a static DCS, so the board hears
|
|
// about the joint MOVING.
|
|
//
|
|
// Why this is the whole game: a static node's matrix is written and
|
|
// flushed once at build time. Torso::TorsoSimulation faithfully calls
|
|
// SetRotation() on its Joint every frame, but nothing carried that back
|
|
// to the DCS -- measured 5.3.79, twist sweeping the full authored range
|
|
// with anim_abs=1 joints=0 on the wire and a cockpit view that did not
|
|
// move by more than 74 pixels. The engine's answer is this family
|
|
// (L4VIDRND.CPP:1026+): each renderable holds the joint's rest offset in
|
|
// one DCS and the live rotation in a child DCS, and its Execute compares
|
|
// the watched Hinge/EulerAngles against a cached copy, writes the axis
|
|
// and calls DPL_FLUSH_DCS -- which is what puts more than one node into
|
|
// vr_flush_dcs_artic (0x1f).
|
|
//
|
|
// The joint's live value is read straight out of the mech's own
|
|
// JointSubsystem (Mech::ResolveJoint by segment name -- the .SKL page
|
|
// names ARE the segment names, which is how Torso already resolves
|
|
// 'jointtorso'), so the simulation and the render read ONE source.
|
|
//
|
|
// Env-gated while it proves out: the static path is a working cockpit
|
|
// render and this replaces the node construction wholesale.
|
|
//
|
|
Joint
|
|
*joint = NULL;
|
|
if (
|
|
getenv("BT_JOINTS") != NULL &&
|
|
parent_dcs != NULL &&
|
|
entity->GetClassID() == RegisteredClass::MechClassID
|
|
)
|
|
{
|
|
joint = ((Mech *)entity)->ResolveJoint(page_name);
|
|
}
|
|
|
|
if (joint != NULL)
|
|
{
|
|
LinearMatrix
|
|
offset;
|
|
offset = EulerAngles(rot_pitch, rot_yaw, rot_roll);
|
|
offset(3,0) = tran_x;
|
|
offset(3,1) = tran_y;
|
|
offset(3,2) = tran_z;
|
|
|
|
ChildOffsetRenderable
|
|
*joint_renderable = NULL;
|
|
|
|
switch (joint->GetJointType())
|
|
{
|
|
case Joint::HingeXJointType:
|
|
case Joint::HingeYJointType:
|
|
case Joint::HingeZJointType:
|
|
//
|
|
// BTL4HingeRenderable, not HingeRenderable: the stock one
|
|
// flushes (sin,cos) with the axis implied by which libDPL
|
|
// entry point it called, and the axis never reaches the wire.
|
|
// See the class comment at the top of this file.
|
|
//
|
|
joint_renderable = new BTL4HingeRenderable(
|
|
entity, VideoRenderable::Dynamic, object, zone,
|
|
dpl_isect_mode_obj, 0, parent_dcs, &offset,
|
|
&joint->GetHinge());
|
|
break;
|
|
|
|
case Joint::BallJointType:
|
|
joint_renderable = new BallJointRenderable(
|
|
entity, VideoRenderable::Dynamic, object, zone,
|
|
dpl_isect_mode_obj, 0, parent_dcs, &offset,
|
|
&joint->GetEulerAngles());
|
|
break;
|
|
|
|
default:
|
|
//
|
|
// StaticJointType and BallTranslation: the static path below.
|
|
// (BallTranslate has its own renderable, but no MAD.SKL node
|
|
// animates a translation -- only jointeye is balltranslate, and
|
|
// it carries the camera, not geometry.)
|
|
//
|
|
break;
|
|
}
|
|
|
|
if (joint_renderable != NULL)
|
|
{
|
|
Register_Object(joint_renderable);
|
|
dcs = joint_renderable->GetDCS();
|
|
if (object != NULL)
|
|
{
|
|
++(*object_count);
|
|
}
|
|
++(*joint_count);
|
|
}
|
|
}
|
|
|
|
if (dcs == NULL)
|
|
{
|
|
//
|
|
// STATIC node: baked matrix, flushed once. Correct for anything the
|
|
// simulation never moves.
|
|
//
|
|
dcs = dpl_NewDCS();
|
|
Check_Pointer(dcs);
|
|
dpl_SetDCSZone(dcs, zone);
|
|
|
|
//
|
|
// Write in place and flush -- the same idiom as RootRenderable's ctor
|
|
// (dpl_GetDCSMatrix + assign), not dpl_SetDCSMatrix.
|
|
//
|
|
float32
|
|
*dcs_matrix = dpl_GetDCSMatrix(dcs);
|
|
Check_Pointer(dcs_matrix);
|
|
*(Matrix4x4 *)dcs_matrix = node_matrix;
|
|
|
|
if (parent_dcs != NULL)
|
|
{
|
|
dpl_AddDCSToDCS(parent_dcs, dcs);
|
|
}
|
|
else
|
|
{
|
|
dpl_AddDCSToScene(dcs);
|
|
}
|
|
|
|
if (object != NULL)
|
|
{
|
|
dpl_INSTANCE
|
|
*instance = dpl_NewInstance();
|
|
Check_Pointer(instance);
|
|
dpl_SetInstanceObject(instance, object);
|
|
dpl_AddInstanceToDCS(dcs, instance);
|
|
dpl_FlushInstance(instance);
|
|
++(*object_count);
|
|
}
|
|
|
|
dpl_FlushDCS(dcs);
|
|
}
|
|
|
|
++(*node_count);
|
|
|
|
//
|
|
// Children. Repeated "joint=" entries: the entry NAME is "joint" and the
|
|
// VALUE (dataReference) is the child page -- the same shape as the
|
|
// engine's objectpath= walk at L4VIDEO.CPP:1858.
|
|
//
|
|
NameList
|
|
*children = skeleton->MakeEntryList(page_name, "joint");
|
|
if (children != NULL)
|
|
{
|
|
Register_Object(children);
|
|
|
|
NameList::Entry
|
|
*entry;
|
|
for (entry = children->GetFirstEntry();
|
|
entry != NULL;
|
|
entry = entry->GetNextEntry())
|
|
{
|
|
const char
|
|
*child_page = (const char *)entry->dataReference;
|
|
if (child_page != NULL && *child_page != '\0')
|
|
{
|
|
RecurseSKLFile(
|
|
entity, dcs, skeleton, child_page, recursion_depth + 1,
|
|
view_type, zone, node_count, object_count, eye_count,
|
|
joint_count);
|
|
}
|
|
}
|
|
|
|
Unregister_Object(children);
|
|
delete children;
|
|
}
|
|
|
|
//
|
|
// "site=" children. Sites get NO draw component of their own -- the
|
|
// real pod's capture decodes exactly 26 DCS bodies for this skeleton,
|
|
// which is joints+root only, and this walk matched that count precisely
|
|
// BECAUSE it ignored sites. One site spawns hardware instead of
|
|
// geometry: siteeyepoint is the COCKPIT CAMERA.
|
|
//
|
|
// The authentic construction comes from the donor's decompile
|
|
// (bt411 btl4vid.cpp:462, decomp FUN_004579a8): the eye's offset matrix
|
|
// is the SITE'S OWN local rest transform, and the eye is parented on the
|
|
// site's PARENT segment's draw component -- this page's DCS -- NOT the
|
|
// hull root. World orientation and all live motion (torso twist, gait)
|
|
// then come from the parent-chain composition for free. For MAD.SKL the
|
|
// chain is jointtorso -> jointeye (trany +1.687, tranz -1.318) ->
|
|
// siteeyepoint (identity), which puts the eye in the canopy where the
|
|
// cockpit sits, instead of at the hull origin staring through the torso
|
|
// panels.
|
|
//
|
|
if (view_type == insideEntity)
|
|
{
|
|
NameList
|
|
*sites = skeleton->MakeEntryList(page_name, "site");
|
|
if (sites != NULL)
|
|
{
|
|
Register_Object(sites);
|
|
|
|
NameList::Entry
|
|
*site_entry;
|
|
for (site_entry = sites->GetFirstEntry();
|
|
site_entry != NULL;
|
|
site_entry = site_entry->GetNextEntry())
|
|
{
|
|
const char
|
|
*site_page = (const char *)site_entry->dataReference;
|
|
if (
|
|
site_page != NULL &&
|
|
strcmp(site_page, "siteeyepoint") == 0 &&
|
|
skeleton->PageExists(site_page)
|
|
)
|
|
{
|
|
Scalar
|
|
site_tx = 0.0f, site_ty = 0.0f, site_tz = 0.0f,
|
|
site_pitch = 0.0f, site_yaw = 0.0f, site_roll = 0.0f;
|
|
skeleton->GetEntry(site_page, "tranx", &site_tx);
|
|
skeleton->GetEntry(site_page, "trany", &site_ty);
|
|
skeleton->GetEntry(site_page, "tranz", &site_tz);
|
|
skeleton->GetEntry(site_page, "pitch", &site_pitch);
|
|
skeleton->GetEntry(site_page, "yaw", &site_yaw);
|
|
skeleton->GetEntry(site_page, "roll", &site_roll);
|
|
|
|
//
|
|
// Default ctor identities; the EulerAngles assignment
|
|
// writes ONLY the 3x3 rotation (AFFNMTRX.CPP:179), so
|
|
// the identity's zero translation survives it and the
|
|
// site translation goes in after.
|
|
//
|
|
LinearMatrix
|
|
site_offset;
|
|
site_offset =
|
|
EulerAngles(site_pitch, site_yaw, site_roll);
|
|
site_offset(3,0) = site_tx;
|
|
site_offset(3,1) = site_ty;
|
|
site_offset(3,2) = site_tz;
|
|
|
|
EulerAngles
|
|
*eyepoint_rotation =
|
|
(EulerAngles *)entity->GetAttributePointer(
|
|
"EyepointRotation");
|
|
DPLEyeRenderable
|
|
*cockpit_eye = new DPLEyeRenderable(
|
|
entity,
|
|
dplMainZone,
|
|
site_offset,
|
|
dcs,
|
|
dplMainView,
|
|
eyepoint_rotation);
|
|
Register_Object(cockpit_eye);
|
|
++(*eye_count);
|
|
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [eye] cockpit eye on '"
|
|
<< page_name << "' offset=("
|
|
<< site_tx << "," << site_ty << ","
|
|
<< site_tz << ")\n" << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
Unregister_Object(sites);
|
|
delete sites;
|
|
}
|
|
}
|
|
|
|
return dcs;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// MakeEntityRenderables -- the game level of the renderable factory.
|
|
//
|
|
// The engine's DPLRenderer::MakeEntityRenderables (L4VIDEO.CPP:4151) knows
|
|
// the ENGINE entity classes and calls DOWN to
|
|
// VideoRenderer::MakeEntityRenderables for anything else, which only prints
|
|
// Entity <id> class<n> couldn't figure out how to MakeEntityRenderables
|
|
// So every BT class has to be answered here.
|
|
//
|
|
// FIRST ANSWER: BTPlayer (class 3035) carries no graphics. The engine
|
|
// already does exactly this for its own PlayerClassID -- an empty case --
|
|
// and BT's player is simply a different id it cannot know about. This is
|
|
// the class the live pod run complained about.
|
|
//
|
|
// Everything else still chains to the engine, so this override can only
|
|
// ADD answers, never remove the ones DPLRenderer already gives.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
BTL4VideoRenderer::MakeEntityRenderables(
|
|
Entity *entity,
|
|
ResourceDescription *model_resource,
|
|
ViewFrom view_type)
|
|
{
|
|
Check(this);
|
|
Check(entity);
|
|
|
|
//
|
|
// RENDERABLE-BUILD TRACE (env BT_MER_LOG).
|
|
//
|
|
// The mission never launches because 530 renderer events queue at
|
|
// priority 0 during load and the background pump drains only one per
|
|
// seven frames, and the page fault lands on one of the LAST ~35 of them.
|
|
// So the fault belongs to a specific entity, not to elapsed time. The
|
|
// oldest log named it -- class 42, UnscalableTerrain -- but that was a
|
|
// different build, so this prints the class of every entity as its
|
|
// renderables are built and the last line before the fault names the
|
|
// culprit outright. One line per entity, not per frame: ~530 lines for a
|
|
// whole run, which the COM3 log carries without the 3x cost that per-frame
|
|
// logging brought.
|
|
//
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
static int
|
|
mer_count = 0;
|
|
|
|
mer_count++;
|
|
DEBUG_STREAM << "[mer] " << mer_count
|
|
<< " class=" << (int)entity->GetClassID()
|
|
<< " view=" << (int)view_type
|
|
<< " res=" << (model_resource ? 1 : 0)
|
|
<< endl << flush;
|
|
}
|
|
|
|
switch (entity->GetClassID())
|
|
{
|
|
case RegisteredClass::MechClassID:
|
|
//
|
|
// The mech's video resource is a SKELETON. Walk the chain the
|
|
// same way the engine does (L4VIDEO.CPP:4250) and hand every
|
|
// Skeleton entry to ReadSKLFile; anything else falls through to
|
|
// the engine, which knows what to do with plain objects.
|
|
//
|
|
{
|
|
if (model_resource == NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
ChainOf<L4VideoObjectWrapper*>
|
|
video_chain(NULL);
|
|
L4VideoObjectWrapper::BuildVideoObjectChainFromResource(
|
|
&video_chain, model_resource);
|
|
|
|
ChainIteratorOf<L4VideoObjectWrapper*>
|
|
video_iterator(video_chain);
|
|
L4VideoObjectWrapper
|
|
*video_wrapper;
|
|
Logical
|
|
handled = False;
|
|
|
|
//
|
|
// BT_NO_SKL skips the skeleton build so the SAME binary can be
|
|
// run with and without it -- the only way to attribute the
|
|
// intermittent pod crash without a rebuild between samples.
|
|
//
|
|
if (getenv("BT_NO_SKL") != NULL)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Entity_Being_Created = entity;
|
|
video_iterator.First();
|
|
while ((video_wrapper = video_iterator.ReadAndNext()) != NULL)
|
|
{
|
|
const L4VideoObject
|
|
*video_object = video_wrapper->GetVideoObject();
|
|
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [vid] type="
|
|
<< (int)video_object->GetResourceType()
|
|
<< " file='" << video_object->GetObjectFilename()
|
|
<< "'" << endl << flush;
|
|
}
|
|
|
|
if (video_object->GetResourceType()
|
|
== L4VideoObject::Skeleton)
|
|
{
|
|
//
|
|
// THE ENGINE COMPOSITION, mirrored from the Mover branch
|
|
// of DPLRenderer::MakeEntityRenderables
|
|
// (L4VIDEO.CPP:4795-4860), which the base cannot apply
|
|
// here because it only accepts Object/Rubble resources --
|
|
// chaining it with a Skeleton just prints "wrong video
|
|
// resource type" and builds NOTHING (measured: zero
|
|
// vr_flush_dcs_artic records on the wire, camera frozen
|
|
// at the world origin, and the mech statue parked there
|
|
// with the eye inside it).
|
|
//
|
|
// 1. A DYNAMIC RootRenderable. Its ctor adds its DCS to
|
|
// the scene and seeds it from entity->localToWorld;
|
|
// its Execute re-flushes whenever the entity moves.
|
|
// That per-frame flush is the ONLY source of wire
|
|
// articulation (0x1f) for the vehicle -- without it
|
|
// nothing on the board ever moves. NULL graphical
|
|
// object exactly like the CameraShip inside-view case
|
|
// (L4VIDEO.CPP:4548): the skeleton supplies the
|
|
// geometry.
|
|
//
|
|
RootRenderable
|
|
*this_root = new RootRenderable(
|
|
entity,
|
|
RootRenderable::Dynamic,
|
|
NULL,
|
|
dplMainZone,
|
|
dpl_isect_mode_obj,
|
|
NULL);
|
|
Register_Object(this_root);
|
|
|
|
dpl_DCS
|
|
*root_DCS = this_root->GetDCS();
|
|
|
|
//
|
|
// 2. The skeleton hangs UNDER the root DCS -- so the whole
|
|
// model rides the entity's localToWorld instead of
|
|
// being parked at the world origin. The walk also
|
|
// builds the COCKPIT EYE when it meets the
|
|
// siteeyepoint site (donor construction: offset =
|
|
// the site's local rest, parent = the site's parent
|
|
// joint DCS -- so the eye sits in the canopy and
|
|
// rides torso twist through chain composition).
|
|
//
|
|
// THE INSIDE VIEW LOADS THE X-VARIANT SKELETON. The
|
|
// resource names one skeleton (mad.skl) for both views;
|
|
// the cockpit build is derived from it by the fleet-wide
|
|
// naming convention -- third letter X: MAD->MAX,
|
|
// AVA->AVX, BAT->BAX, BLH->BLX, FIR->FIX, JAK->JAX,
|
|
// LOK->LOX, with the numbered chassis following
|
|
// (MAD1->MAX1, LOK1->LOX1; all present in VIDEO/). The
|
|
// X skeleton carries the SAME 25-joint chain -- so the
|
|
// canopy eye and torso twist compose identically -- but
|
|
// its only geometry is the cockpit shell (MAX.SKL ->
|
|
// max_cop.bgf, the MAX_COP canopy with PUNCH-texel
|
|
// windows from the real-pod capture forensics). The
|
|
// donor names the same mechanism from the decomp side:
|
|
// inside = SkeletonType_A with '_cop' selection
|
|
// (btl4vid.hpp:678). Without this, the pilot sits
|
|
// inside the OUTSIDE model staring at torso panels.
|
|
//
|
|
int
|
|
eye_count = 0,
|
|
joint_count = 0;
|
|
const char
|
|
*skeleton_name = video_object->GetObjectFilename();
|
|
char
|
|
inside_name[64];
|
|
dpl_DCS
|
|
*skl_result = NULL;
|
|
|
|
if (
|
|
view_type == insideEntity &&
|
|
strlen(skeleton_name) >= 3 &&
|
|
strlen(skeleton_name) < sizeof(inside_name)
|
|
)
|
|
{
|
|
strcpy(inside_name, skeleton_name);
|
|
inside_name[2] =
|
|
(inside_name[2] >= 'a' && inside_name[2] <= 'z')
|
|
? 'x' : 'X';
|
|
skl_result = ReadSKLFile(entity, root_DCS,
|
|
inside_name, view_type, &eye_count,
|
|
&joint_count);
|
|
if (skl_result == NULL)
|
|
{
|
|
DEBUG_STREAM << "[skl] no cockpit variant '"
|
|
<< inside_name
|
|
<< "' -- falling back to the body skeleton\n"
|
|
<< flush;
|
|
}
|
|
}
|
|
if (skl_result == NULL)
|
|
{
|
|
ReadSKLFile(entity, root_DCS,
|
|
skeleton_name, view_type, &eye_count,
|
|
&joint_count);
|
|
}
|
|
|
|
//
|
|
// 3. FALLBACK eyepoint only. A skeleton without a
|
|
// siteeyepoint still needs a camera for the inside
|
|
// view, and the hull root with a zero offset is the
|
|
// engine's own zero-construction default
|
|
// (L4VIDEO.CPP:4849). For MAD.SKL this no longer
|
|
// runs -- the walk builds the real cockpit eye.
|
|
//
|
|
if (view_type == insideEntity && eye_count == 0)
|
|
{
|
|
EulerAngles
|
|
*eyepoint_rotation =
|
|
(EulerAngles *)entity->GetAttributePointer(
|
|
"EyepointRotation");
|
|
DPLEyeRenderable
|
|
*this_eye = new DPLEyeRenderable(
|
|
entity,
|
|
dplMainZone,
|
|
LinearMatrix::Identity,
|
|
root_DCS,
|
|
dplMainView,
|
|
eyepoint_rotation);
|
|
Register_Object(this_eye);
|
|
}
|
|
handled = True;
|
|
}
|
|
}
|
|
Entity_Being_Created = NULL;
|
|
|
|
//
|
|
// Only chain the base when NO skeleton was found -- for a
|
|
// skeleton it contributes nothing but the complaint, and the
|
|
// root/eye it would otherwise build were built above.
|
|
//
|
|
if (!handled)
|
|
{
|
|
if (getenv("BT_MER_LOG"))
|
|
{
|
|
DEBUG_STREAM << " [chain] no skeleton -> DPLRenderer"
|
|
<< endl << flush;
|
|
}
|
|
DPLRenderer::MakeEntityRenderables(
|
|
entity, model_resource, view_type);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RegisteredClass::BTPlayerClassID:
|
|
//
|
|
// No graphics -- the player is a control/scoring entity.
|
|
//
|
|
break;
|
|
|
|
default:
|
|
DPLRenderer::MakeEntityRenderables(entity, model_resource, view_type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
BTL4VideoRenderer::LoadMissionImplementation(Mission *mission)
|
|
{
|
|
Check(this);
|
|
//
|
|
// CHAIN THE BASE. DPLRenderer::LoadMissionImplementation
|
|
// (L4VIDEO.CPP:6007) is NOT empty -- it reads the renderer environment
|
|
// and loads the name bitmaps. DPLReadEnvironment opens the
|
|
// notation file named by L4DPLCFG (SETENV.BAT defaults it to
|
|
// btdpl.ini) and hands its "main" page to DPLReadINIPage, which walks
|
|
// the compare/branch pages for this location/time and calls
|
|
// dpl_SetObjectFilePath / material / texmap from the objectpath=
|
|
// entries (L4VIDEO.CPP:1852). It is PRIVATE to DPLRenderer, so the
|
|
// game renderer reaches it only by chaining -- which is the whole
|
|
// point: an override here REPLACES the base, it does not extend it.
|
|
//
|
|
// WITHOUT IT every dpl_LoadObject returns NULL. The live pod run
|
|
// failed all 40 arena objects (sky / aw01..aw04 / afloor / bcor1 /
|
|
// bdet1 / bdet2 / bpip1) and ended in "NULL instance", while the
|
|
// SHIPPED binary on the SAME rig loaded every one of them. The
|
|
// models were never missing -- the loader simply had no paths, because
|
|
// the bring-up no-op that used to live here SUPPRESSED the base.
|
|
//
|
|
DPLRenderer::LoadMissionImplementation(mission);
|
|
}
|
|
|