// // AnimSplineIK.cpp // // Created by Angus Antley on 1/7/19. // Copyright (c) 2019 High Fidelity, Inc. All rights reserved. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AnimSplineIK.h" #include "AnimationLogging.h" #include "CubicHermiteSpline.h" #include #include "AnimUtil.h" static const float FRAMES_PER_SECOND = 30.0f; AnimSplineIK::AnimSplineIK(const QString& id, float alpha, bool enabled, float interpDuration, const QString& baseJointName, const QString& midJointName, const QString& tipJointName, const QString& basePositionVar, const QString& baseRotationVar, const QString& midPositionVar, const QString& midRotationVar, const QString& tipPositionVar, const QString& tipRotationVar, const QString& alphaVar, const QString& enabledVar, const std::vector tipTargetFlexCoefficients, const std::vector midTargetFlexCoefficients) : AnimNode(AnimNode::Type::SplineIK, id), _alpha(alpha), _enabled(enabled), _interpDuration(interpDuration), _baseJointName(baseJointName), _midJointName(midJointName), _tipJointName(tipJointName), _basePositionVar(basePositionVar), _baseRotationVar(baseRotationVar), _midPositionVar(midPositionVar), _midRotationVar(midRotationVar), _tipPositionVar(tipPositionVar), _tipRotationVar(tipRotationVar), _alphaVar(alphaVar), _enabledVar(enabledVar) { for (int i = 0; i < (int)tipTargetFlexCoefficients.size(); i++) { if (i < MAX_NUMBER_FLEX_VARIABLES) { _tipTargetFlexCoefficients[i] = tipTargetFlexCoefficients[i]; } } _numTipTargetFlexCoefficients = std::min((int)tipTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES); for (int i = 0; i < (int)midTargetFlexCoefficients.size(); i++) { if (i < MAX_NUMBER_FLEX_VARIABLES) { _midTargetFlexCoefficients[i] = midTargetFlexCoefficients[i]; } } _numMidTargetFlexCoefficients = std::min((int)midTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES); } AnimSplineIK::~AnimSplineIK() { } const AnimPoseVec& AnimSplineIK::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) { assert(_children.size() == 1); if (_children.size() != 1) { return _poses; } const float MIN_ALPHA = 0.0f; const float MAX_ALPHA = 1.0f; float alpha = glm::clamp(animVars.lookup(_alphaVar, _alpha), MIN_ALPHA, MAX_ALPHA); // evaluate underPoses AnimPoseVec underPoses = _children[0]->evaluate(animVars, context, dt, triggersOut); // if we don't have a skeleton, or jointName lookup failed or the spline alpha is 0 or there are no underposes. if (!_skeleton || _baseJointIndex == -1 || _midJointIndex == -1 || _tipJointIndex == -1 || alpha < EPSILON || underPoses.size() == 0) { // pass underPoses through unmodified. _poses = underPoses; return _poses; } // guard against size change if (underPoses.size() != _poses.size()) { _poses = underPoses; } // determine if we should interpolate bool enabled = animVars.lookup(_enabledVar, _enabled); if (enabled != _enabled) { AnimChain poseChain; poseChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex); if (enabled) { beginInterp(InterpType::SnapshotToSolve, poseChain); } else { beginInterp(InterpType::SnapshotToUnderPoses, poseChain); } } _enabled = enabled; // now that we have saved the previous _poses in _snapshotChain, we can update to the current underposes _poses = underPoses; // don't build chains or do IK if we are disabled & not interping. if (_interpType == InterpType::None && !enabled) { return _poses; } // compute under chain for possible interpolation AnimChain underChain; underChain.buildFromRelativePoses(_skeleton, underPoses, _tipJointIndex); AnimPose baseTargetAbsolutePose; // if there is a baseJoint ik target in animvars then set the joint to that // otherwise use the underpose AnimPose baseJointUnderPose = _skeleton->getAbsolutePose(_baseJointIndex, _poses); baseTargetAbsolutePose.rot() = animVars.lookupRigToGeometry(_baseRotationVar, baseJointUnderPose.rot()); baseTargetAbsolutePose.trans() = animVars.lookupRigToGeometry(_basePositionVar, baseJointUnderPose.trans()); int baseParentIndex = _skeleton->getParentIndex(_baseJointIndex); AnimPose baseParentAbsPose(Quaternions::IDENTITY,glm::vec3()); if (baseParentIndex >= 0) { baseParentAbsPose = _skeleton->getAbsolutePose(baseParentIndex, _poses); } _poses[_baseJointIndex] = baseParentAbsPose.inverse() * baseTargetAbsolutePose; _poses[_baseJointIndex].scale() = glm::vec3(1.0f); // initialize the middle joint target IKTarget midTarget; midTarget.setType((int)IKTarget::Type::Spline); midTarget.setIndex(_midJointIndex); AnimPose absPoseMid = _skeleton->getAbsolutePose(_midJointIndex, _poses); glm::quat midTargetRotation = animVars.lookupRigToGeometry(_midRotationVar, absPoseMid.rot()); glm::vec3 midTargetPosition = animVars.lookupRigToGeometry(_midPositionVar, absPoseMid.trans()); midTarget.setPose(midTargetRotation, midTargetPosition); midTarget.setWeight(1.0f); midTarget.setFlexCoefficients(_numMidTargetFlexCoefficients, _midTargetFlexCoefficients); // solve the lower spine spline AnimChain midJointChain; AnimPoseVec absolutePosesAfterBaseTipSpline; absolutePosesAfterBaseTipSpline.resize(_poses.size()); computeAbsolutePoses(absolutePosesAfterBaseTipSpline); midJointChain.buildFromRelativePoses(_skeleton, _poses, midTarget.getIndex()); solveTargetWithSpline(context, _baseJointIndex, midTarget, absolutePosesAfterBaseTipSpline, context.getEnableDebugDrawIKChains(), midJointChain); midJointChain.outputRelativePoses(_poses); // initialize the tip target IKTarget tipTarget; tipTarget.setType((int)IKTarget::Type::Spline); tipTarget.setIndex(_tipJointIndex); AnimPose absPoseTip = _skeleton->getAbsolutePose(_tipJointIndex, _poses); glm::quat tipRotation = animVars.lookupRigToGeometry(_tipRotationVar, absPoseTip.rot()); glm::vec3 tipTranslation = animVars.lookupRigToGeometry(_tipPositionVar, absPoseTip.trans()); tipTarget.setPose(tipRotation, tipTranslation); tipTarget.setWeight(1.0f); tipTarget.setFlexCoefficients(_numTipTargetFlexCoefficients, _tipTargetFlexCoefficients); // solve the upper spine spline AnimChain upperJointChain; AnimPoseVec finalAbsolutePoses; finalAbsolutePoses.resize(_poses.size()); computeAbsolutePoses(finalAbsolutePoses); upperJointChain.buildFromRelativePoses(_skeleton, _poses, tipTarget.getIndex()); solveTargetWithSpline(context, _midJointIndex, tipTarget, finalAbsolutePoses, context.getEnableDebugDrawIKChains(), upperJointChain); upperJointChain.buildDirtyAbsolutePoses(); upperJointChain.outputRelativePoses(_poses); // compute chain AnimChain ikChain; ikChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex); // blend with the underChain ikChain.blend(underChain, alpha); // apply smooth interpolation when turning ik on and off if (_interpType != InterpType::None) { _interpAlpha += _interpAlphaVel * dt; // ease in expo float easeInAlpha = 1.0f - powf(2.0f, -10.0f * _interpAlpha); if (_interpAlpha < 1.0f) { AnimChain interpChain; if (_interpType == InterpType::SnapshotToUnderPoses) { interpChain = underChain; interpChain.blend(_snapshotChain, easeInAlpha); } else if (_interpType == InterpType::SnapshotToSolve) { interpChain = ikChain; interpChain.blend(_snapshotChain, easeInAlpha); } // copy interpChain into _poses interpChain.outputRelativePoses(_poses); } else { // interpolation complete _interpType = InterpType::None; } } if (_interpType == InterpType::None) { if (enabled) { // copy chain into _poses ikChain.outputRelativePoses(_poses); } else { // copy under chain into _poses underChain.outputRelativePoses(_poses); } } // debug render ik targets if (context.getEnableDebugDrawIKTargets()) { const vec4 WHITE(1.0f); const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f); glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); glm::mat4 geomTargetMat = createMatFromQuatAndPos(tipTarget.getRotation(), tipTarget.getTranslation()); glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; QString name = QString("ikTargetSplineTip"); DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); glm::mat4 geomTargetMat2 = createMatFromQuatAndPos(midTarget.getRotation(), midTarget.getTranslation()); glm::mat4 avatarTargetMat2 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat2; QString name2 = QString("ikTargetSplineMid"); DebugDraw::getInstance().addMyAvatarMarker(name2, glmExtractRotation(avatarTargetMat2), extractTranslation(avatarTargetMat2), WHITE); glm::mat4 geomTargetMat3 = createMatFromQuatAndPos(baseTargetAbsolutePose.rot(), baseTargetAbsolutePose.trans()); glm::mat4 avatarTargetMat3 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat3; QString name3 = QString("ikTargetSplineBase"); DebugDraw::getInstance().addMyAvatarMarker(name3, glmExtractRotation(avatarTargetMat3), extractTranslation(avatarTargetMat3), WHITE); } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { // remove markers if they were added last frame. QString name = QString("ikTargetSplineTip"); DebugDraw::getInstance().removeMyAvatarMarker(name); QString name2 = QString("ikTargetSplineMid"); DebugDraw::getInstance().removeMyAvatarMarker(name2); QString name3 = QString("ikTargetSplineBase"); DebugDraw::getInstance().removeMyAvatarMarker(name3); } _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); return _poses; } void AnimSplineIK::lookUpIndices() { assert(_skeleton); // look up bone indices by name std::vector indices = _skeleton->lookUpJointIndices({ _baseJointName, _tipJointName, _midJointName }); // cache the results _baseJointIndex = indices[0]; _tipJointIndex = indices[1]; _midJointIndex = indices[2]; } void AnimSplineIK::computeAbsolutePoses(AnimPoseVec& absolutePoses) const { int numJoints = (int)_poses.size(); assert(numJoints <= _skeleton->getNumJoints()); assert(numJoints == (int)absolutePoses.size()); for (int i = 0; i < numJoints; ++i) { int parentIndex = _skeleton->getParentIndex(i); if (parentIndex < 0) { absolutePoses[i] = _poses[i]; } else { absolutePoses[i] = absolutePoses[parentIndex] * _poses[i]; } } } // for AnimDebugDraw rendering const AnimPoseVec& AnimSplineIK::getPosesInternal() const { return _poses; } void AnimSplineIK::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { AnimNode::setSkeletonInternal(skeleton); lookUpIndices(); } void AnimSplineIK::solveTargetWithSpline(const AnimContext& context, int base, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug, AnimChain& chainInfoOut) const { // build spline from tip to base AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); AnimPose basePose = absolutePoses[base]; CubicHermiteSplineFunctorWithArcLength spline; if (target.getIndex() == _tipJointIndex) { // set gain factors so that more curvature occurs near the tip of the spline. const float HIPS_GAIN = 0.5f; const float HEAD_GAIN = 1.0f; spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN); } else { spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(),tipPose.trans(), basePose.rot(), basePose.trans()); } float totalArcLength = spline.arcLength(1.0f); // This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way) // when the head is arched backwards very far. glm::quat halfRot = safeLerp(basePose.rot(), tipPose.rot(), 0.5f); if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { tipPose.rot() = -tipPose.rot(); } // find or create splineJointInfo for this target const std::vector* splineJointInfoVec = findOrCreateSplineJointInfo(context, base, target); if (splineJointInfoVec && splineJointInfoVec->size() > 0) { const int baseParentIndex = _skeleton->getParentIndex(base); AnimPose parentAbsPose = (baseParentIndex >= 0) ? absolutePoses[baseParentIndex] : AnimPose(); // go thru splineJointInfoVec backwards (base to tip) for (int i = (int)splineJointInfoVec->size() - 1; i >= 0; i--) { const SplineJointInfo& splineJointInfo = (*splineJointInfoVec)[i]; float t = spline.arcLengthInverse(splineJointInfo.ratio * totalArcLength); glm::vec3 trans = spline(t); // for base->tip splines, preform most twist toward the tip by using ease in function. t^2 float rotT = t; if (target.getIndex() == _tipJointIndex) { rotT = t * t; } glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT); // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis glm::vec3 y = glm::normalize(spline.d(t)); glm::vec3 x = twistRot * Vectors::UNIT_X; glm::vec3 u, v, w; generateBasisVectors(y, x, v, u, w); glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); AnimPose desiredAbsPose = AnimPose(glm::vec3(1.0f), rot, trans) * splineJointInfo.offsetPose; // apply flex coefficent AnimPose flexedAbsPose; // get the number of flex coeff for this spline float interpedCoefficient = 1.0f; int numFlexCoeff = target.getNumFlexCoefficients(); if (numFlexCoeff == (int)splineJointInfoVec->size()) { // then do nothing special interpedCoefficient = target.getFlexCoefficient(i); } else { // interp based on ratio of the joint. if (splineJointInfo.ratio < 1.0f) { float flexInterp = splineJointInfo.ratio * (float)(numFlexCoeff - 1); int startCoeff = (int)glm::floor(flexInterp); float partial = flexInterp - startCoeff; interpedCoefficient = target.getFlexCoefficient(startCoeff) * (1.0f - partial) + target.getFlexCoefficient(startCoeff + 1) * partial; } else { interpedCoefficient = target.getFlexCoefficient(numFlexCoeff - 1); } } ::blend(1, &absolutePoses[splineJointInfo.jointIndex], &desiredAbsPose, interpedCoefficient, &flexedAbsPose); AnimPose relPose = parentAbsPose.inverse() * flexedAbsPose; if (splineJointInfo.jointIndex != base) { // constrain the amount the spine can stretch or compress float length = glm::length(relPose.trans()); const float EPSILON = 0.0001f; if (length > EPSILON) { float defaultLength = glm::length(_skeleton->getRelativeDefaultPose(splineJointInfo.jointIndex).trans()); const float STRETCH_COMPRESS_PERCENTAGE = 0.15f; const float MAX_LENGTH = defaultLength * (1.0f + STRETCH_COMPRESS_PERCENTAGE); const float MIN_LENGTH = defaultLength * (1.0f - STRETCH_COMPRESS_PERCENTAGE); if (length > MAX_LENGTH) { relPose.trans() = (relPose.trans() / length) * MAX_LENGTH; } else if (length < MIN_LENGTH) { relPose.trans() = (relPose.trans() / length) * MIN_LENGTH; } } else { relPose.trans() = glm::vec3(0.0f); } } if (!chainInfoOut.setRelativePoseAtJointIndex(splineJointInfo.jointIndex, relPose)) { qCDebug(animation) << "error: joint not found in spline chain"; } parentAbsPose = flexedAbsPose; } } if (debug) { const vec4 CYAN(0.0f, 1.0f, 1.0f, 1.0f); chainInfoOut.debugDraw(context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(), CYAN); } } const std::vector* AnimSplineIK::findOrCreateSplineJointInfo(const AnimContext& context, int base, const IKTarget& target) const { // find or create splineJointInfo for this target auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); } else { computeAndCacheSplineJointInfosForIKTarget(context, base, target); auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); } } return nullptr; } // pre-compute information about each joint influenced by this spline IK target. void AnimSplineIK::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, int base, const IKTarget& target) const { std::vector splineJointInfoVec; // build spline between the default poses. AnimPose tipPose = _skeleton->getAbsoluteDefaultPose(target.getIndex()); AnimPose basePose = _skeleton->getAbsoluteDefaultPose(base); CubicHermiteSplineFunctorWithArcLength spline; if (target.getIndex() == _tipJointIndex) { // set gain factors so that more curvature occurs near the tip of the spline. const float HIPS_GAIN = 0.5f; const float HEAD_GAIN = 1.0f; spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN); } else { spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans()); } // measure the total arc length along the spline float totalArcLength = spline.arcLength(1.0f); glm::vec3 baseToTip = tipPose.trans() - basePose.trans(); float baseToTipLength = glm::length(baseToTip); glm::vec3 baseToTipNormal = baseToTip / baseToTipLength; int index = target.getIndex(); int endIndex = _skeleton->getParentIndex(base); while (index != endIndex) { AnimPose defaultPose = _skeleton->getAbsoluteDefaultPose(index); glm::vec3 baseToCurrentJoint = defaultPose.trans() - basePose.trans(); float ratio = glm::dot(baseToCurrentJoint, baseToTipNormal) / baseToTipLength; // compute offset from spline to the default pose. float t = spline.arcLengthInverse(ratio * totalArcLength); // compute the rotation by using the derivative of the spline as the y-axis, and the defaultPose x-axis glm::vec3 y = glm::normalize(spline.d(t)); glm::vec3 x = defaultPose.rot() * Vectors::UNIT_X; glm::vec3 u, v, w; generateBasisVectors(y, x, v, u, w); glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); AnimPose pose(glm::vec3(1.0f), rot, spline(t)); AnimPose offsetPose = pose.inverse() * defaultPose; SplineJointInfo splineJointInfo = { index, ratio, offsetPose }; splineJointInfoVec.push_back(splineJointInfo); index = _skeleton->getParentIndex(index); } _splineJointInfoMap[target.getIndex()] = splineJointInfoVec; } void AnimSplineIK::beginInterp(InterpType interpType, const AnimChain& chain) { // capture the current poses in a snapshot. _snapshotChain = chain; _interpType = interpType; _interpAlphaVel = FRAMES_PER_SECOND / _interpDuration; _interpAlpha = 0.0f; }