/* * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ #include "TrajectoryCorrector.h" #include "InverseKinematics.h" #include "Utility.h" #include #include #include #include #include using Pose = std::vector; static const float pos_weight = 0.001f; static const float vel_weight = 1.0f; static const float acc_weight = 10.0f; namespace { // Enable with: MOTIONCORRECTION_DEBUG_INTERVALS=1 // Default: off (no Interval printing). bool DebugPrintIntervalsEnabled() { const char* v = std::getenv("MOTIONCORRECTION_DEBUG_INTERVALS"); if (v == nullptr || v[0] == '\0') { return false; } // Treat "0" as false; any other non-empty value enables. return v[0] != '0'; } void FilterContactIntervals( std::vector>& contactIntervals, const std::vector& mask, bool one_bone_contact = false) { std::vector keepIntervals; for (size_t i = 0; i < contactIntervals.size(); ++i) { const auto& interval = contactIntervals[i]; bool startConstrained = (interval.first != 0 && mask[interval.first - 1]); bool endConstrained; endConstrained = (interval.second != mask.size() && mask[interval.second]); if (one_bone_contact) { if (startConstrained || endConstrained) { continue; } } else { // If both the start and end of the contact interval are masked, // there's no way we can correct the contact without popping, so // let's filter these out: if (startConstrained && endConstrained) { continue; } } keepIntervals.push_back(i); } for (size_t i = 0; i < keepIntervals.size(); ++i) { contactIntervals[i] = contactIntervals[keepIntervals[i]]; } contactIntervals.resize(keepIntervals.size()); } std::vector> ComputeContactIntervals( const std::vector& contacts, const std::vector& mask, float contactThreshold) { // turn off the contacts for all frames that are constrained/masked: std::vector contactsNoMask = contacts; for (size_t i = 0; i < mask.size(); ++i) { if (mask[i]) { contactsNoMask[i] = 0; } } // Find intervals that are in contact: std::vector> contactIntervals; int start = -1; for (int frame = 0; frame < mask.size(); ++frame) { bool isContact = contactsNoMask[frame] > contactThreshold; if (isContact && start == -1) { start = frame; } else if (!isContact && start != -1) { contactIntervals.emplace_back(start, frame); start = -1; } } // Close the final interval if needed: if (start != -1) { contactIntervals.emplace_back(start, mask.size()); } return contactIntervals; } void FindContactPoints( std::vector &points, std::vector &inContact, const std::vector& joint_parents_vec, int32_t jointIndex, const std::vector &poses, const std::vector>& contactIntervals, const std::vector& mask, size_t frameCount, float minHeight) { // Find a representative frame for each interval. // If the interval starts after a masked frame, use the start // of the interval, if it ends before a mask use the end, // otherwise use the middle frame. inContact.clear(); inContact.resize(frameCount, 0); points.clear(); points.resize(frameCount); for (size_t i = 0; i < contactIntervals.size(); ++i) { const auto& interval = contactIntervals[i]; int frame = -1; bool startConstrained = (interval.first != 0 && mask[interval.first - 1]); bool endConstrained; endConstrained = (interval.second != mask.size() && mask[interval.second]); // Debug output (opt-in via env var) if (DebugPrintIntervalsEnabled()) { std::cout << "Interval " << i << ": start=" << interval.first << ", end=" << interval.second << ", startConstrained=" << startConstrained << ", endConstrained=" << endConstrained << std::endl; } if(startConstrained) { // If the interval starts on a constraint, use the constrained frame // as a target (doing this modulo mask.size() in case we're looping) frame = interval.first - 1; } else if (endConstrained) { // If the interval ends on a constraint, use the constrained frame // as a target: frame = interval.second; } else { // Otherwise use the midpoint of the interval: frame = (interval.first + interval.second) / 2; } // get the target point: Math::Vector target = Animation::JointLocalToGlobal(joint_parents_vec, jointIndex, poses[frame]).GetTranslation(); for(int i = interval.first; i < interval.second; ++i) { Math::Vector framePt = Animation::JointLocalToGlobal(joint_parents_vec, jointIndex, poses[i]).GetTranslation(); inContact[i] = 1; points[i] = target; if (!startConstrained && !endConstrained) { points[i].SetY(std::max(framePt.GetY(), minHeight)); // std::cout << " Frame " << i << ": SetY with framePt.GetY()=" << framePt.GetY() // << ", minHeight=" << minHeight << std::endl; } } } } float TargetReachFalloff( const std::vector& joint_parents_vec, const Pose& defaultPose, int32_t jointIndex, Animation::IKType ikType, const Math::Vector& target, const Pose& pose, const Math::Transform& rootTx = Math::Transform::Identity) { float maxReach = defaultPose[jointIndex].GetTranslation().GetLength3(); if (ikType == Animation::IKType::kTwoBone) { jointIndex = joint_parents_vec[jointIndex]; ASSERT(jointIndex > -1); maxReach += defaultPose[jointIndex].GetTranslation().GetLength3(); } // Get base joint world Tx jointIndex = joint_parents_vec[jointIndex]; ASSERT(jointIndex > -1); const auto worldTx = Animation::JointLocalToGlobal(joint_parents_vec, jointIndex, pose, rootTx); // Gaussian falloff float targetDist = target.GetDistance3(worldTx.GetTranslation()); float tmp = Math::Max(targetDist / maxReach - 0.99f, 0.f) / 0.01f; tmp = tmp * tmp; return std::exp(-2.f * tmp * tmp); } void CorrectHipsY( std::vector& poses, const std::vector& targetPoses, const std::vector& fullBodyMask, const std::vector& contacts, float contactThreshold ) { // Correct the y coordinates of the root. auto N = poses.size(); Eigen::MatrixXd x(N, 1); Eigen::MatrixXd observations(N, 1); Eigen::MatrixXd xfixed(N, 1); // Fill in the initial trajectory (x) and the values we want to hit when we // warp it (observations): Eigen::VectorXd yCorrectMargins(N); for(size_t frame = 0; frame < N; ++frame) { yCorrectMargins[frame] = fullBodyMask[frame] ? 0.0f : -1.0f; x(frame, 0) = ((float*)&poses[frame][0].GetTranslation())[1]; observations(frame, 0) = ((float*)&targetPoses[frame][0].GetTranslation())[1]; } TrajectoryCorrector ycorrector( yCorrectMargins, pos_weight * 10, vel_weight, acc_weight * 0.1f ); ycorrector.Interpolate( xfixed, observations, x ); // fill channel again: for (uint32_t frame = 0; frame < N; ++frame) { ((float*)&poses[frame][0].GetTranslation())[1] = float(xfixed(frame, 0)); } } void SmoothChannels( Eigen::MatrixXd &x, const std::vector& mask ) { for( uint32_t i=0; i < mask.size(); ++i) { uint32_t i_prev = i == 0 ? 0 : i-1; uint32_t i_next = std::min(uint32_t(i+1), uint32_t(mask.size()-1)); if(i > 0 && mask[i] > 0 && mask[i_prev] == 0) { // if the previous frame is unconstrained and the current frame is constrained, // replace the current frame with the average of its neighbors: for(long j=0; j < x.cols(); ++j) { x(i, j) = 0.5f * (x(i_prev, j) + x(i_next, j)); } } if(mask[i] > 0 && mask[i_next] == 0) { // if the next frame is unconstrained and the current frame is constrained, // replace the current frame with the average of its neighbors: for(long j=0; j < x.cols(); ++j) { x(i, j) = 0.5f * (x(i_prev, j) + x(i_next, j)); } } } } void CorrectHipsXZ( std::vector& poses, const std::vector& targetPoses, const std::vector& fullBodyMask, const std::vector& rootMask, const std::vector& endEffectorPins, const Eigen::VectorXd& velocity_weights, float root_margin ) { auto N = poses.size(); Eigen::VectorXd margins(N); for( size_t i = 0; i < N; ++i ) { margins[i] = fullBodyMask[i] ? 0.0f : -1.0f; } std::vector rootCombinedMask(N, 0.0f); for(size_t i = 0; i < N; ++i) { rootCombinedMask[i] = (fullBodyMask[i] > 0) || (rootMask[i] > 0); if(rootMask[i] > 0 && margins[i] != 0) { margins[i] = root_margin; } for (auto& c : endEffectorPins) { if (c.contactMask[i] && margins[i] != 0) { margins[i] = root_margin; } } } TrajectoryCorrector xzcorrector( margins, pos_weight, vel_weight, acc_weight, velocity_weights ); // Enforce pose constraints on root xz trajectory: Eigen::MatrixXd x(N, 2); Eigen::MatrixXd observations(N, 2); Eigen::MatrixXd x_fixed(N, 2); observations.setZero(); for (uint32_t frame = 0; frame < N; ++frame) { x(frame, 0) = ((float*)&poses[frame][0].GetTranslation())[0]; x(frame, 1) = ((float*)&poses[frame][0].GetTranslation())[2]; observations(frame, 0) = ((float*)&targetPoses[frame][0].GetTranslation())[0]; observations(frame, 1) = ((float*)&targetPoses[frame][0].GetTranslation())[2]; } SmoothChannels(x, rootCombinedMask); xzcorrector.Interpolate( x_fixed, observations, x ); // fill channels again: for (uint32_t frame = 0; frame < N; ++frame) { ((float*)&poses[frame][0].GetTranslation())[0] = float(x_fixed(frame, 0)); ((float*)&poses[frame][0].GetTranslation())[2] = float(x_fixed(frame, 1)); } } void CorrectRotationsForBone( std::vector& poses, const std::vector& targetPoses, const std::vector& mask, const TrajectoryCorrector& corrector, int boneIdx, bool performChannelSmoothing) { auto N = poses.size(); Eigen::MatrixXd x(N, 1); Eigen::MatrixXd observations(N, 1); observations.setZero(); Eigen::MatrixXd x_fixed(N, 1); // Quaternion components can flip when they pass through 180 degree // rotations, so let's convert all the quaternions in this channel to // the forward/up vector representation, modify them, then convert back // to quaternions: // convert time series to 6d forward/up: std::vector forwardUp(6 * N); std::vector targetForwardUp(6 * N); for (uint32_t frame = 0; frame < N; ++frame) { auto q = poses[frame][boneIdx].GetRotation(); auto forward = q.ZAxis(); auto up = q.YAxis(); forwardUp[N * 0 + frame] = forward.GetX(); forwardUp[N * 1 + frame] = forward.GetY(); forwardUp[N * 2 + frame] = forward.GetZ(); forwardUp[N * 3 + frame] = up.GetX(); forwardUp[N * 4 + frame] = up.GetY(); forwardUp[N * 5 + frame] = up.GetZ(); q = targetPoses[frame][boneIdx].GetRotation(); forward = q.ZAxis(); up = q.YAxis(); targetForwardUp[N * 0 + frame] = forward.GetX(); targetForwardUp[N * 1 + frame] = forward.GetY(); targetForwardUp[N * 2 + frame] = forward.GetZ(); targetForwardUp[N * 3 + frame] = up.GetX(); targetForwardUp[N * 4 + frame] = up.GetY(); targetForwardUp[N * 5 + frame] = up.GetZ(); } // correct trajectories: for (uint32_t dim = 0; dim < 6; ++dim) { for (uint32_t frame = 0; frame < N; ++frame) { x(frame, 0) = forwardUp[N * dim + frame]; observations(frame, 0) = mask[frame] * targetForwardUp[N * dim + frame]; } if (performChannelSmoothing) { SmoothChannels(x, mask); } corrector.Interpolate( x_fixed, observations, x ); // fill channel again: for (uint32_t frame = 0; frame < N; ++frame) { forwardUp[N * dim + frame] = float(x_fixed(frame, 0)); } } for (uint32_t frame = 0; frame < N; ++frame) { Math::Vector forward = { forwardUp[N * 0 + frame] ,forwardUp[N * 1 + frame] ,forwardUp[N * 2 + frame] }; Math::Vector up = { forwardUp[N * 3 + frame] ,forwardUp[N * 4 + frame] ,forwardUp[N * 5 + frame] }; forward.Normalize3(); up.Normalize3(); poses[frame][boneIdx].SetRotation(Math::Quaternion::LookRotation(forward, up)); } } void CorrectJointRotations( std::vector& poses, const std::vector& targetPoses, const std::vector& fullBodyMask, const Eigen::VectorXd& velocity_weights ) { auto N = poses.size(); // Create a trajectory corrector for fixing the full body fullBodyMask positions: Eigen::VectorXd margins(N); for( size_t i = 0; i < N; ++i ) { margins[i] = fullBodyMask[i] ? 0.0f : -1.0f; } TrajectoryCorrector corrector( margins, pos_weight * 10, vel_weight, acc_weight, velocity_weights ); for (uint32_t boneIdx = 0; boneIdx < poses[0].size(); ++boneIdx) { CorrectRotationsForBone( poses, targetPoses, fullBodyMask, corrector, boneIdx, true ); } } void DoEffectorIK( std::vector& poses, const std::vector& targetPoses, const std::vector& fullBodyMask, const std::vector& endEffectorPins, const std::vector& joint_parents_vec, const std::vector& defaultPose ) { // Apply IK for effector pins auto N = poses.size(); std::map> jointCorrectionMasks; std::vector ikFixedPoses = poses; for (auto& c : endEffectorPins) { auto jointIdx = c.jointIndex; if(jointCorrectionMasks[jointIdx].empty()) { // initialize to the full body constraint mask because we // want to constrain that anyway: jointCorrectionMasks[jointIdx] = fullBodyMask; } // Add a trajectory correction mask for the parent joint: auto parentIdx = joint_parents_vec[jointIdx]; if(jointCorrectionMasks[parentIdx].empty()) { // initialize to the full body constraint mask because we // want to constrain that anyway: jointCorrectionMasks[parentIdx] = fullBodyMask; } // Add a trajectory correction mask for its parent if this is // 2 bone IK: auto parentParentIdx = joint_parents_vec[parentIdx]; if(c.contactType == Animation::kTwoBone) { if(jointCorrectionMasks[parentParentIdx].empty()) { // initialize to the full body constraint mask because we // want to constrain that anyway: jointCorrectionMasks[parentParentIdx] = fullBodyMask; } } for (uint32_t fixFrame = 0; fixFrame < fullBodyMask.size(); ++fixFrame) { if (c.contactMask[fixFrame]) { const auto targetGlobalTransform = Animation::JointLocalToGlobal(joint_parents_vec, jointIdx, targetPoses[fixFrame]); // flag the parent joint as fixed in its correction mask: jointCorrectionMasks[parentIdx][fixFrame] = 1; switch(c.contactType) { case Animation::kOneBone: { IK::OneBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, jointIdx, 1.0, targetGlobalTransform.GetTranslation(), joint_parents_vec ); break; } case Animation::kTwoBone: { // flag the parent parent joint as fixed in its correction mask: jointCorrectionMasks[parentParentIdx][fixFrame] = 1; IK::TwoBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, jointIdx, 1.0, targetGlobalTransform.GetTranslation(), joint_parents_vec, c.hintOffset ); break; } } // now we need to fix things so the global rotation of the joint // matches the input: jointCorrectionMasks[jointIdx][fixFrame] = 1; auto parentGlobalTransform = Animation::JointLocalToGlobal(joint_parents_vec, parentIdx, ikFixedPoses[fixFrame]); ikFixedPoses[fixFrame][jointIdx].SetRotation( targetGlobalTransform.GetRotation() * parentGlobalTransform.GetRotation().GetConjugate() ); } } } // Applying the effector pin IK introduces popping into the animation, // so let's apply the interpolator to all the joints we modified so as to // line the trajectory up properly again: Eigen::VectorXd margins(N); for( auto &kv : jointCorrectionMasks) { for( size_t i = 0; i < N; ++i ) { margins[i] = kv.second[i] ? 0.0f : -1.0f; } TrajectoryCorrector corrector(margins, pos_weight * 10, vel_weight, acc_weight); CorrectRotationsForBone( poses, ikFixedPoses, kv.second, corrector, kv.first, false ); } } void DoContactIK( std::vector& poses, const std::vector& fullBodyMask, const std::vector& contacts, const std::vector& endEffectorPins, const std::vector& joint_parents_vec, const std::vector& defaultPose, float contactThreshold, bool has_double_ankle_joints ) { auto N = poses.size(); Eigen::VectorXd margins = Eigen::VectorXd::Zero(N); // Apply IK to stabilize limbs on contacts std::map> jointCorrectionMasks; std::vector ikFixedPoses = poses; // Save original poses before any modifications (for double ankle correction later) const std::vector originalPoses = poses; // Track which frames were corrected for each 2-bone contact (for double ankle correction later) std::map> twoBoneContactFrames; auto addEndEffectorMask = [&](uint32_t jointIdx, uint32_t parentIdx, std::vector& jointMask) { auto it = std::find_if( endEffectorPins.begin(), endEffectorPins.end(), [&](const auto &c) { if(jointIdx == c.jointIndex) { return true; } return false; } ); if(it == endEffectorPins.end()) { // We could be correcting the toe joint, in which case we need to use // the parent joint instead: it = std::find_if( endEffectorPins.begin(), endEffectorPins.end(), [&](const auto &c) { if(parentIdx == c.jointIndex) { return true; } return false; } ); } if(it != endEffectorPins.end()) { const auto &msk = it->contactMask; for(size_t i=0; i < msk.size(); ++i) { if(msk[i]) { jointMask[i] = 1.0f; } } } }; // Process two bone contacts first: for (auto& c : contacts) { if(c.contactType != Animation::kTwoBone) { continue; } const auto jointIdx = c.jointIndex; auto parentIdx = joint_parents_vec[jointIdx]; auto parentParentIdx = joint_parents_vec[parentIdx]; auto jointMask = fullBodyMask; addEndEffectorMask(jointIdx, parentIdx, jointMask); // We'll actually be modifying 3 joints here: // * The two joints immediately up in the hierarchy because of the 2 bone IK // * The joint itself because we restore its original global rotation if(jointCorrectionMasks[parentIdx].empty()) { jointCorrectionMasks[parentIdx] = jointMask; } if(jointCorrectionMasks[parentParentIdx].empty()) { jointCorrectionMasks[parentParentIdx] = jointMask; } if(jointCorrectionMasks[jointIdx].empty()) { jointCorrectionMasks[jointIdx] = jointMask; } // Compute the intervals in which the joint is in contact with the floor: auto contactIntervals = ComputeContactIntervals(c.contactMask, jointMask, contactThreshold); FilterContactIntervals(contactIntervals, jointMask); std::vector contactPoints; std::vector inContact; FindContactPoints( contactPoints, inContact, joint_parents_vec, jointIdx, poses, contactIntervals, jointMask, c.contactMask.size(), c.minHeight ); for (uint32_t fixFrame = 0; fixFrame < fullBodyMask.size(); ++fixFrame) { if (inContact[fixFrame]) { auto target = contactPoints[fixFrame]; jointCorrectionMasks[parentIdx][fixFrame] = 1.0f; jointCorrectionMasks[parentParentIdx][fixFrame] = 1.0f; jointCorrectionMasks[jointIdx][fixFrame] = 1.0f; // Track this frame for double ankle correction later if (has_double_ankle_joints) { if (twoBoneContactFrames[jointIdx].empty()) twoBoneContactFrames[jointIdx].resize(fullBodyMask.size(), false); twoBoneContactFrames[jointIdx][fixFrame] = true; } // save the original global rotation of the joint: auto jointGlobalRotation = Animation::JointLocalToGlobal( joint_parents_vec, jointIdx, ikFixedPoses[fixFrame] ).GetRotation(); const float w = TargetReachFalloff( joint_parents_vec, defaultPose, jointIdx, c.contactType, target, ikFixedPoses[fixFrame] ); // std::cout << "Frame " << fixFrame << ": w=" << w << std::endl; // apply the 2 bone IK: auto origParentRotation = ikFixedPoses[fixFrame][parentIdx].GetRotation(); auto origParentParentRotation = ikFixedPoses[fixFrame][parentParentIdx].GetRotation(); IK::TwoBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, jointIdx, 1.0f, target, joint_parents_vec, c.hintOffset ); ikFixedPoses[fixFrame][parentIdx].SetRotation(Math::Quaternion::SLerp(origParentRotation, ikFixedPoses[fixFrame][parentIdx].GetRotation(), w)); ikFixedPoses[fixFrame][parentParentIdx].SetRotation(Math::Quaternion::SLerp(origParentParentRotation, ikFixedPoses[fixFrame][parentParentIdx].GetRotation(), w)); // restore previous global rotation of this joint: auto parentGloblalRotation = Animation::JointLocalToGlobal( joint_parents_vec, parentIdx, ikFixedPoses[fixFrame] ).GetRotation(); jointCorrectionMasks[jointIdx][fixFrame] = 1.0f; ikFixedPoses[fixFrame][jointIdx].SetRotation( jointGlobalRotation * parentGloblalRotation.GetConjugate() ); auto result = Animation::JointLocalToGlobal( joint_parents_vec, jointIdx, ikFixedPoses[fixFrame] ).GetTranslation(); } } } for( auto &kv : jointCorrectionMasks) { for( size_t i = 0; i < N; ++i ) { margins[i] = kv.second[i] ? 0.0f : -1.0f; } TrajectoryCorrector corrector(margins, pos_weight * 10, vel_weight, acc_weight); CorrectRotationsForBone( poses, ikFixedPoses, kv.second, corrector, kv.first, false ); } jointCorrectionMasks.clear(); // Then process one bone contacts: for(auto &c : contacts) { if(c.contactType != Animation::kOneBone) { continue; } const auto jointIdx = c.jointIndex; auto parentIdx = joint_parents_vec[jointIdx]; // We can't touch frames that have been constrained with full body constraints // or the end effector constraints for this joint, so let's combine fullBodyMask // with the end effector mask for this joint if it exists so we can use that // information later: auto jointMask = fullBodyMask; addEndEffectorMask(jointIdx, parentIdx, jointMask); // Add a trajectory correction mask for the parent joint: if(jointCorrectionMasks[parentIdx].empty()) { jointCorrectionMasks[parentIdx] = jointMask; } // Compute the intervals in which the joint is in contact with the floor: auto contactIntervals = ComputeContactIntervals(c.contactMask, jointMask, contactThreshold); FilterContactIntervals(contactIntervals, jointMask, true); for(const auto &interval : contactIntervals) { for (int fixFrame = interval.first; fixFrame < interval.second; ++fixFrame) { // All we're going to do here is stick the joint to the floor - // we're going to allow it to slide from side to side. // Find a target position that lies on the floor by iteratively // projecting the joint to the floor (pure laziness really, this could // be done analytically): Math::Vector parentPos = Animation::JointLocalToGlobal(joint_parents_vec, parentIdx, ikFixedPoses[fixFrame]).GetTranslation(); Math::Vector target = Animation::JointLocalToGlobal(joint_parents_vec, jointIdx, ikFixedPoses[fixFrame]).GetTranslation(); float jointLength = (target - parentPos).GetLength3(); for(int32_t i = 0; i < 10; ++i) { target.SetY(c.minHeight); auto dir = (target - parentPos).GetNormalized3(); target = parentPos + dir * jointLength; } IK::OneBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, jointIdx, 1.0f, target, joint_parents_vec ); jointCorrectionMasks[parentIdx][fixFrame] = 1.0f; } } } // Fixing the contacts with IK will introduce popping into the animation, // so let's apply the interpolator to all the joints we modified so as to // line the trajectory up properly again: for( auto &kv : jointCorrectionMasks) { for( size_t i = 0; i < N; ++i ) { margins[i] = kv.second[i] ? 0.0f : -1.0f; } TrajectoryCorrector corrector(margins, pos_weight * 10, vel_weight, acc_weight); CorrectRotationsForBone( poses, ikFixedPoses, kv.second, corrector, kv.first, false ); } if (has_double_ankle_joints) { // Maps to save target positions BEFORE 2-bone IK modifies them std::map> savedFirstAnkleTargets; // [firstAnkleIdx][frame] -> position std::map> savedToeTargets; // [firstAnkleIdx][frame] -> position std::map contactToToeIdx; // firstAnkleIdx -> toeIdx // Find toe joints for each leg for (const auto& tc : contacts) { if (tc.contactType == Animation::kOneBone) { // The parent of the toe is the 1st ankle int parentIdx = joint_parents_vec[tc.jointIndex]; if (parentIdx >= 0) { contactToToeIdx[parentIdx] = tc.jointIndex; } } } // For each 2-bone contact, correct the parent (2nd ankle) joint for (auto& c : contacts) { if (c.contactType != Animation::kTwoBone) continue; const auto firstAnkleIdx = c.jointIndex; const auto secondAnkleIdx = joint_parents_vec[firstAnkleIdx]; const auto kneeIdx = joint_parents_vec[secondAnkleIdx]; const auto hipIdx = joint_parents_vec[kneeIdx]; if (hipIdx < 0) continue; // safety check // Get saved contact frames for this ankle auto it = twoBoneContactFrames.find(firstAnkleIdx); if (it == twoBoneContactFrames.end()) continue; const auto& contactFrames = it->second; // Add correction mask for knee and hip auto jointMask = fullBodyMask; addEndEffectorMask(firstAnkleIdx, secondAnkleIdx, jointMask); if (jointCorrectionMasks[kneeIdx].empty()) jointCorrectionMasks[kneeIdx] = jointMask; if (jointCorrectionMasks[hipIdx].empty()) jointCorrectionMasks[hipIdx] = jointMask; for (uint32_t fixFrame = 0; fixFrame < fullBodyMask.size(); ++fixFrame) { // Only correct frames where the 1st ankle was corrected if (!contactFrames[fixFrame]) continue; // *** SAVE TARGET POSITIONS BEFORE 2-BONE IK *** savedFirstAnkleTargets[firstAnkleIdx][fixFrame] = Animation::JointLocalToGlobal( joint_parents_vec, firstAnkleIdx, ikFixedPoses[fixFrame]).GetTranslation(); if (contactToToeIdx.count(firstAnkleIdx)) { savedToeTargets[firstAnkleIdx][fixFrame] = Animation::JointLocalToGlobal( joint_parents_vec, contactToToeIdx[firstAnkleIdx], ikFixedPoses[fixFrame]).GetTranslation(); } // Get original global transforms (before any IK corrections) auto originalFirstAnkleGlobal = Animation::JointLocalToGlobal( joint_parents_vec, firstAnkleIdx, originalPoses[fixFrame]); auto originalSecondAnkleGlobal = Animation::JointLocalToGlobal( joint_parents_vec, secondAnkleIdx, originalPoses[fixFrame]); // Compute delta from 1st ankle to 2nd ankle in original animation auto deltaFirstToSecond = originalFirstAnkleGlobal.GetDeltaToOther(originalSecondAnkleGlobal); // Get corrected 1st ankle global transform auto correctedFirstAnkleGlobal = Animation::JointLocalToGlobal( joint_parents_vec, firstAnkleIdx, ikFixedPoses[fixFrame]); // Apply the original delta to the corrected 1st ankle to get target for 2nd ankle auto target = (deltaFirstToSecond * correctedFirstAnkleGlobal).GetTranslation(); // print current and target second ankle positions auto currPos = Animation::JointLocalToGlobal( joint_parents_vec, secondAnkleIdx, ikFixedPoses[fixFrame]).GetTranslation(); // Apply 2-bone IK: Hip -> Knee -> 2nd Ankle IK::TwoBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, secondAnkleIdx, 1.0f, target, joint_parents_vec, c.hintOffset ); // auto correctedPos = Animation::JointLocalToGlobal( // joint_parents_vec, secondAnkleIdx, ikFixedPoses[fixFrame]).GetTranslation(); // std::cout << "Frame " << fixFrame << ": target second ankle=(" << target.GetX() << ", " << target.GetY() << ", " << target.GetZ() << "), corrected second ankle position=(" << correctedPos.GetX() << ", " << correctedPos.GetY() << ", " << correctedPos.GetZ() << ")" << std::endl; jointCorrectionMasks[kneeIdx][fixFrame] = 1.0f; jointCorrectionMasks[hipIdx][fixFrame] = 1.0f; } } // Smooth the corrected joints for (auto& kv : jointCorrectionMasks) { for (size_t i = 0; i < N; ++i) margins[i] = kv.second[i] ? 0.0f : -1.0f; TrajectoryCorrector corrector(margins, pos_weight * 10, vel_weight, acc_weight); CorrectRotationsForBone(poses, ikFixedPoses, kv.second, corrector, kv.first, false); } // *** PHASE 2: 1-bone IKs to restore 1st ankle and toe *** jointCorrectionMasks.clear(); for (auto& c : contacts) { if (c.contactType != Animation::kTwoBone) continue; const auto firstAnkleIdx = c.jointIndex; const auto secondAnkleIdx = joint_parents_vec[firstAnkleIdx]; auto it = twoBoneContactFrames.find(firstAnkleIdx); if (it == twoBoneContactFrames.end()) continue; // Setup correction masks auto jointMask = fullBodyMask; addEndEffectorMask(firstAnkleIdx, secondAnkleIdx, jointMask); if (jointCorrectionMasks[secondAnkleIdx].empty()) jointCorrectionMasks[secondAnkleIdx] = jointMask; if (jointCorrectionMasks[firstAnkleIdx].empty()) jointCorrectionMasks[firstAnkleIdx] = jointMask; for (uint32_t fixFrame = 0; fixFrame < fullBodyMask.size(); ++fixFrame) { if (!it->second[fixFrame]) continue; // 1-bone IK: Rotate 2nd ankle so 1st ankle reaches saved target IK::OneBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, firstAnkleIdx, 1.0f, savedFirstAnkleTargets[firstAnkleIdx][fixFrame], joint_parents_vec ); jointCorrectionMasks[secondAnkleIdx][fixFrame] = 1.0f; // auto target = savedFirstAnkleTargets[firstAnkleIdx][fixFrame]; // auto corrected = Animation::JointLocalToGlobal( // joint_parents_vec, firstAnkleIdx, ikFixedPoses[fixFrame]).GetTranslation(); // std::cout << "Frame " << fixFrame << ": target first ankle=(" << target.GetX() << ", " << target.GetY() << ", " << target.GetZ() << "), corrected first ankle=(" << corrected.GetX() << ", " << corrected.GetY() << ", " << corrected.GetZ() << ")" << std::endl; // 1-bone IK: Rotate 1st ankle so toe reaches saved target if (contactToToeIdx.count(firstAnkleIdx) && savedToeTargets[firstAnkleIdx].count(fixFrame)) { IK::OneBoneIk( ikFixedPoses[fixFrame], Math::Transform::Identity, contactToToeIdx[firstAnkleIdx], 1.0f, savedToeTargets[firstAnkleIdx][fixFrame], joint_parents_vec ); jointCorrectionMasks[firstAnkleIdx][fixFrame] = 1.0f; } // target = savedToeTargets[firstAnkleIdx][fixFrame]; // corrected = Animation::JointLocalToGlobal( // joint_parents_vec, contactToToeIdx[firstAnkleIdx], ikFixedPoses[fixFrame]).GetTranslation(); // std::cout << "Frame " << fixFrame << ": target toe=(" << target.GetX() << ", " << target.GetY() << ", " << target.GetZ() << "), corrected toe=(" << corrected.GetX() << ", " << corrected.GetY() << ", " << corrected.GetZ() << ")" << std::endl; } } // Smooth 2nd ankle and 1st ankle for (auto& kv : jointCorrectionMasks) { for (size_t i = 0; i < N; ++i) margins[i] = kv.second[i] ? 0.0f : -1.0f; TrajectoryCorrector corrector(margins, pos_weight * 10, vel_weight, acc_weight); CorrectRotationsForBone(poses, ikFixedPoses, kv.second, corrector, kv.first, false); } } } } Math::Transform Animation::JointLocalToGlobal( const std::vector& joint_parents_vec, int32_t index, const Pose& localPose, const Math::Transform& rootTx) { Math::Transform worldTx = Math::Transform::Identity; while (index > -1) { worldTx = worldTx * localPose[index]; index = joint_parents_vec[index]; } return worldTx * rootTx; } void Animation::CorrectMotion( std::vector& poses, const std::vector& targetPoses, const std::vector& fullBodyMask, const std::vector& rootMask, const std::vector& contacts, const std::vector& endEffectorPins, const std::vector& joint_parents_vec, const std::vector& defaultPose, float contactThreshold, float root_margin, bool has_double_ankle_joints ) { // Calculate some weights so we can preserve velocities more strongly on frames where // the root velocity is low const uint32_t N = poses.size(); Eigen::VectorXd velocity_weights(N); for (uint32_t frame = 1; frame < N; ++frame) { // work out xz velocity for this frame: float xdiff = poses[frame][0].GetTranslation()[0] - poses[frame - 1][0].GetTranslation()[0]; float zdiff = poses[frame][0].GetTranslation()[2] - poses[frame - 1][0].GetTranslation()[2]; // find velocity magnitude, divided by a typical walking speed: float v_mag = sqrtf(xdiff*xdiff + zdiff*zdiff) / 0.05f; // weight lower velocities higher so that the corrector doesn't make the character drift around // when it's supposed to stand still: v_mag = std::max(v_mag, 1.0f/1000.0f); velocity_weights(frame) = 1.0f / v_mag; } velocity_weights[0] = velocity_weights[1]; // Correct root y coordinates. // This will warp the root y coordinates in "poses" so they match the root y coordinates // in "targetPoses", on frames where the root y coordinates are constrained, ie the frames // where fullBodyMask = 1. // In addition to this, it preserves the root y coordinates in "pose" on frames where foot // contacts are active, to avoid mushiness when characters are jumping. CorrectHipsY( poses, targetPoses, fullBodyMask, contacts, contactThreshold ); // Correct root xz coordinates: // This will warp the root xz coordinates in "poses" so they match the xz coordinates // in "targetPoses" on frames where fullBodyMask = 1, and warp them so they're within // "root_margin" units of targetPoses on frames where rootMask = 1. CorrectHipsXZ( poses, targetPoses, fullBodyMask, rootMask, endEffectorPins, velocity_weights, root_margin ); // Correct joint rotations by warping the rotations so they match targetPoses on frames // where fullBodyMask = 1: CorrectJointRotations( poses, targetPoses, fullBodyMask, velocity_weights ); // Apply IK for end effector pins DoEffectorIK( poses, targetPoses, fullBodyMask, endEffectorPins, joint_parents_vec, defaultPose ); // Apply IK to stabilize limbs on contacts DoContactIK( poses, fullBodyMask, contacts, endEffectorPins, joint_parents_vec, defaultPose, contactThreshold, has_double_ankle_joints ); // std::cout << "Running post processing." << std::endl; }