mirror of
https://github.com/overte-org/overte.git
synced 2025-04-20 03:44:02 +02:00
Merge pull request #5700 from hyperlogic/ajt/new-anim-system
New Animation System
This commit is contained in:
commit
d623ba3ef2
41 changed files with 3390 additions and 101 deletions
|
@ -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...
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -606,3 +606,7 @@ bool SkeletonModel::hasSkeleton() {
|
|||
|
||||
void SkeletonModel::onInvalidate() {
|
||||
}
|
||||
|
||||
void SkeletonModel::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) {
|
||||
_rig->initAnimGraph(url, fbxGeometry);
|
||||
}
|
||||
|
|
|
@ -105,6 +105,8 @@ public:
|
|||
|
||||
virtual void onInvalidate() override;
|
||||
|
||||
void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry);
|
||||
|
||||
signals:
|
||||
|
||||
void skeletonLoaded();
|
||||
|
|
62
libraries/animation/src/AnimBlendLinear.cpp
Normal file
62
libraries/animation/src/AnimBlendLinear.cpp
Normal 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;
|
||||
}
|
52
libraries/animation/src/AnimBlendLinear.h
Normal file
52
libraries/animation/src/AnimBlendLinear.h
Normal 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
|
161
libraries/animation/src/AnimClip.cpp
Normal file
161
libraries/animation/src/AnimClip.cpp
Normal 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;
|
||||
}
|
74
libraries/animation/src/AnimClip.h
Normal file
74
libraries/animation/src/AnimClip.h
Normal 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
|
115
libraries/animation/src/AnimNode.h
Normal file
115
libraries/animation/src/AnimNode.h
Normal 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
|
434
libraries/animation/src/AnimNodeLoader.cpp
Normal file
434
libraries/animation/src/AnimNodeLoader.cpp
Normal 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");
|
||||
}
|
52
libraries/animation/src/AnimNodeLoader.h
Normal file
52
libraries/animation/src/AnimNodeLoader.h
Normal 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
|
181
libraries/animation/src/AnimOverlay.cpp
Normal file
181
libraries/animation/src/AnimOverlay.cpp
Normal 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);
|
||||
}
|
80
libraries/animation/src/AnimOverlay.h
Normal file
80
libraries/animation/src/AnimOverlay.h
Normal 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
|
176
libraries/animation/src/AnimSkeleton.cpp
Normal file
176
libraries/animation/src/AnimSkeleton.cpp
Normal 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
|
78
libraries/animation/src/AnimSkeleton.h
Normal file
78
libraries/animation/src/AnimSkeleton.h
Normal 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
|
114
libraries/animation/src/AnimStateMachine.cpp
Normal file
114
libraries/animation/src/AnimStateMachine.cpp
Normal 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;
|
||||
}
|
134
libraries/animation/src/AnimStateMachine.h
Normal file
134
libraries/animation/src/AnimStateMachine.h
Normal 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
|
22
libraries/animation/src/AnimUtil.cpp
Normal file
22
libraries/animation/src/AnimUtil.cpp
Normal 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);
|
||||
}
|
||||
}
|
24
libraries/animation/src/AnimUtil.h
Normal file
24
libraries/animation/src/AnimUtil.h
Normal 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
|
||||
|
||||
|
161
libraries/animation/src/AnimVariant.h
Normal file
161
libraries/animation/src/AnimVariant.h
Normal 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
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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__) */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
430
libraries/render-utils/src/AnimDebugDraw.cpp
Normal file
430
libraries/render-utils/src/AnimDebugDraw.cpp
Normal 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);
|
||||
}
|
56
libraries/render-utils/src/AnimDebugDraw.h
Normal file
56
libraries/render-utils/src/AnimDebugDraw.h
Normal 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
|
|
@ -1419,6 +1419,7 @@ void Model::deleteGeometry() {
|
|||
_rig->clearJointStates();
|
||||
_meshStates.clear();
|
||||
_rig->deleteAnimations();
|
||||
_rig->destroyAnimGraph();
|
||||
_blendedBlendshapeCoefficients.clear();
|
||||
}
|
||||
|
||||
|
|
19
libraries/render-utils/src/animdebugdraw.slf
Normal file
19
libraries/render-utils/src/animdebugdraw.slf
Normal 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;
|
||||
}
|
26
libraries/render-utils/src/animdebugdraw.slv
Normal file
26
libraries/render-utils/src/animdebugdraw.slv
Normal 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)$>
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 ()
|
||||
|
|
225
tests/animation/src/AnimTests.cpp
Normal file
225
tests/animation/src/AnimTests.cpp
Normal 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);
|
||||
}
|
28
tests/animation/src/AnimTests.h
Normal file
28
tests/animation/src/AnimTests.h
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
191
tests/animation/src/data/avatar.json
Normal file
191
tests/animation/src/data/avatar.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
48
tests/animation/src/data/test.json
Normal file
48
tests/animation/src/data/test.json
Normal 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue