//===========================================================================// // File: seqctl.cpp // // Project: BattleTech Brick: Mech animation // // Contents: The Mech keyframe-animation player (leg / body gait clips) // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(SEQCTL_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(JOINT_HPP) # include #endif #if !defined(APP_HPP) # include #endif // //############################################################################# // Local helpers. //############################################################################# // // // Slot -> skeleton joint, through the clip's own index map. // Joint * SequenceController::GetAnimatedJoint(int slot) const { if (jointSubsystem == NULL || jointIndices == NULL) { return NULL; } Verify(slot >= 0 && slot < jointCount); int joint_index = jointIndices[slot]; if (joint_index < 0 || joint_index >= jointSubsystem->GetJointCount()) { return NULL; } return jointSubsystem->GetJoint(joint_index); } // // Component-wise ease between two orientations. // static EulerAngles InterpolateAngles( const EulerAngles &from, const EulerAngles &to, Scalar ratio ) { return EulerAngles( Radian((Scalar)from.pitch + ((Scalar)to.pitch - (Scalar)from.pitch) * ratio), Radian((Scalar)from.yaw + ((Scalar)to.yaw - (Scalar)from.yaw) * ratio), Radian((Scalar)from.roll + ((Scalar)to.roll - (Scalar)from.roll) * ratio)); } // //############################################################################# //############################################################################# // SequenceController::SequenceController() { keyframeCount = 0; jointCount = 0; footStepThreshold = NULL; jointIndices = NULL; keyframeTimes = NULL; keyframeBase = NULL; keyframeCursor = NULL; keyframeData = NULL; currentFrame = 0; currentTime = 0.0f; jointSubsystem = NULL; owner = NULL; clipResource = NULL; finishedCallback = NULL; callbackArg2 = 0; callbackArg3 = 0; } // //############################################################################# // Init Bind the player to its owning mech and cache the joint subsystem the // gait writes through. Called from the Mech ctor for legAnimation/bodyAnimation. //############################################################################# // void SequenceController::Init(Mech *owner_mech) { Check(owner_mech); owner = owner_mech; jointSubsystem = owner_mech->GetJointSubsystem(); clipResource = NULL; } // //############################################################################# //############################################################################# // SequenceController::~SequenceController() { // // Release the clip this controller holds locked. // if (clipResource != NULL) { ((ResourceDescription *)clipResource)->Unlock(); clipResource = NULL; } } // //############################################################################# // The clip resource layout, as SelectSequence parses it. Offsets are the // binary's (@004277a8): // // int frameCount hdr[0] // int jointCount hdr[1] // Scalar footStepThreshold hdr[2] -- authored contact height // int jointIndices[jointCount] keyframe slot -> skeleton joint // Scalar frameTimes[frameCount] keyframe timestamps, seconds // per frame, per joint, PACKED by DOF // Keyframe rootTranslations[frameCount] .stride == the forward step // // The pose block is packed by joint TYPE, so the root-translation table can // only be located by walking the skeleton and summing the per-joint sizes -- // there is no count to seek by. PoseSize is that walk's per-entry size. //############################################################################# // static int PoseSize(Joint *joint) { // // Hinge (X/Y/Z) = a Hinge; ball = EulerAngles; ball+translation = both. // if (joint == NULL) { return 0; } switch (joint->GetJointType()) { case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: return sizeof(Hinge); case Joint::BallJointType: return sizeof(EulerAngles); case Joint::BallTranslationJointType: return sizeof(EulerAngles) + sizeof(Point3D); } return 0; } // //############################################################################# // SelectSequence (@004277a8) -- bind a gait clip. Stores the finished // callback, rewinds playback, releases the previous clip, then finds, locks // and parses the new one. // // The fetch is FindResourceDescription, NOT SearchList: the clip ID arriving // here is already resolved, and SearchList would treat it as a resource LIST // and walk the clip bytes as resource IDs. //############################################################################# // void SequenceController::SelectSequence( int clip_id, void *finished_callback, unsigned callback_arg2, unsigned callback_arg3 ) { Check_Pointer(this); finishedCallback = finished_callback; callbackArg2 = callback_arg2; callbackArg3 = callback_arg3; currentTime = 0.0f; currentFrame = 0; if (clipResource != NULL) { ((ResourceDescription *)clipResource)->Unlock(); clipResource = NULL; } ResourceFile *resources = application->GetResourceFile(); Check_Pointer(resources); ResourceDescription *description = resources->FindResourceDescription(clip_id); clipResource = description; if (description == NULL) { // // A mech whose skeleton omits this gait keeps an empty controller; // Advance below is guarded on keyframeCount so playback is inert. // keyframeCount = 0; jointCount = 0; return; } description->Lock(); int *header = (int *)description->resourceAddress; Check_Pointer(header); keyframeCount = header[0]; jointCount = header[1]; footStepThreshold = (Scalar *)(header + 2); int *cursor = header + 3; jointIndices = cursor; cursor += jointCount; keyframeTimes = (Scalar *)cursor; cursor += keyframeCount; keyframeBase = cursor; keyframeCursor = cursor; // // Walk the packed pose block to find the root-translation table behind it. // char *pose = (char *)cursor; int frame, slot; for (frame = 0; frame < keyframeCount; ++frame) { for (slot = 0; slot < jointCount; ++slot) { pose += PoseSize(GetAnimatedJoint(slot)); } } keyframeData = (Keyframe *)pose; Check_Fpu(); } // //############################################################################# // Advance (@0042790c) -- play the clip forward by time_slice and return the // forward distance covered, which is what the gait feeds into the mech's // motion. // // Two phases. First SNAP through every keyframe whose timestamp the new time // has passed, writing each animated joint's authored pose as it goes. Then // INTERPOLATE the remaining partial frame, easing every joint from where it // is toward the next keyframe. // // move_joints == 0 advances the clock and accumulates distance WITHOUT // touching the skeleton -- that is how the body channel measures a stride // without fighting the leg channel for the same joints. //############################################################################# // Scalar SequenceController::Advance(Scalar time_slice, int move_joints) { Check_Pointer(this); Scalar target_time = currentTime + time_slice, distance = 0.0f; char *cursor = (char *)keyframeCursor; int slot; // // Phase 1 -- snap through the keyframes reached this frame. // while ( currentFrame < keyframeCount && keyframeTimes[currentFrame] <= target_time ) { for (slot = 0; slot < jointCount; ++slot) { Joint *joint = GetAnimatedJoint(slot); switch (joint != NULL ? joint->GetJointType() : Joint::StaticJointType) { case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: if (move_joints) { joint->SetHinge(*(const Hinge *)cursor); } cursor += sizeof(Hinge); break; case Joint::BallJointType: if (move_joints) { joint->SetRotation(*(const EulerAngles *)cursor); } cursor += sizeof(EulerAngles); break; case Joint::BallTranslationJointType: if (move_joints) { joint->SetRotation(*(const EulerAngles *)cursor); } cursor += sizeof(EulerAngles); if (move_joints) { joint->SetTranslation(*(const Point3D *)cursor); } cursor += sizeof(Point3D); break; default: break; } } distance += (keyframeTimes[currentFrame] - currentTime) * keyframeData[currentFrame].stride; currentTime = keyframeTimes[currentFrame]; ++currentFrame; } if (currentFrame == keyframeCount) { // // End of clip. The finished callback picks the next gait state and // RE-ARMS this controller through SelectSequence (which rewinds us to // frame 0), then advances the leftover time itself -- so on return the // controller is already playing the next clip and its distance folds // into ours. A NULL callback simply parks the clip at its end. // if (finishedCallback != NULL && keyframeCount > 0) { Scalar carryover = target_time - keyframeTimes[keyframeCount - 1]; distance += (*(ClipFinishedCallback)finishedCallback)( owner, callbackArg2, carryover, move_joints); } } else { // // Phase 2 -- the partial frame. // keyframeCursor = cursor; Scalar span = keyframeTimes[currentFrame] - currentTime, ratio = (span > 0.0f) ? (target_time - currentTime) / span : 0.0f; for (slot = 0; slot < jointCount; ++slot) { Joint *joint = GetAnimatedJoint(slot); switch (joint != NULL ? joint->GetJointType() : Joint::StaticJointType) { case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: if (move_joints) { Scalar from = (Scalar)joint->GetRadians(), to = (Scalar)((const Hinge *)cursor)->rotationAmount; joint->SetRotation(Radian(from + (to - from) * ratio)); } cursor += sizeof(Hinge); break; case Joint::BallJointType: if (move_joints) { joint->SetRotation( InterpolateAngles( joint->GetEulerAngles(), *(const EulerAngles *)cursor, ratio)); } cursor += sizeof(EulerAngles); break; case Joint::BallTranslationJointType: if (move_joints) { joint->SetRotation( InterpolateAngles( joint->GetEulerAngles(), *(const EulerAngles *)cursor, ratio)); } cursor += sizeof(EulerAngles); if (move_joints) { const Point3D &to = *(const Point3D *)cursor; Point3D from = joint->GetTranslation(); Point3D blended( from.x + (to.x - from.x) * ratio, from.y + (to.y - from.y) * ratio, from.z + (to.z - from.z) * ratio); joint->SetTranslation(blended); } cursor += sizeof(Point3D); break; default: break; } } distance += (target_time - currentTime) * keyframeData[currentFrame].stride; currentTime = target_time; } Check_Fpu(); return distance; } // //############################################################################# // Reset (@004283b8) -- return every animated joint to its neutral pose. Only // the loop form does anything; the mech calls it when a gait is abandoned // (fall, death, respawn) so the skeleton does not keep the last frame of a // clip that is no longer playing. //############################################################################# // void SequenceController::Reset(int loop) { Check_Pointer(this); if (loop == 0) { return; } EulerAngles neutral_angles(Radian(0.0f), Radian(0.0f), Radian(0.0f)); Point3D neutral_point(0.0f, 0.0f, 0.0f); int slot; for (slot = 0; slot < jointCount; ++slot) { Joint *joint = GetAnimatedJoint(slot); if (joint == NULL) { continue; } switch (joint->GetJointType()) { case Joint::HingeXJointType: case Joint::HingeYJointType: case Joint::HingeZJointType: joint->SetRotation(Radian(0.0f)); break; case Joint::BallJointType: joint->SetRotation(neutral_angles); break; case Joint::BallTranslationJointType: joint->SetRotation(neutral_angles); joint->SetTranslation(neutral_point); break; default: break; } } Check_Fpu(); }