diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d477db936e..bafca2844d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1268,10 +1268,15 @@ void MyAvatar::initHeadBones() { } void MyAvatar::initAnimGraph() { + // avatar.json // https://gist.github.com/hyperlogic/7d6a0892a7319c69e2b9 - // python2 -m SimpleHTTPServer& - //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); - auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/7d6a0892a7319c69e2b9/raw/e2cb37aee601b6fba31d60eac3f6ae3ef72d4a66/avatar.json"); + // + // ik-avatar.json + // https://gist.github.com/hyperlogic/e58e0a24cc341ad5d060 + // + // or run a local web-server + // python -m SimpleHTTPServer& + auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/e58e0a24cc341ad5d060/raw/2a994bef7726ce8e9efcee7622b8b1a1b6b67490/ik-avatar.json"); _rig->initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); } diff --git a/libraries/animation/src/AnimController.cpp b/libraries/animation/src/AnimController.cpp new file mode 100644 index 0000000000..c021124732 --- /dev/null +++ b/libraries/animation/src/AnimController.cpp @@ -0,0 +1,106 @@ +// +// AnimController.cpp +// +// Created by Anthony J. Thibault on 9/8/15. +// Copyright (c) 2015 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 "AnimController.h" +#include "AnimationLogging.h" + +AnimController::AnimController(const std::string& id, float alpha) : + AnimNode(AnimNode::Type::Controller, id), + _alpha(alpha) { + +} + +AnimController::~AnimController() { + +} + +const AnimPoseVec& AnimController::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + return overlay(animVars, dt, triggersOut, _skeleton->getRelativeBindPoses()); +} + +const AnimPoseVec& AnimController::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + _alpha = animVars.lookup(_alphaVar, _alpha); + + for (auto& jointVar : _jointVars) { + if (!jointVar.hasPerformedJointLookup) { + QString qJointName = QString::fromStdString(jointVar.jointName); + jointVar.jointIndex = _skeleton->nameToJointIndex(qJointName); + if (jointVar.jointIndex < 0) { + qCWarning(animation) << "AnimController could not find jointName" << qJointName << "in skeleton"; + } + jointVar.hasPerformedJointLookup = true; + } + + if (jointVar.jointIndex >= 0) { + + AnimPose defaultPose; + glm::quat absRot; + glm::quat parentAbsRot; + if (jointVar.jointIndex <= (int)underPoses.size()) { + + // jointVar is an absolute rotation, if it is not set we will use the underPose as our default value + defaultPose = _skeleton->getAbsolutePose(jointVar.jointIndex, underPoses); + absRot = animVars.lookup(jointVar.var, defaultPose.rot); + + // because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose. + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsRot = _skeleton->getAbsolutePose(parentIndex, underPoses).rot; + } + + } else { + + // jointVar is an absolute rotation, if it is not set we will use the bindPose as our default value + defaultPose = _skeleton->getAbsoluteBindPose(jointVar.jointIndex); + absRot = animVars.lookup(jointVar.var, defaultPose.rot); + + // because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose + // here we use the bind pose + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsRot = _skeleton->getAbsoluteBindPose(parentIndex).rot; + } + } + + // convert from absolute to relative + glm::quat relRot = glm::inverse(parentAbsRot) * absRot; + _poses[jointVar.jointIndex] = AnimPose(defaultPose.scale, relRot, defaultPose.trans); + } + } + + return _poses; +} + +void AnimController::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + AnimNode::setSkeletonInternal(skeleton); + + // invalidate all jointVar indices + for (auto& jointVar : _jointVars) { + jointVar.jointIndex = -1; + jointVar.hasPerformedJointLookup = false; + } + + // potentially allocate new joints. + _poses.resize(skeleton->getNumJoints()); + + // set all joints to identity + for (auto& pose : _poses) { + pose = AnimPose::identity; + } +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimController::getPosesInternal() const { + return _poses; +} + +void AnimController::addJointVar(const JointVar& jointVar) { + _jointVars.push_back(jointVar); +} diff --git a/libraries/animation/src/AnimController.h b/libraries/animation/src/AnimController.h new file mode 100644 index 0000000000..15805d72fd --- /dev/null +++ b/libraries/animation/src/AnimController.h @@ -0,0 +1,58 @@ +// +// AnimController.h +// +// Created by Anthony J. Thibault on 9/8/15. +// Copyright (c) 2015 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 +// + +#ifndef hifi_AnimController_h +#define hifi_AnimController_h + +#include "AnimNode.h" + +// Allows procedural control over a set of joints. + +class AnimController : public AnimNode { +public: + friend class AnimTests; + + AnimController(const std::string& id, float alpha); + virtual ~AnimController() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; + + void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; + + struct JointVar { + JointVar(const std::string& varIn, const std::string& jointNameIn) : var(varIn), jointName(jointNameIn), jointIndex(-1), hasPerformedJointLookup(false) {} + std::string var = ""; + std::string jointName = ""; + int jointIndex = -1; + bool hasPerformedJointLookup = false; + }; + + void addJointVar(const JointVar& jointVar); + +protected: + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + float _alpha; + std::string _alphaVar; + + std::vector _jointVars; + + // no copies + AnimController(const AnimController&) = delete; + AnimController& operator=(const AnimController&) = delete; + +}; + +#endif // hifi_AnimController_h diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp new file mode 100644 index 0000000000..71401b6c60 --- /dev/null +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -0,0 +1,584 @@ +// +// AnimInverseKinematics.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimInverseKinematics.h" + +#include +#include + +#include "ElbowConstraint.h" +#include "SwingTwistConstraint.h" +#include "AnimationLogging.h" + +AnimInverseKinematics::AnimInverseKinematics(const std::string& id) : AnimNode(AnimNode::Type::InverseKinematics, id) { +} + +AnimInverseKinematics::~AnimInverseKinematics() { + clearConstraints(); +} + +void AnimInverseKinematics::loadDefaultPoses(const AnimPoseVec& poses) { + _defaultRelativePoses = poses; + assert(_skeleton && _skeleton->getNumJoints() == (int)poses.size()); +} + +void AnimInverseKinematics::loadPoses(const AnimPoseVec& poses) { + assert(_skeleton && _skeleton->getNumJoints() == (int)poses.size()); + if (_skeleton->getNumJoints() == (int)poses.size()) { + _relativePoses = poses; + } else { + _relativePoses.clear(); + } +} + +void AnimInverseKinematics::computeAbsolutePoses(AnimPoseVec& absolutePoses) const { + int numJoints = (int)_relativePoses.size(); + absolutePoses.clear(); + absolutePoses.reserve(numJoints); + assert(numJoints <= _skeleton->getNumJoints()); + for (int i = 0; i < numJoints; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex < 0) { + absolutePoses[i] = _relativePoses[i]; + } else { + absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; + } + } +} + +void AnimInverseKinematics::setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar) { + // if there are dups, last one wins. + _targetVarVec.push_back(IKTargetVar(jointName, positionVar.toStdString(), rotationVar.toStdString())); +} + +static int findRootJointInSkeleton(AnimSkeleton::ConstPointer skeleton, int index) { + // walk down the skeleton hierarchy to find the joint's root + int rootIndex = -1; + int parentIndex = skeleton->getParentIndex(index); + while (parentIndex != -1) { + rootIndex = parentIndex; + parentIndex = skeleton->getParentIndex(parentIndex); + } + return rootIndex; +} + +//virtual +const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) { + + // NOTE: we assume that _relativePoses are up to date (e.g. loadPoses() was just called) + if (_relativePoses.empty()) { + return _relativePoses; + } + + // evaluate target vars + for (auto& targetVar : _targetVarVec) { + + // lazy look up of jointIndices and insertion into _absoluteTargets map + if (!targetVar.hasPerformedJointLookup) { + targetVar.jointIndex = _skeleton->nameToJointIndex(targetVar.jointName); + if (targetVar.jointIndex >= 0) { + // insert into _absoluteTargets map + IKTarget target; + target.pose = AnimPose::identity; + target.rootIndex = findRootJointInSkeleton(_skeleton, targetVar.jointIndex); + _absoluteTargets[targetVar.jointIndex] = target; + + if (targetVar.jointIndex > _maxTargetIndex) { + _maxTargetIndex = targetVar.jointIndex; + } + } else { + qCWarning(animation) << "AnimInverseKinematics could not find jointName" << targetVar.jointName << "in skeleton"; + } + targetVar.hasPerformedJointLookup = true; + } + + if (targetVar.jointIndex >= 0) { + // update pose in _absoluteTargets map + auto iter = _absoluteTargets.find(targetVar.jointIndex); + if (iter != _absoluteTargets.end()) { + AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, _relativePoses); + iter->second.pose.trans = animVars.lookup(targetVar.positionVar, defaultPose.trans); + iter->second.pose.rot = animVars.lookup(targetVar.rotationVar, defaultPose.rot); + } + } + } + + // RELAX! Don't do it. + // relaxTowardDefaults(dt); + + if (_absoluteTargets.empty()) { + // no IK targets but still need to enforce constraints + std::map::iterator constraintItr = _constraints.begin(); + while (constraintItr != _constraints.end()) { + int index = constraintItr->first; + glm::quat rotation = _relativePoses[index].rot; + constraintItr->second->apply(rotation); + _relativePoses[index].rot = rotation; + ++constraintItr; + } + } else { + // compute absolute poses that correspond to relative target poses + AnimPoseVec absolutePoses; + computeAbsolutePoses(absolutePoses); + + float largestError = 0.0f; + const float ACCEPTABLE_RELATIVE_ERROR = 1.0e-3f; + int numLoops = 0; + const int MAX_IK_LOOPS = 16; + const quint64 MAX_IK_TIME = 10 * USECS_PER_MSEC; + quint64 expiry = usecTimestampNow() + MAX_IK_TIME; + do { + largestError = 0.0f; + for (auto& targetPair: _absoluteTargets) { + int lowestMovedIndex = _relativePoses.size() - 1; + int tipIndex = targetPair.first; + AnimPose targetPose = targetPair.second.pose; + int rootIndex = targetPair.second.rootIndex; + if (rootIndex != -1) { + // transform targetPose into skeleton's absolute frame + AnimPose& rootPose = _relativePoses[rootIndex]; + targetPose.trans = rootPose.trans + rootPose.rot * targetPose.trans; + targetPose.rot = rootPose.rot * targetPose.rot; + } + + glm::vec3 tip = absolutePoses[tipIndex].trans; + float error = glm::length(targetPose.trans - tip); + if (error < ACCEPTABLE_RELATIVE_ERROR) { + if (largestError < error) { + largestError = error; + } + // this targetPose has been met + // finally set the relative rotation of the tip to agree with absolute target rotation + int parentIndex = _skeleton->getParentIndex(tipIndex); + if (parentIndex != -1) { + // compute tip's new parent-relative rotation + // Q = Qp * q --> q' = Qp^ * Q + glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; + RotationConstraint* constraint = getConstraint(tipIndex); + if (constraint) { + constraint->apply(newRelativeRotation); + // TODO: ATM the final rotation target may fails but we need to provide + // feedback to the IK system so that it can adjust the bones up the skeleton + // to help this rotation target get met. + } + _relativePoses[tipIndex].rot = newRelativeRotation; + } + break; + } + + // descend toward root, rotating each joint to get tip closer to target + int index = _skeleton->getParentIndex(tipIndex); + while (index != -1 && error > ACCEPTABLE_RELATIVE_ERROR) { + // compute the two lines that should be aligned + glm::vec3 jointPosition = absolutePoses[index].trans; + glm::vec3 leverArm = tip - jointPosition; + glm::vec3 pivotLine = targetPose.trans - jointPosition; + + // compute the axis of the rotation that would align them + glm::vec3 axis = glm::cross(leverArm, pivotLine); + float axisLength = glm::length(axis); + if (axisLength > EPSILON) { + // compute deltaRotation for alignment (brings tip closer to target) + axis /= axisLength; + float angle = acosf(glm::dot(leverArm, pivotLine) / (glm::length(leverArm) * glm::length(pivotLine))); + + // NOTE: even when axisLength is not zero (e.g. lever-arm and pivot-arm are not quite aligned) it is + // still possible for the angle to be zero so we also check that to avoid unnecessary calculations. + if (angle > EPSILON) { + glm::quat deltaRotation = glm::angleAxis(angle, axis); + + int parentIndex = _skeleton->getParentIndex(index); + if (parentIndex == -1) { + // TODO? apply constraints to root? + // TODO? harvest the root's transform as movement of entire skeleton? + } else { + // compute joint's new parent-relative rotation + // Q' = dQ * Q and Q = Qp * q --> q' = Qp^ * dQ * Q + glm::quat newRot = glm::normalize(glm::inverse(absolutePoses[parentIndex].rot) * deltaRotation * absolutePoses[index].rot); + RotationConstraint* constraint = getConstraint(index); + if (constraint) { + constraint->apply(newRot); + } + _relativePoses[index].rot = newRot; + } + // this joint has been changed so we check to see if it has the lowest index + if (index < lowestMovedIndex) { + lowestMovedIndex = index; + } + + // keep track of tip's new position as we descend towards root + tip = jointPosition + deltaRotation * leverArm; + error = glm::length(targetPose.trans - tip); + } + } + index = _skeleton->getParentIndex(index); + } + if (largestError < error) { + largestError = error; + } + + if (lowestMovedIndex <= _maxTargetIndex && lowestMovedIndex < tipIndex) { + // only update the absolutePoses that matter: those between lowestMovedIndex and _maxTargetIndex + for (int i = lowestMovedIndex; i <= _maxTargetIndex; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex != -1) { + absolutePoses[i] = absolutePoses[parentIndex] * _relativePoses[i]; + } + } + } + + // finally set the relative rotation of the tip to agree with absolute target rotation + int parentIndex = _skeleton->getParentIndex(tipIndex); + if (parentIndex != -1) { + // compute tip's new parent-relative rotation + // Q = Qp * q --> q' = Qp^ * Q + glm::quat newRelativeRotation = glm::inverse(absolutePoses[parentIndex].rot) * targetPose.rot; + RotationConstraint* constraint = getConstraint(tipIndex); + if (constraint) { + constraint->apply(newRelativeRotation); + // TODO: ATM the final rotation target just fails but we need to provide + // feedback to the IK system so that it can adjust the bones up the skeleton + // to help this rotation target get met. + } + _relativePoses[tipIndex].rot = newRelativeRotation; + absolutePoses[tipIndex].rot = targetPose.rot; + } + } + ++numLoops; + } while (largestError > ACCEPTABLE_RELATIVE_ERROR && numLoops < MAX_IK_LOOPS && usecTimestampNow() < expiry); + } + return _relativePoses; +} + +//virtual +const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + loadPoses(underPoses); + return evaluate(animVars, dt, triggersOut); +} + +RotationConstraint* AnimInverseKinematics::getConstraint(int index) { + RotationConstraint* constraint = nullptr; + std::map::iterator constraintItr = _constraints.find(index); + if (constraintItr != _constraints.end()) { + constraint = constraintItr->second; + } + return constraint; +} + +void AnimInverseKinematics::clearConstraints() { + std::map::iterator constraintItr = _constraints.begin(); + while (constraintItr != _constraints.end()) { + delete constraintItr->second; + ++constraintItr; + } + _constraints.clear(); +} + +const glm::vec3 xAxis(1.0f, 0.0f, 0.0f); +const glm::vec3 yAxis(0.0f, 1.0f, 0.0f); +const glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + +void AnimInverseKinematics::initConstraints() { + if (!_skeleton) { + return; + } + // We create constraints for the joints shown here + // (and their Left counterparts if applicable). + // + // O RightHand + // / + // O / + // | O RightForeArm + // Neck O / + // | / + // O-------O-------O----O----O RightArm + // Spine2| + // | + // O Spine1 + // | + // | + // O Spine + // | + // | + // O---O---O RightUpLeg + // | | + // | | + // | | + // O O RightLeg + // | | + // y | | + // | | | + // | O O RightFoot + // z | / / + // \ | O--O O--O + // \| + // x -----+ + + loadDefaultPoses(_skeleton->getRelativeBindPoses()); + + // compute corresponding absolute poses + int numJoints = (int)_defaultRelativePoses.size(); + AnimPoseVec absolutePoses; + absolutePoses.reserve(numJoints); + for (int i = 0; i < numJoints; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex < 0) { + absolutePoses[i] = _defaultRelativePoses[i]; + } else { + absolutePoses[i] = absolutePoses[parentIndex] * _defaultRelativePoses[i]; + } + } + + _constraints.clear(); + for (int i = 0; i < numJoints; ++i) { + QString name = _skeleton->getJointName(i); + bool isLeft = name.startsWith("Left", Qt::CaseInsensitive); + float mirror = isLeft ? -1.0f : 1.0f; + if (isLeft) { + //name.remove(0, 4); + } else if (name.startsWith("Right", Qt::CaseInsensitive)) { + //name.remove(0, 5); + } + + RotationConstraint* constraint = nullptr; + if (0 == name.compare("Arm", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + stConstraint->setTwistLimits(-PI / 2.0f, PI / 2.0f); + + // these directions are approximate swing limits in root-frame + // NOTE: they don't need to be normalized + std::vector swungDirections; + swungDirections.push_back(glm::vec3(mirror * 1.0f, 1.0f, 1.0f)); + swungDirections.push_back(glm::vec3(mirror * 1.0f, 0.0f, 1.0f)); + swungDirections.push_back(glm::vec3(mirror * 1.0f, -1.0f, 0.5f)); + swungDirections.push_back(glm::vec3(mirror * 0.0f, -1.0f, 0.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.0f, -1.0f, -1.0f)); + swungDirections.push_back(glm::vec3(mirror * -0.5f, 0.0f, -1.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.0f, 1.0f, -1.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.0f, 1.0f, 0.0f)); + + // rotate directions into joint-frame + glm::quat invAbsoluteRotation = glm::inverse(absolutePoses[i].rot); + int numDirections = (int)swungDirections.size(); + for (int j = 0; j < numDirections; ++j) { + swungDirections[j] = invAbsoluteRotation * swungDirections[j]; + } + stConstraint->setSwingLimits(swungDirections); + + constraint = static_cast(stConstraint); + } else if (0 == name.compare("UpLeg", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + stConstraint->setTwistLimits(-PI / 4.0f, PI / 4.0f); + + // these directions are approximate swing limits in root-frame + // NOTE: they don't need to be normalized + std::vector swungDirections; + swungDirections.push_back(glm::vec3(mirror * 0.25f, 0.0f, 1.0f)); + swungDirections.push_back(glm::vec3(mirror * -0.5f, 0.0f, 1.0f)); + swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 1.0f)); + swungDirections.push_back(glm::vec3(mirror * -1.0f, 0.0f, 0.0f)); + swungDirections.push_back(glm::vec3(mirror * -0.5f, -0.5f, -1.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.0f, -0.75f, -1.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 0.0f)); + swungDirections.push_back(glm::vec3(mirror * 0.25f, -1.0f, 1.0f)); + + // rotate directions into joint-frame + glm::quat invAbsoluteRotation = glm::inverse(absolutePoses[i].rot); + int numDirections = (int)swungDirections.size(); + for (int j = 0; j < numDirections; ++j) { + swungDirections[j] = invAbsoluteRotation * swungDirections[j]; + } + stConstraint->setSwingLimits(swungDirections); + + constraint = static_cast(stConstraint); + } else if (0 == name.compare("RightHand", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + const float MAX_HAND_TWIST = PI / 2.0f; + stConstraint->setTwistLimits(-MAX_HAND_TWIST, MAX_HAND_TWIST); + + // these directions are approximate swing limits in parent-frame + // NOTE: they don't need to be normalized + std::vector swungDirections; + swungDirections.push_back(glm::vec3(1.0f, 1.0f, 0.0f)); + swungDirections.push_back(glm::vec3(0.75f, 1.0f, -1.0f)); + swungDirections.push_back(glm::vec3(-0.75f, 1.0f, -1.0f)); + swungDirections.push_back(glm::vec3(-1.0f, 1.0f, 0.0f)); + swungDirections.push_back(glm::vec3(-0.75f, 1.0f, 1.0f)); + swungDirections.push_back(glm::vec3(0.75f, 1.0f, 1.0f)); + + // rotate directions into joint-frame + glm::quat invRelativeRotation = glm::inverse(_defaultRelativePoses[i].rot); + int numDirections = (int)swungDirections.size(); + for (int j = 0; j < numDirections; ++j) { + swungDirections[j] = invRelativeRotation * swungDirections[j]; + } + stConstraint->setSwingLimits(swungDirections); + + /* + std::vector minDots; + const float MAX_HAND_SWING = PI / 3.0f; + minDots.push_back(cosf(MAX_HAND_SWING)); + stConstraint->setSwingLimits(minDots); + */ + constraint = static_cast(stConstraint); + } else if (name.startsWith("SpineXXX", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + const float MAX_SPINE_TWIST = PI / 8.0f; + stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); + + std::vector minDots; + const float MAX_SPINE_SWING = PI / 14.0f; + minDots.push_back(cosf(MAX_SPINE_SWING)); + stConstraint->setSwingLimits(minDots); + + constraint = static_cast(stConstraint); + } else if (0 == name.compare("NeckXXX", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + const float MAX_NECK_TWIST = PI / 2.0f; + stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST); + + std::vector minDots; + const float MAX_NECK_SWING = PI / 3.0f; + minDots.push_back(cosf(MAX_NECK_SWING)); + stConstraint->setSwingLimits(minDots); + + constraint = static_cast(stConstraint); + } else if (0 == name.compare("ForeArm", Qt::CaseInsensitive)) { + // The elbow joint rotates about the parent-frame's zAxis (-zAxis) for the Right (Left) arm. + ElbowConstraint* eConstraint = new ElbowConstraint(); + glm::quat referenceRotation = _defaultRelativePoses[i].rot; + eConstraint->setReferenceRotation(referenceRotation); + + // we determine the max/min angles by rotating the swing limit lines from parent- to child-frame + // then measure the angles to swing the yAxis into alignment + glm::vec3 hingeAxis = - mirror * zAxis; + const float MIN_ELBOW_ANGLE = 0.0f; + const float MAX_ELBOW_ANGLE = 7.0f * PI / 8.0f; + glm::quat invReferenceRotation = glm::inverse(referenceRotation); + glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_ELBOW_ANGLE, hingeAxis) * yAxis; + glm::vec3 maxSwingAxis = invReferenceRotation * glm::angleAxis(MAX_ELBOW_ANGLE, hingeAxis) * yAxis; + + // for the rest of the math we rotate hingeAxis into the child frame + hingeAxis = referenceRotation * hingeAxis; + eConstraint->setHingeAxis(hingeAxis); + + glm::vec3 projectedYAxis = glm::normalize(yAxis - glm::dot(yAxis, hingeAxis) * hingeAxis); + float minAngle = acosf(glm::dot(projectedYAxis, minSwingAxis)); + if (glm::dot(hingeAxis, glm::cross(projectedYAxis, minSwingAxis)) < 0.0f) { + minAngle = - minAngle; + } + float maxAngle = acosf(glm::dot(projectedYAxis, maxSwingAxis)); + if (glm::dot(hingeAxis, glm::cross(projectedYAxis, maxSwingAxis)) < 0.0f) { + maxAngle = - maxAngle; + } + eConstraint->setAngleLimits(minAngle, maxAngle); + + constraint = static_cast(eConstraint); + } else if (0 == name.compare("Leg", Qt::CaseInsensitive)) { + // The knee joint rotates about the parent-frame's -xAxis. + ElbowConstraint* eConstraint = new ElbowConstraint(); + glm::quat referenceRotation = _defaultRelativePoses[i].rot; + eConstraint->setReferenceRotation(referenceRotation); + glm::vec3 hingeAxis = -1.0f * xAxis; + + // we determine the max/min angles by rotating the swing limit lines from parent- to child-frame + // then measure the angles to swing the yAxis into alignment + const float MIN_KNEE_ANGLE = 0.0f; + const float MAX_KNEE_ANGLE = 3.0f * PI / 4.0f; + glm::quat invReferenceRotation = glm::inverse(referenceRotation); + glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * yAxis; + glm::vec3 maxSwingAxis = invReferenceRotation * glm::angleAxis(MAX_KNEE_ANGLE, hingeAxis) * yAxis; + + // for the rest of the math we rotate hingeAxis into the child frame + hingeAxis = referenceRotation * hingeAxis; + eConstraint->setHingeAxis(hingeAxis); + + glm::vec3 projectedYAxis = glm::normalize(yAxis - glm::dot(yAxis, hingeAxis) * hingeAxis); + float minAngle = acosf(glm::dot(projectedYAxis, minSwingAxis)); + if (glm::dot(hingeAxis, glm::cross(projectedYAxis, minSwingAxis)) < 0.0f) { + minAngle = - minAngle; + } + float maxAngle = acosf(glm::dot(projectedYAxis, maxSwingAxis)); + if (glm::dot(hingeAxis, glm::cross(projectedYAxis, maxSwingAxis)) < 0.0f) { + maxAngle = - maxAngle; + } + eConstraint->setAngleLimits(minAngle, maxAngle); + + constraint = static_cast(eConstraint); + } else if (0 == name.compare("Foot", Qt::CaseInsensitive)) { + SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); + stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot); + stConstraint->setTwistLimits(-PI / 4.0f, PI / 4.0f); + + // these directions are approximate swing limits in parent-frame + // NOTE: they don't need to be normalized + std::vector swungDirections; + swungDirections.push_back(yAxis); + swungDirections.push_back(xAxis); + swungDirections.push_back(glm::vec3(1.0f, 1.0f, 1.0f)); + swungDirections.push_back(glm::vec3(1.0f, 1.0f, -1.0f)); + + // rotate directions into joint-frame + glm::quat invRelativeRotation = glm::inverse(_defaultRelativePoses[i].rot); + int numDirections = (int)swungDirections.size(); + for (int j = 0; j < numDirections; ++j) { + swungDirections[j] = invRelativeRotation * swungDirections[j]; + } + stConstraint->setSwingLimits(swungDirections); + + constraint = static_cast(stConstraint); + } + if (constraint) { + _constraints[i] = constraint; + } + } +} + +void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + AnimNode::setSkeletonInternal(skeleton); + + // invalidate all targetVars + for (auto& targetVar : _targetVarVec) { + targetVar.hasPerformedJointLookup = false; + } + + // invalidate all targets + _absoluteTargets.clear(); + + _maxTargetIndex = 0; + + // No constraints! + /* + if (skeleton) { + initConstraints(); + } else { + clearConstraints(); + } + */ +} + +void AnimInverseKinematics::relaxTowardDefaults(float dt) { + // NOTE: for now we just use a single relaxation timescale for all joints, but in the future + // we could vary the timescale on a per-joint basis or do other fancy things. + + // for each joint: lerp towards the default pose + const float RELAXATION_TIMESCALE = 0.25f; + const float alpha = glm::clamp(dt / RELAXATION_TIMESCALE, 0.0f, 1.0f); + int numJoints = (int)_relativePoses.size(); + for (int i = 0; i < numJoints; ++i) { + float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot, _defaultRelativePoses[i].rot)); + _relativePoses[i].rot = glm::normalize(glm::lerp(_relativePoses[i].rot, dotSign * _defaultRelativePoses[i].rot, alpha)); + } +} + diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h new file mode 100644 index 0000000000..8452f654a2 --- /dev/null +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -0,0 +1,82 @@ +// +// AnimInverseKinematics.h +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimInverseKinematics_h +#define hifi_AnimInverseKinematics_h + +#include + +#include "AnimNode.h" + +class RotationConstraint; + +class AnimInverseKinematics : public AnimNode { +public: + + AnimInverseKinematics(const std::string& id); + virtual ~AnimInverseKinematics() override; + + void loadDefaultPoses(const AnimPoseVec& poses); + void loadPoses(const AnimPoseVec& poses); + const AnimPoseVec& getRelativePoses() const { return _relativePoses; } + void computeAbsolutePoses(AnimPoseVec& absolutePoses) const; + + void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar); + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) override; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; + +protected: + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } + + void relaxTowardDefaults(float dt); + + RotationConstraint* getConstraint(int index); + void clearConstraints(); + void initConstraints(); + + struct IKTargetVar { + IKTargetVar(const QString& jointNameIn, const std::string& positionVarIn, const std::string& rotationVarIn) : + jointName(jointNameIn), + positionVar(positionVarIn), + rotationVar(rotationVarIn), + jointIndex(-1), + hasPerformedJointLookup(false) {} + + std::string positionVar; + std::string rotationVar; + QString jointName; + int jointIndex; // cached joint index + bool hasPerformedJointLookup = false; + }; + + struct IKTarget { + AnimPose pose; + int rootIndex; + }; + + std::map _constraints; + std::vector _targetVarVec; + std::map _absoluteTargets; // IK targets of end-points + AnimPoseVec _defaultRelativePoses; // poses of the relaxed state + AnimPoseVec _relativePoses; // current relative poses + + // no copies + AnimInverseKinematics(const AnimInverseKinematics&) = delete; + AnimInverseKinematics& operator=(const AnimInverseKinematics&) = delete; + + // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses + // during the the cyclic coordinate descent algorithm + int _maxTargetIndex = 0; +}; + +#endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index 4675ae358f..f994867181 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -40,6 +40,8 @@ public: BlendLinear, Overlay, StateMachine, + Controller, + InverseKinematics, NumTypes }; using Pointer = std::shared_ptr; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 3ba042cba0..aa58845f01 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -20,6 +20,8 @@ #include "AnimOverlay.h" #include "AnimNodeLoader.h" #include "AnimStateMachine.h" +#include "AnimController.h" +#include "AnimInverseKinematics.h" using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); using NodeProcessFunc = bool (*)(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); @@ -29,12 +31,17 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadControllerNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); // called after children have been loaded +// returns node on success, nullptr on failure. static bool processClipNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } static bool processBlendLinearNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } static bool processOverlayNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static bool processControllerNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +static bool processInverseKinematicsNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } static const char* animNodeTypeToString(AnimNode::Type type) { switch (type) { @@ -42,6 +49,8 @@ static const char* animNodeTypeToString(AnimNode::Type type) { case AnimNode::Type::BlendLinear: return "blendLinear"; case AnimNode::Type::Overlay: return "overlay"; case AnimNode::Type::StateMachine: return "stateMachine"; + case AnimNode::Type::Controller: return "controller"; + case AnimNode::Type::InverseKinematics: return "inverseKinematics"; case AnimNode::Type::NumTypes: return nullptr; }; return nullptr; @@ -53,6 +62,8 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) { case AnimNode::Type::BlendLinear: return loadBlendLinearNode; case AnimNode::Type::Overlay: return loadOverlayNode; case AnimNode::Type::StateMachine: return loadStateMachineNode; + case AnimNode::Type::Controller: return loadControllerNode; + case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode; case AnimNode::Type::NumTypes: return nullptr; }; return nullptr; @@ -64,6 +75,8 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { case AnimNode::Type::BlendLinear: return processBlendLinearNode; case AnimNode::Type::Overlay: return processOverlayNode; case AnimNode::Type::StateMachine: return processStateMachineNode; + case AnimNode::Type::Controller: return processControllerNode; + case AnimNode::Type::InverseKinematics: return processInverseKinematicsNode; case AnimNode::Type::NumTypes: return nullptr; }; return nullptr; @@ -268,6 +281,67 @@ static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const return node; } +static AnimNode::Pointer loadControllerNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); + auto node = std::make_shared(id.toStdString(), alpha); + + READ_OPTIONAL_STRING(alphaVar, jsonObj); + if (!alphaVar.isEmpty()) { + node->setAlphaVar(alphaVar.toStdString()); + } + + auto jointsValue = jsonObj.value("joints"); + if (!jointsValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"joints\" in controller node, id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto jointsArray = jointsValue.toArray(); + for (const auto& jointValue : jointsArray) { + if (!jointValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad state object in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto jointObj = jointValue.toObject(); + + READ_STRING(var, jointObj, id, jsonUrl, nullptr); + READ_STRING(jointName, jointObj, id, jsonUrl, nullptr); + + AnimController::JointVar jointVar(var.toStdString(), jointName.toStdString()); + node->addJointVar(jointVar); + }; + + return node; +} + +AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + auto node = std::make_shared(id.toStdString()); + + auto targetsValue = jsonObj.value("targets"); + if (!targetsValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"targets\" in inverseKinematics node, id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto targetsArray = targetsValue.toArray(); + for (const auto& targetValue : targetsArray) { + if (!targetValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad state object in \"targets\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto targetObj = targetValue.toObject(); + + READ_STRING(jointName, targetObj, id, jsonUrl, nullptr); + READ_STRING(positionVar, targetObj, id, jsonUrl, nullptr); + READ_STRING(rotationVar, targetObj, id, jsonUrl, nullptr); + + node->setTargetVars(jointName, positionVar, rotationVar); + }; + + return node; +} + void buildChildMap(std::map& map, AnimNode::Pointer node) { for ( auto child : node->_children ) { map.insert(std::pair(child->_id, child)); diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h index 713d980f06..bc7d574c39 100644 --- a/libraries/animation/src/AnimNodeLoader.h +++ b/libraries/animation/src/AnimNodeLoader.h @@ -15,7 +15,7 @@ #include #include -#include +#include #include "AnimNode.h" diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 5dfed265a2..c6bae37edd 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -9,10 +9,12 @@ // #include "AnimSkeleton.h" -#include "AnimationLogging.h" -#include "GLMHelpers.h" + #include -#include + +#include + +#include "AnimationLogging.h" const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), glm::quat(), @@ -103,6 +105,14 @@ const QString& AnimSkeleton::getJointName(int jointIndex) const { return _joints[jointIndex].name; } +AnimPose AnimSkeleton::getAbsolutePose(int jointIndex, const AnimPoseVec& poses) const { + if (jointIndex < 0) { + return AnimPose::identity; + } else { + return getAbsolutePose(_joints[jointIndex].parentIndex, poses) * poses[jointIndex]; + } +} + void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints, const AnimPose& geometryOffset) { _joints = joints; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 4a6c3c0087..056d8fbd9b 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -12,8 +12,10 @@ #define hifi_AnimSkeleton #include +#include +#include -#include "FBXReader.h" +#include struct AnimPose { AnimPose() {} @@ -58,9 +60,12 @@ public: // relative to parent pose const AnimPose& getRelativeBindPose(int jointIndex) const; + const AnimPoseVec& getRelativeBindPoses() const { return _relativeBindPoses; } int getParentIndex(int jointIndex) const; + AnimPose getAbsolutePose(int jointIndex, const AnimPoseVec& poses) const; + #ifndef NDEBUG void dump() const; void dump(const AnimPoseVec& poses) const; diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index 50184862eb..3a8fbf3a61 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -12,9 +12,9 @@ #ifndef hifi_AnimationCache_h #define hifi_AnimationCache_h -#include -#include -#include +#include +#include +#include #include #include diff --git a/libraries/animation/src/ElbowConstraint.cpp b/libraries/animation/src/ElbowConstraint.cpp index a6b2210644..6833c1762e 100644 --- a/libraries/animation/src/ElbowConstraint.cpp +++ b/libraries/animation/src/ElbowConstraint.cpp @@ -15,7 +15,6 @@ #include ElbowConstraint::ElbowConstraint() : - _referenceRotation(), _minAngle(-PI), _maxAngle(PI) { diff --git a/libraries/animation/src/ElbowConstraint.h b/libraries/animation/src/ElbowConstraint.h index 3c4481cfa8..21288715b5 100644 --- a/libraries/animation/src/ElbowConstraint.h +++ b/libraries/animation/src/ElbowConstraint.h @@ -15,12 +15,10 @@ class ElbowConstraint : public RotationConstraint { public: ElbowConstraint(); - virtual void setReferenceRotation(const glm::quat& rotation) override { _referenceRotation = rotation; } void setHingeAxis(const glm::vec3& axis); void setAngleLimits(float minAngle, float maxAngle); virtual bool apply(glm::quat& rotation) const override; protected: - glm::quat _referenceRotation; glm::vec3 _axis; glm::vec3 _perpAxis; float _minAngle; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index f96c0b91fd..e0a2b31622 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -737,6 +737,19 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::q } int numFree = freeLineage.size(); + if (_enableAnimGraph && _animSkeleton) { + if (endIndex == _leftHandJointIndex) { + auto rootTrans = _animSkeleton->getAbsoluteBindPose(_rootJointIndex).trans; + _animVars.set("leftHandPosition", targetPosition + rootTrans); + _animVars.set("leftHandRotation", targetRotation); + } else if (endIndex == _rightHandJointIndex) { + auto rootTrans = _animSkeleton->getAbsoluteBindPose(_rootJointIndex).trans; + _animVars.set("rightHandPosition", targetPosition + rootTrans); + _animVars.set("rightHandRotation", targetRotation); + } + return; + } + // store and remember topmost parent transform glm::mat4 topParentTransform; { @@ -947,18 +960,28 @@ void Rig::updateFromHeadParameters(const HeadParameters& params) { void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { - auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; + if (_enableAnimGraph && _animSkeleton) { + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 yAxis(0.0f, 1.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + glm::quat absRot = (glm::angleAxis(-RADIANS_PER_DEGREE * leanSideways, zAxis) * + glm::angleAxis(-RADIANS_PER_DEGREE * leanForward, xAxis) * + glm::angleAxis(RADIANS_PER_DEGREE * torsoTwist, yAxis)); + _animVars.set("lean", absRot); + } else if (!_enableAnimGraph) { + auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; - // get the rotation axes in joint space and use them to adjust the rotation - glm::vec3 xAxis(1.0f, 0.0f, 0.0f); - glm::vec3 yAxis(0.0f, 1.0f, 0.0f); - glm::vec3 zAxis(0.0f, 0.0f, 1.0f); - glm::quat inverse = glm::inverse(parentState.getRotation() * getJointDefaultRotationInParentFrame(index)); - setJointRotationInConstrainedFrame(index, - glm::angleAxis(- RADIANS_PER_DEGREE * leanSideways, inverse * zAxis) * - glm::angleAxis(- RADIANS_PER_DEGREE * leanForward, inverse * xAxis) * - glm::angleAxis(RADIANS_PER_DEGREE * torsoTwist, inverse * yAxis) * - getJointState(index).getDefaultRotation(), DEFAULT_PRIORITY); + // get the rotation axes in joint space and use them to adjust the rotation + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 yAxis(0.0f, 1.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + glm::quat inverse = glm::inverse(parentState.getRotation() * getJointDefaultRotationInParentFrame(index)); + setJointRotationInConstrainedFrame(index, + glm::angleAxis(- RADIANS_PER_DEGREE * leanSideways, inverse * zAxis) * + glm::angleAxis(- RADIANS_PER_DEGREE * leanForward, inverse * xAxis) * + glm::angleAxis(RADIANS_PER_DEGREE * torsoTwist, inverse * yAxis) * + getJointState(index).getDefaultRotation(), DEFAULT_PRIORITY); + } } } @@ -1021,7 +1044,7 @@ void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { _animNode = nodeIn; _animNode->setSkeleton(_animSkeleton); }); - connect(_animLoader.get(), &AnimNodeLoader::error, [this, url](int error, QString str) { + connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; }); } diff --git a/libraries/animation/src/RotationConstraint.h b/libraries/animation/src/RotationConstraint.h index a05c0d0526..6926302ec8 100644 --- a/libraries/animation/src/RotationConstraint.h +++ b/libraries/animation/src/RotationConstraint.h @@ -15,15 +15,21 @@ class RotationConstraint { public: - RotationConstraint() {} + RotationConstraint() : _referenceRotation() {} virtual ~RotationConstraint() {} - /// \param rotation the default rotation that represents - virtual void setReferenceRotation(const glm::quat& rotation) = 0; + /// \param referenceRotation the rotation from which rotation changes are measured. + virtual void setReferenceRotation(const glm::quat& rotation) { _referenceRotation = rotation; } + + /// \return the rotation from which rotation changes are measured. + const glm::quat& getReferenceRotation() const { return _referenceRotation; } /// \param rotation rotation to clamp /// \return true if rotation is clamped virtual bool apply(glm::quat& rotation) const = 0; + +protected: + glm::quat _referenceRotation = glm::quat(); }; #endif // hifi_RotationConstraint_h diff --git a/libraries/animation/src/SwingTwistConstraint.cpp b/libraries/animation/src/SwingTwistConstraint.cpp index 96140f4deb..cd50588f02 100644 --- a/libraries/animation/src/SwingTwistConstraint.cpp +++ b/libraries/animation/src/SwingTwistConstraint.cpp @@ -32,14 +32,20 @@ void SwingTwistConstraint::SwingLimitFunction::setCone(float maxAngle) { } void SwingTwistConstraint::SwingLimitFunction::setMinDots(const std::vector& minDots) { - int numDots = minDots.size(); + uint32_t numDots = minDots.size(); _minDots.clear(); - _minDots.reserve(numDots); - for (int i = 0; i < numDots; ++i) { - _minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT)); + if (numDots == 0) { + // push two copies of MIN_MINDOT + _minDots.push_back(MIN_MINDOT); + _minDots.push_back(MIN_MINDOT); + } else { + _minDots.reserve(numDots); + for (uint32_t i = 0; i < numDots; ++i) { + _minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT)); + } + // push the first value to the back to establish cyclic boundary conditions + _minDots.push_back(_minDots[0]); } - // push the first value to the back to establish cyclic boundary conditions - _minDots.push_back(_minDots[0]); } float SwingTwistConstraint::SwingLimitFunction::getMinDot(float theta) const { @@ -58,8 +64,8 @@ float SwingTwistConstraint::SwingLimitFunction::getMinDot(float theta) const { } SwingTwistConstraint::SwingTwistConstraint() : + RotationConstraint(), _swingLimitFunction(), - _referenceRotation(), _minTwist(-PI), _maxTwist(PI) { @@ -69,6 +75,85 @@ void SwingTwistConstraint::setSwingLimits(std::vector minDots) { _swingLimitFunction.setMinDots(minDots); } +void SwingTwistConstraint::setSwingLimits(const std::vector& swungDirections) { + struct SwingLimitData { + SwingLimitData() : _theta(0.0f), _minDot(1.0f) {} + SwingLimitData(float theta, float minDot) : _theta(theta), _minDot(minDot) {} + float _theta; + float _minDot; + bool operator<(const SwingLimitData& other) const { return _theta < other._theta; } + }; + std::vector limits; + + uint32_t numLimits = swungDirections.size(); + limits.reserve(numLimits); + + // compute the limit pairs: + const glm::vec3 yAxis = glm::vec3(0.0f, 1.0f, 0.0f); + for (uint32_t i = 0; i < numLimits; ++i) { + float directionLength = glm::length(swungDirections[i]); + if (directionLength > EPSILON) { + glm::vec3 swingAxis = glm::cross(yAxis, swungDirections[i]); + float theta = atan2f(-swingAxis.z, swingAxis.x); + if (theta < 0.0f) { + theta += TWO_PI; + } + limits.push_back(SwingLimitData(theta, swungDirections[i].y / directionLength)); + } + } + + std::vector minDots; + numLimits = limits.size(); + if (numLimits == 0) { + // trivial case: nearly free constraint + std::vector minDots; + _swingLimitFunction.setMinDots(minDots); + } else if (numLimits == 1) { + // trivial case: uniform conical constraint + std::vector minDots; + minDots.push_back(limits[0]._minDot); + _swingLimitFunction.setMinDots(minDots); + } else { + // interesting case: potentially non-uniform constraints + + // sort limits by theta + std::sort(limits.begin(), limits.end()); + + // extrapolate evenly distributed limits for fast lookup table + float deltaTheta = TWO_PI / (float)(numLimits); + uint32_t rightIndex = 0; + for (uint32_t i = 0; i < numLimits; ++i) { + float theta = (float)i * deltaTheta; + uint32_t leftIndex = (rightIndex - 1) % numLimits; + while (rightIndex < numLimits && theta > limits[rightIndex]._theta) { + leftIndex = rightIndex++; + } + + if (leftIndex == numLimits - 1) { + // we straddle the boundary + rightIndex = 0; + } + + float rightTheta = limits[rightIndex]._theta; + float leftTheta = limits[leftIndex]._theta; + if (leftTheta > rightTheta) { + // we straddle the boundary, but we need to figure out which way to stride + // in order to keep theta between left and right + if (leftTheta > theta) { + leftTheta -= TWO_PI; + } else { + rightTheta += TWO_PI; + } + } + + // blend between the left and right minDots to get the value that corresponds to this theta + float rightWeight = (theta - leftTheta) / (rightTheta - leftTheta); + minDots.push_back((1.0f - rightWeight) * limits[leftIndex]._minDot + rightWeight * limits[rightIndex]._minDot); + } + } + _swingLimitFunction.setMinDots(minDots); +} + void SwingTwistConstraint::setTwistLimits(float minTwist, float maxTwist) { // NOTE: min/maxTwist angles should be in the range [-PI, PI] _minTwist = glm::min(minTwist, maxTwist); @@ -107,13 +192,10 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const { float theta = atan2f(-swingAxis.z, swingAxis.x); float minDot = _swingLimitFunction.getMinDot(theta); if (glm::dot(swungY, yAxis) < minDot) { - // The swing limits are violated so we use the maxAngle to supply a new rotation. - float maxAngle = acosf(minDot); - if (minDot < 0.0f) { - maxAngle = PI - maxAngle; - } + // The swing limits are violated so we extract the angle from midDot and + // use it to supply a new rotation. swingAxis /= axisLength; - swingRotation = glm::angleAxis(maxAngle, swingAxis); + swingRotation = glm::angleAxis(acosf(minDot), swingAxis); swingWasClamped = true; } } diff --git a/libraries/animation/src/SwingTwistConstraint.h b/libraries/animation/src/SwingTwistConstraint.h index c5e6d9e569..b516e984b4 100644 --- a/libraries/animation/src/SwingTwistConstraint.h +++ b/libraries/animation/src/SwingTwistConstraint.h @@ -16,7 +16,7 @@ #include -class SwingTwistConstraint : RotationConstraint { +class SwingTwistConstraint : public RotationConstraint { public: // The SwingTwistConstraint starts in the "referenceRotation" and then measures an initial twist // about the yAxis followed by a swing about some axis that lies in the XZ plane, such that the twist @@ -25,10 +25,7 @@ public: SwingTwistConstraint(); - /// \param referenceRotation the rotation from which rotation changes are measured. - virtual void setReferenceRotation(const glm::quat& referenceRotation) override { _referenceRotation = referenceRotation; } - - /// \param minDots vector of minimum dot products between the twist and swung axes. + /// \param minDots vector of minimum dot products between the twist and swung axes /// \brief The values are minimum dot-products between the twist axis and the swung axes /// that correspond to swing axes equally spaced around the XZ plane. Another way to /// think about it is that the dot-products correspond to correspond to angles (theta) @@ -38,6 +35,13 @@ public: /// description of how this works. void setSwingLimits(std::vector minDots); + /// \param swungDirections vector of directions that lie on the swing limit boundary + /// \brief For each swungDirection we compute the corresponding [theta, minDot] pair. + /// We expect the values of theta to NOT be uniformly spaced around the range [0, TWO_PI] + /// so we'll use the input set to extrapolate a lookup function of evenly spaced values. + void setSwingLimits(const std::vector& swungDirections); + + /// \param minTwist the minimum angle of rotation about the twist axis /// \param maxTwist the maximum angle of rotation about the twist axis void setTwistLimits(float minTwist, float maxTwist); @@ -71,7 +75,6 @@ public: protected: SwingLimitFunction _swingLimitFunction; - glm::quat _referenceRotation; float _minTwist; float _maxTwist; }; diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index a11ee30174..3bae7f5b9d 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -12,17 +12,17 @@ #ifndef hifi_ResourceCache_h #define hifi_ResourceCache_h -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp new file mode 100644 index 0000000000..3958db1242 --- /dev/null +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -0,0 +1,269 @@ +// +// AnimInverseKinematicsTests.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimInverseKinematicsTests.h" + +#include + +#include +#include +#include +#include +#include + +#include "../QTestExtensions.h" + +QTEST_MAIN(AnimInverseKinematicsTests) + +const glm::vec3 origin(0.0f); +const glm::vec3 xAxis(1.0f, 0.0f, 0.0f); +const glm::vec3 yAxis(0.0f, 1.0f, 0.0f); +const glm::vec3 zAxis(0.0f, 0.0f, 1.0f); +const glm::quat identity = glm::quat(); +const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); + + +void makeTestFBXJoints(std::vector& fbxJoints) { + FBXJoint joint; + joint.isFree = false; + joint.freeLineage.clear(); + joint.parentIndex = -1; + joint.distanceToParent = 1.0f; + joint.boneRadius = 1.0f; + + joint.translation = origin; // T + joint.preTransform = glm::mat4(); // Roff * Rp + joint.preRotation = identity; // Rpre + joint.rotation = identity; // R + joint.postRotation = identity; // Rpost + joint.postTransform = glm::mat4(); // Rp-1 * Soff * Sp * S * Sp-1 + + // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) + joint.transform = glm::mat4(); + joint.rotationMin = glm::vec3(-PI); + joint.rotationMax = glm::vec3(PI); + joint.inverseDefaultRotation = identity; + joint.inverseBindRotation = identity; + joint.bindTransform = glm::mat4(); + + joint.name = ""; + joint.isSkeletonJoint = false; + + // we make a list of joints that look like this: + // + // A------>B------>C------>D + + joint.name = "A"; + joint.parentIndex = -1; + joint.translation = origin; + fbxJoints.push_back(joint); + + joint.name = "B"; + joint.parentIndex = 0; + joint.translation = xAxis; + fbxJoints.push_back(joint); + + joint.name = "C"; + joint.parentIndex = 1; + joint.translation = xAxis; + fbxJoints.push_back(joint); + + joint.name = "D"; + joint.parentIndex = 2; + joint.translation = xAxis; + fbxJoints.push_back(joint); + + // compute each joint's transform + for (int i = 1; i < (int)fbxJoints.size(); ++i) { + FBXJoint& j = fbxJoints[i]; + int parentIndex = j.parentIndex; + // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) + j.transform = fbxJoints[parentIndex].transform * + glm::translate(j.translation) * + j.preTransform * + glm::mat4_cast(j.preRotation * j.rotation * j.postRotation) * + j.postTransform; + j.inverseBindRotation = identity; + j.bindTransform = j.transform; + } +} + +void AnimInverseKinematicsTests::testSingleChain() { + std::vector fbxJoints; + AnimPose offset; + makeTestFBXJoints(fbxJoints, offset); + + // create a skeleton and doll + AnimSkeleton* skeleton = new AnimSkeleton(fbxJoints); + AnimSkeleton::Pointer skeletonPtr(skeleton); + AnimInverseKinematics ikDoll("doll"); + ikDoll.setSkeleton(skeletonPtr); + + { // easy test IK of joint C + // load intial poses that look like this: + // + // A------>B------>C------>D + AnimPose pose; + pose.scale = glm::vec3(1.0f); + pose.rot = identity; + pose.trans = origin; + + std::vector poses; + poses.push_back(pose); + + pose.trans = xAxis; + for (int i = 1; i < (int)fbxJoints.size(); ++i) { + poses.push_back(pose); + } + ikDoll.loadPoses(poses); + + // we'll put a target t on D for <2, 1, 0> like this: + // + // t + // + // + // A------>B------>C------>D + // + int indexD = 3; + glm::vec3 targetPosition(2.0f, 1.0f, 0.0f); + glm::quat targetRotation = glm::angleAxis(PI / 2.0f, zAxis); + ikDoll.updateTarget(indexD, targetPosition, targetRotation); + + // the IK solution should be: + // + // D + // | + // | + // A------>B------>C + // + float dt = 1.0f; + ikDoll.evaluate(dt); + + // verify absolute results + // NOTE: since we expect this solution to converge very quickly (one loop) + // we can impose very tight error thresholds. + std::vector absolutePoses; + ikDoll.computeAbsolutePoses(absolutePoses); + float acceptableAngle = 0.0001f; + QCOMPARE_QUATS(absolutePoses[0].rot, identity, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[1].rot, identity, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[2].rot, quaterTurnAroundZ, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[3].rot, quaterTurnAroundZ, acceptableAngle); + + QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, EPSILON); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[1].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[2].trans, 2.0f * xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[3].trans, targetPosition, EPSILON); + + // verify relative results + const std::vector& relativePoses = ikDoll.getRelativePoses(); + QCOMPARE_QUATS(relativePoses[0].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[1].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[2].rot, quaterTurnAroundZ, acceptableAngle); + QCOMPARE_QUATS(relativePoses[3].rot, identity, acceptableAngle); + + QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, EPSILON); + } + + { // hard test IK of joint C + // load intial poses that look like this: + // + // D<------C + // | + // | + // A------>B + // + AnimPose pose; + pose.scale = glm::vec3(1.0f); + pose.rot = identity; + pose.trans = origin; + + std::vector poses; + poses.push_back(pose); + pose.trans = xAxis; + + pose.rot = quaterTurnAroundZ; + poses.push_back(pose); + poses.push_back(pose); + poses.push_back(pose); + ikDoll.loadPoses(poses); + + // we'll put a target t on D for <3, 0, 0> like this: + // + // D<------C + // | + // | + // A------>B t + // + int indexD = 3; + glm::vec3 targetPosition(3.0f, 0.0f, 0.0f); + glm::quat targetRotation = identity; + ikDoll.updateTarget(indexD, targetPosition, targetRotation); + + // the IK solution should be: + // + // A------>B------>C------>D + // + float dt = 1.0f; + ikDoll.evaluate(dt); + + // verify absolute results + // NOTE: the IK algorithm doesn't converge very fast for full-reach targets, + // so we'll consider the poses as good if they are closer than they started. + // + // NOTE: constraints may help speed up convergence since some joints may get clamped + // to maximum extension. TODO: experiment with tightening the error thresholds when + // constraints are working. + std::vector absolutePoses; + ikDoll.computeAbsolutePoses(absolutePoses); + float acceptableAngle = 0.1f; // radians + QCOMPARE_QUATS(absolutePoses[0].rot, identity, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[1].rot, identity, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[2].rot, identity, acceptableAngle); + QCOMPARE_QUATS(absolutePoses[3].rot, identity, acceptableAngle); + + float acceptableDistance = 0.4f; + QCOMPARE_WITH_ABS_ERROR(absolutePoses[0].trans, origin, EPSILON); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[1].trans, xAxis, acceptableDistance); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[2].trans, 2.0f * xAxis, acceptableDistance); + QCOMPARE_WITH_ABS_ERROR(absolutePoses[3].trans, 3.0f * xAxis, acceptableDistance); + + // verify relative results + const std::vector& relativePoses = ikDoll.getRelativePoses(); + QCOMPARE_QUATS(relativePoses[0].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[1].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[2].rot, identity, acceptableAngle); + QCOMPARE_QUATS(relativePoses[3].rot, identity, acceptableAngle); + + QCOMPARE_WITH_ABS_ERROR(relativePoses[0].trans, origin, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[1].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[2].trans, xAxis, EPSILON); + QCOMPARE_WITH_ABS_ERROR(relativePoses[3].trans, xAxis, EPSILON); + } +} + +void AnimInverseKinematicsTests::testBar() { + // test AnimPose math + // TODO: move this to other test file + glm::vec3 transA = glm::vec3(1.0f, 2.0f, 3.0f); + glm::vec3 transB = glm::vec3(5.0f, 7.0f, 9.0f); + glm::quat rot = identity; + glm::vec3 scale = glm::vec3(1.0f); + + AnimPose poseA(scale, rot, transA); + AnimPose poseB(scale, rot, transB); + AnimPose poseC = poseA * poseB; + + glm::vec3 expectedTransC = transA + transB; + QCOMPARE_WITH_ABS_ERROR(expectedTransC, poseC.trans, EPSILON); +} + diff --git a/tests/animation/src/AnimInverseKinematicsTests.h b/tests/animation/src/AnimInverseKinematicsTests.h new file mode 100644 index 0000000000..dd1b18e9ae --- /dev/null +++ b/tests/animation/src/AnimInverseKinematicsTests.h @@ -0,0 +1,27 @@ +// +// AnimInverseKinematicsTests.h +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimInverseKinematicsTests_h +#define hifi_AnimInverseKinematicsTests_h + +#include +#include + +inline float getErrorDifference(float a, float b) { + return fabs(a - b); +} + +class AnimInverseKinematicsTests : public QObject { + Q_OBJECT +private slots: + void testSingleChain(); + void testBar(); +}; + +#endif // hifi_AnimInverseKinematicsTests_h diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json index 24967979ea..3122fd433d 100644 --- a/tests/animation/src/data/avatar.json +++ b/tests/animation/src/data/avatar.json @@ -1,190 +1,240 @@ { "version": "1.0", "root": { - "id": "root", - "type": "stateMachine", + "id": "ikOverlay", + "type": "overlay", "data": { - "currentState": "idle", - "states": [ - { - "id": "idle", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "walkFwd", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "walkBwd", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "strafeRight", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "strafeLeft", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotMoving", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isTurningRight", "state": "turnRight" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "turnRight", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningLeft", "state": "turnLeft" } - ] - }, - { - "id": "turnLeft", - "interpTarget": 6, - "interpDuration": 6, - "transitions": [ - { "var": "isNotTurning", "state": "idle" }, - { "var": "isMovingForward", "state": "walkFwd" }, - { "var": "isMovingBackward", "state": "walkBwd" }, - { "var": "isMovingRight", "state": "strafeRight" }, - { "var": "isMovingLeft", "state": "strafeLeft" }, - { "var": "isTurningRight", "state": "turnRight" } - ] - } - ] + "alpha": 1.0, + "boneSet": "fullBody" }, "children": [ { - "id": "idle", - "type": "clip", + "id": "ik", + "type": "inverseKinematics", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", - "startFrame": 0.0, - "endFrame": 90.0, - "timeScale": 1.0, - "loopFlag": true + "targets": [ + { + "jointName": "RightHand", + "positionVar": "rightHandPosition", + "rotationVar": "rightHandRotation" + }, + { + "jointName": "LeftHand", + "positionVar": "leftHandPosition", + "rotationVar": "leftHandRotation" + } + ] }, "children": [] }, { - "id": "walkFwd", - "type": "clip", + "id": "controllerOverlay", + "type": "overlay", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", - "startFrame": 0.0, - "endFrame": 35.0, - "timeScale": 1.0, - "loopFlag": true, - "timeScaleVar": "walkTimeScale" + "alpha": 1.0, + "boneSet": "spineOnly" }, - "children": [] - }, - { - "id": "walkBwd", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", - "startFrame": 0.0, - "endFrame": 37.0, - "timeScale": 1.0, - "loopFlag": true, - "timeScaleVar": "walkTimeScale" - }, - "children": [] - }, - { - "id": "turnLeft", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", - "startFrame": 0.0, - "endFrame": 28.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "turnRight", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", - "startFrame": 0.0, - "endFrame": 30.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "strafeLeft", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", - "startFrame": 0.0, - "endFrame": 31.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] - }, - { - "id": "strafeRight", - "type": "clip", - "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", - "startFrame": 0.0, - "endFrame": 31.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + "children": [ + { + "id": "spineLean", + "type": "controller", + "data": { + "alpha": 1.0, + "joints": [ + { "var": "lean", "jointName": "Spine" } + ] + }, + "children": [] + }, + { + "id": "mainStateMachine", + "type": "stateMachine", + "data": { + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkFwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkBwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "walkBwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", + "startFrame": 0.0, + "endFrame": 37.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 28.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] } ] }