mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-17 11:27:05 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into punk
This commit is contained in:
commit
ae03d32a84
28 changed files with 1716 additions and 315 deletions
|
@ -9,71 +9,80 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var HIFI_OZAN_BUCKET = "http://hifi-public.s3.amazonaws.com/ozan/";
|
||||
|
||||
var rightHandAnimation = HIFI_PUBLIC_BUCKET + "animations/RightHandAnimPhilip.fbx";
|
||||
var leftHandAnimation = HIFI_PUBLIC_BUCKET + "animations/LeftHandAnimPhilip.fbx";
|
||||
var rightHandAnimation = HIFI_OZAN_BUCKET + "anim/squeeze_hands/right_hand_anim.fbx";
|
||||
var leftHandAnimation = HIFI_OZAN_BUCKET + "anim/squeeze_hands/left_hand_anim.fbx";
|
||||
|
||||
var LEFT = 0;
|
||||
var RIGHT = 1;
|
||||
var lastLeftTrigger = 0;
|
||||
var lastRightTrigger = 0;
|
||||
|
||||
var lastLeftFrame = 0;
|
||||
var lastRightFrame = 0;
|
||||
|
||||
var leftDirection = true;
|
||||
var rightDirection = true;
|
||||
var leftIsClosing = true;
|
||||
var rightIsClosing = true;
|
||||
|
||||
var LAST_FRAME = 15.0; // What is the number of the last frame we want to use in the animation?
|
||||
var SMOOTH_FACTOR = 0.0;
|
||||
var SMOOTH_FACTOR = 0.75;
|
||||
var MAX_FRAMES = 30.0;
|
||||
|
||||
var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK");
|
||||
var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK");
|
||||
|
||||
var CONTROLLER_DEAD_SPOT = 0.25;
|
||||
|
||||
function clamp(val, min, max) {
|
||||
if (val < min) {
|
||||
return min;
|
||||
} else if (val > max) {
|
||||
return max;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeControllerValue(val) {
|
||||
return clamp((val - CONTROLLER_DEAD_SPOT) / (1.0 - CONTROLLER_DEAD_SPOT), 0.0, 1.0);
|
||||
}
|
||||
|
||||
Script.update.connect(function(deltaTime) {
|
||||
var leftTriggerValue = Controller.getActionValue(LEFT_HAND_CLICK);
|
||||
var rightTriggerValue = Controller.getActionValue(RIGHT_HAND_CLICK);
|
||||
var leftTrigger = normalizeControllerValue(Controller.getActionValue(LEFT_HAND_CLICK));
|
||||
var rightTrigger = normalizeControllerValue(Controller.getActionValue(RIGHT_HAND_CLICK));
|
||||
|
||||
var leftFrame, rightFrame;
|
||||
// Average last few trigger values together for a bit of smoothing
|
||||
var smoothLeftTrigger = leftTrigger * (1.0 - SMOOTH_FACTOR) + lastLeftTrigger * SMOOTH_FACTOR;
|
||||
var smoothRightTrigger = rightTrigger * (1.0 - SMOOTH_FACTOR) + lastRightTrigger * SMOOTH_FACTOR;
|
||||
|
||||
// Average last few trigger frames together for a bit of smoothing
|
||||
leftFrame = (leftTriggerValue * LAST_FRAME) * (1.0 - SMOOTH_FACTOR) + lastLeftFrame * SMOOTH_FACTOR;
|
||||
rightFrame = (rightTriggerValue * LAST_FRAME) * (1.0 - SMOOTH_FACTOR) + lastRightFrame * SMOOTH_FACTOR;
|
||||
|
||||
if (!leftDirection) {
|
||||
leftFrame = MAX_FRAMES - leftFrame;
|
||||
}
|
||||
if (!rightDirection) {
|
||||
rightFrame = MAX_FRAMES - rightFrame;
|
||||
if (leftTrigger == 0.0) {
|
||||
leftIsClosing = true;
|
||||
} else if (leftTrigger == 1.0) {
|
||||
leftIsClosing = false;
|
||||
}
|
||||
|
||||
if ((leftTriggerValue == 1.0) && (leftDirection == true)) {
|
||||
leftDirection = false;
|
||||
lastLeftFrame = MAX_FRAMES - leftFrame;
|
||||
} else if ((leftTriggerValue == 0.0) && (leftDirection == false)) {
|
||||
leftDirection = true;
|
||||
lastLeftFrame = leftFrame;
|
||||
}
|
||||
if ((rightTriggerValue == 1.0) && (rightDirection == true)) {
|
||||
rightDirection = false;
|
||||
lastRightFrame = MAX_FRAMES - rightFrame;
|
||||
} else if ((rightTriggerValue == 0.0) && (rightDirection == false)) {
|
||||
rightDirection = true;
|
||||
lastRightFrame = rightFrame;
|
||||
if (rightTrigger == 0.0) {
|
||||
rightIsClosing = true;
|
||||
} else if (rightTrigger == 1.0) {
|
||||
rightIsClosing = false;
|
||||
}
|
||||
|
||||
if ((leftFrame != lastLeftFrame) && leftHandAnimation.length){
|
||||
MyAvatar.startAnimation(leftHandAnimation, 30.0, 1.0, false, true, leftFrame, leftFrame);
|
||||
}
|
||||
if ((rightFrame != lastRightFrame) && rightHandAnimation.length) {
|
||||
MyAvatar.startAnimation(rightHandAnimation, 30.0, 1.0, false, true, rightFrame, rightFrame);
|
||||
lastLeftTrigger = smoothLeftTrigger;
|
||||
lastRightTrigger = smoothRightTrigger;
|
||||
|
||||
// 0..15
|
||||
var leftFrame = smoothLeftTrigger * LAST_FRAME;
|
||||
var rightFrame = smoothRightTrigger * LAST_FRAME;
|
||||
|
||||
var adjustedLeftFrame = (leftIsClosing) ? leftFrame : (MAX_FRAMES - leftFrame);
|
||||
if (leftHandAnimation.length) {
|
||||
MyAvatar.startAnimation(leftHandAnimation, 30.0, 1.0, false, true, adjustedLeftFrame, adjustedLeftFrame);
|
||||
}
|
||||
|
||||
lastLeftFrame = leftFrame;
|
||||
lastRightFrame = rightFrame;
|
||||
var adjustedRightFrame = (rightIsClosing) ? rightFrame : (MAX_FRAMES - rightFrame);
|
||||
if (rightHandAnimation.length) {
|
||||
MyAvatar.startAnimation(rightHandAnimation, 30.0, 1.0, false, true, adjustedRightFrame, adjustedRightFrame);
|
||||
}
|
||||
});
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
MyAvatar.stopAnimation(leftHandAnimation);
|
||||
MyAvatar.stopAnimation(rightHandAnimation);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -260,7 +260,6 @@ var toolBar = (function () {
|
|||
cameraManager.disable();
|
||||
} else {
|
||||
hasShownPropertiesTool = false;
|
||||
cameraManager.enable();
|
||||
entityListTool.setVisible(true);
|
||||
gridTool.setVisible(true);
|
||||
grid.setEnabled(true);
|
||||
|
@ -670,15 +669,11 @@ function mouseMove(event) {
|
|||
|
||||
lastMousePosition = { x: event.x, y: event.y };
|
||||
|
||||
highlightEntityUnderCursor(lastMousePosition, false);
|
||||
idleMouseTimerId = Script.setTimeout(handleIdleMouse, IDLE_MOUSE_TIMEOUT);
|
||||
}
|
||||
|
||||
function handleIdleMouse() {
|
||||
idleMouseTimerId = null;
|
||||
if (isActive) {
|
||||
highlightEntityUnderCursor(lastMousePosition, true);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightEntityUnderCursor(position, accurateRay) {
|
||||
|
@ -802,6 +797,7 @@ function mouseClickEvent(event) {
|
|||
selectionDisplay.select(selectedEntityID, event);
|
||||
|
||||
if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) {
|
||||
cameraManager.enable();
|
||||
cameraManager.focus(selectionManager.worldPosition,
|
||||
selectionManager.worldDimensions,
|
||||
Menu.isOptionChecked(MENU_EASE_ON_FOCUS));
|
||||
|
@ -1142,6 +1138,7 @@ Controller.keyReleaseEvent.connect(function (event) {
|
|||
} else if (event.text == "f") {
|
||||
if (isActive) {
|
||||
if (selectionManager.hasSelection()) {
|
||||
cameraManager.enable();
|
||||
cameraManager.focus(selectionManager.worldPosition,
|
||||
selectionManager.worldDimensions,
|
||||
Menu.isOptionChecked(MENU_EASE_ON_FOCUS));
|
||||
|
|
|
@ -49,7 +49,7 @@ EntityListTool = function(opts) {
|
|||
|
||||
var selectedIDs = [];
|
||||
for (var i = 0; i < selectionManager.selections.length; i++) {
|
||||
selectedIDs.push(selectionManager.selections[i].id); // ?
|
||||
selectedIDs.push(selectionManager.selections[i].id);
|
||||
}
|
||||
|
||||
var data = {
|
||||
|
@ -70,6 +70,7 @@ EntityListTool = function(opts) {
|
|||
}
|
||||
selectionManager.setSelections(entityIDs);
|
||||
if (data.focus) {
|
||||
cameraManager.enable();
|
||||
cameraManager.focus(selectionManager.worldPosition,
|
||||
selectionManager.worldDimensions,
|
||||
Menu.isOptionChecked(MENU_EASE_ON_FOCUS));
|
||||
|
|
|
@ -786,12 +786,12 @@ void Application::aboutToQuit() {
|
|||
}
|
||||
|
||||
void Application::cleanupBeforeQuit() {
|
||||
// Terminate third party processes so that they're not left running in the event of a subsequent shutdown crash
|
||||
// Stop third party processes so that they're not left running in the event of a subsequent shutdown crash.
|
||||
#ifdef HAVE_DDE
|
||||
DependencyManager::destroy<DdeFaceTracker>();
|
||||
DependencyManager::get<DdeFaceTracker>()->setEnabled(false);
|
||||
#endif
|
||||
#ifdef HAVE_IVIEWHMD
|
||||
DependencyManager::destroy<EyeTracker>();
|
||||
DependencyManager::get<EyeTracker>()->setEnabled(false, true);
|
||||
#endif
|
||||
|
||||
if (_keyboardFocusHighlightID > 0) {
|
||||
|
@ -842,6 +842,14 @@ void Application::cleanupBeforeQuit() {
|
|||
|
||||
// destroy the AudioClient so it and its thread have a chance to go down safely
|
||||
DependencyManager::destroy<AudioClient>();
|
||||
|
||||
// Destroy third party processes after scripts have finished using them.
|
||||
#ifdef HAVE_DDE
|
||||
DependencyManager::destroy<DdeFaceTracker>();
|
||||
#endif
|
||||
#ifdef HAVE_IVIEWHMD
|
||||
DependencyManager::destroy<EyeTracker>();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Application::emptyLocalCache() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
106
libraries/animation/src/AnimController.cpp
Normal file
106
libraries/animation/src/AnimController.cpp
Normal file
|
@ -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);
|
||||
}
|
58
libraries/animation/src/AnimController.h
Normal file
58
libraries/animation/src/AnimController.h
Normal file
|
@ -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<JointVar> _jointVars;
|
||||
|
||||
// no copies
|
||||
AnimController(const AnimController&) = delete;
|
||||
AnimController& operator=(const AnimController&) = delete;
|
||||
|
||||
};
|
||||
|
||||
#endif // hifi_AnimController_h
|
584
libraries/animation/src/AnimInverseKinematics.cpp
Normal file
584
libraries/animation/src/AnimInverseKinematics.cpp
Normal file
|
@ -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 <NumericalConstants.h>
|
||||
#include <SharedUtil.h>
|
||||
|
||||
#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<int, RotationConstraint*>::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<int, RotationConstraint*>::iterator constraintItr = _constraints.find(index);
|
||||
if (constraintItr != _constraints.end()) {
|
||||
constraint = constraintItr->second;
|
||||
}
|
||||
return constraint;
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::clearConstraints() {
|
||||
std::map<int, RotationConstraint*>::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<glm::vec3> 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<RotationConstraint*>(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<glm::vec3> 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<RotationConstraint*>(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<glm::vec3> 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<float> minDots;
|
||||
const float MAX_HAND_SWING = PI / 3.0f;
|
||||
minDots.push_back(cosf(MAX_HAND_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
*/
|
||||
constraint = static_cast<RotationConstraint*>(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<float> minDots;
|
||||
const float MAX_SPINE_SWING = PI / 14.0f;
|
||||
minDots.push_back(cosf(MAX_SPINE_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
|
||||
constraint = static_cast<RotationConstraint*>(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<float> minDots;
|
||||
const float MAX_NECK_SWING = PI / 3.0f;
|
||||
minDots.push_back(cosf(MAX_NECK_SWING));
|
||||
stConstraint->setSwingLimits(minDots);
|
||||
|
||||
constraint = static_cast<RotationConstraint*>(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<RotationConstraint*>(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<RotationConstraint*>(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<glm::vec3> 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<RotationConstraint*>(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));
|
||||
}
|
||||
}
|
||||
|
82
libraries/animation/src/AnimInverseKinematics.h
Normal file
82
libraries/animation/src/AnimInverseKinematics.h
Normal file
|
@ -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 <string>
|
||||
|
||||
#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<int, RotationConstraint*> _constraints;
|
||||
std::vector<IKTargetVar> _targetVarVec;
|
||||
std::map<int, IKTarget> _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
|
|
@ -40,6 +40,8 @@ public:
|
|||
BlendLinear,
|
||||
Overlay,
|
||||
StateMachine,
|
||||
Controller,
|
||||
InverseKinematics,
|
||||
NumTypes
|
||||
};
|
||||
using Pointer = std::shared_ptr<AnimNode>;
|
||||
|
|
|
@ -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,18 +75,20 @@ 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;
|
||||
}
|
||||
|
||||
#define READ_STRING(NAME, JSON_OBJ, ID, URL) \
|
||||
#define READ_STRING(NAME, JSON_OBJ, ID, URL, ERROR_RETURN) \
|
||||
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
|
||||
if (!NAME##_VAL.isString()) { \
|
||||
qCCritical(animation) << "AnimNodeLoader, error reading string" \
|
||||
<< #NAME << ", id =" << ID \
|
||||
<< ", url =" << URL.toDisplayString(); \
|
||||
return nullptr; \
|
||||
return ERROR_RETURN; \
|
||||
} \
|
||||
QString NAME = NAME##_VAL.toString()
|
||||
|
||||
|
@ -86,23 +99,23 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
|
|||
NAME = NAME##_VAL.toString(); \
|
||||
}
|
||||
|
||||
#define READ_BOOL(NAME, JSON_OBJ, ID, URL) \
|
||||
#define READ_BOOL(NAME, JSON_OBJ, ID, URL, ERROR_RETURN) \
|
||||
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
|
||||
if (!NAME##_VAL.isBool()) { \
|
||||
qCCritical(animation) << "AnimNodeLoader, error reading bool" \
|
||||
<< #NAME << ", id =" << ID \
|
||||
<< ", url =" << URL.toDisplayString(); \
|
||||
return nullptr; \
|
||||
return ERROR_RETURN; \
|
||||
} \
|
||||
bool NAME = NAME##_VAL.toBool()
|
||||
|
||||
#define READ_FLOAT(NAME, JSON_OBJ, ID, URL) \
|
||||
#define READ_FLOAT(NAME, JSON_OBJ, ID, URL, ERROR_RETURN) \
|
||||
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
|
||||
if (!NAME##_VAL.isDouble()) { \
|
||||
qCCritical(animation) << "AnimNodeLoader, error reading double" \
|
||||
<< #NAME << "id =" << ID \
|
||||
<< ", url =" << URL.toDisplayString(); \
|
||||
return nullptr; \
|
||||
return ERROR_RETURN; \
|
||||
} \
|
||||
float NAME = (float)NAME##_VAL.toDouble()
|
||||
|
||||
|
@ -171,11 +184,11 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUr
|
|||
|
||||
static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
|
||||
|
||||
READ_STRING(url, jsonObj, id, jsonUrl);
|
||||
READ_FLOAT(startFrame, jsonObj, id, jsonUrl);
|
||||
READ_FLOAT(endFrame, jsonObj, id, jsonUrl);
|
||||
READ_FLOAT(timeScale, jsonObj, id, jsonUrl);
|
||||
READ_BOOL(loopFlag, jsonObj, id, jsonUrl);
|
||||
READ_STRING(url, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(startFrame, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(endFrame, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(timeScale, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_BOOL(loopFlag, jsonObj, id, jsonUrl, nullptr);
|
||||
|
||||
READ_OPTIONAL_STRING(startFrameVar, jsonObj);
|
||||
READ_OPTIONAL_STRING(endFrameVar, jsonObj);
|
||||
|
@ -202,7 +215,7 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString&
|
|||
|
||||
static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
|
||||
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl);
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr);
|
||||
|
||||
READ_OPTIONAL_STRING(alphaVar, jsonObj);
|
||||
|
||||
|
@ -239,8 +252,8 @@ static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {
|
|||
|
||||
static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
|
||||
|
||||
READ_STRING(boneSet, jsonObj, id, jsonUrl);
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl);
|
||||
READ_STRING(boneSet, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr);
|
||||
|
||||
auto boneSetEnum = stringToBoneSetEnum(boneSet);
|
||||
if (boneSetEnum == AnimOverlay::NumBoneSets) {
|
||||
|
@ -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<AnimController>(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<AnimInverseKinematics>(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<std::string, AnimNode::Pointer>& map, AnimNode::Pointer node) {
|
||||
for ( auto child : node->_children ) {
|
||||
map.insert(std::pair<std::string, AnimNode::Pointer>(child->_id, child));
|
||||
|
@ -278,12 +352,12 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
auto smNode = std::static_pointer_cast<AnimStateMachine>(node);
|
||||
assert(smNode);
|
||||
|
||||
READ_STRING(currentState, jsonObj, nodeId, jsonUrl);
|
||||
READ_STRING(currentState, jsonObj, nodeId, jsonUrl, false);
|
||||
|
||||
auto statesValue = jsonObj.value("states");
|
||||
if (!statesValue.isArray()) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in stateMachine node, id =" << nodeId << ", url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// build a map for all children by name.
|
||||
|
@ -302,13 +376,13 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
for (const auto& stateValue : statesArray) {
|
||||
if (!stateValue.isObject()) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad state object in \"states\", id =" << nodeId << ", url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
auto stateObj = stateValue.toObject();
|
||||
|
||||
READ_STRING(id, stateObj, nodeId, jsonUrl);
|
||||
READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl);
|
||||
READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl);
|
||||
READ_STRING(id, stateObj, nodeId, jsonUrl, false);
|
||||
READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl, false);
|
||||
READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl, false);
|
||||
|
||||
READ_OPTIONAL_STRING(interpTargetVar, stateObj);
|
||||
READ_OPTIONAL_STRING(interpDurationVar, stateObj);
|
||||
|
@ -318,7 +392,7 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
auto iter = childMap.find(stdId);
|
||||
if (iter == childMap.end()) {
|
||||
qCCritical(animation) << "AnimNodeLoader, could not find stateMachine child (state) with nodeId =" << nodeId << "stateId =" << id << "url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto statePtr = std::make_shared<AnimStateMachine::State>(stdId, iter->second, interpTarget, interpDuration);
|
||||
|
@ -337,19 +411,19 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
auto transitionsValue = stateObj.value("transitions");
|
||||
if (!transitionsValue.isArray()) {
|
||||
qCritical(animation) << "AnimNodeLoader, bad array \"transitions\" in stateMachine node, stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto transitionsArray = transitionsValue.toArray();
|
||||
for (const auto& transitionValue : transitionsArray) {
|
||||
if (!transitionValue.isObject()) {
|
||||
qCritical(animation) << "AnimNodeLoader, bad transition object in \"transtions\", stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
auto transitionObj = transitionValue.toObject();
|
||||
|
||||
READ_STRING(var, transitionObj, nodeId, jsonUrl);
|
||||
READ_STRING(state, transitionObj, nodeId, jsonUrl);
|
||||
READ_STRING(var, transitionObj, nodeId, jsonUrl, false);
|
||||
READ_STRING(state, transitionObj, nodeId, jsonUrl, false);
|
||||
|
||||
transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var.toStdString(), state.toStdString())));
|
||||
}
|
||||
|
@ -363,7 +437,7 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj,
|
|||
srcState->addTransition(AnimStateMachine::State::Transition(transition.second.first, iter->second));
|
||||
} else {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad state machine transtion from srcState =" << srcState->_id.c_str() << "dstState =" << transition.second.second.c_str() << "nodeId =" << nodeId << "url = " << jsonUrl.toDisplayString();
|
||||
return nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
#include "AnimNode.h"
|
||||
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
//
|
||||
|
||||
#include "AnimSkeleton.h"
|
||||
#include "AnimationLogging.h"
|
||||
#include "GLMHelpers.h"
|
||||
|
||||
#include <glm/gtx/transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <GLMHelpers.h>
|
||||
|
||||
#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<FBXJoint>& joints, const AnimPose& geometryOffset) {
|
||||
_joints = joints;
|
||||
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
#define hifi_AnimSkeleton
|
||||
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include "FBXReader.h"
|
||||
#include <FBXReader.h>
|
||||
|
||||
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;
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
#ifndef hifi_AnimationCache_h
|
||||
#define hifi_AnimationCache_h
|
||||
|
||||
#include <QRunnable>
|
||||
#include <QScriptEngine>
|
||||
#include <QScriptValue>
|
||||
#include <QtCore/QRunnable>
|
||||
#include <QtScript/QScriptEngine>
|
||||
#include <QtScript/QScriptValue>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <FBXReader.h>
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
#include <NumericalConstants.h>
|
||||
|
||||
ElbowConstraint::ElbowConstraint() :
|
||||
_referenceRotation(),
|
||||
_minAngle(-PI),
|
||||
_maxAngle(PI)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,14 +32,20 @@ void SwingTwistConstraint::SwingLimitFunction::setCone(float maxAngle) {
|
|||
}
|
||||
|
||||
void SwingTwistConstraint::SwingLimitFunction::setMinDots(const std::vector<float>& 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<float> minDots) {
|
|||
_swingLimitFunction.setMinDots(minDots);
|
||||
}
|
||||
|
||||
void SwingTwistConstraint::setSwingLimits(const std::vector<glm::vec3>& 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<SwingLimitData> limits;
|
||||
|
||||
uint32_t numLimits = swungDirections.size();
|
||||
limits.reserve(numLimits);
|
||||
|
||||
// compute the limit pairs: <theta, minDot>
|
||||
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<float> minDots;
|
||||
numLimits = limits.size();
|
||||
if (numLimits == 0) {
|
||||
// trivial case: nearly free constraint
|
||||
std::vector<float> minDots;
|
||||
_swingLimitFunction.setMinDots(minDots);
|
||||
} else if (numLimits == 1) {
|
||||
// trivial case: uniform conical constraint
|
||||
std::vector<float> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
#include <math.h>
|
||||
|
||||
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<float> 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<glm::vec3>& 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;
|
||||
};
|
||||
|
|
|
@ -12,17 +12,17 @@
|
|||
#ifndef hifi_ResourceCache_h
|
||||
#define hifi_ResourceCache_h
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
#include <QUrl>
|
||||
#include <QWeakPointer>
|
||||
#include <QReadWriteLock>
|
||||
#include <QQueue>
|
||||
#include <QtCore/QHash>
|
||||
#include <QtCore/QList>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QPointer>
|
||||
#include <QtCore/QSharedPointer>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QWeakPointer>
|
||||
#include <QtCore/QReadWriteLock>
|
||||
#include <QtCore/QQueue>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
|
||||
|
|
|
@ -216,7 +216,10 @@ void OctreeHeadlessViewer::queryOctree() {
|
|||
|
||||
// setup the query packet
|
||||
auto queryPacket = NLPacket::create(packetType);
|
||||
_octreeQuery.getBroadcastData(reinterpret_cast<unsigned char*>(queryPacket->getPayload()));
|
||||
|
||||
// read the data to our packet and set the payload size to fit the query
|
||||
int querySize = _octreeQuery.getBroadcastData(reinterpret_cast<unsigned char*>(queryPacket->getPayload()));
|
||||
queryPacket->setPayloadSize(querySize);
|
||||
|
||||
// ask the NodeList to send it
|
||||
nodeList->sendPacket(std::move(queryPacket), *node);
|
||||
|
|
|
@ -162,7 +162,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& size) {
|
|||
if (replaceIndex != std::string::npos) {
|
||||
fragmentShaderSource.replace(replaceIndex, PROCEDURAL_BLOCK.size(), _shaderSource.toLocal8Bit().data());
|
||||
}
|
||||
qDebug() << "FragmentShader:\n" << fragmentShaderSource.c_str();
|
||||
//qDebug() << "FragmentShader:\n" << fragmentShaderSource.c_str();
|
||||
_fragmentShader = gpu::ShaderPointer(gpu::Shader::createPixel(fragmentShaderSource));
|
||||
_shader = gpu::ShaderPointer(gpu::Shader::createProgram(_vertexShader, _fragmentShader));
|
||||
gpu::Shader::makeProgram(*_shader);
|
||||
|
|
269
tests/animation/src/AnimInverseKinematicsTests.cpp
Normal file
269
tests/animation/src/AnimInverseKinematicsTests.cpp
Normal file
|
@ -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 <glm/gtx/transform.hpp>
|
||||
|
||||
#include <AnimNodeLoader.h>
|
||||
#include <AnimInverseKinematics.h>
|
||||
#include <AnimBlendLinear.h>
|
||||
#include <AnimationLogging.h>
|
||||
#include <NumericalConstants.h>
|
||||
|
||||
#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<FBXJoint>& 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<FBXJoint> 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<AnimPose> 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<AnimPose> 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<AnimPose>& 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<AnimPose> 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<AnimPose> 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<AnimPose>& 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);
|
||||
}
|
||||
|
27
tests/animation/src/AnimInverseKinematicsTests.h
Normal file
27
tests/animation/src/AnimInverseKinematicsTests.h
Normal file
|
@ -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 <QtTest/QtTest>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
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
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -84,8 +84,8 @@ void testByteCountCodedStable(const T& value) {
|
|||
Q_ASSERT(decoder.data == coder.data);
|
||||
#ifndef QT_NO_DEBUG
|
||||
auto consumed = decoder.decode(encoded.data(), encoded.size());
|
||||
#endif
|
||||
Q_ASSERT(consumed == (unsigned int)originalEncodedSize);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
@ -124,9 +124,9 @@ void testPropertyFlags(uint32_t value) {
|
|||
{
|
||||
#ifndef QT_NO_DEBUG
|
||||
int decodeSize = decodeNew.decode((const uint8_t*)encoded.data(), encoded.size());
|
||||
#endif
|
||||
Q_ASSERT(originalSize == decodeSize);
|
||||
Q_ASSERT(decodeNew == original);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue