mirror of
https://github.com/overte-org/overte.git
synced 2025-08-08 17:17:58 +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/Stats.h"
|
||||||
#include "ui/AddressBarDialog.h"
|
#include "ui/AddressBarDialog.h"
|
||||||
#include "ui/UpdateDialog.h"
|
#include "ui/UpdateDialog.h"
|
||||||
|
|
||||||
#include "ui/overlays/Cube3DOverlay.h"
|
#include "ui/overlays/Cube3DOverlay.h"
|
||||||
|
|
||||||
#include "PluginContainerProxy.h"
|
#include "PluginContainerProxy.h"
|
||||||
|
#include "AnimDebugDraw.h"
|
||||||
|
|
||||||
// ON WIndows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
|
// ON WIndows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
|
||||||
// FIXME seems to be broken.
|
// FIXME seems to be broken.
|
||||||
|
@ -2889,6 +2889,11 @@ void Application::update(float deltaTime) {
|
||||||
loadViewFrustum(_myCamera, _viewFrustum);
|
loadViewFrustum(_myCamera, _viewFrustum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update animation debug draw renderer
|
||||||
|
{
|
||||||
|
AnimDebugDraw::getInstance().update();
|
||||||
|
}
|
||||||
|
|
||||||
quint64 now = usecTimestampNow();
|
quint64 now = usecTimestampNow();
|
||||||
|
|
||||||
// Update my voxel servers with my current voxel query...
|
// Update my voxel servers with my current voxel query...
|
||||||
|
|
|
@ -434,6 +434,14 @@ Menu::Menu() {
|
||||||
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false);
|
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false);
|
||||||
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, false,
|
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, false,
|
||||||
avatar, SLOT(setEnableRigAnimations(bool)));
|
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::DisableEyelidAdjustment, 0, false);
|
||||||
addCheckableActionToQMenuAndActionHash(avatarDebugMenu,
|
addCheckableActionToQMenuAndActionHash(avatarDebugMenu,
|
||||||
MenuOption::Connexion,
|
MenuOption::Connexion,
|
||||||
|
|
|
@ -132,6 +132,8 @@ namespace MenuOption {
|
||||||
const QString AddRemoveFriends = "Add/Remove Friends...";
|
const QString AddRemoveFriends = "Add/Remove Friends...";
|
||||||
const QString AddressBar = "Show Address Bar";
|
const QString AddressBar = "Show Address Bar";
|
||||||
const QString Animations = "Animations...";
|
const QString Animations = "Animations...";
|
||||||
|
const QString AnimDebugDrawAnimPose = "Debug Draw Animation";
|
||||||
|
const QString AnimDebugDrawBindPose = "Debug Draw Bind Pose";
|
||||||
const QString Antialiasing = "Antialiasing";
|
const QString Antialiasing = "Antialiasing";
|
||||||
const QString Atmosphere = "Atmosphere";
|
const QString Atmosphere = "Atmosphere";
|
||||||
const QString Attachments = "Attachments...";
|
const QString Attachments = "Attachments...";
|
||||||
|
@ -186,6 +188,7 @@ namespace MenuOption {
|
||||||
const QString EchoServerAudio = "Echo Server Audio";
|
const QString EchoServerAudio = "Echo Server Audio";
|
||||||
const QString EditEntitiesHelp = "Edit Entities Help...";
|
const QString EditEntitiesHelp = "Edit Entities Help...";
|
||||||
const QString Enable3DTVMode = "Enable 3DTV Mode";
|
const QString Enable3DTVMode = "Enable 3DTV Mode";
|
||||||
|
const QString EnableAnimGraph = "Enable Anim Graph";
|
||||||
const QString EnableCharacterController = "Enable avatar collisions";
|
const QString EnableCharacterController = "Enable avatar collisions";
|
||||||
const QString EnableRigAnimations = "Enable Rig Animations";
|
const QString EnableRigAnimations = "Enable Rig Animations";
|
||||||
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";
|
const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation";
|
||||||
|
@ -215,6 +218,7 @@ namespace MenuOption {
|
||||||
const QString Log = "Log";
|
const QString Log = "Log";
|
||||||
const QString LogExtraTimings = "Log Extra Timing Details";
|
const QString LogExtraTimings = "Log Extra Timing Details";
|
||||||
const QString LowVelocityFilter = "Low Velocity Filter";
|
const QString LowVelocityFilter = "Low Velocity Filter";
|
||||||
|
const QString MeshVisible = "Draw Mesh";
|
||||||
const QString Mirror = "Mirror";
|
const QString Mirror = "Mirror";
|
||||||
const QString MuteAudio = "Mute Microphone";
|
const QString MuteAudio = "Mute Microphone";
|
||||||
const QString MuteEnvironment = "Mute Environment";
|
const QString MuteEnvironment = "Mute Environment";
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
#include <SharedUtil.h>
|
#include <SharedUtil.h>
|
||||||
#include <TextRenderer3D.h>
|
#include <TextRenderer3D.h>
|
||||||
#include <UserActivityLogger.h>
|
#include <UserActivityLogger.h>
|
||||||
|
#include <AnimDebugDraw.h>
|
||||||
|
|
||||||
#include "devices/Faceshift.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() {
|
void MyAvatar::loadData() {
|
||||||
Settings settings;
|
Settings settings;
|
||||||
settings.beginGroup("Avatar");
|
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) {
|
void MyAvatar::preRender(RenderArgs* renderArgs) {
|
||||||
|
|
||||||
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
|
render::ScenePointer scene = Application::getInstance()->getMain3DScene();
|
||||||
|
@ -1213,6 +1260,28 @@ void MyAvatar::preRender(RenderArgs* renderArgs) {
|
||||||
if (_skeletonModel.initWhenReady(scene)) {
|
if (_skeletonModel.initWhenReady(scene)) {
|
||||||
initHeadBones();
|
initHeadBones();
|
||||||
_skeletonModel.setCauterizeBoneSet(_headBoneSet);
|
_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) {
|
if (shouldDrawHead != _prevShouldDrawHead) {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#include "Avatar.h"
|
#include "Avatar.h"
|
||||||
|
|
||||||
class ModelItemID;
|
class ModelItemID;
|
||||||
|
class AnimNode;
|
||||||
|
|
||||||
enum eyeContactTarget {
|
enum eyeContactTarget {
|
||||||
LEFT_EYE,
|
LEFT_EYE,
|
||||||
|
@ -189,7 +190,12 @@ public slots:
|
||||||
void loadLastRecording();
|
void loadLastRecording();
|
||||||
|
|
||||||
virtual void rebuildSkeletonBody();
|
virtual void rebuildSkeletonBody();
|
||||||
|
|
||||||
void setEnableRigAnimations(bool isEnabled);
|
void setEnableRigAnimations(bool isEnabled);
|
||||||
|
void setEnableAnimGraph(bool isEnabled);
|
||||||
|
void setEnableDebugDrawBindPose(bool isEnabled);
|
||||||
|
void setEnableDebugDrawAnimPose(bool isEnabled);
|
||||||
|
void setEnableMeshVisible(bool isEnabled);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void transformChanged();
|
void transformChanged();
|
||||||
|
@ -283,6 +289,8 @@ private:
|
||||||
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
|
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
|
||||||
void maybeUpdateBillboard();
|
void maybeUpdateBillboard();
|
||||||
void initHeadBones();
|
void initHeadBones();
|
||||||
|
void initAnimGraph();
|
||||||
|
void destroyAnimGraph();
|
||||||
|
|
||||||
// Avatar Preferences
|
// Avatar Preferences
|
||||||
QUrl _fullAvatarURLFromPreferences;
|
QUrl _fullAvatarURLFromPreferences;
|
||||||
|
@ -310,6 +318,9 @@ private:
|
||||||
std::unordered_set<int> _headBoneSet;
|
std::unordered_set<int> _headBoneSet;
|
||||||
RigPointer _rig;
|
RigPointer _rig;
|
||||||
bool _prevShouldDrawHead;
|
bool _prevShouldDrawHead;
|
||||||
|
|
||||||
|
bool _enableDebugDrawBindPose = false;
|
||||||
|
bool _enableDebugDrawAnimPose = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_MyAvatar_h
|
#endif // hifi_MyAvatar_h
|
||||||
|
|
|
@ -606,3 +606,7 @@ bool SkeletonModel::hasSkeleton() {
|
||||||
|
|
||||||
void SkeletonModel::onInvalidate() {
|
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;
|
virtual void onInvalidate() override;
|
||||||
|
|
||||||
|
void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
||||||
void skeletonLoaded();
|
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 "AnimationHandle.h"
|
||||||
#include "AnimationLogging.h"
|
#include "AnimationLogging.h"
|
||||||
|
#include "AnimSkeleton.h"
|
||||||
|
|
||||||
|
#include "Rig.h"
|
||||||
|
|
||||||
void Rig::HeadParameters::dump() const {
|
void Rig::HeadParameters::dump() const {
|
||||||
qCDebug(animation, "HeadParameters =");
|
qCDebug(animation, "HeadParameters =");
|
||||||
|
@ -186,6 +189,12 @@ void Rig::deleteAnimations() {
|
||||||
_animationHandles.clear();
|
_animationHandles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Rig::destroyAnimGraph() {
|
||||||
|
_animSkeleton = nullptr;
|
||||||
|
_animLoader = nullptr;
|
||||||
|
_animNode = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void Rig::initJointStates(QVector<JointState> states, glm::mat4 rootTransform,
|
void Rig::initJointStates(QVector<JointState> states, glm::mat4 rootTransform,
|
||||||
int rootJointIndex,
|
int rootJointIndex,
|
||||||
int leftHandJointIndex,
|
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) {
|
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 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,
|
// It can be more accurate/smooth to use velocity rather than position,
|
||||||
// but some modes (e.g., hmd standing) update position without updating velocity.
|
// 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...)
|
// 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...
|
// So, let's create our own workingVelocity from the worldPosition...
|
||||||
glm::vec3 positionDelta = worldPosition - _lastPosition;
|
glm::vec3 positionDelta = worldPosition - _lastPosition;
|
||||||
glm::vec3 workingVelocity = positionDelta / deltaTime;
|
glm::vec3 workingVelocity = positionDelta / deltaTime;
|
||||||
// But for smoothest (non-hmd standing) results, go ahead and use velocity:
|
|
||||||
#if !WANT_DEBUG
|
#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,
|
// But for smoothest (non-hmd standing) results, go ahead and use velocity:
|
||||||
// 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.
|
|
||||||
if (!positionDelta.x && !positionDelta.y && !positionDelta.z) {
|
if (!positionDelta.x && !positionDelta.y && !positionDelta.z) {
|
||||||
workingVelocity = worldVelocity;
|
workingVelocity = worldVelocity;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
float forwardSpeed = glm::dot(workingVelocity, front);
|
if (_enableAnimGraph) {
|
||||||
float rightLateralSpeed = glm::dot(workingVelocity, right);
|
|
||||||
float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP);
|
glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity;
|
||||||
float rightTurningSpeed = rightTurningDelta / deltaTime;
|
float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT);
|
||||||
bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED);
|
float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT);
|
||||||
bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED;
|
float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime;
|
||||||
auto updateRole = [&](const QString& role, bool isOn) {
|
|
||||||
isMoving = isMoving || isOn;
|
// sine wave LFO var for testing.
|
||||||
if (isOn) {
|
static float t = 0.0f;
|
||||||
if (!isRunningRole(role)) {
|
_animVars.set("sine", static_cast<float>(0.5 * sin(t) + 0.5));
|
||||||
qCDebug(animation) << "Rig STARTING" << role;
|
|
||||||
startAnimationByRole(role);
|
// 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 {
|
} else {
|
||||||
if (isRunningRole(role)) {
|
moveThresh = MOVE_EXIT_SPEED_THRESHOLD;
|
||||||
qCDebug(animation) << "Rig stopping" << role;
|
}
|
||||||
stopAnimationByRole(role);
|
|
||||||
|
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);
|
t += deltaTime;
|
||||||
updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED);
|
}
|
||||||
updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f));
|
|
||||||
updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f));
|
if (_enableRig) {
|
||||||
isStrafing = isStrafing && !isMoving;
|
bool isMoving = false;
|
||||||
updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f));
|
|
||||||
updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f));
|
glm::vec3 right = worldRotation * IDENTITY_RIGHT;
|
||||||
updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus.
|
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;
|
_lastFront = front;
|
||||||
_lastPosition = worldPosition;
|
_lastPosition = worldPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) {
|
void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) {
|
||||||
|
|
||||||
// First normalize the fades so that they sum to 1.0.
|
if (_enableAnimGraph) {
|
||||||
// update the fade data in each animation (not normalized as they are an independent propert of animation)
|
if (!_animNode) {
|
||||||
foreach (const AnimationHandlePointer& handle, _runningAnimations) {
|
return;
|
||||||
float fadePerSecond = handle->getFadePerSecond();
|
}
|
||||||
float fade = handle->getFade();
|
|
||||||
if (fadePerSecond != 0.0f) {
|
// evaluate the animation
|
||||||
fade += fadePerSecond * deltaTime;
|
AnimNode::Triggers triggersOut;
|
||||||
if ((0.0f >= fade) || (fade >= 1.0f)) {
|
AnimPoseVec poses = _animNode->evaluate(_animVars, deltaTime, triggersOut);
|
||||||
fade = glm::clamp(fade, 0.0f, 1.0f);
|
_animVars.clearTriggers();
|
||||||
handle->setFadePerSecond(0.0f);
|
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
|
// sum the remaining fade data
|
||||||
handle->setRunning(false, false); // but do not restore joints as it causes a flicker
|
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++) {
|
for (int i = 0; i < _jointStates.size(); i++) {
|
||||||
updateJointState(i, rootTransform);
|
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(leftEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade);
|
||||||
updateEyeJoint(rightEyeIndex, 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) {
|
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) {
|
if (index >= 0 && _jointStates[index].getParentIndex() >= 0) {
|
||||||
auto& state = _jointStates[index];
|
auto& state = _jointStates[index];
|
||||||
|
@ -877,3 +1004,30 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm
|
||||||
state.getDefaultRotation(), DEFAULT_PRIORITY);
|
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 "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;
|
class AnimationHandle;
|
||||||
typedef std::shared_ptr<AnimationHandle> AnimationHandlePointer;
|
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.
|
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; }
|
const QList<AnimationHandlePointer>& getRunningAnimations() const { return _runningAnimations; }
|
||||||
void deleteAnimations();
|
void deleteAnimations();
|
||||||
|
void destroyAnimGraph();
|
||||||
const QList<AnimationHandlePointer>& getAnimationHandles() const { return _animationHandles; }
|
const QList<AnimationHandlePointer>& getAnimationHandles() const { return _animationHandles; }
|
||||||
void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false,
|
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());
|
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;
|
virtual void updateJointState(int index, glm::mat4 rootTransform) = 0;
|
||||||
|
|
||||||
void setEnableRig(bool isEnabled) { _enableRig = isEnabled; }
|
void setEnableRig(bool isEnabled) { _enableRig = isEnabled; }
|
||||||
|
void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; }
|
||||||
|
bool getEnableAnimGraph() const { return _enableAnimGraph; }
|
||||||
|
|
||||||
void updateFromHeadParameters(const HeadParameters& params);
|
void updateFromHeadParameters(const HeadParameters& params);
|
||||||
void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation,
|
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,
|
virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation,
|
||||||
float scale, float priority) = 0;
|
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:
|
protected:
|
||||||
|
|
||||||
void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist);
|
void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist);
|
||||||
|
@ -183,9 +194,21 @@ public:
|
||||||
QList<AnimationHandlePointer> _animationHandles;
|
QList<AnimationHandlePointer> _animationHandles;
|
||||||
QList<AnimationHandlePointer> _runningAnimations;
|
QList<AnimationHandlePointer> _runningAnimations;
|
||||||
|
|
||||||
bool _enableRig;
|
bool _enableRig = false;
|
||||||
|
bool _enableAnimGraph = false;
|
||||||
glm::vec3 _lastFront;
|
glm::vec3 _lastFront;
|
||||||
glm::vec3 _lastPosition;
|
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__) */
|
#endif /* defined(__hifi__Rig__) */
|
||||||
|
|
|
@ -1722,7 +1722,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
|
||||||
glm::vec3 rotationOffset;
|
glm::vec3 rotationOffset;
|
||||||
glm::vec3 preRotation, rotation, postRotation;
|
glm::vec3 preRotation, rotation, postRotation;
|
||||||
glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f);
|
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 rotationMinX = false, rotationMinY = false, rotationMinZ = false;
|
||||||
bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false;
|
bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false;
|
||||||
glm::vec3 rotationMin, rotationMax;
|
glm::vec3 rotationMin, rotationMax;
|
||||||
|
@ -1771,12 +1771,14 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
|
||||||
} else if (property.properties.at(0) == "Lcl Scaling") {
|
} else if (property.properties.at(0) == "Lcl Scaling") {
|
||||||
scale = getVec3(property.properties, index);
|
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") {
|
} else if (property.properties.at(0) == "RotationMin") {
|
||||||
rotationMin = getVec3(property.properties, index);
|
rotationMin = getVec3(property.properties, index);
|
||||||
|
|
||||||
}
|
} else if (property.properties.at(0) == "RotationMax") {
|
||||||
// NOTE: these rotation limits are stored in degrees (NOT radians)
|
|
||||||
else if (property.properties.at(0) == "RotationMax") {
|
|
||||||
rotationMax = getVec3(property.properties, index);
|
rotationMax = getVec3(property.properties, index);
|
||||||
|
|
||||||
} else if (property.properties.at(0) == "RotationMinX") {
|
} 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.preRotation = glm::quat(glm::radians(preRotation));
|
||||||
model.rotation = glm::quat(glm::radians(rotation));
|
model.rotation = glm::quat(glm::radians(rotation));
|
||||||
model.postRotation = glm::quat(glm::radians(postRotation));
|
model.postRotation = glm::quat(glm::radians(postRotation));
|
||||||
model.postTransform = glm::translate(-rotationPivot) * glm::translate(scalePivot) *
|
model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) *
|
||||||
glm::scale(scale) * glm::translate(-scalePivot);
|
glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot);
|
||||||
// NOTE: angles from the FBX file are in degrees
|
// NOTE: angles from the FBX file are in degrees
|
||||||
// so we convert them to radians for the FBXModel class
|
// so we convert them to radians for the FBXModel class
|
||||||
model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f,
|
model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f,
|
||||||
|
@ -2306,7 +2308,9 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joint.bindTransformFoundInCluster = false;
|
||||||
|
|
||||||
geometry.joints.append(joint);
|
geometry.joints.append(joint);
|
||||||
geometry.jointIndices.insert(model.name, geometry.joints.size());
|
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];
|
FBXJoint& joint = geometry.joints[fbxCluster.jointIndex];
|
||||||
joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink));
|
joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink));
|
||||||
joint.bindTransform = cluster.transformLink;
|
joint.bindTransform = cluster.transformLink;
|
||||||
|
joint.bindTransformFoundInCluster = true;
|
||||||
|
|
||||||
// update the bind pose extents
|
// update the bind pose extents
|
||||||
glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform);
|
glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform);
|
||||||
geometry.bindExtents.addPoint(bindTranslation);
|
geometry.bindExtents.addPoint(bindTranslation);
|
||||||
|
|
|
@ -64,12 +64,18 @@ public:
|
||||||
int parentIndex;
|
int parentIndex;
|
||||||
float distanceToParent;
|
float distanceToParent;
|
||||||
float boneRadius;
|
float boneRadius;
|
||||||
glm::vec3 translation;
|
|
||||||
glm::mat4 preTransform;
|
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
|
||||||
glm::quat preRotation;
|
|
||||||
glm::quat rotation;
|
glm::vec3 translation; // T
|
||||||
glm::quat postRotation;
|
glm::mat4 preTransform; // Roff * Rp
|
||||||
glm::mat4 postTransform;
|
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::mat4 transform;
|
||||||
glm::vec3 rotationMin; // radians
|
glm::vec3 rotationMin; // radians
|
||||||
glm::vec3 rotationMax; // radians
|
glm::vec3 rotationMax; // radians
|
||||||
|
@ -78,6 +84,7 @@ public:
|
||||||
glm::mat4 bindTransform;
|
glm::mat4 bindTransform;
|
||||||
QString name;
|
QString name;
|
||||||
bool isSkeletonJoint;
|
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();
|
_rig->clearJointStates();
|
||||||
_meshStates.clear();
|
_meshStates.clear();
|
||||||
_rig->deleteAnimations();
|
_rig->deleteAnimations();
|
||||||
|
_rig->destroyAnimGraph();
|
||||||
_blendedBlendshapeCoefficients.clear();
|
_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);
|
glm::vec4 temp = m * glm::vec4(p, 1.0f);
|
||||||
return glm::vec3(temp.x / temp.w, temp.y / temp.w, temp.z / temp.w);
|
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 PITCH(euler) euler.x
|
||||||
#define ROLL(euler) euler.z
|
#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::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p);
|
||||||
glm::quat cancelOutRollAndPitch(const glm::quat& q);
|
glm::quat cancelOutRollAndPitch(const glm::quat& q);
|
||||||
glm::mat4 cancelOutRollAndPitch(const glm::mat4& m);
|
glm::mat4 cancelOutRollAndPitch(const glm::mat4& m);
|
||||||
glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p);
|
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
|
#endif // hifi_GLMHelpers_h
|
||||||
|
|
|
@ -129,6 +129,7 @@ public:
|
||||||
static Transform& inverseMult(Transform& result, const Transform& left, const Transform& right);
|
static Transform& inverseMult(Transform& result, const Transform& left, const Transform& right);
|
||||||
|
|
||||||
Vec4 transform(const Vec4& pos) const;
|
Vec4 transform(const Vec4& pos) const;
|
||||||
|
Vec3 transform(const Vec3& pos) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
|
@ -504,6 +505,12 @@ inline Transform::Vec4 Transform::transform(const Vec4& pos) const {
|
||||||
return m * pos;
|
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 {
|
inline Transform::Mat4& Transform::getCachedMatrix(Transform::Mat4& result) const {
|
||||||
updateCache();
|
updateCache();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Declare dependencies
|
# Declare dependencies
|
||||||
macro (setup_testcase_dependencies)
|
macro (setup_testcase_dependencies)
|
||||||
# link in the shared libraries
|
# 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()
|
copy_dlls_beside_windows_executable()
|
||||||
endmacro ()
|
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
|
#ifdef FROM_FILE
|
||||||
QFile file(FROM_FILE);
|
QFile file(FROM_FILE);
|
||||||
QCOMPARE(file.open(QIODevice::ReadOnly), true);
|
QCOMPARE(file.open(QIODevice::ReadOnly), true);
|
||||||
FBXGeometry geometry = readFBX(file.readAll(), QVariantHash());
|
FBXGeometry* geometry = readFBX(file.readAll(), QVariantHash());
|
||||||
#else
|
#else
|
||||||
QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx");
|
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
|
QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request
|
||||||
auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
QCOMPARE(fbxHttpCode, 200);
|
QCOMPARE(fbxHttpCode, 200);
|
||||||
FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash());
|
FBXGeometry* geometry = readFBX(reply->readAll(), QVariantHash());
|
||||||
#endif
|
#endif
|
||||||
|
QVERIFY((bool)geometry);
|
||||||
|
|
||||||
QVector<JointState> jointStates;
|
QVector<JointState> jointStates;
|
||||||
for (int i = 0; i < geometry.joints.size(); ++i) {
|
for (int i = 0; i < geometry->joints.size(); ++i) {
|
||||||
JointState state(geometry.joints[i]);
|
JointState state(geometry->joints[i]);
|
||||||
jointStates.append(state);
|
jointStates.append(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
_rig = std::make_shared<AvatarRig>();
|
_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?
|
_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);
|
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