BT410 5.3.80: joint articulation is live -- 22 nodes, and the twist reaches the board
RecurseSKLFile now builds a joint renderable for any node whose page name resolves to a live skeleton Joint: HingeX/Y/Z -> HingeRenderable (watching Joint::GetHinge), Ball -> BallJointRenderable (watching GetEulerAngles), otherwise the static path. Each holds the rest offset in one DCS and the live rotation in a child DCS, and its Execute diffs the watched value and calls DPL_FLUSH_DCS -- the engine's own mechanism (L4VIDRND.CPP:1026+). The value comes from the mech's JointSubsystem via ResolveJoint, so sim and renderer read one source. Gated on BT_JOINTS while it proves out. [skl] video\max.skl -> 26 nodes, 1 objects, 1 eye, 22 articulated The bridge reported anim_abs=1 joints=0 twist=+0.00 before; it now reports joints=1 twist=-0.86, matching the game's [torso] twist=-0.856. With the mech stationary, frames that differed by 0.0% now differ by 62-80%. A crash it exposed: Mech::ResolveJoint passed segment->GetJointIndex() straight to GetJoint unchecked, and a segment with no joint reports -1 -- GetNthImplementation then indexes [base + -1*4] and dies (guest 00426A1D). Torso never hit it because it only asks for its own authored joint name; the walk asks for every page. Now bounds-checked against GetJointCount. Open: the canopy does not stay rigid in the view, though it and the eye hang off the same articulated node. Cancelling the bridge's cage compensation (CAGE_TWIST_SIGN=0) did not close it. Leading hypothesis: SetupCull builds worldToEyeMatrix from GetSegmentToWorld(siteeyepoint) -- the SIMULATION's segment transform -- independent of the render tree, so the canopy follows our render chain and the eye follows the sim's, and they diverge whenever one carries the twist and the other does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -858,7 +858,22 @@ Joint*
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return joints->GetJoint(segment->GetJointIndex());
|
||||
//
|
||||
// A segment that HAS no joint reports index -1, and GetJoint indexes a
|
||||
// table with it unchecked -- TableIterator::GetNthImplementation walks to
|
||||
// [base + -1*4] and dies (guest 00426A1D, ECX=FFFFFFFF). Torso never hit
|
||||
// this because it only ever asks for its own authored twist-joint name;
|
||||
// the skeleton walk asks for EVERY page, and most .SKL pages are sites or
|
||||
// static segments with no joint at all.
|
||||
//
|
||||
int
|
||||
joint_index = segment->GetJointIndex();
|
||||
if (joint_index < 0 || joint_index >= joints->GetJointCount())
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return joints->GetJoint(joint_index);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
#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
|
||||
@@ -126,11 +135,13 @@ dpl_DCS *
|
||||
dpl_DCS *parent_dcs,
|
||||
const char *skeleton_filename,
|
||||
ViewFrom view_type,
|
||||
int *eye_count)
|
||||
int *eye_count,
|
||||
int *joint_count)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(skeleton_filename);
|
||||
Check_Pointer(eye_count);
|
||||
Check_Pointer(joint_count);
|
||||
|
||||
char
|
||||
path[256];
|
||||
@@ -169,12 +180,13 @@ dpl_DCS *
|
||||
dpl_DCS
|
||||
*root = RecurseSKLFile(
|
||||
entity, parent_dcs, skeleton, "ROOT", 0, view_type,
|
||||
zone, &node_count, &object_count, eye_count);
|
||||
zone, &node_count, &object_count, eye_count, joint_count);
|
||||
|
||||
DEBUG_STREAM << "[skl] " << path
|
||||
<< " -> " << node_count << " nodes, "
|
||||
<< object_count << " objects, "
|
||||
<< *eye_count << " eye\n" << flush;
|
||||
<< *eye_count << " eye, "
|
||||
<< *joint_count << " articulated\n" << flush;
|
||||
|
||||
//
|
||||
// The engine guards this the same way (L4VIDEO.CPP, DPLReadEnvironment):
|
||||
@@ -203,7 +215,8 @@ dpl_DCS *
|
||||
dpl_ZONE *zone,
|
||||
int *node_count,
|
||||
int *object_count,
|
||||
int *eye_count)
|
||||
int *eye_count,
|
||||
int *joint_count)
|
||||
{
|
||||
Check(this);
|
||||
Check(skeleton);
|
||||
@@ -227,11 +240,6 @@ dpl_DCS *
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dpl_DCS
|
||||
*dcs = dpl_NewDCS();
|
||||
Check_Pointer(dcs);
|
||||
dpl_SetDCSZone(dcs, zone);
|
||||
|
||||
//
|
||||
// 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
|
||||
@@ -281,35 +289,146 @@ dpl_DCS *
|
||||
node_matrix(3,2) = tran_z;
|
||||
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
++(*node_count);
|
||||
|
||||
//
|
||||
// This node's geometry, if it has any. 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).
|
||||
// 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)
|
||||
{
|
||||
dpl_OBJECT
|
||||
*object = dpl_LoadObject((char *)object_name, dpl_load_normal);
|
||||
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:
|
||||
joint_renderable = new HingeRenderable(
|
||||
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
|
||||
@@ -320,14 +439,11 @@ dpl_DCS *
|
||||
dpl_FlushInstance(instance);
|
||||
++(*object_count);
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "[skl] couldn't load object " << object_name
|
||||
<< " for '" << page_name << "'\n" << flush;
|
||||
}
|
||||
|
||||
dpl_FlushDCS(dcs);
|
||||
}
|
||||
|
||||
dpl_FlushDCS(dcs);
|
||||
++(*node_count);
|
||||
|
||||
//
|
||||
// Children. Repeated "joint=" entries: the entry NAME is "joint" and the
|
||||
@@ -352,7 +468,8 @@ dpl_DCS *
|
||||
{
|
||||
RecurseSKLFile(
|
||||
entity, dcs, skeleton, child_page, recursion_depth + 1,
|
||||
view_type, zone, node_count, object_count, eye_count);
|
||||
view_type, zone, node_count, object_count, eye_count,
|
||||
joint_count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +746,8 @@ void
|
||||
// inside the OUTSIDE model staring at torso panels.
|
||||
//
|
||||
int
|
||||
eye_count = 0;
|
||||
eye_count = 0,
|
||||
joint_count = 0;
|
||||
const char
|
||||
*skeleton_name = video_object->GetObjectFilename();
|
||||
char
|
||||
@@ -648,7 +766,8 @@ void
|
||||
(inside_name[2] >= 'a' && inside_name[2] <= 'z')
|
||||
? 'x' : 'X';
|
||||
skl_result = ReadSKLFile(entity, root_DCS,
|
||||
inside_name, view_type, &eye_count);
|
||||
inside_name, view_type, &eye_count,
|
||||
&joint_count);
|
||||
if (skl_result == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "[skl] no cockpit variant '"
|
||||
@@ -660,7 +779,8 @@ void
|
||||
if (skl_result == NULL)
|
||||
{
|
||||
ReadSKLFile(entity, root_DCS,
|
||||
skeleton_name, view_type, &eye_count);
|
||||
skeleton_name, view_type, &eye_count,
|
||||
&joint_count);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -79,7 +79,8 @@
|
||||
dpl_DCS *parent_dcs,
|
||||
const char *skeleton_filename,
|
||||
ViewFrom view_type,
|
||||
int *eye_count);
|
||||
int *eye_count,
|
||||
int *joint_count);
|
||||
|
||||
dpl_DCS *
|
||||
RecurseSKLFile(
|
||||
@@ -92,7 +93,8 @@
|
||||
dpl_ZONE *zone,
|
||||
int *node_count,
|
||||
int *object_count,
|
||||
int *eye_count);
|
||||
int *eye_count,
|
||||
int *joint_count);
|
||||
|
||||
protected:
|
||||
int reserved[16];
|
||||
|
||||
@@ -1867,3 +1867,55 @@ hangs off that chain), the LEG GAIT (the 12-joint flurry the real-pod capture
|
||||
shows while walking), and weapon-pod aim. Until then the mech translates
|
||||
through the world as a rigid body -- which is exactly what every frame so far
|
||||
has shown.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
JOINT ARTICULATION IS LIVE -- 22 NODES, AND THE TWIST REACHES THE BOARD
|
||||
--------------------------------------------------------------------------------
|
||||
RecurseSKLFile now builds a JOINT RENDERABLE for any node whose page name
|
||||
resolves to a live skeleton Joint, instead of a static baked DCS:
|
||||
|
||||
HingeX/Y/Z -> HingeRenderable (watches Joint::GetHinge)
|
||||
Ball -> BallJointRenderable (watches Joint::GetEulerAngles)
|
||||
otherwise -> the static path, unchanged
|
||||
|
||||
Each holds the node's rest offset in one DCS and the live rotation in a child
|
||||
DCS, and its Execute compares the watched value against a cached copy and
|
||||
calls DPL_FLUSH_DCS -- the engine's own mechanism (L4VIDRND.CPP:1026+). The
|
||||
joint's value comes from the mech's JointSubsystem via Mech::ResolveJoint, so
|
||||
the simulation and the renderer read ONE source. Env-gated on BT_JOINTS
|
||||
while it proves out; the static path is a working cockpit render.
|
||||
|
||||
[skl] video\max.skl -> 26 nodes, 1 objects, 1 eye, 22 articulated
|
||||
|
||||
and the bridge, which reported `anim_abs=1 joints=0 twist=+0.00` before, now
|
||||
reports `joints=1 twist=-0.86` -- matching the game's own
|
||||
[torso] twist=-0.856 to two decimals. The board is hearing the joint move.
|
||||
|
||||
VISUALLY: with the mech stationary, frames that differed by 0.0%% now differ
|
||||
by 62-80%%. The view swings with the torso. That is the brick landing.
|
||||
|
||||
A CRASH IT EXPOSED, worth keeping: Mech::ResolveJoint passed
|
||||
segment->GetJointIndex() straight to GetJoint with NO validity check, and a
|
||||
segment that HAS no joint reports -1 -- TableIterator::GetNthImplementation
|
||||
then indexes [base + -1*4] and dies (guest 00426A1D, ECX=FFFFFFFF). Torso
|
||||
never hit it because it only ever asks for its own authored twist-joint name;
|
||||
the skeleton walk asks for every page, and most .SKL pages are sites or
|
||||
static segments. Now bounds-checked against GetJointCount.
|
||||
|
||||
OPEN, and the next thing to chase: the canopy does not stay rigid in the
|
||||
view. Both the canopy (Object on jointtorso) and the eye (site on jointeye,
|
||||
whose parent is jointtorso) hang off the SAME articulated node, so they
|
||||
should move together and the world alone should pan. Instead the canopy
|
||||
silhouette varies 144k-207k px across a sweep. Cancelling the bridge's own
|
||||
cage compensation (CAGE_TWIST_SIGN=0 -- it exists because the SHIPPED game
|
||||
links the cage under the vehicle root and never touches it, which is no
|
||||
longer true of ours) did not close it.
|
||||
|
||||
Leading hypothesis: the eye is not actually driven by our DCS chain at all.
|
||||
DPLRenderer::SetupCull builds worldToEyeMatrix from
|
||||
GetSegmentToWorld(siteeyepoint) -- the SIMULATION's segment transform --
|
||||
independent of the render tree. So the canopy follows our render chain while
|
||||
the eye follows the sim's segment chain, and they diverge exactly like this
|
||||
whenever one carries the twist and the other does not. Test: log both
|
||||
per frame and compare; if that is it, the fix is making the segment chain and
|
||||
the render chain agree about which node owns the twist.
|
||||
|
||||
Reference in New Issue
Block a user