#include "math/ik_solver.h" ik_solver_fabrik::ik_solver_fabrik(unsigned int maxIterations, float targetDistance, float minProgress) : m_maxIterations(maxIterations), m_targetDistance(targetDistance), m_minProgress(minProgress) { } void ik_solver_fabrik::solve(const vec3d& targetPos, const matrix* targetOrient) { //Implementation of the paper "FABRIK: A fast iterative solver for the Inverse Kinematics problem" //http://andreasaristidou.com/publications/papers/FABRIK.pdf Assertion(m_nodes.size() > 1, "Node-list for IK does not contain at least two nodes."); //Calculate distances and set up nodes for(size_t i = 1; i < m_nodes.size(); ++i){ //This assumes that m_nodes[n] is _always_ a direct child of m_nodes[n-1] m_nodes[i - 1].distance = vm_vec_mag(&m_nodes[i].submodel->offset); m_nodes[i].calculatedPos = m_nodes[i - 1].calculatedPos + m_nodes[i].submodel->offset; } //Abort condition counter unsigned int iterationCounter = 0; float endEffectorDistance = FLT_MAX; float endEffectorDistanceDelta = FLT_MAX; while(iterationCounter++ < m_maxIterations && endEffectorDistance > m_targetDistance && endEffectorDistanceDelta > m_minProgress) { endEffectorDistanceDelta = endEffectorDistance; for(auto& node : m_nodes) node.lastPos = node.calculatedPos; m_nodes.back().calculatedPos = targetPos; //Backwards pass for(size_t i = m_nodes.size() - 2; i != static_cast(-1); --i) { auto& child = m_nodes[i + 1]; auto& parent = m_nodes[i]; // Find new position for parent float localDistance = vm_vec_dist(&parent.calculatedPos, &child.calculatedPos); float lambda = parent.distance / localDistance; parent.calculatedPos *= lambda; parent.calculatedPos += child.calculatedPos * (1.0f - lambda); // Find the parent's global rotation based on parent-child combination parent.calculateGlobalRotation(child); if(i == m_nodes.size() - 2){ //Now that we know where the first parent sits, we can update the end effector rotation. //If we have a target rotation, point to it, otherwise take its parent's rotation child.calculatedRot = targetOrient == nullptr ? parent.calculatedRot : *targetOrient; } // Find local rotation of child for constraint matrix localRot; vm_copy_transpose(&localRot, &parent.calculatedRot); vm_matrix_x_matrix(&localRot, &child.calculatedRot, &localRot); // Check if constraint requires change of local rotation if(child.constraint->constrain(localRot, true)){ // Reposition parent to fulfill child local rotation constraint by changing parent global rotation // Convert new local rotation of child and global rotation of child to new parent global rotation vm_transpose(&localRot); vm_matrix_x_matrix(&parent.calculatedRot, &localRot, &child.calculatedRot); // Find new parent position from new parent global rotation, child offset and child position vm_vec_unrotate(&parent.calculatedPos, &child.submodel->offset, &parent.calculatedRot); parent.calculatedPos *= -1; parent.calculatedPos += child.calculatedPos; } } //Technically the base node would now need to be rotation constrained, but since the base position is reset anyways for the forwards pass with a fixed base, this has no effect m_nodes.front().calculatedPos = ZERO_VECTOR; m_nodes.front().calculatedRot = IDENTITY_MATRIX; //Forwards pass for(size_t i = 0; i < m_nodes.size() - 1; ++i) { auto& child = m_nodes[i + 1]; auto& parent = m_nodes[i]; // Find new position for child float localDistance = vm_vec_dist(&parent.calculatedPos, &child.calculatedPos); float lambda = parent.distance / localDistance; child.calculatedPos *= lambda; child.calculatedPos += parent.calculatedPos * (1.0f - lambda); // Find the parent's global rotation based on parent-child combination parent.calculateGlobalRotation(child); // Find local rotation of parent for constraint const matrix& parentRot = i == 0 ? vmd_identity_matrix : m_nodes[i - 1].calculatedRot; matrix localRot; vm_copy_transpose(&localRot, &parentRot); vm_matrix_x_matrix(&localRot, &parent.calculatedRot, &localRot); // Check if constraint requires change of local rotation if(parent.constraint->constrain(localRot)){ // Reposition child to fulfill parent local rotation constraint by changing parent global rotation // Convert new local rotation and global rotation of tha parent's parent to new parent global rotation vm_matrix_x_matrix(&parent.calculatedRot, &localRot, &parentRot); // Find new child position from new parent global rotation, child offset and parent position vm_vec_unrotate(&child.calculatedPos, &child.submodel->offset, &parent.calculatedRot); child.calculatedPos += parent.calculatedPos; } } //After positioning the end effector, it now needs to be oriented correctly. If we have a target orientation use it, otherwise assume its parents rotation. Then constrain. { const matrix& parentRot = m_nodes[m_nodes.size() - 2].calculatedRot; // Find end effector target m_nodes.back().calculatedRot = targetOrient == nullptr ? parentRot : *targetOrient; // Calculate end effector local rotation matrix localRot; vm_copy_transpose(&localRot, &parentRot); vm_matrix_x_matrix(&localRot, &m_nodes.back().calculatedRot, &localRot); // Constrain if(m_nodes.back().constraint->constrain(localRot)) { // Calculate new global end effector rotation vm_matrix_x_matrix(&localRot, &localRot, &parentRot); m_nodes.back().calculatedRot = localRot; } } // Calculate abort conditions endEffectorDistance = vm_vec_dist(&targetPos, &m_nodes.back().calculatedPos); endEffectorDistanceDelta -= endEffectorDistance; } if (endEffectorDistanceDelta < 0.0f) { // Solution got worse, likely a singularity from hinges or something. Restore last good solution for(auto& node : m_nodes) node.calculatedPos = node.lastPos; for(size_t i = 0; i < m_nodes.size() - 1; ++i) { m_nodes[i].calculateGlobalRotation(m_nodes[i + 1]); } } //Localize rotations for(size_t i = m_nodes.size() - 1; i > 0; --i) { matrix parent; vm_copy_transpose(&parent, &m_nodes[i - 1].calculatedRot); vm_matrix_x_matrix(&m_nodes[i].calculatedRot, &parent, &m_nodes[i].calculatedRot); } } void ik_node::calculateGlobalRotation(const ik_node& child) { vec3d rotax; vec3d n_off; vec3d n_diff = child.calculatedPos - calculatedPos; vm_vec_copy_normalize(&n_off, &child.submodel->offset); vm_vec_normalize(&n_diff); vec3d checkSafeTransform = n_diff - n_off; if(vm_vec_mag(&checkSafeTransform) <= 0.001){ calculatedRot = IDENTITY_MATRIX; return; } vm_vec_cross(&rotax, &n_off, &n_diff); vm_vec_normalize(&rotax); vm_quaternion_rotate(&calculatedRot, acosf(vm_vec_dot(&n_diff, &n_off)), &rotax); } static constexpr float angles::*pbh[] = { &angles::p, &angles::b, &angles::h }; ik_constraint_hinge::ik_constraint_hinge(const vec3d& _axis) : axis(_axis) { } bool ik_constraint_hinge::constrain(matrix& localRot, bool /*backwardsPass*/) const { float angle; float distance = vm_closest_angle_to_matrix(&localRot, &axis, &angle); // Tolerance for off-axis. if(distance < fl_radians(0.5f)){ return false; } /*if(backwardsPass){ TODO: If Hinge joints cause singularities in FABRIK, this might need random perturbations on the backwards pass }*/ vm_quaternion_rotate(&localRot, angle, &axis); return true; } ik_constraint_window::ik_constraint_window(const angles& _absLimit) : absLimit(_absLimit) { } bool ik_constraint_window::constrain(matrix& localRot, bool /*backwardsPass*/) const { //Convert to angles angles currentAngles; vm_extract_angles_matrix_alternate(¤tAngles, &localRot); bool needsClamp = false; //Clamp absolute value of individual angles to window for (float angles::* i : pbh) { const float absAngle = std::abs(currentAngles.*i); if(absAngle > absLimit.*i){ needsClamp = true; currentAngles.*i = copysignf(std::min(absAngle, absLimit.*i), currentAngles.*i); } } //If any axis needed clamping, recalc the local rotation if(needsClamp){ vm_angles_2_matrix(&localRot, ¤tAngles); } return needsClamp; }