diff --git a/libraries/animation/src/AnimController.cpp b/libraries/animation/src/AnimController.cpp index 6cf95da26b..c021124732 100644 --- a/libraries/animation/src/AnimController.cpp +++ b/libraries/animation/src/AnimController.cpp @@ -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; } diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index ce6499557e..cd38d23f68 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -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::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::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) { diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 03ca5640eb..7fb1a615be 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -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 _constraints; + std::vector _targetVarVec; std::map _absoluteTargets; // IK targets of end-points AnimPoseVec _defaultRelativePoses; // poses of the relaxed state AnimPoseVec _relativePoses; // current relative poses diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 984f0dbe0a..a210327f66 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -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(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& map, AnimNode::Pointer node) { for ( auto child : node->_children ) { map.insert(std::pair(child->_id, child)); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 5e39f334c5..660e2eaaab 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -444,6 +444,9 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos static float t = 0.0f; _animVars.set("sine", static_cast(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); diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json index 9ca887c4db..3122fd433d 100644 --- a/tests/animation/src/data/avatar.json +++ b/tests/animation/src/data/avatar.json @@ -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": [] + } + ] } ] }