AnimNodeLoader support for InverseKinematics node.

This commit is contained in:
Anthony J. Thibault 2015-09-09 15:28:21 -07:00
parent c1d7287652
commit 756eb54a0a
6 changed files with 340 additions and 220 deletions

View file

@ -22,38 +22,7 @@ AnimController::~AnimController() {
}
const AnimPoseVec& AnimController::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) {
_alpha = animVars.lookup(_alphaVar, _alpha);
for (auto& jointVar : _jointVars) {
if (!jointVar.hasPerformedJointLookup) {
QString qJointName = QString::fromStdString(jointVar.jointName);
jointVar.jointIndex = _skeleton->nameToJointIndex(qJointName);
if (jointVar.jointIndex < 0) {
qCWarning(animation) << "AnimController could not find jointName" << jointVar.jointIndex << "in skeleton";
}
jointVar.hasPerformedJointLookup = true;
}
if (jointVar.jointIndex >= 0) {
// jointVar is an absolute rotation, if it is not set we will use the bindPose as our default value
AnimPose defaultPose = _skeleton->getAbsoluteBindPose(jointVar.jointIndex);
glm::quat absRot = animVars.lookup(jointVar.var, defaultPose.rot);
// because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose
// here we use the bind pose
int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex);
glm::quat parentAbsRot;
if (parentIndex >= 0) {
parentAbsRot = _skeleton->getAbsoluteBindPose(parentIndex).rot;
}
// convert from absolute to relative
glm::quat relRot = glm::inverse(parentAbsRot) * absRot;
_poses[jointVar.jointIndex] = AnimPose(defaultPose.scale, relRot, defaultPose.trans);
}
}
return _poses;
return overlay(animVars, dt, triggersOut, _skeleton->getRelativeBindPoses());
}
const AnimPoseVec& AnimController::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) {
@ -64,7 +33,7 @@ const AnimPoseVec& AnimController::overlay(const AnimVariantMap& animVars, float
QString qJointName = QString::fromStdString(jointVar.jointName);
jointVar.jointIndex = _skeleton->nameToJointIndex(qJointName);
if (jointVar.jointIndex < 0) {
qCWarning(animation) << "AnimController could not find jointName" << jointVar.jointIndex << "in skeleton";
qCWarning(animation) << "AnimController could not find jointName" << qJointName << "in skeleton";
}
jointVar.hasPerformedJointLookup = true;
}

View file

@ -14,6 +14,7 @@
#include "ElbowConstraint.h"
#include "SwingTwistConstraint.h"
#include "AnimationLogging.h"
AnimInverseKinematics::AnimInverseKinematics(const std::string& id) : AnimNode(AnimNode::Type::InverseKinematics, id) {
}
@ -51,6 +52,23 @@ void AnimInverseKinematics::computeAbsolutePoses(AnimPoseVec& absolutePoses) con
}
}
void AnimInverseKinematics::setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar) {
// if there are dups, last one wins.
_targetVarVec.push_back(IKTargetVar(jointName, positionVar.toStdString(), rotationVar.toStdString()));
}
static int findRootJointInSkeleton(AnimSkeleton::ConstPointer skeleton, int index) {
// walk down the skeleton hierarchy to find the joint's root
int rootIndex = -1;
int parentIndex = skeleton->getParentIndex(index);
while (parentIndex != -1) {
rootIndex = parentIndex;
parentIndex = skeleton->getParentIndex(parentIndex);
}
return rootIndex;
}
/*
void AnimInverseKinematics::updateTarget(int index, const glm::vec3& position, const glm::quat& rotation) {
std::map<int, IKTarget>::iterator targetItr = _absoluteTargets.find(index);
if (targetItr != _absoluteTargets.end()) {
@ -104,15 +122,47 @@ void AnimInverseKinematics::clearAllTargets() {
_absoluteTargets.clear();
_maxTargetIndex = 0;
}
*/
//virtual
const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) {
// NOTE: we assume that _relativePoses are up to date (e.g. loadPoses() was just called)
if (_relativePoses.empty()) {
return _relativePoses;
}
relaxTowardDefaults(dt);
// evaluate target vars
for (auto& targetVar : _targetVarVec) {
// lazy look up of jointIndices and insertion into _absoluteTargets map
if (!targetVar.hasPerformedJointLookup) {
targetVar.jointIndex = _skeleton->nameToJointIndex(targetVar.jointName);
if (targetVar.jointIndex >= 0) {
// insert into _absoluteTargets map
IKTarget target;
target.pose = AnimPose::identity;
target.rootIndex = findRootJointInSkeleton(_skeleton, targetVar.jointIndex);
_absoluteTargets[targetVar.jointIndex] = target;
} else {
qCWarning(animation) << "AnimInverseKinematics could not find jointName" << targetVar.jointName << "in skeleton";
}
targetVar.hasPerformedJointLookup = true;
}
if (targetVar.jointIndex >= 0) {
// update pose in _absoluteTargets map
auto iter = _absoluteTargets.find(targetVar.jointIndex);
if (iter != _absoluteTargets.end()) {
AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, _relativePoses);
iter->second.pose.trans = animVars.lookup(targetVar.positionVar, defaultPose.trans);
iter->second.pose.rot = animVars.lookup(targetVar.rotationVar, defaultPose.rot);
}
}
}
// RELAX! Don't do it.
// relaxTowardDefaults(dt);
if (_absoluteTargets.empty()) {
// no IK targets but still need to enforce constraints
@ -258,6 +308,12 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar
return _relativePoses;
}
//virtual
const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) {
loadPoses(underPoses);
return evaluate(animVars, dt, triggersOut);
}
RotationConstraint* AnimInverseKinematics::getConstraint(int index) {
RotationConstraint* constraint = nullptr;
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.find(index);
@ -284,7 +340,7 @@ void AnimInverseKinematics::initConstraints() {
if (!_skeleton) {
return;
}
// We create constraints for the joints shown here
// We create constraints for the joints shown here
// (and their Left counterparts if applicable).
//
// O RightHand
@ -543,11 +599,23 @@ void AnimInverseKinematics::initConstraints() {
void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) {
AnimNode::setSkeletonInternal(skeleton);
// invalidate all targetVars
for (auto& targetVar : _targetVarVec) {
targetVar.hasPerformedJointLookup = false;
}
// invalidate all targets
_absoluteTargets.clear();
// No constraints!
/*
if (skeleton) {
initConstraints();
} else {
clearConstraints();
}
*/
}
void AnimInverseKinematics::relaxTowardDefaults(float dt) {

View file

@ -27,6 +27,9 @@ public:
const AnimPoseVec& getRelativePoses() const { return _relativePoses; }
void computeAbsolutePoses(AnimPoseVec& absolutePoses) const;
void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar);
/*
/// \param index index of end effector
/// \param position target position in the frame of the end-effector's root (not the parent!)
/// \param rotation target rotation in the frame of the end-effector's root (not the parent!)
@ -39,8 +42,10 @@ public:
void clearTarget(int index);
void clearAllTargets();
*/
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) override;
virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override;
protected:
virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton);
@ -54,12 +59,28 @@ protected:
void clearConstraints();
void initConstraints();
struct IKTargetVar {
IKTargetVar(const QString& jointNameIn, const std::string& positionVarIn, const std::string& rotationVarIn) :
jointName(jointNameIn),
positionVar(positionVarIn),
rotationVar(rotationVarIn),
jointIndex(-1),
hasPerformedJointLookup(false) {}
std::string positionVar;
std::string rotationVar;
QString jointName;
int jointIndex; // cached joint index
bool hasPerformedJointLookup = false;
};
struct IKTarget {
AnimPose pose; // in root-frame
int rootIndex; // cached root index
AnimPose pose;
int rootIndex;
};
std::map<int, RotationConstraint*> _constraints;
std::vector<IKTargetVar> _targetVarVec;
std::map<int, IKTarget> _absoluteTargets; // IK targets of end-points
AnimPoseVec _defaultRelativePoses; // poses of the relaxed state
AnimPoseVec _relativePoses; // current relative poses

View file

@ -21,6 +21,7 @@
#include "AnimNodeLoader.h"
#include "AnimStateMachine.h"
#include "AnimController.h"
#include "AnimInverseKinematics.h"
using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
using NodeProcessFunc = AnimNode::Pointer (*)(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
@ -31,6 +32,7 @@ static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const Q
static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadControllerNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
// called after children have been loaded
// returns node on success, nullptr on failure.
@ -39,6 +41,7 @@ static AnimNode::Pointer processBlendLinearNode(AnimNode::Pointer node, const QJ
static AnimNode::Pointer processOverlayNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return node; }
AnimNode::Pointer processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer processControllerNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return node; }
static AnimNode::Pointer processInverseKinematicsNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return node; }
static const char* animNodeTypeToString(AnimNode::Type type) {
switch (type) {
@ -47,7 +50,7 @@ static const char* animNodeTypeToString(AnimNode::Type type) {
case AnimNode::Type::Overlay: return "overlay";
case AnimNode::Type::StateMachine: return "stateMachine";
case AnimNode::Type::Controller: return "controller";
case AnimNode::Type::InverseKinematics: return nullptr;
case AnimNode::Type::InverseKinematics: return "inverseKinematics";
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
@ -60,7 +63,7 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) {
case AnimNode::Type::Overlay: return loadOverlayNode;
case AnimNode::Type::StateMachine: return loadStateMachineNode;
case AnimNode::Type::Controller: return loadControllerNode;
case AnimNode::Type::InverseKinematics: return nullptr;
case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode;
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
@ -73,7 +76,7 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
case AnimNode::Type::Overlay: return processOverlayNode;
case AnimNode::Type::StateMachine: return processStateMachineNode;
case AnimNode::Type::Controller: return processControllerNode;
case AnimNode::Type::InverseKinematics: return nullptr;
case AnimNode::Type::InverseKinematics: return processInverseKinematicsNode;
case AnimNode::Type::NumTypes: return nullptr;
};
return nullptr;
@ -312,6 +315,33 @@ static AnimNode::Pointer loadControllerNode(const QJsonObject& jsonObj, const QS
return node;
}
AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
auto node = std::make_shared<AnimInverseKinematics>(id.toStdString());
auto targetsValue = jsonObj.value("targets");
if (!targetsValue.isArray()) {
qCCritical(animation) << "AnimNodeLoader, bad array \"targets\" in inverseKinematics node, id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto targetsArray = targetsValue.toArray();
for (const auto& targetValue : targetsArray) {
if (!targetValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad state object in \"targets\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr;
}
auto targetObj = targetValue.toObject();
READ_STRING(jointName, targetObj, id, jsonUrl);
READ_STRING(positionVar, targetObj, id, jsonUrl);
READ_STRING(rotationVar, targetObj, id, jsonUrl);
node->setTargetVars(jointName, positionVar, rotationVar);
};
return node;
}
void buildChildMap(std::map<std::string, AnimNode::Pointer>& map, AnimNode::Pointer node) {
for ( auto child : node->_children ) {
map.insert(std::pair<std::string, AnimNode::Pointer>(child->_id, child));

View file

@ -444,6 +444,9 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
static float t = 0.0f;
_animVars.set("sine", static_cast<float>(0.5 * sin(t) + 0.5));
_animVars.set("rightHandPosition", glm::vec3(cos(t), sin(t) + 1.0f, 1.0f));
_animVars.set("leftHandPosition", glm::vec3(cos(-t + 3.1415f), sin(-t + 3.1415f) + 1.0f, 1.0f));
// default anim vars to notMoving and notTurning
_animVars.set("isMovingForward", false);
_animVars.set("isMovingBackward", false);

View file

@ -1,209 +1,238 @@
{
"version": "1.0",
"root": {
"id": "root",
"id": "ikOverlay",
"type": "overlay",
"data": {
"alpha": 1.0,
"boneSet": "spineOnly"
"boneSet": "fullBody"
},
"children": [
{
"id": "spineLean",
"type": "controller",
"id": "ik",
"type": "inverseKinematics",
"data": {
"alpha": 1.0,
"joints": [
{ "var": "lean", "jointName": "Spine" }
"targets": [
{
"jointName": "RightHand",
"positionVar": "rightHandPosition",
"rotationVar": "rightHandRotation"
},
{
"jointName": "LeftHand",
"positionVar": "leftHandPosition",
"rotationVar": "leftHandRotation"
}
]
},
"children": []
},
{
"id": "mainStateMachine",
"type": "stateMachine",
"id": "controllerOverlay",
"type": "overlay",
"data": {
"currentState": "idle",
"states": [
{
"id": "idle",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkFwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "walkBwd",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "strafeLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotMoving", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isTurningRight", "state": "turnRight" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnRight",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningLeft", "state": "turnLeft" }
]
},
{
"id": "turnLeft",
"interpTarget": 6,
"interpDuration": 6,
"transitions": [
{ "var": "isNotTurning", "state": "idle" },
{ "var": "isMovingForward", "state": "walkFwd" },
{ "var": "isMovingBackward", "state": "walkBwd" },
{ "var": "isMovingRight", "state": "strafeRight" },
{ "var": "isMovingLeft", "state": "strafeLeft" },
{ "var": "isTurningRight", "state": "turnRight" }
]
}
]
"alpha": 1.0,
"boneSet": "spineOnly"
},
"children": [
{
"id": "idle",
"type": "clip",
"id": "spineLean",
"type": "controller",
"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
"alpha": 1.0,
"joints": [
{ "var": "lean", "jointName": "Spine" }
]
},
"children": []
},
{
"id": "walkFwd",
"type": "clip",
"id": "mainStateMachine",
"type": "stateMachine",
"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"
"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": "walkBwd",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx",
"startFrame": 0.0,
"endFrame": 37.0,
"timeScale": 1.0,
"loopFlag": true,
"timeScaleVar": "walkTimeScale"
},
"children": []
},
{
"id": "turnLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx",
"startFrame": 0.0,
"endFrame": 28.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "turnRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx",
"startFrame": 0.0,
"endFrame": 30.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeLeft",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
},
{
"id": "strafeRight",
"type": "clip",
"data": {
"url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx",
"startFrame": 0.0,
"endFrame": 31.0,
"timeScale": 1.0,
"loopFlag": true
},
"children": []
"children": [
{
"id": "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": []
}
]
}
]
}