Merge pull request #5700 from hyperlogic/ajt/new-anim-system

New Animation System
This commit is contained in:
Howard Stearns 2015-09-04 11:24:31 -07:00
commit d623ba3ef2
41 changed files with 3390 additions and 101 deletions

View file

@ -150,10 +150,10 @@
#include "ui/Stats.h"
#include "ui/AddressBarDialog.h"
#include "ui/UpdateDialog.h"
#include "ui/overlays/Cube3DOverlay.h"
#include "PluginContainerProxy.h"
#include "AnimDebugDraw.h"
// ON WIndows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
// FIXME seems to be broken.
@ -2889,6 +2889,11 @@ void Application::update(float deltaTime) {
loadViewFrustum(_myCamera, _viewFrustum);
}
// Update animation debug draw renderer
{
AnimDebugDraw::getInstance().update();
}
quint64 now = usecTimestampNow();
// Update my voxel servers with my current voxel query...

View file

@ -434,6 +434,14 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, false,
avatar, SLOT(setEnableRigAnimations(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableAnimGraph, 0, false,
avatar, SLOT(setEnableAnimGraph(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBindPose, 0, false,
avatar, SLOT(setEnableDebugDrawBindPose(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false,
avatar, SLOT(setEnableDebugDrawAnimPose(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true,
avatar, SLOT(setEnableMeshVisible(bool)));
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false);
addCheckableActionToQMenuAndActionHash(avatarDebugMenu,
MenuOption::Connexion,

View file

@ -132,6 +132,8 @@ namespace MenuOption {
const QString AddRemoveFriends = "Add/Remove Friends...";
const QString AddressBar = "Show Address Bar";
const QString Animations = "Animations...";
const QString AnimDebugDrawAnimPose = "Debug Draw Animation";
const QString AnimDebugDrawBindPose = "Debug Draw Bind Pose";
const QString Antialiasing = "Antialiasing";
const QString Atmosphere = "Atmosphere";
const QString Attachments = "Attachments...";
@ -186,6 +188,7 @@ namespace MenuOption {
const QString EchoServerAudio = "Echo Server Audio";
const QString EditEntitiesHelp = "Edit Entities Help...";
const QString Enable3DTVMode = "Enable 3DTV Mode";
const QString EnableAnimGraph = "Enable Anim Graph";
const QString EnableCharacterController = "Enable avatar collisions";
const QString EnableRigAnimations = "Enable Rig Animations";
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";
@ -215,6 +218,7 @@ namespace MenuOption {
const QString Log = "Log";
const QString LogExtraTimings = "Log Extra Timing Details";
const QString LowVelocityFilter = "Low Velocity Filter";
const QString MeshVisible = "Draw Mesh";
const QString Mirror = "Mirror";
const QString MuteAudio = "Mute Microphone";
const QString MuteEnvironment = "Mute Environment";

View file

@ -33,6 +33,7 @@
#include <SharedUtil.h>
#include <TextRenderer3D.h>
#include <UserActivityLogger.h>
#include <AnimDebugDraw.h>
#include "devices/Faceshift.h"
@ -711,6 +712,38 @@ void MyAvatar::setEnableRigAnimations(bool isEnabled) {
}
}
void MyAvatar::setEnableAnimGraph(bool isEnabled) {
_rig->setEnableAnimGraph(isEnabled);
if (isEnabled) {
if (_skeletonModel.readyToAddToScene()) {
initAnimGraph();
}
} else {
destroyAnimGraph();
}
}
void MyAvatar::setEnableDebugDrawBindPose(bool isEnabled) {
_enableDebugDrawBindPose = isEnabled;
if (!isEnabled) {
AnimDebugDraw::getInstance().removeSkeleton("myAvatar");
}
}
void MyAvatar::setEnableDebugDrawAnimPose(bool isEnabled) {
_enableDebugDrawAnimPose = isEnabled;
if (!isEnabled) {
AnimDebugDraw::getInstance().removeAnimNode("myAvatar");
}
}
void MyAvatar::setEnableMeshVisible(bool isEnabled) {
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
_skeletonModel.setVisibleInScene(isEnabled, scene);
}
void MyAvatar::loadData() {
Settings settings;
settings.beginGroup("Avatar");
@ -1205,6 +1238,20 @@ void MyAvatar::initHeadBones() {
}
}
void MyAvatar::initAnimGraph() {
// 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");
_skeletonModel.initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry());
}
void MyAvatar::destroyAnimGraph() {
_rig->destroyAnimGraph();
AnimDebugDraw::getInstance().removeSkeleton("myAvatar");
AnimDebugDraw::getInstance().removeAnimNode("myAvatar");
}
void MyAvatar::preRender(RenderArgs* renderArgs) {
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
@ -1213,6 +1260,28 @@ void MyAvatar::preRender(RenderArgs* renderArgs) {
if (_skeletonModel.initWhenReady(scene)) {
initHeadBones();
_skeletonModel.setCauterizeBoneSet(_headBoneSet);
initAnimGraph();
}
if (_enableDebugDrawBindPose || _enableDebugDrawAnimPose) {
AnimSkeleton::ConstPointer animSkeleton = _rig->getAnimSkeleton();
AnimNode::ConstPointer animNode = _rig->getAnimNode();
// bones space is rotated
glm::quat rotY180 = glm::angleAxis((float)M_PI, glm::vec3(0.0f, 1.0f, 0.0f));
AnimPose xform(glm::vec3(1), rotY180 * getOrientation(), getPosition());
if (animSkeleton && _enableDebugDrawBindPose) {
glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f);
AnimDebugDraw::getInstance().addSkeleton("myAvatar", animSkeleton, xform, gray);
}
// This only works for when new anim system is enabled, at the moment.
if (animNode && animSkeleton && _enableDebugDrawAnimPose && _rig->getEnableAnimGraph()) {
glm::vec4 cyan(0.1f, 0.6f, 0.6f, 1.0f);
AnimDebugDraw::getInstance().addAnimNode("myAvatar", animNode, xform, cyan);
}
}
if (shouldDrawHead != _prevShouldDrawHead) {

View file

@ -19,6 +19,7 @@
#include "Avatar.h"
class ModelItemID;
class AnimNode;
enum eyeContactTarget {
LEFT_EYE,
@ -189,7 +190,12 @@ public slots:
void loadLastRecording();
virtual void rebuildSkeletonBody();
void setEnableRigAnimations(bool isEnabled);
void setEnableAnimGraph(bool isEnabled);
void setEnableDebugDrawBindPose(bool isEnabled);
void setEnableDebugDrawAnimPose(bool isEnabled);
void setEnableMeshVisible(bool isEnabled);
signals:
void transformChanged();
@ -283,6 +289,8 @@ private:
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
void maybeUpdateBillboard();
void initHeadBones();
void initAnimGraph();
void destroyAnimGraph();
// Avatar Preferences
QUrl _fullAvatarURLFromPreferences;
@ -310,6 +318,9 @@ private:
std::unordered_set<int> _headBoneSet;
RigPointer _rig;
bool _prevShouldDrawHead;
bool _enableDebugDrawBindPose = false;
bool _enableDebugDrawAnimPose = false;
};
#endif // hifi_MyAvatar_h

View file

@ -606,3 +606,7 @@ bool SkeletonModel::hasSkeleton() {
void SkeletonModel::onInvalidate() {
}
void SkeletonModel::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) {
_rig->initAnimGraph(url, fbxGeometry);
}

View file

@ -105,6 +105,8 @@ public:
virtual void onInvalidate() override;
void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry);
signals:
void skeletonLoaded();

View file

@ -0,0 +1,62 @@
//
// AnimBlendLinear.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "AnimBlendLinear.h"
#include "GLMHelpers.h"
#include "AnimationLogging.h"
#include "AnimUtil.h"
AnimBlendLinear::AnimBlendLinear(const std::string& id, float alpha) :
AnimNode(AnimNode::Type::BlendLinear, id),
_alpha(alpha) {
}
AnimBlendLinear::~AnimBlendLinear() {
}
const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) {
_alpha = animVars.lookup(_alphaVar, _alpha);
if (_children.size() == 0) {
for (auto&& pose : _poses) {
pose = AnimPose::identity;
}
} else if (_children.size() == 1) {
_poses = _children[0]->evaluate(animVars, dt, triggersOut);
} else {
float clampedAlpha = glm::clamp(_alpha, 0.0f, (float)(_children.size() - 1));
size_t prevPoseIndex = glm::floor(clampedAlpha);
size_t nextPoseIndex = glm::ceil(clampedAlpha);
float alpha = glm::fract(clampedAlpha);
if (prevPoseIndex == nextPoseIndex) {
// this can happen if alpha is on an integer boundary
_poses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut);
} else {
// need to eval and blend between two children.
auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut);
auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, dt, triggersOut);
if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) {
_poses.resize(prevPoses.size());
::blend(_poses.size(), &prevPoses[0], &nextPoses[0], alpha, &_poses[0]);
}
}
}
return _poses;
}
// for AnimDebugDraw rendering
const AnimPoseVec& AnimBlendLinear::getPosesInternal() const {
return _poses;
}

View file

@ -0,0 +1,52 @@
//
// AnimBlendLinear.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimBlendLinear_h
#define hifi_AnimBlendLinear_h
#include "AnimNode.h"
// Linear blend between two AnimNodes.
// the amount of blending is determined by the alpha parameter.
// If the number of children is 2, then the alpha parameters should be between
// 0 and 1. The first animation will have a (1 - alpha) factor, and the second
// will have factor of alpha.
// This node supports more then 2 children. In this case the alpha should be
// between 0 and n - 1. This alpha can be used to linearly interpolate between
// the closest two children poses. This can be used to sweep through a series
// of animation poses.
class AnimBlendLinear : public AnimNode {
public:
friend class AnimTests;
AnimBlendLinear(const std::string& id, float alpha);
virtual ~AnimBlendLinear() override;
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; }
protected:
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const override;
AnimPoseVec _poses;
float _alpha;
std::string _alphaVar;
// no copies
AnimBlendLinear(const AnimBlendLinear&) = delete;
AnimBlendLinear& operator=(const AnimBlendLinear&) = delete;
};
#endif // hifi_AnimBlendLinear_h

View file

@ -0,0 +1,161 @@
//
// AnimClip.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "GLMHelpers.h"
#include "AnimClip.h"
#include "AnimationLogging.h"
#include "AnimUtil.h"
AnimClip::AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) :
AnimNode(AnimNode::Type::Clip, id),
_startFrame(startFrame),
_endFrame(endFrame),
_timeScale(timeScale),
_loopFlag(loopFlag),
_frame(startFrame)
{
loadURL(url);
}
AnimClip::~AnimClip() {
}
const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) {
// lookup parameters from animVars, using current instance variables as defaults.
_startFrame = animVars.lookup(_startFrameVar, _startFrame);
_endFrame = animVars.lookup(_endFrameVar, _endFrame);
_timeScale = animVars.lookup(_timeScaleVar, _timeScale);
_loopFlag = animVars.lookup(_loopFlagVar, _loopFlag);
_frame = accumulateTime(animVars.lookup(_frameVar, _frame), dt, triggersOut);
// poll network anim to see if it's finished loading yet.
if (_networkAnim && _networkAnim->isLoaded() && _skeleton) {
// loading is complete, copy animation frames from network animation, then throw it away.
copyFromNetworkAnim();
_networkAnim.reset();
}
if (_anim.size()) {
int frameCount = _anim.size();
int prevIndex = (int)glm::floor(_frame);
int nextIndex = (int)glm::ceil(_frame);
if (_loopFlag && nextIndex >= frameCount) {
nextIndex = 0;
}
// It can be quite possible for the user to set _startFrame and _endFrame to
// values before or past valid ranges. We clamp the frames here.
prevIndex = std::min(std::max(0, prevIndex), frameCount - 1);
nextIndex = std::min(std::max(0, nextIndex), frameCount - 1);
const AnimPoseVec& prevFrame = _anim[prevIndex];
const AnimPoseVec& nextFrame = _anim[nextIndex];
float alpha = glm::fract(_frame);
::blend(_poses.size(), &prevFrame[0], &nextFrame[0], alpha, &_poses[0]);
}
return _poses;
}
void AnimClip::loadURL(const std::string& url) {
auto animCache = DependencyManager::get<AnimationCache>();
_networkAnim = animCache->getAnimation(QString::fromStdString(url));
_url = url;
}
void AnimClip::setCurrentFrameInternal(float frame) {
// because dt is 0, we should not encounter any triggers
const float dt = 0.0f;
Triggers triggers;
_frame = accumulateTime(frame * _timeScale, dt, triggers);
}
float AnimClip::accumulateTime(float frame, float dt, Triggers& triggersOut) const {
const float startFrame = std::min(_startFrame, _endFrame);
if (startFrame == _endFrame) {
// when startFrame >= endFrame
frame = _endFrame;
} else if (_timeScale > 0.0f) {
// accumulate time, keeping track of loops and end of animation events.
const float FRAMES_PER_SECOND = 30.0f;
float framesRemaining = (dt * _timeScale) * FRAMES_PER_SECOND;
while (framesRemaining > 0.0f) {
float framesTillEnd = _endFrame - _frame;
if (framesRemaining >= framesTillEnd) {
if (_loopFlag) {
// anim loop
triggersOut.push_back(_id + "OnLoop");
framesRemaining -= framesTillEnd;
frame = startFrame;
} else {
// anim end
triggersOut.push_back(_id + "OnDone");
frame = _endFrame;
framesRemaining = 0.0f;
}
} else {
frame += framesRemaining;
framesRemaining = 0.0f;
}
}
}
return frame;
}
void AnimClip::copyFromNetworkAnim() {
assert(_networkAnim && _networkAnim->isLoaded() && _skeleton);
_anim.clear();
// build a mapping from animation joint indices to skeleton joint indices.
// by matching joints with the same name.
const FBXGeometry& geom = _networkAnim->getGeometry();
const QVector<FBXJoint>& animJoints = geom.joints;
std::vector<int> jointMap;
const int animJointCount = animJoints.count();
jointMap.reserve(animJointCount);
for (int i = 0; i < animJointCount; i++) {
int skeletonJoint = _skeleton->nameToJointIndex(animJoints.at(i).name);
if (skeletonJoint == -1) {
qCWarning(animation) << "animation contains joint =" << animJoints.at(i).name << " which is not in the skeleton, url =" << _url.c_str();
}
jointMap.push_back(skeletonJoint);
}
const int frameCount = geom.animationFrames.size();
const int skeletonJointCount = _skeleton->getNumJoints();
_anim.resize(frameCount);
for (int i = 0; i < frameCount; i++) {
// init all joints in animation to bind pose
_anim[i].reserve(skeletonJointCount);
for (int j = 0; j < skeletonJointCount; j++) {
_anim[i].push_back(_skeleton->getRelativeBindPose(j));
}
// init over all joint animations
for (int j = 0; j < animJointCount; j++) {
int k = jointMap[j];
if (k >= 0 && k < skeletonJointCount) {
// currently FBX animations only have rotation.
_anim[i][k].rot = _skeleton->getRelativeBindPose(k).rot * geom.animationFrames[i].rotations[j];
}
}
}
_poses.resize(skeletonJointCount);
}
const AnimPoseVec& AnimClip::getPosesInternal() const {
return _poses;
}

View file

@ -0,0 +1,74 @@
//
// AnimClip.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimClip_h
#define hifi_AnimClip_h
#include <string>
#include "AnimationCache.h"
#include "AnimNode.h"
// Playback a single animation timeline.
// url determines the location of the fbx file to use within this clip.
// startFrame and endFrame are in frames 1/30th of a second.
// timescale can be used to speed-up or slow-down the animation.
// loop flag, when true, will loop the animation as it reaches the end frame.
class AnimClip : public AnimNode {
public:
friend class AnimTests;
AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag);
virtual ~AnimClip() override;
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
void setStartFrameVar(const std::string& startFrameVar) { _startFrameVar = startFrameVar; }
void setEndFrameVar(const std::string& endFrameVar) { _endFrameVar = endFrameVar; }
void setTimeScaleVar(const std::string& timeScaleVar) { _timeScaleVar = timeScaleVar; }
void setLoopFlagVar(const std::string& loopFlagVar) { _loopFlagVar = loopFlagVar; }
void setFrameVar(const std::string& frameVar) { _frameVar = frameVar; }
protected:
void loadURL(const std::string& url);
virtual void setCurrentFrameInternal(float frame) override;
float accumulateTime(float frame, float dt, Triggers& triggersOut) const;
void copyFromNetworkAnim();
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const override;
AnimationPointer _networkAnim;
AnimPoseVec _poses;
// _anim[frame][joint]
std::vector<AnimPoseVec> _anim;
std::string _url;
float _startFrame;
float _endFrame;
float _timeScale;
bool _loopFlag;
float _frame;
std::string _startFrameVar;
std::string _endFrameVar;
std::string _timeScaleVar;
std::string _loopFlagVar;
std::string _frameVar;
// no copies
AnimClip(const AnimClip&) = delete;
AnimClip& operator=(const AnimClip&) = delete;
};
#endif // hifi_AnimClip_h

View file

@ -0,0 +1,115 @@
//
// AnimNode.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimNode_h
#define hifi_AnimNode_h
#include <string>
#include <vector>
#include <memory>
#include <cassert>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include "AnimSkeleton.h"
#include "AnimVariant.h"
class QJsonObject;
// Base class for all elements in the animation blend tree.
// It provides the following categories of functions:
//
// * id getter, id is used to identify a node, useful for debugging and node searching.
// * type getter, helpful for determining the derived type of this node.
// * hierarchy accessors, for adding, removing and iterating over child AnimNodes
// * skeleton accessors, the skeleton is from the model whose bones we are going to manipulate
// * evaluate method, perform actual joint manipulations here and return result by reference.
// Also, append any triggers that are detected during evaluation.
class AnimNode {
public:
enum class Type {
Clip = 0,
BlendLinear,
Overlay,
StateMachine,
NumTypes
};
using Pointer = std::shared_ptr<AnimNode>;
using ConstPointer = std::shared_ptr<const AnimNode>;
using Triggers = std::vector<std::string>;
friend class AnimDebugDraw;
friend void buildChildMap(std::map<std::string, Pointer>& map, Pointer node);
friend class AnimStateMachine;
AnimNode(Type type, const std::string& id) : _type(type), _id(id) {}
virtual ~AnimNode() {}
const std::string& getID() const { return _id; }
Type getType() const { return _type; }
// hierarchy accessors
void addChild(Pointer child) { _children.push_back(child); }
void removeChild(Pointer child) {
auto iter = std::find(_children.begin(), _children.end(), child);
if (iter != _children.end()) {
_children.erase(iter);
}
}
Pointer getChild(int i) const {
assert(i >= 0 && i < (int)_children.size());
return _children[i];
}
int getChildCount() const { return (int)_children.size(); }
// pair this AnimNode graph with a skeleton.
void setSkeleton(const AnimSkeleton::Pointer skeleton) {
setSkeletonInternal(skeleton);
for (auto&& child : _children) {
child->setSkeleton(skeleton);
}
}
AnimSkeleton::ConstPointer getSkeleton() const { return _skeleton; }
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) = 0;
virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) {
return evaluate(animVars, dt, triggersOut);
}
protected:
void setCurrentFrame(float frame) {
setCurrentFrameInternal(frame);
for (auto&& child : _children) {
child->setCurrentFrameInternal(frame);
}
}
virtual void setCurrentFrameInternal(float frame) {}
virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) {
_skeleton = skeleton;
}
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const = 0;
Type _type;
std::string _id;
std::vector<AnimNode::Pointer> _children;
AnimSkeleton::ConstPointer _skeleton;
// no copies
AnimNode(const AnimNode&) = delete;
AnimNode& operator=(const AnimNode&) = delete;
};
#endif // hifi_AnimNode_h

View file

@ -0,0 +1,434 @@
//
// AnimNodeLoader.cpp
//
// Created by Anthony J. Thibault on 9/2/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 <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include "AnimNode.h"
#include "AnimClip.h"
#include "AnimBlendLinear.h"
#include "AnimationLogging.h"
#include "AnimOverlay.h"
#include "AnimNodeLoader.h"
#include "AnimStateMachine.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);
// factory functions
static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
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);
// called after children have been loaded
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 const char* animNodeTypeToString(AnimNode::Type type) {
switch (type) {
case AnimNode::Type::Clip: return "clip";
case AnimNode::Type::BlendLinear: return "blendLinear";
case AnimNode::Type::Overlay: return "overlay";
case AnimNode::Type::StateMachine: return "stateMachine";
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
}
static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) {
switch (type) {
case AnimNode::Type::Clip: return loadClipNode;
case AnimNode::Type::BlendLinear: return loadBlendLinearNode;
case AnimNode::Type::Overlay: return loadOverlayNode;
case AnimNode::Type::StateMachine: return loadStateMachineNode;
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
}
static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
switch (type) {
case AnimNode::Type::Clip: return processClipNode;
case AnimNode::Type::BlendLinear: return processBlendLinearNode;
case AnimNode::Type::Overlay: return processOverlayNode;
case AnimNode::Type::StateMachine: return processStateMachineNode;
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
}
#define READ_STRING(NAME, JSON_OBJ, ID, URL) \
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; \
} \
QString NAME = NAME##_VAL.toString()
#define READ_OPTIONAL_STRING(NAME, JSON_OBJ) \
auto NAME##_VAL = JSON_OBJ.value(#NAME); \
QString NAME; \
if (NAME##_VAL.isString()) { \
NAME = NAME##_VAL.toString(); \
}
#define READ_BOOL(NAME, JSON_OBJ, ID, URL) \
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; \
} \
bool NAME = NAME##_VAL.toBool()
#define READ_FLOAT(NAME, JSON_OBJ, ID, URL) \
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; \
} \
float NAME = (float)NAME##_VAL.toDouble()
static AnimNode::Type stringToEnum(const QString& str) {
// O(n), move to map when number of types becomes large.
const int NUM_TYPES = static_cast<int>(AnimNode::Type::NumTypes);
for (int i = 0; i < NUM_TYPES; i++ ) {
AnimNode::Type type = static_cast<AnimNode::Type>(i);
if (str == animNodeTypeToString(type)) {
return type;
}
}
return AnimNode::Type::NumTypes;
}
static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUrl) {
auto idVal = jsonObj.value("id");
if (!idVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl.toDisplayString();
return nullptr;
}
QString id = idVal.toString();
auto typeVal = jsonObj.value("type");
if (!typeVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
QString typeStr = typeVal.toString();
AnimNode::Type type = stringToEnum(typeStr);
if (type == AnimNode::Type::NumTypes) {
qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto dataValue = jsonObj.value("data");
if (!dataValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto dataObj = dataValue.toObject();
assert((int)type >= 0 && type < AnimNode::Type::NumTypes);
auto node = (animNodeTypeToLoaderFunc(type))(dataObj, id, jsonUrl);
auto childrenValue = jsonObj.value("children");
if (!childrenValue.isArray()) {
qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto childrenArray = childrenValue.toArray();
for (const auto& childValue : childrenArray) {
if (!childValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
node->addChild(loadNode(childValue.toObject(), jsonUrl));
}
if ((animNodeTypeToProcessFunc(type))(node, dataObj, id, jsonUrl)) {
return node;
} else {
return nullptr;
}
}
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_OPTIONAL_STRING(startFrameVar, jsonObj);
READ_OPTIONAL_STRING(endFrameVar, jsonObj);
READ_OPTIONAL_STRING(timeScaleVar, jsonObj);
READ_OPTIONAL_STRING(loopFlagVar, jsonObj);
auto node = std::make_shared<AnimClip>(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag);
if (!startFrameVar.isEmpty()) {
node->setStartFrameVar(startFrameVar.toStdString());
}
if (!endFrameVar.isEmpty()) {
node->setEndFrameVar(endFrameVar.toStdString());
}
if (!timeScaleVar.isEmpty()) {
node->setTimeScaleVar(timeScaleVar.toStdString());
}
if (!loopFlagVar.isEmpty()) {
node->setLoopFlagVar(loopFlagVar.toStdString());
}
return node;
}
static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
READ_FLOAT(alpha, jsonObj, id, jsonUrl);
READ_OPTIONAL_STRING(alphaVar, jsonObj);
auto node = std::make_shared<AnimBlendLinear>(id.toStdString(), alpha);
if (!alphaVar.isEmpty()) {
node->setAlphaVar(alphaVar.toStdString());
}
return node;
}
static const char* boneSetStrings[AnimOverlay::NumBoneSets] = {
"fullBody",
"upperBody",
"lowerBody",
"rightArm",
"leftArm",
"aboveTheHead",
"belowTheHead",
"headOnly",
"spineOnly",
"empty"
};
static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {
for (int i = 0; i < (int)AnimOverlay::NumBoneSets; i++) {
if (str == boneSetStrings[i]) {
return (AnimOverlay::BoneSet)i;
}
}
return AnimOverlay::NumBoneSets;
}
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);
auto boneSetEnum = stringToBoneSetEnum(boneSet);
if (boneSetEnum == AnimOverlay::NumBoneSets) {
qCCritical(animation) << "AnimNodeLoader, unknown bone set =" << boneSet << ", defaulting to \"fullBody\", url =" << jsonUrl.toDisplayString();
boneSetEnum = AnimOverlay::FullBodyBoneSet;
}
READ_OPTIONAL_STRING(boneSetVar, jsonObj);
READ_OPTIONAL_STRING(alphaVar, jsonObj);
auto node = std::make_shared<AnimOverlay>(id.toStdString(), boneSetEnum, alpha);
if (!boneSetVar.isEmpty()) {
node->setBoneSetVar(boneSetVar.toStdString());
}
if (!alphaVar.isEmpty()) {
node->setAlphaVar(alphaVar.toStdString());
}
return node;
}
static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
auto node = std::make_shared<AnimStateMachine>(id.toStdString());
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));
}
}
bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) {
auto smNode = std::static_pointer_cast<AnimStateMachine>(node);
assert(smNode);
READ_STRING(currentState, jsonObj, nodeId, jsonUrl);
auto statesValue = jsonObj.value("states");
if (!statesValue.isArray()) {
qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in stateMachine node, id =" << nodeId << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
// build a map for all children by name.
std::map<std::string, AnimNode::Pointer> childMap;
buildChildMap(childMap, node);
// first pass parse all the states and build up the state and transition map.
using StringPair = std::pair<std::string, std::string>;
using TransitionMap = std::multimap<AnimStateMachine::State::Pointer, StringPair>;
TransitionMap transitionMap;
using StateMap = std::map<std::string, AnimStateMachine::State::Pointer>;
StateMap stateMap;
auto statesArray = statesValue.toArray();
for (const auto& stateValue : statesArray) {
if (!stateValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad state object in \"states\", id =" << nodeId << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto stateObj = stateValue.toObject();
READ_STRING(id, stateObj, nodeId, jsonUrl);
READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl);
READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl);
READ_OPTIONAL_STRING(interpTargetVar, stateObj);
READ_OPTIONAL_STRING(interpDurationVar, stateObj);
auto stdId = id.toStdString();
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;
}
auto statePtr = std::make_shared<AnimStateMachine::State>(stdId, iter->second, interpTarget, interpDuration);
assert(statePtr);
if (!interpTargetVar.isEmpty()) {
statePtr->setInterpTargetVar(interpTargetVar.toStdString());
}
if (!interpDurationVar.isEmpty()) {
statePtr->setInterpDurationVar(interpDurationVar.toStdString());
}
smNode->addState(statePtr);
stateMap.insert(StateMap::value_type(statePtr->getID(), statePtr));
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;
}
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;
}
auto transitionObj = transitionValue.toObject();
READ_STRING(var, transitionObj, nodeId, jsonUrl);
READ_STRING(state, transitionObj, nodeId, jsonUrl);
transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var.toStdString(), state.toStdString())));
}
}
// second pass: now iterate thru all transitions and add them to the appropriate states.
for (auto& transition : transitionMap) {
AnimStateMachine::State::Pointer srcState = transition.first;
auto iter = stateMap.find(transition.second.second);
if (iter != stateMap.end()) {
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;
}
}
auto iter = stateMap.find(currentState.toStdString());
if (iter == stateMap.end()) {
qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId << "url = " << jsonUrl.toDisplayString();
}
smNode->setCurrentState(iter->second);
return true;
}
AnimNodeLoader::AnimNodeLoader(const QUrl& url) :
_url(url),
_resource(nullptr) {
_resource = new Resource(url);
connect(_resource, SIGNAL(loaded(QNetworkReply&)), SLOT(onRequestDone(QNetworkReply&)));
connect(_resource, SIGNAL(failed(QNetworkReply::NetworkError)), SLOT(onRequestError(QNetworkReply::NetworkError)));
}
AnimNode::Pointer AnimNodeLoader::load(const QByteArray& contents, const QUrl& jsonUrl) {
// convert string into a json doc
QJsonParseError error;
auto doc = QJsonDocument::fromJson(contents, &error);
if (error.error != QJsonParseError::NoError) {
qCCritical(animation) << "AnimNodeLoader, failed to parse json, error =" << error.errorString() << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
QJsonObject obj = doc.object();
// version
QJsonValue versionVal = obj.value("version");
if (!versionVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl.toDisplayString();
return nullptr;
}
QString version = versionVal.toString();
// check version
if (version != "1.0") {
qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl.toDisplayString();
return nullptr;
}
// root
QJsonValue rootVal = obj.value("root");
if (!rootVal.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl.toDisplayString();
return nullptr;
}
return loadNode(rootVal.toObject(), jsonUrl);
}
void AnimNodeLoader::onRequestDone(QNetworkReply& request) {
auto node = load(request.readAll(), _url);
if (node) {
emit success(node);
} else {
emit error(0, "json parse error");
}
}
void AnimNodeLoader::onRequestError(QNetworkReply::NetworkError netError) {
emit error((int)netError, "Resource download error");
}

View file

@ -0,0 +1,52 @@
//
// AnimNodeLoader.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimNodeLoader_h
#define hifi_AnimNodeLoader_h
#include <memory>
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#include "AnimNode.h"
class Resource;
class AnimNodeLoader : public QObject {
Q_OBJECT
public:
AnimNodeLoader(const QUrl& url);
signals:
void success(AnimNode::Pointer node);
void error(int error, QString str);
protected:
// synchronous
static AnimNode::Pointer load(const QByteArray& contents, const QUrl& jsonUrl);
protected slots:
void onRequestDone(QNetworkReply& request);
void onRequestError(QNetworkReply::NetworkError error);
protected:
QUrl _url;
Resource* _resource;
private:
// no copies
AnimNodeLoader(const AnimNodeLoader&) = delete;
AnimNodeLoader& operator=(const AnimNodeLoader&) = delete;
};
#endif // hifi_AnimNodeLoader

View file

@ -0,0 +1,181 @@
//
// AnimOverlay.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "AnimOverlay.h"
#include "AnimUtil.h"
#include <queue>
AnimOverlay::AnimOverlay(const std::string& id, BoneSet boneSet, float alpha) :
AnimNode(AnimNode::Type::Overlay, id), _boneSet(boneSet), _alpha(alpha) {
}
AnimOverlay::~AnimOverlay() {
}
void AnimOverlay::buildBoneSet(BoneSet boneSet) {
assert(_skeleton);
switch (boneSet) {
case FullBodyBoneSet: buildFullBodyBoneSet(); break;
case UpperBodyBoneSet: buildUpperBodyBoneSet(); break;
case LowerBodyBoneSet: buildLowerBodyBoneSet(); break;
case RightArmBoneSet: buildRightArmBoneSet(); break;
case LeftArmBoneSet: buildLeftArmBoneSet(); break;
case AboveTheHeadBoneSet: buildAboveTheHeadBoneSet(); break;
case BelowTheHeadBoneSet: buildBelowTheHeadBoneSet(); break;
case HeadOnlyBoneSet: buildHeadOnlyBoneSet(); break;
case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break;
default:
case EmptyBoneSet: buildEmptyBoneSet(); break;
}
}
const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) {
// lookup parameters from animVars, using current instance variables as defaults.
// NOTE: switching bonesets can be an expensive operation, let's try to avoid it.
auto prevBoneSet = _boneSet;
_boneSet = (BoneSet)animVars.lookup(_boneSetVar, (int)_boneSet);
if (_boneSet != prevBoneSet && _skeleton) {
buildBoneSet(_boneSet);
}
_alpha = animVars.lookup(_alphaVar, _alpha);
if (_children.size() >= 2) {
auto underPoses = _children[1]->evaluate(animVars, dt, triggersOut);
auto overPoses = _children[0]->overlay(animVars, dt, triggersOut, underPoses);
if (underPoses.size() > 0 && underPoses.size() == overPoses.size()) {
_poses.resize(underPoses.size());
assert(_boneSetVec.size() == _poses.size());
for (size_t i = 0; i < _poses.size(); i++) {
float alpha = _boneSetVec[i] * _alpha;
::blend(1, &underPoses[i], &overPoses[i], alpha, &_poses[i]);
}
}
}
return _poses;
}
template <typename Func>
void for_each_child_joint(AnimSkeleton::ConstPointer skeleton, int startJoint, Func f) {
std::queue<int> q;
q.push(startJoint);
while(q.size() > 0) {
int jointIndex = q.front();
for (int i = 0; i < skeleton->getNumJoints(); i++) {
if (jointIndex == skeleton->getParentIndex(i)) {
f(i);
q.push(i);
}
}
q.pop();
}
}
void AnimOverlay::buildFullBodyBoneSet() {
assert(_skeleton);
_boneSetVec.resize(_skeleton->getNumJoints());
for (int i = 0; i < _skeleton->getNumJoints(); i++) {
_boneSetVec[i] = 1.0f;
}
}
void AnimOverlay::buildUpperBodyBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int spineJoint = _skeleton->nameToJointIndex("Spine");
for_each_child_joint(_skeleton, spineJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildLowerBodyBoneSet() {
assert(_skeleton);
buildFullBodyBoneSet();
int hipsJoint = _skeleton->nameToJointIndex("Hips");
int spineJoint = _skeleton->nameToJointIndex("Spine");
_boneSetVec.resize(_skeleton->getNumJoints());
for_each_child_joint(_skeleton, spineJoint, [&](int i) {
_boneSetVec[i] = 0.0f;
});
_boneSetVec[hipsJoint] = 0.0f;
}
void AnimOverlay::buildRightArmBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder");
for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildLeftArmBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int leftShoulderJoint = _skeleton->nameToJointIndex("LeftShoulder");
for_each_child_joint(_skeleton, leftShoulderJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildAboveTheHeadBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int headJoint = _skeleton->nameToJointIndex("Head");
for_each_child_joint(_skeleton, headJoint, [&](int i) {
_boneSetVec[i] = 1.0f;
});
}
void AnimOverlay::buildBelowTheHeadBoneSet() {
assert(_skeleton);
buildFullBodyBoneSet();
int headJoint = _skeleton->nameToJointIndex("Head");
for_each_child_joint(_skeleton, headJoint, [&](int i) {
_boneSetVec[i] = 0.0f;
});
}
void AnimOverlay::buildHeadOnlyBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int headJoint = _skeleton->nameToJointIndex("Head");
_boneSetVec[headJoint] = 1.0f;
}
void AnimOverlay::buildSpineOnlyBoneSet() {
assert(_skeleton);
buildEmptyBoneSet();
int spineJoint = _skeleton->nameToJointIndex("Spine");
_boneSetVec[spineJoint] = 1.0f;
}
void AnimOverlay::buildEmptyBoneSet() {
assert(_skeleton);
_boneSetVec.resize(_skeleton->getNumJoints());
for (int i = 0; i < _skeleton->getNumJoints(); i++) {
_boneSetVec[i] = 0.0f;
}
}
// for AnimDebugDraw rendering
const AnimPoseVec& AnimOverlay::getPosesInternal() const {
return _poses;
}
void AnimOverlay::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) {
_skeleton = skeleton;
// we have to re-build the bone set when the skeleton changes.
buildBoneSet(_boneSet);
}

View file

@ -0,0 +1,80 @@
//
// AnimOverlay.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimOverlay_h
#define hifi_AnimOverlay_h
#include "AnimNode.h"
// Overlay the AnimPoses from one AnimNode on top of another AnimNode.
// child[0] is overlayed on top of child[1]. The boneset is used
// to control blending on a per-bone bases.
// alpha gives the ability to fade in and fade out overlays.
// alpha of 0, will have no overlay, final pose will be 100% from child[1].
// alpha of 1, will be a full overlay.
class AnimOverlay : public AnimNode {
public:
friend class AnimTests;
enum BoneSet {
FullBodyBoneSet = 0,
UpperBodyBoneSet,
LowerBodyBoneSet,
RightArmBoneSet,
LeftArmBoneSet,
AboveTheHeadBoneSet,
BelowTheHeadBoneSet,
HeadOnlyBoneSet,
SpineOnlyBoneSet,
EmptyBoneSet,
NumBoneSets,
};
AnimOverlay(const std::string& id, BoneSet boneSet, float alpha);
virtual ~AnimOverlay() override;
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
void setBoneSetVar(const std::string& boneSetVar) { _boneSetVar = boneSetVar; }
void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; }
protected:
void buildBoneSet(BoneSet boneSet);
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const override;
virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override;
AnimPoseVec _poses;
BoneSet _boneSet;
float _alpha;
std::vector<float> _boneSetVec;
std::string _boneSetVar;
std::string _alphaVar;
void buildFullBodyBoneSet();
void buildUpperBodyBoneSet();
void buildLowerBodyBoneSet();
void buildRightArmBoneSet();
void buildLeftArmBoneSet();
void buildAboveTheHeadBoneSet();
void buildBelowTheHeadBoneSet();
void buildHeadOnlyBoneSet();
void buildSpineOnlyBoneSet();
void buildEmptyBoneSet();
// no copies
AnimOverlay(const AnimOverlay&) = delete;
AnimOverlay& operator=(const AnimOverlay&) = delete;
};
#endif // hifi_AnimOverlay_h

View file

@ -0,0 +1,176 @@
//
// AnimSkeleton.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "AnimSkeleton.h"
#include "AnimationLogging.h"
#include "GLMHelpers.h"
#include <glm/gtx/transform.hpp>
#include <glm/gtc/quaternion.hpp>
const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f),
glm::quat(),
glm::vec3(0.0f));
AnimPose::AnimPose(const glm::mat4& mat) {
scale = extractScale(mat);
rot = extractRotation(mat);
trans = extractTranslation(mat);
}
glm::vec3 AnimPose::operator*(const glm::vec3& rhs) const {
return trans + (rot * (scale * rhs));
}
glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const {
return *this * rhs;
}
// really slow
glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const {
glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f);
glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f);
glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z);
glm::mat3 mat(xAxis, yAxis, zAxis);
glm::mat3 transInvMat = glm::inverse(glm::transpose(mat));
return transInvMat * rhs;
}
AnimPose AnimPose::operator*(const AnimPose& rhs) const {
return AnimPose(static_cast<glm::mat4>(*this) * static_cast<glm::mat4>(rhs));
}
AnimPose AnimPose::inverse() const {
return AnimPose(glm::inverse(static_cast<glm::mat4>(*this)));
}
AnimPose::operator glm::mat4() const {
glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f);
glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f);
glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z);
return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f),
glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f));
}
AnimSkeleton::AnimSkeleton(const std::vector<FBXJoint>& joints, const AnimPose& geometryOffset) {
_joints = joints;
// build a cache of bind poses
_absoluteBindPoses.reserve(joints.size());
_relativeBindPoses.reserve(joints.size());
// iterate over FBXJoints and extract the bind pose information.
for (size_t i = 0; i < joints.size(); i++) {
if (_joints[i].bindTransformFoundInCluster) {
// Use the FBXJoint::bindTransform, which is absolute model coordinates
// i.e. not relative to it's parent.
AnimPose absoluteBindPose(_joints[i].bindTransform);
_absoluteBindPoses.push_back(absoluteBindPose);
int parentIndex = getParentIndex(i);
if (parentIndex >= 0) {
AnimPose inverseParentAbsoluteBindPose = _absoluteBindPoses[parentIndex].inverse();
_relativeBindPoses.push_back(inverseParentAbsoluteBindPose * absoluteBindPose);
} else {
_relativeBindPoses.push_back(absoluteBindPose);
}
} else {
// use FBXJoint's local transform, instead
glm::mat4 rotTransform = glm::mat4_cast(_joints[i].preRotation * _joints[i].rotation * _joints[i].postRotation);
glm::mat4 relBindMat = glm::translate(_joints[i].translation) * _joints[i].preTransform * rotTransform * _joints[i].postTransform;
AnimPose relBindPose(relBindMat);
_relativeBindPoses.push_back(relBindPose);
int parentIndex = getParentIndex(i);
if (parentIndex >= 0) {
_absoluteBindPoses.push_back(_absoluteBindPoses[parentIndex] * relBindPose);
} else {
_absoluteBindPoses.push_back(relBindPose);
}
}
}
// now we want to normalize scale from geometryOffset to all poses.
// This will ensure our bone translations will be in meters, even if the model was authored with some other unit of mesure.
for (auto& absPose : _absoluteBindPoses) {
absPose.trans = (geometryOffset * absPose).trans;
absPose.scale = vec3(1, 1, 1);
}
// re-compute relative poses based on the modified absolute poses.
for (size_t i = 0; i < _relativeBindPoses.size(); i++) {
int parentIndex = getParentIndex(i);
if (parentIndex >= 0) {
_relativeBindPoses[i] = _absoluteBindPoses[parentIndex].inverse() * _absoluteBindPoses[i];
} else {
_relativeBindPoses[i] = _absoluteBindPoses[i];
}
}
}
int AnimSkeleton::nameToJointIndex(const QString& jointName) const {
for (size_t i = 0; i < _joints.size(); i++) {
if (_joints[i].name == jointName) {
return i;
}
}
return -1;
}
int AnimSkeleton::getNumJoints() const {
return _joints.size();
}
AnimPose AnimSkeleton::getAbsoluteBindPose(int jointIndex) const {
return _absoluteBindPoses[jointIndex];
}
AnimPose AnimSkeleton::getRelativeBindPose(int jointIndex) const {
return _relativeBindPoses[jointIndex];
}
int AnimSkeleton::getParentIndex(int jointIndex) const {
return _joints[jointIndex].parentIndex;
}
const QString& AnimSkeleton::getJointName(int jointIndex) const {
return _joints[jointIndex].name;
}
#ifndef NDEBUG
void AnimSkeleton::dump() const {
qCDebug(animation) << "[";
for (int i = 0; i < getNumJoints(); i++) {
qCDebug(animation) << " {";
qCDebug(animation) << " name =" << getJointName(i);
qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i);
qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i);
if (getParentIndex(i) >= 0) {
qCDebug(animation) << " parent =" << getJointName(getParentIndex(i));
}
qCDebug(animation) << " },";
}
qCDebug(animation) << "]";
}
void AnimSkeleton::dump(const AnimPoseVec& poses) const {
qCDebug(animation) << "[";
for (int i = 0; i < getNumJoints(); i++) {
qCDebug(animation) << " {";
qCDebug(animation) << " name =" << getJointName(i);
qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i);
qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i);
qCDebug(animation) << " pose =" << poses[i];
if (getParentIndex(i) >= 0) {
qCDebug(animation) << " parent =" << getJointName(getParentIndex(i));
}
qCDebug(animation) << " },";
}
qCDebug(animation) << "]";
}
#endif

View file

@ -0,0 +1,78 @@
//
// AnimSkeleton.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimSkeleton
#define hifi_AnimSkeleton
#include <vector>
#include "FBXReader.h"
struct AnimPose {
AnimPose() {}
explicit AnimPose(const glm::mat4& mat);
AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : scale(scaleIn), rot(rotIn), trans(transIn) {}
static const AnimPose identity;
glm::vec3 xformPoint(const glm::vec3& rhs) const;
glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow
glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint
AnimPose operator*(const AnimPose& rhs) const;
AnimPose inverse() const;
operator glm::mat4() const;
glm::vec3 scale;
glm::quat rot;
glm::vec3 trans;
};
inline QDebug operator<<(QDebug debug, const AnimPose& pose) {
debug << "AnimPose, trans = (" << pose.trans.x << pose.trans.y << pose.trans.z << "), rot = (" << pose.rot.x << pose.rot.y << pose.rot.z << pose.rot.w << "), scale = (" << pose.scale.x << pose.scale.y << pose.scale.z << ")";
return debug;
}
using AnimPoseVec = std::vector<AnimPose>;
class AnimSkeleton {
public:
using Pointer = std::shared_ptr<AnimSkeleton>;
using ConstPointer = std::shared_ptr<const AnimSkeleton>;
AnimSkeleton(const std::vector<FBXJoint>& joints, const AnimPose& geometryOffset);
int nameToJointIndex(const QString& jointName) const;
const QString& getJointName(int jointIndex) const;
int getNumJoints() const;
// absolute pose, not relative to parent
AnimPose getAbsoluteBindPose(int jointIndex) const;
// relative to parent pose
AnimPose getRelativeBindPose(int jointIndex) const;
int getParentIndex(int jointIndex) const;
#ifndef NDEBUG
void dump() const;
void dump(const AnimPoseVec& poses) const;
#endif
protected:
std::vector<FBXJoint> _joints;
AnimPoseVec _absoluteBindPoses;
AnimPoseVec _relativeBindPoses;
// no copies
AnimSkeleton(const AnimSkeleton&) = delete;
AnimSkeleton& operator=(const AnimSkeleton&) = delete;
};
#endif

View file

@ -0,0 +1,114 @@
//
// AnimStateMachine.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "AnimStateMachine.h"
#include "AnimUtil.h"
#include "AnimationLogging.h"
AnimStateMachine::AnimStateMachine(const std::string& id) :
AnimNode(AnimNode::Type::StateMachine, id) {
}
AnimStateMachine::~AnimStateMachine() {
}
const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) {
std::string desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID());
if (_currentState->getID() != desiredStateID) {
// switch states
bool foundState = false;
for (auto& state : _states) {
if (state->getID() == desiredStateID) {
switchState(animVars, state);
foundState = true;
break;
}
}
if (!foundState) {
qCCritical(animation) << "AnimStateMachine could not find state =" << desiredStateID.c_str() << ", referenced by _currentStateVar =" << _currentStateVar.c_str();
}
}
// evaluate currentState transitions
auto desiredState = evaluateTransitions(animVars);
if (desiredState != _currentState) {
switchState(animVars, desiredState);
}
assert(_currentState);
auto currentStateNode = _currentState->getNode();
assert(currentStateNode);
if (_duringInterp) {
_alpha += _alphaVel * dt;
if (_alpha < 1.0f) {
if (_poses.size() > 0) {
::blend(_poses.size(), &_prevPoses[0], &_nextPoses[0], _alpha, &_poses[0]);
}
} else {
_duringInterp = false;
_prevPoses.clear();
_nextPoses.clear();
}
}
if (!_duringInterp) {
_poses = currentStateNode->evaluate(animVars, dt, triggersOut);
}
return _poses;
}
void AnimStateMachine::setCurrentState(State::Pointer state) {
_currentState = state;
}
void AnimStateMachine::addState(State::Pointer state) {
_states.push_back(state);
}
void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointer desiredState) {
qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID().c_str() << "->" << desiredState->getID().c_str();
const float FRAMES_PER_SECOND = 30.0f;
auto prevStateNode = _currentState->getNode();
auto nextStateNode = desiredState->getNode();
_duringInterp = true;
_alpha = 0.0f;
float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration));
_alphaVel = FRAMES_PER_SECOND / duration;
_prevPoses = _poses;
nextStateNode->setCurrentFrame(desiredState->_interpTarget);
// because dt is 0, we should not encounter any triggers
const float dt = 0.0f;
Triggers triggers;
_nextPoses = nextStateNode->evaluate(animVars, dt, triggers);
_currentState = desiredState;
}
AnimStateMachine::State::Pointer AnimStateMachine::evaluateTransitions(const AnimVariantMap& animVars) const {
assert(_currentState);
for (auto& transition : _currentState->_transitions) {
if (animVars.lookup(transition._var, false)) {
return transition._state;
}
}
return _currentState;
}
const AnimPoseVec& AnimStateMachine::getPosesInternal() const {
return _poses;
}

View file

@ -0,0 +1,134 @@
//
// AnimStateMachine.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimStateMachine_h
#define hifi_AnimStateMachine_h
#include <string>
#include <vector>
#include "AnimNode.h"
// State Machine for transitioning between children AnimNodes
//
// This is mechinisim for playing animations and smoothly interpolating/fading
// between them. A StateMachine has a set of States, which typically reference
// child AnimNodes. Each State has a list of Transitions, which are evaluated
// to determine when we should switch to a new State. Parameters for the smooth
// interpolation/fading are read from the State that you are transitioning to.
//
// The currentState can be set directly via the setCurrentStateVar() and will override
// any State transitions.
//
// Each State has two parameters that can be changed via AnimVars,
// * interpTarget - (frames) The destination frame of the interpolation. i.e. the first frame of the animation that will
// visible after interpolation is complete.
// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the
// interpTarget frame.
class AnimStateMachine : public AnimNode {
public:
friend class AnimNodeLoader;
friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl);
protected:
class State {
public:
friend AnimStateMachine;
friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl);
using Pointer = std::shared_ptr<State>;
using ConstPointer = std::shared_ptr<const State>;
class Transition {
public:
friend AnimStateMachine;
Transition(const std::string& var, State::Pointer state) : _var(var), _state(state) {}
protected:
std::string _var;
State::Pointer _state;
};
State(const std::string& id, AnimNode::Pointer node, float interpTarget, float interpDuration) :
_id(id),
_node(node),
_interpTarget(interpTarget),
_interpDuration(interpDuration) {}
void setInterpTargetVar(const std::string& interpTargetVar) { _interpTargetVar = interpTargetVar; }
void setInterpDurationVar(const std::string& interpDurationVar) { _interpDurationVar = interpDurationVar; }
AnimNode::Pointer getNode() const { return _node; }
const std::string& getID() const { return _id; }
protected:
void setInterpTarget(float interpTarget) { _interpTarget = interpTarget; }
void setInterpDuration(float interpDuration) { _interpDuration = interpDuration; }
void addTransition(const Transition& transition) { _transitions.push_back(transition); }
std::string _id;
AnimNode::Pointer _node;
float _interpTarget; // frames
float _interpDuration; // frames
std::string _interpTargetVar;
std::string _interpDurationVar;
std::vector<Transition> _transitions;
private:
// no copies
State(const State&) = delete;
State& operator=(const State&) = delete;
};
public:
AnimStateMachine(const std::string& id);
virtual ~AnimStateMachine() override;
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
void setCurrentStateVar(std::string& currentStateVar) { _currentStateVar = currentStateVar; }
protected:
void setCurrentState(State::Pointer state);
void addState(State::Pointer state);
void switchState(const AnimVariantMap& animVars, State::Pointer desiredState);
State::Pointer evaluateTransitions(const AnimVariantMap& animVars) const;
// for AnimDebugDraw rendering
virtual const AnimPoseVec& getPosesInternal() const override;
AnimPoseVec _poses;
// interpolation state
bool _duringInterp = false;
float _alphaVel = 0.0f;
float _alpha = 0.0f;
AnimPoseVec _prevPoses;
AnimPoseVec _nextPoses;
State::Pointer _currentState;
std::vector<State::Pointer> _states;
std::string _currentStateVar;
private:
// no copies
AnimStateMachine(const AnimStateMachine&) = delete;
AnimStateMachine& operator=(const AnimStateMachine&) = delete;
};
#endif // hifi_AnimStateMachine_h

View file

@ -0,0 +1,22 @@
//
// AnimUtil.cpp
//
// Created by Anthony J. Thibault on 9/2/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 "AnimUtil.h"
#include "GLMHelpers.h"
void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result) {
for (size_t i = 0; i < numPoses; i++) {
const AnimPose& aPose = a[i];
const AnimPose& bPose = b[i];
result[i].scale = lerp(aPose.scale, bPose.scale, alpha);
result[i].rot = glm::normalize(glm::lerp(aPose.rot, bPose.rot, alpha));
result[i].trans = lerp(aPose.trans, bPose.trans, alpha);
}
}

View file

@ -0,0 +1,24 @@
//
// AnimUtil.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimUtil_h
#define hifi_AnimUtil_h
#include "AnimNode.h"
// TODO: use restrict keyword
// TODO: excellent candidate for simd vectorization.
// this is where the magic happens
void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result);
#endif

View file

@ -0,0 +1,161 @@
//
// AnimVariant.h
//
// Created by Anthony J. Thibault on 9/2/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_AnimVariant_h
#define hifi_AnimVariant_h
#include <cassert>
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <map>
#include <set>
class AnimVariant {
public:
enum class Type {
Bool = 0,
Int,
Float,
Vec3,
Quat,
Mat4,
String,
NumTypes
};
AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); }
AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; }
AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; }
AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; }
AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast<glm::vec3*>(&_val) = value; }
AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast<glm::quat*>(&_val) = value; }
AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast<glm::mat4*>(&_val) = value; }
AnimVariant(const std::string& value) : _type(Type::String) { _stringVal = value; }
bool isBool() const { return _type == Type::Bool; }
bool isInt() const { return _type == Type::Int; }
bool isFloat() const { return _type == Type::Float; }
bool isVec3() const { return _type == Type::Vec3; }
bool isQuat() const { return _type == Type::Quat; }
bool isMat4() const { return _type == Type::Mat4; }
bool isString() const { return _type == Type::String; }
void setBool(bool value) { assert(_type == Type::Bool); _val.boolVal = value; }
void setInt(int value) { assert(_type == Type::Int); _val.intVal = value; }
void setFloat(float value) { assert(_type == Type::Float); _val.floats[0] = value; }
void setVec3(const glm::vec3& value) { assert(_type == Type::Vec3); *reinterpret_cast<glm::vec3*>(&_val) = value; }
void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast<glm::quat*>(&_val) = value; }
void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast<glm::mat4*>(&_val) = value; }
void setString(const std::string& value) { assert(_type == Type::String); _stringVal = value; }
bool getBool() const { assert(_type == Type::Bool); return _val.boolVal; }
int getInt() const { assert(_type == Type::Int); return _val.intVal; }
float getFloat() const { assert(_type == Type::Float); return _val.floats[0]; }
const glm::vec3& getVec3() const { assert(_type == Type::Vec3); return *reinterpret_cast<const glm::vec3*>(&_val); }
const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast<const glm::quat*>(&_val); }
const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast<const glm::mat4*>(&_val); }
const std::string& getString() const { assert(_type == Type::String); return _stringVal; }
protected:
Type _type;
std::string _stringVal;
union {
bool boolVal;
int intVal;
float floats[16];
} _val;
};
class AnimVariantMap {
public:
bool lookup(const std::string& key, bool defaultValue) const {
// check triggers first, then map
if (key.empty()) {
return defaultValue;
} else if (_triggers.find(key) != _triggers.end()) {
return true;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getBool() : defaultValue;
}
}
int lookup(const std::string& key, int defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getInt() : defaultValue;
}
}
float lookup(const std::string& key, float defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getFloat() : defaultValue;
}
}
const glm::vec3& lookup(const std::string& key, const glm::vec3& defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getVec3() : defaultValue;
}
}
const glm::quat& lookup(const std::string& key, const glm::quat& defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getQuat() : defaultValue;
}
}
const glm::mat4& lookup(const std::string& key, const glm::mat4& defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getMat4() : defaultValue;
}
}
const std::string& lookup(const std::string& key, const std::string& defaultValue) const {
if (key.empty()) {
return defaultValue;
} else {
auto iter = _map.find(key);
return iter != _map.end() ? iter->second.getString() : defaultValue;
}
}
void set(const std::string& key, bool value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, int value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, float value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, const glm::vec3& value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, const glm::quat& value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, const glm::mat4& value) { _map[key] = AnimVariant(value); }
void set(const std::string& key, const std::string& value) { _map[key] = AnimVariant(value); }
void setTrigger(const std::string& key) { _triggers.insert(key); }
void clearTriggers() { _triggers.clear(); }
protected:
std::map<std::string, AnimVariant> _map;
std::set<std::string> _triggers;
};
#endif // hifi_AnimVariant_h

View file

@ -16,6 +16,9 @@
#include "AnimationHandle.h"
#include "AnimationLogging.h"
#include "AnimSkeleton.h"
#include "Rig.h"
void Rig::HeadParameters::dump() const {
qCDebug(animation, "HeadParameters =");
@ -186,6 +189,12 @@ void Rig::deleteAnimations() {
_animationHandles.clear();
}
void Rig::destroyAnimGraph() {
_animSkeleton = nullptr;
_animLoader = nullptr;
_animNode = nullptr;
}
void Rig::initJointStates(QVector<JointState> states, glm::mat4 rootTransform,
int rootJointIndex,
int leftHandJointIndex,
@ -407,106 +416,223 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const {
}
void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) {
if (!_enableRig) {
return;
}
bool isMoving = false;
glm::vec3 front = worldRotation * IDENTITY_FRONT;
glm::vec3 right = worldRotation * IDENTITY_RIGHT;
const float PERCEPTIBLE_DELTA = 0.001f;
const float PERCEPTIBLE_SPEED = 0.1f;
// It can be more accurate/smooth to use velocity rather than position,
// but some modes (e.g., hmd standing) update position without updating velocity.
// It's very hard to debug hmd standing. (Look down at yourself, or have a second person observe. HMD third person is a bit undefined...)
// So, let's create our own workingVelocity from the worldPosition...
glm::vec3 positionDelta = worldPosition - _lastPosition;
glm::vec3 workingVelocity = positionDelta / deltaTime;
// But for smoothest (non-hmd standing) results, go ahead and use velocity:
#if !WANT_DEBUG
// Note: Separately, we've arranged for starting/stopping animations by role (as we've done here) to pick up where they've left off when fading,
// so that you wouldn't notice the start/stop if it happens fast enough (e.g., one frame). But the print below would still be noisy.
// But for smoothest (non-hmd standing) results, go ahead and use velocity:
if (!positionDelta.x && !positionDelta.y && !positionDelta.z) {
workingVelocity = worldVelocity;
}
#endif
float forwardSpeed = glm::dot(workingVelocity, front);
float rightLateralSpeed = glm::dot(workingVelocity, right);
float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP);
float rightTurningSpeed = rightTurningDelta / deltaTime;
bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED);
bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED;
auto updateRole = [&](const QString& role, bool isOn) {
isMoving = isMoving || isOn;
if (isOn) {
if (!isRunningRole(role)) {
qCDebug(animation) << "Rig STARTING" << role;
startAnimationByRole(role);
}
if (_enableAnimGraph) {
glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity;
float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT);
float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT);
float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime;
// sine wave LFO var for testing.
static float t = 0.0f;
_animVars.set("sine", static_cast<float>(0.5 * sin(t) + 0.5));
// default anim vars to notMoving and notTurning
_animVars.set("isMovingForward", false);
_animVars.set("isMovingBackward", false);
_animVars.set("isMovingLeft", false);
_animVars.set("isMovingRight", false);
_animVars.set("isNotMoving", true);
_animVars.set("isTurningLeft", false);
_animVars.set("isTurningRight", false);
_animVars.set("isNotTurning", true);
const float ANIM_WALK_SPEED = 1.4f; // m/s
_animVars.set("walkTimeScale", glm::clamp(0.5f, 2.0f, glm::length(localVel) / ANIM_WALK_SPEED));
const float MOVE_ENTER_SPEED_THRESHOLD = 0.2f; // m/sec
const float MOVE_EXIT_SPEED_THRESHOLD = 0.07f; // m/sec
const float TURN_ENTER_SPEED_THRESHOLD = 0.5f; // rad/sec
const float TURN_EXIT_SPEED_THRESHOLD = 0.2f; // rad/sec
float moveThresh;
if (_state != RigRole::Move) {
moveThresh = MOVE_ENTER_SPEED_THRESHOLD;
} else {
if (isRunningRole(role)) {
qCDebug(animation) << "Rig stopping" << role;
stopAnimationByRole(role);
moveThresh = MOVE_EXIT_SPEED_THRESHOLD;
}
float turnThresh;
if (_state != RigRole::Turn) {
turnThresh = TURN_ENTER_SPEED_THRESHOLD;
} else {
turnThresh = TURN_EXIT_SPEED_THRESHOLD;
}
if (glm::length(localVel) > moveThresh) {
if (fabs(forwardSpeed) > 0.5f * fabs(lateralSpeed)) {
if (forwardSpeed > 0.0f) {
// forward
_animVars.set("isMovingForward", true);
_animVars.set("isNotMoving", false);
} else {
// backward
_animVars.set("isMovingBackward", true);
_animVars.set("isNotMoving", false);
}
} else {
if (lateralSpeed > 0.0f) {
// right
_animVars.set("isMovingRight", true);
_animVars.set("isNotMoving", false);
} else {
// left
_animVars.set("isMovingLeft", true);
_animVars.set("isNotMoving", false);
}
}
_state = RigRole::Move;
} else {
if (fabs(turningSpeed) > turnThresh) {
if (turningSpeed > 0.0f) {
// turning right
_animVars.set("isTurningRight", true);
_animVars.set("isNotTurning", false);
} else {
// turning left
_animVars.set("isTurningLeft", true);
_animVars.set("isNotTurning", false);
}
_state = RigRole::Turn;
} else {
// idle
_state = RigRole::Idle;
}
}
};
updateRole("walk", forwardSpeed > PERCEPTIBLE_SPEED);
updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED);
updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f));
updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f));
isStrafing = isStrafing && !isMoving;
updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f));
updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f));
updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus.
t += deltaTime;
}
if (_enableRig) {
bool isMoving = false;
glm::vec3 right = worldRotation * IDENTITY_RIGHT;
const float PERCEPTIBLE_DELTA = 0.001f;
const float PERCEPTIBLE_SPEED = 0.1f;
// Note: Separately, we've arranged for starting/stopping animations by role (as we've done here) to pick up where they've left off when fading,
// so that you wouldn't notice the start/stop if it happens fast enough (e.g., one frame). But the print below would still be noisy.
float forwardSpeed = glm::dot(workingVelocity, front);
float rightLateralSpeed = glm::dot(workingVelocity, right);
float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP);
float rightTurningSpeed = rightTurningDelta / deltaTime;
bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED);
bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED;
auto updateRole = [&](const QString& role, bool isOn) {
isMoving = isMoving || isOn;
if (isOn) {
if (!isRunningRole(role)) {
qCDebug(animation) << "Rig STARTING" << role;
startAnimationByRole(role);
}
} else {
if (isRunningRole(role)) {
qCDebug(animation) << "Rig stopping" << role;
stopAnimationByRole(role);
}
}
};
updateRole("walk", forwardSpeed > PERCEPTIBLE_SPEED);
updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED);
updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f));
updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f));
isStrafing = isStrafing && !isMoving;
updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f));
updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f));
updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus.
}
_lastFront = front;
_lastPosition = worldPosition;
}
void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) {
// First normalize the fades so that they sum to 1.0.
// update the fade data in each animation (not normalized as they are an independent propert of animation)
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
float fadePerSecond = handle->getFadePerSecond();
float fade = handle->getFade();
if (fadePerSecond != 0.0f) {
fade += fadePerSecond * deltaTime;
if ((0.0f >= fade) || (fade >= 1.0f)) {
fade = glm::clamp(fade, 0.0f, 1.0f);
handle->setFadePerSecond(0.0f);
if (_enableAnimGraph) {
if (!_animNode) {
return;
}
// evaluate the animation
AnimNode::Triggers triggersOut;
AnimPoseVec poses = _animNode->evaluate(_animVars, deltaTime, triggersOut);
_animVars.clearTriggers();
for (auto& trigger : triggersOut) {
_animVars.setTrigger(trigger);
}
// copy poses into jointStates
const float PRIORITY = 1.0f;
for (size_t i = 0; i < poses.size(); i++) {
setJointRotationInConstrainedFrame((int)i, glm::inverse(_animSkeleton->getRelativeBindPose(i).rot) * poses[i].rot, PRIORITY, false);
}
} else {
// First normalize the fades so that they sum to 1.0.
// update the fade data in each animation (not normalized as they are an independent propert of animation)
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
float fadePerSecond = handle->getFadePerSecond();
float fade = handle->getFade();
if (fadePerSecond != 0.0f) {
fade += fadePerSecond * deltaTime;
if ((0.0f >= fade) || (fade >= 1.0f)) {
fade = glm::clamp(fade, 0.0f, 1.0f);
handle->setFadePerSecond(0.0f);
}
handle->setFade(fade);
if (fade <= 0.0f) { // stop any finished animations now
handle->setRunning(false, false); // but do not restore joints as it causes a flicker
}
}
handle->setFade(fade);
if (fade <= 0.0f) { // stop any finished animations now
handle->setRunning(false, false); // but do not restore joints as it causes a flicker
}
}
}
// sum the remaining fade data
float fadeTotal = 0.0f;
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
fadeTotal += handle->getFade();
}
float fadeSumSoFar = 0.0f;
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
handle->setPriority(1.0f);
// if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally.
float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count());
assert(normalizedFade != 0.0f);
// simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step.
// i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result.
// The formula here for mix is based on the idea that, at each step:
// fadeSum is to normalizedFade, as (1 - mix) is to mix
// i.e., fadeSumSoFar/normalizedFade = (1 - mix)/mix
// Then we solve for mix.
// Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1.
// Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++
float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f);
assert((0.0f <= mix) && (mix <= 1.0f));
fadeSumSoFar += normalizedFade;
handle->setMix(mix);
handle->simulate(deltaTime);
}
}
// sum the remaining fade data
float fadeTotal = 0.0f;
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
fadeTotal += handle->getFade();
}
float fadeSumSoFar = 0.0f;
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
handle->setPriority(1.0f);
// if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally.
float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count());
assert(normalizedFade != 0.0f);
// simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step.
// i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result.
// The formula here for mix is based on the idea that, at each step:
// fadeSum is to normalizedFade, as (1 - mix) is to mix
// i.e., fadeSumSoFar/normalizedFade = (1 - mix)/mix
// Then we solve for mix.
// Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1.
// Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++
float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f);
assert((0.0f <= mix) && (mix <= 1.0f));
fadeSumSoFar += normalizedFade;
handle->setMix(mix);
handle->simulate(deltaTime);
}
for (int i = 0; i < _jointStates.size(); i++) {
updateJointState(i, rootTransform);
}
@ -859,6 +985,7 @@ void Rig::updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3&
updateEyeJoint(leftEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade);
updateEyeJoint(rightEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade);
}
void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) {
if (index >= 0 && _jointStates[index].getParentIndex() >= 0) {
auto& state = _jointStates[index];
@ -877,3 +1004,30 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm
state.getDefaultRotation(), DEFAULT_PRIORITY);
}
}
void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) {
if (!_enableAnimGraph) {
return;
}
// convert to std::vector of joints
std::vector<FBXJoint> joints;
joints.reserve(fbxGeometry.joints.size());
for (auto& joint : fbxGeometry.joints) {
joints.push_back(joint);
}
// create skeleton
AnimPose geometryOffset(fbxGeometry.offset);
_animSkeleton = std::make_shared<AnimSkeleton>(joints, geometryOffset);
// load the anim graph
_animLoader.reset(new AnimNodeLoader(url));
connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) {
_animNode = nodeIn;
_animNode->setSkeleton(_animSkeleton);
});
connect(_animLoader.get(), &AnimNodeLoader::error, [this, url](int error, QString str) {
qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str;
});
}

View file

@ -40,6 +40,9 @@
#include "JointState.h" // We might want to change this (later) to something that doesn't depend on gpu, fbx and model. -HRS
#include "AnimNode.h"
#include "AnimNodeLoader.h"
class AnimationHandle;
typedef std::shared_ptr<AnimationHandle> AnimationHandlePointer;
@ -80,6 +83,7 @@ public:
bool isRunningRole(const QString& role); // There can be multiple animations per role, so this is more general than isRunningAnimation.
const QList<AnimationHandlePointer>& getRunningAnimations() const { return _runningAnimations; }
void deleteAnimations();
void destroyAnimGraph();
const QList<AnimationHandlePointer>& getAnimationHandles() const { return _animationHandles; }
void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false,
bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList());
@ -155,6 +159,8 @@ public:
virtual void updateJointState(int index, glm::mat4 rootTransform) = 0;
void setEnableRig(bool isEnabled) { _enableRig = isEnabled; }
void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; }
bool getEnableAnimGraph() const { return _enableAnimGraph; }
void updateFromHeadParameters(const HeadParameters& params);
void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation,
@ -163,6 +169,11 @@ public:
virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation,
float scale, float priority) = 0;
void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry);
AnimNode::ConstPointer getAnimNode() const { return _animNode; }
AnimSkeleton::ConstPointer getAnimSkeleton() const { return _animSkeleton; }
protected:
void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist);
@ -183,9 +194,21 @@ public:
QList<AnimationHandlePointer> _animationHandles;
QList<AnimationHandlePointer> _runningAnimations;
bool _enableRig;
bool _enableRig = false;
bool _enableAnimGraph = false;
glm::vec3 _lastFront;
glm::vec3 _lastPosition;
std::shared_ptr<AnimNode> _animNode;
std::shared_ptr<AnimSkeleton> _animSkeleton;
std::unique_ptr<AnimNodeLoader> _animLoader;
AnimVariantMap _animVars;
enum class RigRole {
Idle = 0,
Turn,
Move
};
RigRole _state = RigRole::Idle;
};
#endif /* defined(__hifi__Rig__) */

View file

@ -1722,7 +1722,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
glm::vec3 rotationOffset;
glm::vec3 preRotation, rotation, postRotation;
glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f);
glm::vec3 scalePivot, rotationPivot;
glm::vec3 scalePivot, rotationPivot, scaleOffset;
bool rotationMinX = false, rotationMinY = false, rotationMinZ = false;
bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false;
glm::vec3 rotationMin, rotationMax;
@ -1771,12 +1771,14 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
} else if (property.properties.at(0) == "Lcl Scaling") {
scale = getVec3(property.properties, index);
} else if (property.properties.at(0) == "ScalingOffset") {
scaleOffset = getVec3(property.properties, index);
// NOTE: these rotation limits are stored in degrees (NOT radians)
} else if (property.properties.at(0) == "RotationMin") {
rotationMin = getVec3(property.properties, index);
}
// NOTE: these rotation limits are stored in degrees (NOT radians)
else if (property.properties.at(0) == "RotationMax") {
} else if (property.properties.at(0) == "RotationMax") {
rotationMax = getVec3(property.properties, index);
} else if (property.properties.at(0) == "RotationMinX") {
@ -1843,8 +1845,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
model.preRotation = glm::quat(glm::radians(preRotation));
model.rotation = glm::quat(glm::radians(rotation));
model.postRotation = glm::quat(glm::radians(postRotation));
model.postTransform = glm::translate(-rotationPivot) * glm::translate(scalePivot) *
glm::scale(scale) * glm::translate(-scalePivot);
model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) *
glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot);
// NOTE: angles from the FBX file are in degrees
// so we convert them to radians for the FBXModel class
model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f,
@ -2306,7 +2308,9 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
break;
}
}
joint.bindTransformFoundInCluster = false;
geometry.joints.append(joint);
geometry.jointIndices.insert(model.name, geometry.joints.size());
@ -2534,7 +2538,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
FBXJoint& joint = geometry.joints[fbxCluster.jointIndex];
joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink));
joint.bindTransform = cluster.transformLink;
joint.bindTransformFoundInCluster = true;
// update the bind pose extents
glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform);
geometry.bindExtents.addPoint(bindTranslation);

View file

@ -64,12 +64,18 @@ public:
int parentIndex;
float distanceToParent;
float boneRadius;
glm::vec3 translation;
glm::mat4 preTransform;
glm::quat preRotation;
glm::quat rotation;
glm::quat postRotation;
glm::mat4 postTransform;
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
glm::vec3 translation; // T
glm::mat4 preTransform; // Roff * Rp
glm::quat preRotation; // Rpre
glm::quat rotation; // R
glm::quat postRotation; // Rpost
glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1
// World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1)
glm::mat4 transform;
glm::vec3 rotationMin; // radians
glm::vec3 rotationMax; // radians
@ -78,6 +84,7 @@ public:
glm::mat4 bindTransform;
QString name;
bool isSkeletonJoint;
bool bindTransformFoundInCluster;
};

View file

@ -0,0 +1,430 @@
//
// AnimDebugDraw.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 "animdebugdraw_vert.h"
#include "animdebugdraw_frag.h"
#include <gpu/Batch.h>
#include "AbstractViewStateInterface.h"
#include "RenderUtilsLogging.h"
#include "GLMHelpers.h"
#include "AnimDebugDraw.h"
struct Vertex {
glm::vec3 pos;
uint32_t rgba;
};
class AnimDebugDrawData {
public:
typedef render::Payload<AnimDebugDrawData> Payload;
typedef Payload::DataPointer Pointer;
AnimDebugDrawData() {
_vertexFormat = std::make_shared<gpu::Stream::Format>();
_vertexBuffer = std::make_shared<gpu::Buffer>();
_indexBuffer = std::make_shared<gpu::Buffer>();
_vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element::VEC3F_XYZ, 0);
_vertexFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element::COLOR_RGBA_32, offsetof(Vertex, rgba));
}
void render(RenderArgs* args) {
auto& batch = *args->_batch;
batch.setPipeline(_pipeline);
auto transform = Transform{};
batch.setModelTransform(transform);
batch.setInputFormat(_vertexFormat);
batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(Vertex));
batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0);
auto numIndices = _indexBuffer->getSize() / sizeof(uint16_t);
batch.drawIndexed(gpu::LINES, numIndices);
}
gpu::PipelinePointer _pipeline;
render::ItemID _item;
gpu::Stream::FormatPointer _vertexFormat;
gpu::BufferPointer _vertexBuffer;
gpu::BufferPointer _indexBuffer;
};
typedef render::Payload<AnimDebugDrawData> AnimDebugDrawPayload;
namespace render {
template <> const ItemKey payloadGetKey(const AnimDebugDrawData::Pointer& data) { return ItemKey::Builder::opaqueShape(); }
template <> const Item::Bound payloadGetBound(const AnimDebugDrawData::Pointer& data) { return Item::Bound(); }
template <> void payloadRender(const AnimDebugDrawData::Pointer& data, RenderArgs* args) {
data->render(args);
}
}
static AnimDebugDraw* instance = nullptr;
AnimDebugDraw& AnimDebugDraw::getInstance() {
if (!instance) {
instance = new AnimDebugDraw();
}
return *instance;
}
static uint32_t toRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return ((uint32_t)r | (uint32_t)g << 8 | (uint32_t)b << 16 | (uint32_t)a << 24);
}
static uint32_t toRGBA(const glm::vec4& v) {
return toRGBA(static_cast<uint8_t>(v.r * 255.0f),
static_cast<uint8_t>(v.g * 255.0f),
static_cast<uint8_t>(v.b * 255.0f),
static_cast<uint8_t>(v.a * 255.0f));
}
gpu::PipelinePointer AnimDebugDraw::_pipeline;
AnimDebugDraw::AnimDebugDraw() :
_itemID(0) {
auto state = std::make_shared<gpu::State>();
state->setCullMode(gpu::State::CULL_BACK);
state->setDepthTest(true, true, gpu::LESS_EQUAL);
state->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD,
gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA,
gpu::State::BLEND_OP_ADD, gpu::State::ONE);
auto vertShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(animdebugdraw_vert)));
auto fragShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(animdebugdraw_frag)));
auto program = gpu::ShaderPointer(gpu::Shader::createProgram(vertShader, fragShader));
_pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state));
_animDebugDrawData = std::make_shared<AnimDebugDrawData>();
_animDebugDrawPayload = std::make_shared<AnimDebugDrawPayload>(_animDebugDrawData);
_animDebugDrawData->_pipeline = _pipeline;
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
if (scene) {
_itemID = scene->allocateID();
render::PendingChanges pendingChanges;
pendingChanges.resetItem(_itemID, _animDebugDrawPayload);
scene->enqueuePendingChanges(pendingChanges);
}
// HACK: add red, green and blue axis at (1,1,1)
_animDebugDrawData->_vertexBuffer->resize(sizeof(Vertex) * 6);
Vertex* data = (Vertex*)_animDebugDrawData->_vertexBuffer->editData();
data[0].pos = glm::vec3(1.0, 1.0f, 1.0f);
data[0].rgba = toRGBA(255, 0, 0, 255);
data[1].pos = glm::vec3(2.0, 1.0f, 1.0f);
data[1].rgba = toRGBA(255, 0, 0, 255);
data[2].pos = glm::vec3(1.0, 1.0f, 1.0f);
data[2].rgba = toRGBA(0, 255, 0, 255);
data[3].pos = glm::vec3(1.0, 2.0f, 1.0f);
data[3].rgba = toRGBA(0, 255, 0, 255);
data[4].pos = glm::vec3(1.0, 1.0f, 1.0f);
data[4].rgba = toRGBA(0, 0, 255, 255);
data[5].pos = glm::vec3(1.0, 1.0f, 2.0f);
data[5].rgba = toRGBA(0, 0, 255, 255);
_animDebugDrawData->_indexBuffer->resize(sizeof(uint16_t) * 6);
uint16_t* indices = (uint16_t*)_animDebugDrawData->_indexBuffer->editData();
for (int i = 0; i < 6; i++) {
indices[i] = i;
}
}
AnimDebugDraw::~AnimDebugDraw() {
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
if (scene && _itemID) {
render::PendingChanges pendingChanges;
pendingChanges.removeItem(_itemID);
scene->enqueuePendingChanges(pendingChanges);
}
}
void AnimDebugDraw::addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color) {
_skeletons[key] = SkeletonInfo(skeleton, rootPose, color);
}
void AnimDebugDraw::removeSkeleton(std::string key) {
_skeletons.erase(key);
}
void AnimDebugDraw::addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color) {
_animNodes[key] = AnimNodeInfo(animNode, rootPose, color);
}
void AnimDebugDraw::removeAnimNode(std::string key) {
_animNodes.erase(key);
}
static const uint32_t red = toRGBA(255, 0, 0, 255);
static const uint32_t green = toRGBA(0, 255, 0, 255);
static const uint32_t blue = toRGBA(0, 0, 255, 255);
const int NUM_CIRCLE_SLICES = 24;
static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius, Vertex*& v) {
AnimPose finalPose = rootPose * pose;
glm::vec3 base = rootPose * pose.trans;
glm::vec3 xRing[NUM_CIRCLE_SLICES + 1]; // one extra for last index.
glm::vec3 yRing[NUM_CIRCLE_SLICES + 1];
glm::vec3 zRing[NUM_CIRCLE_SLICES + 1];
const float dTheta = (2.0f * (float)M_PI) / NUM_CIRCLE_SLICES;
for (int i = 0; i < NUM_CIRCLE_SLICES + 1; i++) {
float rCosTheta = radius * cos(dTheta * i);
float rSinTheta = radius * sin(dTheta * i);
xRing[i] = finalPose * glm::vec3(0.0f, rCosTheta, rSinTheta);
yRing[i] = finalPose * glm::vec3(rCosTheta, 0.0f, rSinTheta);
zRing[i] = finalPose * glm::vec3(rCosTheta, rSinTheta, 0.0f);
}
// x-axis
v->pos = base;
v->rgba = red;
v++;
v->pos = finalPose * glm::vec3(radius * 2.0f, 0.0f, 0.0f);
v->rgba = red;
v++;
// x-ring
for (int i = 0; i < NUM_CIRCLE_SLICES; i++) {
v->pos = xRing[i];
v->rgba = red;
v++;
v->pos = xRing[i + 1];
v->rgba = red;
v++;
}
// y-axis
v->pos = base;
v->rgba = green;
v++;
v->pos = finalPose * glm::vec3(0.0f, radius * 2.0f, 0.0f);
v->rgba = green;
v++;
// y-ring
for (int i = 0; i < NUM_CIRCLE_SLICES; i++) {
v->pos = yRing[i];
v->rgba = green;
v++;
v->pos = yRing[i + 1];
v->rgba = green;
v++;
}
// z-axis
v->pos = base;
v->rgba = blue;
v++;
v->pos = finalPose * glm::vec3(0.0f, 0.0f, radius * 2.0f);
v->rgba = blue;
v++;
// z-ring
for (int i = 0; i < NUM_CIRCLE_SLICES; i++) {
v->pos = zRing[i];
v->rgba = blue;
v++;
v->pos = zRing[i + 1];
v->rgba = blue;
v++;
}
}
static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPose& parentPose,
float radius, const glm::vec4& colorVec, Vertex*& v) {
uint32_t color = toRGBA(colorVec);
AnimPose pose0 = rootPose * parentPose;
AnimPose pose1 = rootPose * pose;
glm::vec3 boneAxisWorld = glm::normalize(pose1.trans - pose0.trans);
glm::vec3 boneAxis0 = glm::normalize(pose0.inverse().xformVector(boneAxisWorld));
glm::vec3 boneAxis1 = glm::normalize(pose1.inverse().xformVector(boneAxisWorld));
glm::vec3 boneBase = pose0 * (boneAxis0 * radius);
glm::vec3 boneTip = pose1 * (boneAxis1 * -radius);
const int NUM_BASE_CORNERS = 4;
// make sure there's room between the two bones to draw a nice bone link.
if (glm::dot(boneTip - pose0.trans, boneAxisWorld) > glm::dot(boneBase - pose0.trans, boneAxisWorld)) {
// there is room, so lets draw a nice bone
glm::vec3 uAxis, vAxis, wAxis;
generateBasisVectors(boneAxis0, glm::vec3(1, 0, 0), uAxis, vAxis, wAxis);
glm::vec3 boneBaseCorners[NUM_BASE_CORNERS];
boneBaseCorners[0] = pose0 * ((uAxis * radius) + (vAxis * radius) + (wAxis * radius));
boneBaseCorners[1] = pose0 * ((uAxis * radius) - (vAxis * radius) + (wAxis * radius));
boneBaseCorners[2] = pose0 * ((uAxis * radius) - (vAxis * radius) - (wAxis * radius));
boneBaseCorners[3] = pose0 * ((uAxis * radius) + (vAxis * radius) - (wAxis * radius));
for (int i = 0; i < NUM_BASE_CORNERS; i++) {
v->pos = boneBaseCorners[i];
v->rgba = color;
v++;
v->pos = boneBaseCorners[(i + 1) % NUM_BASE_CORNERS];
v->rgba = color;
v++;
}
for (int i = 0; i < NUM_BASE_CORNERS; i++) {
v->pos = boneBaseCorners[i];
v->rgba = color;
v++;
v->pos = boneTip;
v->rgba = color;
v++;
}
} else {
// There's no room between the bones to draw the link.
// just draw a line between the two bone centers.
// We add the same line multiple times, so the vertex count is correct.
for (int i = 0; i < NUM_BASE_CORNERS * 2; i++) {
v->pos = pose0.trans;
v->rgba = color;
v++;
v->pos = pose1.trans;
v->rgba = color;
v++;
}
}
}
void AnimDebugDraw::update() {
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
if (!scene) {
return;
}
render::PendingChanges pendingChanges;
pendingChanges.updateItem<AnimDebugDrawData>(_itemID, [&](AnimDebugDrawData& data) {
const size_t VERTICES_PER_BONE = (6 + (NUM_CIRCLE_SLICES * 2) * 3);
const size_t VERTICES_PER_LINK = 8 * 2;
const float BONE_RADIUS = 0.0075f;
// figure out how many verts we will need.
int numVerts = 0;
for (auto&& iter : _skeletons) {
AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second);
numVerts += skeleton->getNumJoints() * VERTICES_PER_BONE;
for (int i = 0; i < skeleton->getNumJoints(); i++) {
auto parentIndex = skeleton->getParentIndex(i);
if (parentIndex >= 0) {
numVerts += VERTICES_PER_LINK;
}
}
}
for (auto&& iter : _animNodes) {
AnimNode::ConstPointer& animNode = std::get<0>(iter.second);
auto poses = animNode->getPosesInternal();
numVerts += poses.size() * VERTICES_PER_BONE;
auto skeleton = animNode->getSkeleton();
for (size_t i = 0; i < poses.size(); i++) {
auto parentIndex = skeleton->getParentIndex(i);
if (parentIndex >= 0) {
numVerts += VERTICES_PER_LINK;
}
}
}
data._vertexBuffer->resize(sizeof(Vertex) * numVerts);
Vertex* verts = (Vertex*)data._vertexBuffer->editData();
Vertex* v = verts;
for (auto&& iter : _skeletons) {
AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second);
AnimPose rootPose = std::get<1>(iter.second);
int hipsIndex = skeleton->nameToJointIndex("Hips");
if (hipsIndex >= 0) {
rootPose.trans -= skeleton->getRelativeBindPose(hipsIndex).trans;
}
glm::vec4 color = std::get<2>(iter.second);
for (int i = 0; i < skeleton->getNumJoints(); i++) {
AnimPose pose = skeleton->getAbsoluteBindPose(i);
const float radius = BONE_RADIUS / (pose.scale.x * rootPose.scale.x);
// draw bone
addBone(rootPose, pose, radius, v);
// draw link to parent
auto parentIndex = skeleton->getParentIndex(i);
if (parentIndex >= 0) {
assert(parentIndex < skeleton->getNumJoints());
AnimPose parentPose = skeleton->getAbsoluteBindPose(parentIndex);
addLink(rootPose, pose, parentPose, radius, color, v);
}
}
}
for (auto&& iter : _animNodes) {
AnimNode::ConstPointer& animNode = std::get<0>(iter.second);
AnimPose rootPose = std::get<1>(iter.second);
if (animNode->_skeleton) {
int hipsIndex = animNode->_skeleton->nameToJointIndex("Hips");
if (hipsIndex >= 0) {
rootPose.trans -= animNode->_skeleton->getRelativeBindPose(hipsIndex).trans;
}
}
glm::vec4 color = std::get<2>(iter.second);
auto poses = animNode->getPosesInternal();
auto skeleton = animNode->getSkeleton();
std::vector<AnimPose> absAnimPose;
absAnimPose.resize(skeleton->getNumJoints());
for (size_t i = 0; i < poses.size(); i++) {
auto parentIndex = skeleton->getParentIndex(i);
if (parentIndex >= 0) {
absAnimPose[i] = absAnimPose[parentIndex] * poses[i];
} else {
absAnimPose[i] = poses[i];
}
const float radius = BONE_RADIUS / (absAnimPose[i].scale.x * rootPose.scale.x);
addBone(rootPose, absAnimPose[i], radius, v);
if (parentIndex >= 0) {
assert((size_t)parentIndex < poses.size());
// draw line to parent
addLink(rootPose, absAnimPose[i], absAnimPose[parentIndex], radius, color, v);
}
}
}
assert(numVerts == (v - verts));
data._indexBuffer->resize(sizeof(uint16_t) * numVerts);
uint16_t* indices = (uint16_t*)data._indexBuffer->editData();
for (int i = 0; i < numVerts; i++) {
indices[i] = i;
}
});
scene->enqueuePendingChanges(pendingChanges);
}

View file

@ -0,0 +1,56 @@
//
// AnimDebugDraw.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_AnimDebugDraw_h
#define hifi_AnimDebugDraw_h
#include <tuple>
#include "render/Scene.h"
#include "gpu/Pipeline.h"
#include "AnimNode.h"
#include "AnimSkeleton.h"
class AnimDebugDrawData;
typedef render::Payload<AnimDebugDrawData> AnimDebugDrawPayload;
class AnimDebugDraw {
public:
static AnimDebugDraw& getInstance();
AnimDebugDraw();
~AnimDebugDraw();
void addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color);
void removeSkeleton(std::string key);
void addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color);
void removeAnimNode(std::string key);
void update();
protected:
std::shared_ptr<AnimDebugDrawData> _animDebugDrawData;
std::shared_ptr<AnimDebugDrawPayload> _animDebugDrawPayload;
render::ItemID _itemID;
static gpu::PipelinePointer _pipeline;
typedef std::tuple<AnimSkeleton::ConstPointer, AnimPose, glm::vec4> SkeletonInfo;
typedef std::tuple<AnimNode::ConstPointer, AnimPose, glm::vec4> AnimNodeInfo;
std::unordered_map<std::string, SkeletonInfo> _skeletons;
std::unordered_map<std::string, AnimNodeInfo> _animNodes;
// no copies
AnimDebugDraw(const AnimDebugDraw&) = delete;
AnimDebugDraw& operator=(const AnimDebugDraw&) = delete;
};
#endif // hifi_AnimDebugDraw

View file

@ -1419,6 +1419,7 @@ void Model::deleteGeometry() {
_rig->clearJointStates();
_meshStates.clear();
_rig->deleteAnimations();
_rig->destroyAnimGraph();
_blendedBlendshapeCoefficients.clear();
}

View file

@ -0,0 +1,19 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
//
// unlit untextured fragment shader
//
// 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
//
in vec4 _color;
out vec4 _fragColor;
void main(void) {
_fragColor = _color;
}

View file

@ -0,0 +1,26 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
//
// unlit untextured vertex shader
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@include gpu/Inputs.slh@>
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
out vec4 _color;
void main(void) {
// pass along the diffuse color
_color = inColor.rgba;
TransformCamera cam = getTransformCamera();
TransformObject obj = getTransformObject();
<$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
}

View file

@ -408,3 +408,22 @@ glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p) {
glm::vec4 temp = m * glm::vec4(p, 1.0f);
return glm::vec3(temp.x / temp.w, temp.y / temp.w, temp.z / temp.w);
}
glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v) {
glm::mat3 rot(m);
return glm::inverse(glm::transpose(rot)) * v;
}
void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis,
glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut) {
uAxisOut = glm::normalize(primaryAxis);
wAxisOut = glm::cross(uAxisOut, secondaryAxis);
if (glm::length(wAxisOut) > 0.0f) {
wAxisOut = glm::normalize(wAxisOut);
} else {
wAxisOut = glm::normalize(glm::cross(uAxisOut, glm::vec3(0, 1, 0)));
}
vAxisOut = glm::cross(wAxisOut, uAxisOut);
}

View file

@ -177,9 +177,35 @@ T toNormalizedDeviceScale(const T& value, const T& size) {
#define PITCH(euler) euler.x
#define ROLL(euler) euler.z
// vec2 lerp - linear interpolate
template<typename T, glm::precision P>
glm::detail::tvec2<T, P> lerp(const glm::detail::tvec2<T, P>& x, const glm::detail::tvec2<T, P>& y, T a) {
return x * (T(1) - a) + (y * a);
}
// vec3 lerp - linear interpolate
template<typename T, glm::precision P>
glm::detail::tvec3<T, P> lerp(const glm::detail::tvec3<T, P>& x, const glm::detail::tvec3<T, P>& y, T a) {
return x * (T(1) - a) + (y * a);
}
// vec4 lerp - linear interpolate
template<typename T, glm::precision P>
glm::detail::tvec4<T, P> lerp(const glm::detail::tvec4<T, P>& x, const glm::detail::tvec4<T, P>& y, T a) {
return x * (T(1) - a) + (y * a);
}
glm::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p);
glm::quat cancelOutRollAndPitch(const glm::quat& q);
glm::mat4 cancelOutRollAndPitch(const glm::mat4& m);
glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p);
glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v);
// Calculate an orthogonal basis from a primary and secondary axis.
// The uAxis, vAxis & wAxis will form an orthognal basis.
// The primary axis will be the uAxis.
// The vAxis will be as close as possible to to the secondary axis.
void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis,
glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut);
#endif // hifi_GLMHelpers_h

View file

@ -129,6 +129,7 @@ public:
static Transform& inverseMult(Transform& result, const Transform& left, const Transform& right);
Vec4 transform(const Vec4& pos) const;
Vec3 transform(const Vec3& pos) const;
protected:
@ -504,6 +505,12 @@ inline Transform::Vec4 Transform::transform(const Vec4& pos) const {
return m * pos;
}
inline Transform::Vec3 Transform::transform(const Vec3& pos) const {
Mat4 m;
getMatrix(m);
Vec4 result = m * Vec4(pos, 1.0f);
return Vec3(result.x / result.w, result.y / result.w, result.z / result.w);
}
inline Transform::Mat4& Transform::getCachedMatrix(Transform::Mat4& result) const {
updateCache();

View file

@ -1,7 +1,7 @@
# Declare dependencies
macro (setup_testcase_dependencies)
# link in the shared libraries
link_hifi_libraries(shared animation gpu fbx model)
link_hifi_libraries(shared animation gpu fbx model networking)
copy_dlls_beside_windows_executable()
endmacro ()

View file

@ -0,0 +1,225 @@
//
// AnimTests.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 "AnimTests.h"
#include "AnimNodeLoader.h"
#include "AnimClip.h"
#include "AnimBlendLinear.h"
#include "AnimationLogging.h"
#include "AnimVariant.h"
#include <../QTestExtensions.h>
QTEST_MAIN(AnimTests)
const float EPSILON = 0.001f;
void AnimTests::initTestCase() {
auto animationCache = DependencyManager::set<AnimationCache>();
auto resourceCacheSharedItems = DependencyManager::set<ResourceCacheSharedItems>();
}
void AnimTests::cleanupTestCase() {
DependencyManager::destroy<AnimationCache>();
}
void AnimTests::testClipInternalState() {
std::string id = "my anim clip";
std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx";
float startFrame = 2.0f;
float endFrame = 20.0f;
float timeScale = 1.1f;
bool loopFlag = true;
AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag);
QVERIFY(clip.getID() == id);
QVERIFY(clip.getType() == AnimNode::Type::Clip);
QVERIFY(clip._url == url);
QVERIFY(clip._startFrame == startFrame);
QVERIFY(clip._endFrame == endFrame);
QVERIFY(clip._timeScale == timeScale);
QVERIFY(clip._loopFlag == loopFlag);
}
static float framesToSec(float secs) {
const float FRAMES_PER_SECOND = 30.0f;
return secs / FRAMES_PER_SECOND;
}
void AnimTests::testClipEvaulate() {
std::string id = "myClipNode";
std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx";
float startFrame = 2.0f;
float endFrame = 22.0f;
float timeScale = 1.0f;
float loopFlag = true;
auto vars = AnimVariantMap();
vars.set("FalseVar", false);
AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag);
AnimNode::Triggers triggers;
clip.evaluate(vars, framesToSec(10.0f), triggers);
QCOMPARE_WITH_ABS_ERROR(clip._frame, 12.0f, EPSILON);
// does it loop?
triggers.clear();
clip.evaluate(vars, framesToSec(11.0f), triggers);
QCOMPARE_WITH_ABS_ERROR(clip._frame, 3.0f, EPSILON);
// did we receive a loop trigger?
QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnLoop") != triggers.end());
// does it pause at end?
triggers.clear();
clip.setLoopFlagVar("FalseVar");
clip.evaluate(vars, framesToSec(20.0f), triggers);
QCOMPARE_WITH_ABS_ERROR(clip._frame, 22.0f, EPSILON);
// did we receive a done trigger?
QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnDone") != triggers.end());
}
void AnimTests::testClipEvaulateWithVars() {
std::string id = "myClipNode";
std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx";
float startFrame = 2.0f;
float endFrame = 22.0f;
float timeScale = 1.0f;
float loopFlag = true;
float startFrame2 = 22.0f;
float endFrame2 = 100.0f;
float timeScale2 = 1.2f;
bool loopFlag2 = false;
auto vars = AnimVariantMap();
vars.set("startFrame2", startFrame2);
vars.set("endFrame2", endFrame2);
vars.set("timeScale2", timeScale2);
vars.set("loopFlag2", loopFlag2);
AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag);
clip.setStartFrameVar("startFrame2");
clip.setEndFrameVar("endFrame2");
clip.setTimeScaleVar("timeScale2");
clip.setLoopFlagVar("loopFlag2");
AnimNode::Triggers triggers;
clip.evaluate(vars, framesToSec(0.1f), triggers);
// verify that the values from the AnimVariantMap made it into the clipNode's
// internal state
QVERIFY(clip._startFrame == startFrame2);
QVERIFY(clip._endFrame == endFrame2);
QVERIFY(clip._timeScale == timeScale2);
QVERIFY(clip._loopFlag == loopFlag2);
}
void AnimTests::testLoader() {
auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json");
AnimNodeLoader loader(url);
const int timeout = 1000;
QEventLoop loop;
QTimer timer;
timer.setInterval(timeout);
timer.setSingleShot(true);
AnimNode::Pointer node = nullptr;
connect(&loader, &AnimNodeLoader::success, [&](AnimNode::Pointer nodeIn) { node = nodeIn; });
loop.connect(&loader, SIGNAL(success(AnimNode::Pointer)), SLOT(quit()));
loop.connect(&loader, SIGNAL(error(int, QString)), SLOT(quit()));
loop.connect(&timer, SIGNAL(timeout()), SLOT(quit()));
timer.start();
loop.exec();
QVERIFY((bool)node);
QVERIFY(node->getID() == "blend");
QVERIFY(node->getType() == AnimNode::Type::BlendLinear);
QVERIFY((bool)node);
QVERIFY(node->getID() == "blend");
QVERIFY(node->getType() == AnimNode::Type::BlendLinear);
auto blend = std::static_pointer_cast<AnimBlendLinear>(node);
QVERIFY(blend->_alpha == 0.5f);
QVERIFY(node->getChildCount() == 3);
std::shared_ptr<AnimNode> nodes[3] = { node->getChild(0), node->getChild(1), node->getChild(2) };
QVERIFY(nodes[0]->getID() == "test01");
QVERIFY(nodes[0]->getChildCount() == 0);
QVERIFY(nodes[1]->getID() == "test02");
QVERIFY(nodes[1]->getChildCount() == 0);
QVERIFY(nodes[2]->getID() == "test03");
QVERIFY(nodes[2]->getChildCount() == 0);
auto test01 = std::static_pointer_cast<AnimClip>(nodes[0]);
QVERIFY(test01->_url == "test01.fbx");
QVERIFY(test01->_startFrame == 1.0f);
QVERIFY(test01->_endFrame == 20.0f);
QVERIFY(test01->_timeScale == 1.0f);
QVERIFY(test01->_loopFlag == false);
auto test02 = std::static_pointer_cast<AnimClip>(nodes[1]);
QVERIFY(test02->_url == "test02.fbx");
QVERIFY(test02->_startFrame == 2.0f);
QVERIFY(test02->_endFrame == 21.0f);
QVERIFY(test02->_timeScale == 0.9f);
QVERIFY(test02->_loopFlag == true);
}
void AnimTests::testVariant() {
auto defaultVar = AnimVariant();
auto boolVar = AnimVariant(true);
auto intVar = AnimVariant(1);
auto floatVar = AnimVariant(1.0f);
auto vec3Var = AnimVariant(glm::vec3(1.0f, 2.0f, 3.0f));
auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, 3.0f, 4.0f));
auto mat4Var = AnimVariant(glm::mat4(glm::vec4(1.0f, 2.0f, 3.0f, 4.0f),
glm::vec4(5.0f, 6.0f, 7.0f, 8.0f),
glm::vec4(9.0f, 10.0f, 11.0f, 12.0f),
glm::vec4(13.0f, 14.0f, 15.0f, 16.0f)));
QVERIFY(defaultVar.isBool());
QVERIFY(defaultVar.getBool() == false);
QVERIFY(boolVar.isBool());
QVERIFY(boolVar.getBool() == true);
QVERIFY(intVar.isInt());
QVERIFY(intVar.getInt() == 1);
QVERIFY(floatVar.isFloat());
QVERIFY(floatVar.getFloat() == 1.0f);
QVERIFY(vec3Var.isVec3());
auto v = vec3Var.getVec3();
QVERIFY(v.x == 1.0f);
QVERIFY(v.y == 2.0f);
QVERIFY(v.z == 3.0f);
QVERIFY(quatVar.isQuat());
auto q = quatVar.getQuat();
QVERIFY(q.w == 1.0f);
QVERIFY(q.x == 2.0f);
QVERIFY(q.y == 3.0f);
QVERIFY(q.z == 4.0f);
QVERIFY(mat4Var.isMat4());
auto m = mat4Var.getMat4();
QVERIFY(m[0].x == 1.0f);
QVERIFY(m[3].w == 16.0f);
}

View file

@ -0,0 +1,28 @@
//
// AnimTests.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_AnimTests_h
#define hifi_AnimTests_h
#include <QtTest/QtTest>
#include <glm/glm.hpp>
class AnimTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testClipInternalState();
void testClipEvaulate();
void testClipEvaulateWithVars();
void testLoader();
void testVariant();
};
#endif // hifi_AnimTests_h

View file

@ -78,24 +78,25 @@ void RigTests::initTestCase() {
#ifdef FROM_FILE
QFile file(FROM_FILE);
QCOMPARE(file.open(QIODevice::ReadOnly), true);
FBXGeometry geometry = readFBX(file.readAll(), QVariantHash());
FBXGeometry* geometry = readFBX(file.readAll(), QVariantHash());
#else
QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx");
QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request
auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QCOMPARE(fbxHttpCode, 200);
FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash());
FBXGeometry* geometry = readFBX(reply->readAll(), QVariantHash());
#endif
QVERIFY((bool)geometry);
QVector<JointState> jointStates;
for (int i = 0; i < geometry.joints.size(); ++i) {
JointState state(geometry.joints[i]);
for (int i = 0; i < geometry->joints.size(); ++i) {
JointState state(geometry->joints[i]);
jointStates.append(state);
}
_rig = std::make_shared<AvatarRig>();
_rig->initJointStates(jointStates, glm::mat4(), 0, 41, 40, 39, 17, 16, 15); // FIXME? get by name? do we really want to exclude the shoulder blades?
std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl;
std::cout << "Rig is ready " << geometry->joints.count() << " joints " << std::endl;
reportAll(_rig);
}

View file

@ -0,0 +1,191 @@
{
"version": "1.0",
"root": {
"id": "root",
"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": []
}
]
}
}

View file

@ -0,0 +1,48 @@
{
"version": "1.0",
"root": {
"id": "blend",
"type": "blendLinear",
"data": {
"alpha": 0.5
},
"children": [
{
"id": "test01",
"type": "clip",
"data": {
"url": "test01.fbx",
"startFrame": 1.0,
"endFrame": 20.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
},
{
"id": "test02",
"type": "clip",
"data": {
"url": "test02.fbx",
"startFrame": 2.0,
"endFrame": 21.0,
"timeScale": 0.9,
"loopFlag": true
},
"children": []
},
{
"id": "test03",
"type": "clip",
"data": {
"url": "test03.fbx",
"startFrame": 3.0,
"endFrame": 20.0,
"timeScale": 1.0,
"loopFlag": false
},
"children": []
}
]
}
}