mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-07 12:04:25 +02:00
These are the squashed commits for the ik optimization for the Quest
Implmented using a new AnimSplineIK node in the anim graph (cherry picked from commit 4fe03ba238659fee7763991f2499a315482b351f)
This commit is contained in:
parent
1d5a4116b9
commit
87d98e5b85
19 changed files with 3061 additions and 28 deletions
|
@ -52,6 +52,7 @@ else()
|
|||
set(MOBILE 0)
|
||||
endif()
|
||||
|
||||
set(HIFI_USE_OPTIMIZED_IK OFF)
|
||||
set(BUILD_CLIENT_OPTION ON)
|
||||
set(BUILD_SERVER_OPTION ON)
|
||||
set(BUILD_TESTS_OPTION OFF)
|
||||
|
@ -115,7 +116,7 @@ if (USE_GLES AND (NOT ANDROID))
|
|||
set(DISABLE_QML_OPTION ON)
|
||||
endif()
|
||||
|
||||
|
||||
option(HIFI_USE_OPTIMIZED_IK "USE OPTIMIZED IK" ${HIFI_USE_OPTIMIZED_IK_OPTION})
|
||||
option(BUILD_CLIENT "Build client components" ${BUILD_CLIENT_OPTION})
|
||||
option(BUILD_SERVER "Build server components" ${BUILD_SERVER_OPTION})
|
||||
option(BUILD_TESTS "Build tests" ${BUILD_TESTS_OPTION})
|
||||
|
@ -146,6 +147,7 @@ foreach(PLATFORM_QT_COMPONENT ${PLATFORM_QT_COMPONENTS})
|
|||
list(APPEND PLATFORM_QT_LIBRARIES "Qt5::${PLATFORM_QT_COMPONENT}")
|
||||
endforeach()
|
||||
|
||||
MESSAGE(STATUS "USE OPTIMIZED IK: " ${HIFI_USE_OPTIMIZED_IK})
|
||||
MESSAGE(STATUS "Build server: " ${BUILD_SERVER})
|
||||
MESSAGE(STATUS "Build client: " ${BUILD_CLIENT})
|
||||
MESSAGE(STATUS "Build tests: " ${BUILD_TESTS})
|
||||
|
@ -191,6 +193,10 @@ find_package( Threads )
|
|||
add_definitions(-DGLM_FORCE_RADIANS)
|
||||
add_definitions(-DGLM_ENABLE_EXPERIMENTAL)
|
||||
add_definitions(-DGLM_FORCE_CTOR_INIT)
|
||||
if (HIFI_USE_OPTIMIZED_IK)
|
||||
MESSAGE(STATUS "SET THE USE IK DEFINITION ")
|
||||
add_definitions(-DHIFI_USE_OPTIMIZED_IK)
|
||||
endif()
|
||||
set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries")
|
||||
|
||||
set(EXTERNAL_PROJECT_PREFIX "project")
|
||||
|
|
2229
interface/resources/avatar/avatar-animation_withSplineIKNode.json
Normal file
2229
interface/resources/avatar/avatar-animation_withSplineIKNode.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2951,6 +2951,10 @@ void MyAvatar::initAnimGraph() {
|
|||
graphUrl = _fstAnimGraphOverrideUrl;
|
||||
} else {
|
||||
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation.json");
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
|
||||
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation_withSplineIKNode.json");
|
||||
#endif
|
||||
}
|
||||
|
||||
emit animGraphUrlChanged(graphUrl);
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
|
||||
#include <avatars-renderer/Avatar.h>
|
||||
#include <DebugDraw.h>
|
||||
#include <CubicHermiteSpline.h>
|
||||
|
||||
#include "Application.h"
|
||||
#include "InterfaceLogging.h"
|
||||
#include "AnimUtil.h"
|
||||
|
||||
|
||||
|
||||
MySkeletonModel::MySkeletonModel(Avatar* owningAvatar, QObject* parent) : SkeletonModel(owningAvatar, parent) {
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,22 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle
|
|||
};
|
||||
}
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
|
||||
static glm::vec3 computeSpine2WithHeadHipsSpline(MyAvatar* myAvatar, AnimPose hipsIKTargetPose, AnimPose headIKTargetPose) {
|
||||
|
||||
// the the ik targets to compute the spline with
|
||||
CubicHermiteSplineFunctorWithArcLength splineFinal(headIKTargetPose.rot(), headIKTargetPose.trans(), hipsIKTargetPose.rot(), hipsIKTargetPose.trans());
|
||||
|
||||
// measure the total arc length along the spline
|
||||
float totalArcLength = splineFinal.arcLength(1.0f);
|
||||
float tFinal = splineFinal.arcLengthInverse(myAvatar->getSpine2SplineRatio() * totalArcLength);
|
||||
glm::vec3 spine2Translation = splineFinal(tFinal);
|
||||
|
||||
return spine2Translation + myAvatar->getSpine2SplineOffset();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
|
||||
glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix());
|
||||
|
||||
|
@ -233,6 +251,12 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() &&
|
||||
!(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) {
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
|
||||
AnimPose headAvatarSpace(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation());
|
||||
AnimPose headRigSpace = avatarToRigPose * headAvatarSpace;
|
||||
AnimPose hipsRigSpace = sensorToRigPose * sensorHips;
|
||||
glm::vec3 spine2TargetTranslation = computeSpine2WithHeadHipsSpline(myAvatar, hipsRigSpace, headRigSpace);
|
||||
#endif
|
||||
const float SPINE2_ROTATION_FILTER = 0.5f;
|
||||
AnimPose currentSpine2Pose;
|
||||
AnimPose currentHeadPose;
|
||||
|
@ -243,6 +267,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
if (spine2Exists && headExists && hipsExists) {
|
||||
|
||||
AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace());
|
||||
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
|
||||
rigSpaceYaw.rot() = safeLerp(Quaternions::IDENTITY, rigSpaceYaw.rot(), 0.5f);
|
||||
#endif
|
||||
glm::vec3 u, v, w;
|
||||
glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
glm::vec3 up = currentHeadPose.trans() - currentHipsPose.trans();
|
||||
|
@ -253,6 +280,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
|||
}
|
||||
generateBasisVectors(up, fwd, u, v, w);
|
||||
AnimPose newSpinePose(glm::mat4(glm::vec4(w, 0.0f), glm::vec4(u, 0.0f), glm::vec4(v, 0.0f), glm::vec4(glm::vec3(0.0f, 0.0f, 0.0f), 1.0f)));
|
||||
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
|
||||
currentSpine2Pose.trans() = spine2TargetTranslation;
|
||||
#endif
|
||||
currentSpine2Pose.rot() = safeLerp(currentSpine2Pose.rot(), newSpinePose.rot(), SPINE2_ROTATION_FILTER);
|
||||
params.primaryControllerPoses[Rig::PrimaryControllerType_Spine2] = currentSpine2Pose;
|
||||
params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated;
|
||||
|
|
|
@ -28,6 +28,7 @@ enum class AnimNodeType {
|
|||
InverseKinematics,
|
||||
DefaultPose,
|
||||
TwoBoneIK,
|
||||
SplineIK,
|
||||
PoleVectorConstraint,
|
||||
NumTypes
|
||||
};
|
||||
|
|
|
@ -866,6 +866,11 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar
|
|||
//virtual
|
||||
const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut, const AnimPoseVec& underPoses) {
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
// disable IK on android
|
||||
return underPoses;
|
||||
#endif
|
||||
|
||||
// allows solutionSource to be overridden by an animVar
|
||||
auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource);
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "AnimInverseKinematics.h"
|
||||
#include "AnimDefaultPose.h"
|
||||
#include "AnimTwoBoneIK.h"
|
||||
#include "AnimSplineIK.h"
|
||||
#include "AnimPoleVectorConstraint.h"
|
||||
|
||||
using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
|
@ -41,6 +42,7 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q
|
|||
static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
static AnimNode::Pointer loadTwoBoneIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
static AnimNode::Pointer loadSplineIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
static AnimNode::Pointer loadPoleVectorConstraintNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
|
||||
|
||||
static const float ANIM_GRAPH_LOAD_PRIORITY = 10.0f;
|
||||
|
@ -61,6 +63,7 @@ static const char* animNodeTypeToString(AnimNode::Type type) {
|
|||
case AnimNode::Type::InverseKinematics: return "inverseKinematics";
|
||||
case AnimNode::Type::DefaultPose: return "defaultPose";
|
||||
case AnimNode::Type::TwoBoneIK: return "twoBoneIK";
|
||||
case AnimNode::Type::SplineIK: return "splineIK";
|
||||
case AnimNode::Type::PoleVectorConstraint: return "poleVectorConstraint";
|
||||
case AnimNode::Type::NumTypes: return nullptr;
|
||||
};
|
||||
|
@ -123,6 +126,7 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) {
|
|||
case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode;
|
||||
case AnimNode::Type::DefaultPose: return loadDefaultPoseNode;
|
||||
case AnimNode::Type::TwoBoneIK: return loadTwoBoneIKNode;
|
||||
case AnimNode::Type::SplineIK: return loadSplineIKNode;
|
||||
case AnimNode::Type::PoleVectorConstraint: return loadPoleVectorConstraintNode;
|
||||
case AnimNode::Type::NumTypes: return nullptr;
|
||||
};
|
||||
|
@ -140,6 +144,7 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) {
|
|||
case AnimNode::Type::InverseKinematics: return processDoNothing;
|
||||
case AnimNode::Type::DefaultPose: return processDoNothing;
|
||||
case AnimNode::Type::TwoBoneIK: return processDoNothing;
|
||||
case AnimNode::Type::SplineIK: return processDoNothing;
|
||||
case AnimNode::Type::PoleVectorConstraint: return processDoNothing;
|
||||
case AnimNode::Type::NumTypes: return nullptr;
|
||||
};
|
||||
|
@ -574,6 +579,52 @@ static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const Q
|
|||
return node;
|
||||
}
|
||||
|
||||
static AnimNode::Pointer loadSplineIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_BOOL(enabled, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_FLOAT(interpDuration, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(baseJointName, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(midJointName, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(tipJointName, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(basePositionVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(baseRotationVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(midPositionVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(midRotationVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(tipPositionVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(tipRotationVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(alphaVar, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_STRING(enabledVar, jsonObj, id, jsonUrl, nullptr);
|
||||
|
||||
auto tipFlexCoefficientsValue = jsonObj.value("tipTargetFlexCoefficients");
|
||||
if (!tipFlexCoefficientsValue.isArray()) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad or missing tip flex array";
|
||||
return nullptr;
|
||||
}
|
||||
auto tipFlexCoefficientsArray = tipFlexCoefficientsValue.toArray();
|
||||
std::vector<float> tipTargetFlexCoefficients;
|
||||
for (const auto& value : tipFlexCoefficientsArray) {
|
||||
tipTargetFlexCoefficients.push_back((float)value.toDouble());
|
||||
}
|
||||
|
||||
auto midFlexCoefficientsValue = jsonObj.value("midTargetFlexCoefficients");
|
||||
if (!midFlexCoefficientsValue.isArray()) {
|
||||
qCCritical(animation) << "AnimNodeLoader, bad or missing mid flex array";
|
||||
return nullptr;
|
||||
}
|
||||
auto midFlexCoefficientsArray = midFlexCoefficientsValue.toArray();
|
||||
std::vector<float> midTargetFlexCoefficients;
|
||||
for (const auto& midValue : midFlexCoefficientsArray) {
|
||||
midTargetFlexCoefficients.push_back((float)midValue.toDouble());
|
||||
}
|
||||
|
||||
auto node = std::make_shared<AnimSplineIK>(id, alpha, enabled, interpDuration,
|
||||
baseJointName, midJointName, tipJointName,
|
||||
basePositionVar, baseRotationVar, midPositionVar, midRotationVar,
|
||||
tipPositionVar, tipRotationVar, alphaVar, enabledVar,
|
||||
tipTargetFlexCoefficients, midTargetFlexCoefficients);
|
||||
return node;
|
||||
}
|
||||
|
||||
static AnimNode::Pointer loadTwoBoneIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
|
||||
READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr);
|
||||
READ_BOOL(enabled, jsonObj, id, jsonUrl, nullptr);
|
||||
|
|
|
@ -117,7 +117,7 @@ const AnimPoseVec& AnimPoleVectorConstraint::evaluate(const AnimVariantMap& anim
|
|||
if (axisLength > MIN_LENGTH && refVectorLength > MIN_LENGTH && sideVectorLength > MIN_LENGTH &&
|
||||
refVectorProjLength > MIN_LENGTH && poleVectorProjLength > MIN_LENGTH) {
|
||||
|
||||
float dot = glm::clamp(glm::dot(refVectorProj / refVectorProjLength, poleVectorProj / poleVectorProjLength), 0.0f, 1.0f);
|
||||
float dot = glm::clamp(glm::dot(refVectorProj / refVectorProjLength, poleVectorProj / poleVectorProjLength), -1.0f, 1.0f);
|
||||
float sideDot = glm::dot(poleVector, sideVector);
|
||||
float theta = copysignf(1.0f, sideDot) * acosf(dot);
|
||||
|
||||
|
|
473
libraries/animation/src/AnimSplineIK.cpp
Normal file
473
libraries/animation/src/AnimSplineIK.cpp
Normal file
|
@ -0,0 +1,473 @@
|
|||
//
|
||||
// AnimSplineIK.cpp
|
||||
//
|
||||
// Created by Angus Antley on 1/7/19.
|
||||
// Copyright (c) 2019 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 "AnimSplineIK.h"
|
||||
#include "AnimationLogging.h"
|
||||
#include "CubicHermiteSpline.h"
|
||||
#include <DebugDraw.h>
|
||||
#include "AnimUtil.h"
|
||||
|
||||
static const float FRAMES_PER_SECOND = 30.0f;
|
||||
|
||||
AnimSplineIK::AnimSplineIK(const QString& id, float alpha, bool enabled, float interpDuration,
|
||||
const QString& baseJointName,
|
||||
const QString& midJointName,
|
||||
const QString& tipJointName,
|
||||
const QString& basePositionVar,
|
||||
const QString& baseRotationVar,
|
||||
const QString& midPositionVar,
|
||||
const QString& midRotationVar,
|
||||
const QString& tipPositionVar,
|
||||
const QString& tipRotationVar,
|
||||
const QString& alphaVar,
|
||||
const QString& enabledVar,
|
||||
const std::vector<float> tipTargetFlexCoefficients,
|
||||
const std::vector<float> midTargetFlexCoefficients) :
|
||||
AnimNode(AnimNode::Type::SplineIK, id),
|
||||
_alpha(alpha),
|
||||
_enabled(enabled),
|
||||
_interpDuration(interpDuration),
|
||||
_baseJointName(baseJointName),
|
||||
_midJointName(midJointName),
|
||||
_tipJointName(tipJointName),
|
||||
_basePositionVar(basePositionVar),
|
||||
_baseRotationVar(baseRotationVar),
|
||||
_midPositionVar(midPositionVar),
|
||||
_midRotationVar(midRotationVar),
|
||||
_tipPositionVar(tipPositionVar),
|
||||
_tipRotationVar(tipRotationVar),
|
||||
_alphaVar(alphaVar),
|
||||
_enabledVar(enabledVar)
|
||||
{
|
||||
|
||||
for (int i = 0; i < (int)tipTargetFlexCoefficients.size(); i++) {
|
||||
if (i < MAX_NUMBER_FLEX_VARIABLES) {
|
||||
_tipTargetFlexCoefficients[i] = tipTargetFlexCoefficients[i];
|
||||
}
|
||||
}
|
||||
_numTipTargetFlexCoefficients = std::min((int)tipTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES);
|
||||
|
||||
for (int i = 0; i < (int)midTargetFlexCoefficients.size(); i++) {
|
||||
if (i < MAX_NUMBER_FLEX_VARIABLES) {
|
||||
_midTargetFlexCoefficients[i] = midTargetFlexCoefficients[i];
|
||||
}
|
||||
}
|
||||
_numMidTargetFlexCoefficients = std::min((int)midTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES);
|
||||
|
||||
}
|
||||
|
||||
AnimSplineIK::~AnimSplineIK() {
|
||||
|
||||
}
|
||||
|
||||
const AnimPoseVec& AnimSplineIK::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) {
|
||||
assert(_children.size() == 1);
|
||||
if (_children.size() != 1) {
|
||||
return _poses;
|
||||
}
|
||||
|
||||
const float MIN_ALPHA = 0.0f;
|
||||
const float MAX_ALPHA = 1.0f;
|
||||
float alpha = glm::clamp(animVars.lookup(_alphaVar, _alpha), MIN_ALPHA, MAX_ALPHA);
|
||||
|
||||
// evaluate underPoses
|
||||
AnimPoseVec underPoses = _children[0]->evaluate(animVars, context, dt, triggersOut);
|
||||
|
||||
// if we don't have a skeleton, or jointName lookup failed or the spline alpha is 0 or there are no underposes.
|
||||
if (!_skeleton || _baseJointIndex == -1 || _midJointIndex == -1 || _tipJointIndex == -1 || alpha < EPSILON || underPoses.size() == 0) {
|
||||
// pass underPoses through unmodified.
|
||||
_poses = underPoses;
|
||||
return _poses;
|
||||
}
|
||||
|
||||
// guard against size change
|
||||
if (underPoses.size() != _poses.size()) {
|
||||
_poses = underPoses;
|
||||
}
|
||||
|
||||
// determine if we should interpolate
|
||||
bool enabled = animVars.lookup(_enabledVar, _enabled);
|
||||
if (enabled != _enabled) {
|
||||
AnimChain poseChain;
|
||||
poseChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex);
|
||||
if (enabled) {
|
||||
beginInterp(InterpType::SnapshotToSolve, poseChain);
|
||||
} else {
|
||||
beginInterp(InterpType::SnapshotToUnderPoses, poseChain);
|
||||
}
|
||||
}
|
||||
_enabled = enabled;
|
||||
|
||||
// now that we have saved the previous _poses in _snapshotChain, we can update to the current underposes
|
||||
_poses = underPoses;
|
||||
|
||||
// don't build chains or do IK if we are disabled & not interping.
|
||||
if (_interpType == InterpType::None && !enabled) {
|
||||
return _poses;
|
||||
}
|
||||
|
||||
// compute under chain for possible interpolation
|
||||
AnimChain underChain;
|
||||
underChain.buildFromRelativePoses(_skeleton, underPoses, _tipJointIndex);
|
||||
|
||||
AnimPose baseTargetAbsolutePose;
|
||||
// if there is a baseJoint ik target in animvars then set the joint to that
|
||||
// otherwise use the underpose
|
||||
AnimPose baseJointUnderPose = _skeleton->getAbsolutePose(_baseJointIndex, _poses);
|
||||
baseTargetAbsolutePose.rot() = animVars.lookupRigToGeometry(_baseRotationVar, baseJointUnderPose.rot());
|
||||
baseTargetAbsolutePose.trans() = animVars.lookupRigToGeometry(_basePositionVar, baseJointUnderPose.trans());
|
||||
|
||||
int baseParentIndex = _skeleton->getParentIndex(_baseJointIndex);
|
||||
AnimPose baseParentAbsPose(Quaternions::IDENTITY,glm::vec3());
|
||||
if (baseParentIndex >= 0) {
|
||||
baseParentAbsPose = _skeleton->getAbsolutePose(baseParentIndex, _poses);
|
||||
}
|
||||
_poses[_baseJointIndex] = baseParentAbsPose.inverse() * baseTargetAbsolutePose;
|
||||
_poses[_baseJointIndex].scale() = glm::vec3(1.0f);
|
||||
|
||||
// initialize the middle joint target
|
||||
IKTarget midTarget;
|
||||
midTarget.setType((int)IKTarget::Type::Spline);
|
||||
midTarget.setIndex(_midJointIndex);
|
||||
AnimPose absPoseMid = _skeleton->getAbsolutePose(_midJointIndex, _poses);
|
||||
glm::quat midTargetRotation = animVars.lookupRigToGeometry(_midRotationVar, absPoseMid.rot());
|
||||
glm::vec3 midTargetPosition = animVars.lookupRigToGeometry(_midPositionVar, absPoseMid.trans());
|
||||
midTarget.setPose(midTargetRotation, midTargetPosition);
|
||||
midTarget.setWeight(1.0f);
|
||||
midTarget.setFlexCoefficients(_numMidTargetFlexCoefficients, _midTargetFlexCoefficients);
|
||||
|
||||
// solve the lower spine spline
|
||||
AnimChain midJointChain;
|
||||
AnimPoseVec absolutePosesAfterBaseTipSpline;
|
||||
absolutePosesAfterBaseTipSpline.resize(_poses.size());
|
||||
computeAbsolutePoses(absolutePosesAfterBaseTipSpline);
|
||||
midJointChain.buildFromRelativePoses(_skeleton, _poses, midTarget.getIndex());
|
||||
solveTargetWithSpline(context, _baseJointIndex, midTarget, absolutePosesAfterBaseTipSpline, context.getEnableDebugDrawIKChains(), midJointChain);
|
||||
midJointChain.outputRelativePoses(_poses);
|
||||
|
||||
// initialize the tip target
|
||||
IKTarget tipTarget;
|
||||
tipTarget.setType((int)IKTarget::Type::Spline);
|
||||
tipTarget.setIndex(_tipJointIndex);
|
||||
AnimPose absPoseTip = _skeleton->getAbsolutePose(_tipJointIndex, _poses);
|
||||
glm::quat tipRotation = animVars.lookupRigToGeometry(_tipRotationVar, absPoseTip.rot());
|
||||
glm::vec3 tipTranslation = animVars.lookupRigToGeometry(_tipPositionVar, absPoseTip.trans());
|
||||
tipTarget.setPose(tipRotation, tipTranslation);
|
||||
tipTarget.setWeight(1.0f);
|
||||
tipTarget.setFlexCoefficients(_numTipTargetFlexCoefficients, _tipTargetFlexCoefficients);
|
||||
|
||||
// solve the upper spine spline
|
||||
AnimChain upperJointChain;
|
||||
AnimPoseVec finalAbsolutePoses;
|
||||
finalAbsolutePoses.resize(_poses.size());
|
||||
computeAbsolutePoses(finalAbsolutePoses);
|
||||
upperJointChain.buildFromRelativePoses(_skeleton, _poses, tipTarget.getIndex());
|
||||
solveTargetWithSpline(context, _midJointIndex, tipTarget, finalAbsolutePoses, context.getEnableDebugDrawIKChains(), upperJointChain);
|
||||
upperJointChain.buildDirtyAbsolutePoses();
|
||||
upperJointChain.outputRelativePoses(_poses);
|
||||
|
||||
// compute chain
|
||||
AnimChain ikChain;
|
||||
ikChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex);
|
||||
// blend with the underChain
|
||||
ikChain.blend(underChain, alpha);
|
||||
|
||||
// apply smooth interpolation when turning ik on and off
|
||||
if (_interpType != InterpType::None) {
|
||||
_interpAlpha += _interpAlphaVel * dt;
|
||||
|
||||
// ease in expo
|
||||
float easeInAlpha = 1.0f - powf(2.0f, -10.0f * _interpAlpha);
|
||||
|
||||
if (_interpAlpha < 1.0f) {
|
||||
AnimChain interpChain;
|
||||
if (_interpType == InterpType::SnapshotToUnderPoses) {
|
||||
interpChain = underChain;
|
||||
interpChain.blend(_snapshotChain, easeInAlpha);
|
||||
} else if (_interpType == InterpType::SnapshotToSolve) {
|
||||
interpChain = ikChain;
|
||||
interpChain.blend(_snapshotChain, easeInAlpha);
|
||||
}
|
||||
// copy interpChain into _poses
|
||||
interpChain.outputRelativePoses(_poses);
|
||||
} else {
|
||||
// interpolation complete
|
||||
_interpType = InterpType::None;
|
||||
}
|
||||
}
|
||||
|
||||
if (_interpType == InterpType::None) {
|
||||
if (enabled) {
|
||||
// copy chain into _poses
|
||||
ikChain.outputRelativePoses(_poses);
|
||||
} else {
|
||||
// copy under chain into _poses
|
||||
underChain.outputRelativePoses(_poses);
|
||||
}
|
||||
}
|
||||
|
||||
// debug render ik targets
|
||||
if (context.getEnableDebugDrawIKTargets()) {
|
||||
const vec4 WHITE(1.0f);
|
||||
const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
|
||||
|
||||
glm::mat4 geomTargetMat = createMatFromQuatAndPos(tipTarget.getRotation(), tipTarget.getTranslation());
|
||||
glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
|
||||
QString name = QString("ikTargetSplineTip");
|
||||
DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
|
||||
|
||||
glm::mat4 geomTargetMat2 = createMatFromQuatAndPos(midTarget.getRotation(), midTarget.getTranslation());
|
||||
glm::mat4 avatarTargetMat2 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat2;
|
||||
QString name2 = QString("ikTargetSplineMid");
|
||||
DebugDraw::getInstance().addMyAvatarMarker(name2, glmExtractRotation(avatarTargetMat2), extractTranslation(avatarTargetMat2), WHITE);
|
||||
|
||||
glm::mat4 geomTargetMat3 = createMatFromQuatAndPos(baseTargetAbsolutePose.rot(), baseTargetAbsolutePose.trans());
|
||||
glm::mat4 avatarTargetMat3 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat3;
|
||||
QString name3 = QString("ikTargetSplineBase");
|
||||
DebugDraw::getInstance().addMyAvatarMarker(name3, glmExtractRotation(avatarTargetMat3), extractTranslation(avatarTargetMat3), WHITE);
|
||||
|
||||
|
||||
} else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
|
||||
|
||||
// remove markers if they were added last frame.
|
||||
QString name = QString("ikTargetSplineTip");
|
||||
DebugDraw::getInstance().removeMyAvatarMarker(name);
|
||||
QString name2 = QString("ikTargetSplineMid");
|
||||
DebugDraw::getInstance().removeMyAvatarMarker(name2);
|
||||
QString name3 = QString("ikTargetSplineBase");
|
||||
DebugDraw::getInstance().removeMyAvatarMarker(name3);
|
||||
}
|
||||
_previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets();
|
||||
|
||||
return _poses;
|
||||
}
|
||||
|
||||
void AnimSplineIK::lookUpIndices() {
|
||||
assert(_skeleton);
|
||||
|
||||
// look up bone indices by name
|
||||
std::vector<int> indices = _skeleton->lookUpJointIndices({ _baseJointName, _tipJointName, _midJointName });
|
||||
|
||||
// cache the results
|
||||
_baseJointIndex = indices[0];
|
||||
_tipJointIndex = indices[1];
|
||||
_midJointIndex = indices[2];
|
||||
}
|
||||
|
||||
void AnimSplineIK::computeAbsolutePoses(AnimPoseVec& absolutePoses) const {
|
||||
int numJoints = (int)_poses.size();
|
||||
assert(numJoints <= _skeleton->getNumJoints());
|
||||
assert(numJoints == (int)absolutePoses.size());
|
||||
for (int i = 0; i < numJoints; ++i) {
|
||||
int parentIndex = _skeleton->getParentIndex(i);
|
||||
if (parentIndex < 0) {
|
||||
absolutePoses[i] = _poses[i];
|
||||
} else {
|
||||
absolutePoses[i] = absolutePoses[parentIndex] * _poses[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for AnimDebugDraw rendering
|
||||
const AnimPoseVec& AnimSplineIK::getPosesInternal() const {
|
||||
return _poses;
|
||||
}
|
||||
|
||||
void AnimSplineIK::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) {
|
||||
AnimNode::setSkeletonInternal(skeleton);
|
||||
lookUpIndices();
|
||||
}
|
||||
|
||||
void AnimSplineIK::solveTargetWithSpline(const AnimContext& context, int base, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug, AnimChain& chainInfoOut) const {
|
||||
|
||||
// build spline from tip to base
|
||||
AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation());
|
||||
AnimPose basePose = absolutePoses[base];
|
||||
|
||||
CubicHermiteSplineFunctorWithArcLength spline;
|
||||
if (target.getIndex() == _tipJointIndex) {
|
||||
// set gain factors so that more curvature occurs near the tip of the spline.
|
||||
const float HIPS_GAIN = 0.5f;
|
||||
const float HEAD_GAIN = 1.0f;
|
||||
spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN);
|
||||
} else {
|
||||
spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(),tipPose.trans(), basePose.rot(), basePose.trans());
|
||||
}
|
||||
float totalArcLength = spline.arcLength(1.0f);
|
||||
|
||||
// This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way)
|
||||
// when the head is arched backwards very far.
|
||||
glm::quat halfRot = safeLerp(basePose.rot(), tipPose.rot(), 0.5f);
|
||||
if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) {
|
||||
tipPose.rot() = -tipPose.rot();
|
||||
}
|
||||
|
||||
// find or create splineJointInfo for this target
|
||||
const std::vector<SplineJointInfo>* splineJointInfoVec = findOrCreateSplineJointInfo(context, base, target);
|
||||
|
||||
if (splineJointInfoVec && splineJointInfoVec->size() > 0) {
|
||||
const int baseParentIndex = _skeleton->getParentIndex(base);
|
||||
AnimPose parentAbsPose = (baseParentIndex >= 0) ? absolutePoses[baseParentIndex] : AnimPose();
|
||||
// go thru splineJointInfoVec backwards (base to tip)
|
||||
for (int i = (int)splineJointInfoVec->size() - 1; i >= 0; i--) {
|
||||
const SplineJointInfo& splineJointInfo = (*splineJointInfoVec)[i];
|
||||
float t = spline.arcLengthInverse(splineJointInfo.ratio * totalArcLength);
|
||||
glm::vec3 trans = spline(t);
|
||||
|
||||
// for base->tip splines, preform most twist toward the tip by using ease in function. t^2
|
||||
float rotT = t;
|
||||
if (target.getIndex() == _tipJointIndex) {
|
||||
rotT = t * t;
|
||||
}
|
||||
glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT);
|
||||
|
||||
// compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis
|
||||
glm::vec3 y = glm::normalize(spline.d(t));
|
||||
glm::vec3 x = twistRot * Vectors::UNIT_X;
|
||||
glm::vec3 u, v, w;
|
||||
generateBasisVectors(y, x, v, u, w);
|
||||
glm::mat3 m(u, v, glm::cross(u, v));
|
||||
glm::quat rot = glm::normalize(glm::quat_cast(m));
|
||||
|
||||
AnimPose desiredAbsPose = AnimPose(glm::vec3(1.0f), rot, trans) * splineJointInfo.offsetPose;
|
||||
|
||||
// apply flex coefficent
|
||||
AnimPose flexedAbsPose;
|
||||
// get the number of flex coeff for this spline
|
||||
float interpedCoefficient = 1.0f;
|
||||
int numFlexCoeff = target.getNumFlexCoefficients();
|
||||
if (numFlexCoeff == (int)splineJointInfoVec->size()) {
|
||||
// then do nothing special
|
||||
interpedCoefficient = target.getFlexCoefficient(i);
|
||||
} else {
|
||||
// interp based on ratio of the joint.
|
||||
if (splineJointInfo.ratio < 1.0f) {
|
||||
float flexInterp = splineJointInfo.ratio * (float)(numFlexCoeff - 1);
|
||||
int startCoeff = (int)glm::floor(flexInterp);
|
||||
float partial = flexInterp - startCoeff;
|
||||
interpedCoefficient = target.getFlexCoefficient(startCoeff) * (1.0f - partial) + target.getFlexCoefficient(startCoeff + 1) * partial;
|
||||
} else {
|
||||
interpedCoefficient = target.getFlexCoefficient(numFlexCoeff - 1);
|
||||
}
|
||||
}
|
||||
::blend(1, &absolutePoses[splineJointInfo.jointIndex], &desiredAbsPose, interpedCoefficient, &flexedAbsPose);
|
||||
|
||||
AnimPose relPose = parentAbsPose.inverse() * flexedAbsPose;
|
||||
|
||||
if (splineJointInfo.jointIndex != base) {
|
||||
// constrain the amount the spine can stretch or compress
|
||||
float length = glm::length(relPose.trans());
|
||||
const float EPSILON = 0.0001f;
|
||||
if (length > EPSILON) {
|
||||
float defaultLength = glm::length(_skeleton->getRelativeDefaultPose(splineJointInfo.jointIndex).trans());
|
||||
const float STRETCH_COMPRESS_PERCENTAGE = 0.15f;
|
||||
const float MAX_LENGTH = defaultLength * (1.0f + STRETCH_COMPRESS_PERCENTAGE);
|
||||
const float MIN_LENGTH = defaultLength * (1.0f - STRETCH_COMPRESS_PERCENTAGE);
|
||||
if (length > MAX_LENGTH) {
|
||||
relPose.trans() = (relPose.trans() / length) * MAX_LENGTH;
|
||||
} else if (length < MIN_LENGTH) {
|
||||
relPose.trans() = (relPose.trans() / length) * MIN_LENGTH;
|
||||
}
|
||||
} else {
|
||||
relPose.trans() = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (!chainInfoOut.setRelativePoseAtJointIndex(splineJointInfo.jointIndex, relPose)) {
|
||||
qCDebug(animation) << "error: joint not found in spline chain";
|
||||
}
|
||||
|
||||
parentAbsPose = flexedAbsPose;
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
const vec4 CYAN(0.0f, 1.0f, 1.0f, 1.0f);
|
||||
chainInfoOut.debugDraw(context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(), CYAN);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<AnimSplineIK::SplineJointInfo>* AnimSplineIK::findOrCreateSplineJointInfo(const AnimContext& context, int base, const IKTarget& target) const {
|
||||
// find or create splineJointInfo for this target
|
||||
auto iter = _splineJointInfoMap.find(target.getIndex());
|
||||
if (iter != _splineJointInfoMap.end()) {
|
||||
return &(iter->second);
|
||||
} else {
|
||||
computeAndCacheSplineJointInfosForIKTarget(context, base, target);
|
||||
auto iter = _splineJointInfoMap.find(target.getIndex());
|
||||
if (iter != _splineJointInfoMap.end()) {
|
||||
return &(iter->second);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// pre-compute information about each joint influenced by this spline IK target.
|
||||
void AnimSplineIK::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, int base, const IKTarget& target) const {
|
||||
std::vector<SplineJointInfo> splineJointInfoVec;
|
||||
|
||||
// build spline between the default poses.
|
||||
AnimPose tipPose = _skeleton->getAbsoluteDefaultPose(target.getIndex());
|
||||
AnimPose basePose = _skeleton->getAbsoluteDefaultPose(base);
|
||||
|
||||
CubicHermiteSplineFunctorWithArcLength spline;
|
||||
if (target.getIndex() == _tipJointIndex) {
|
||||
// set gain factors so that more curvature occurs near the tip of the spline.
|
||||
const float HIPS_GAIN = 0.5f;
|
||||
const float HEAD_GAIN = 1.0f;
|
||||
spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN);
|
||||
} else {
|
||||
spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans());
|
||||
}
|
||||
// measure the total arc length along the spline
|
||||
float totalArcLength = spline.arcLength(1.0f);
|
||||
|
||||
glm::vec3 baseToTip = tipPose.trans() - basePose.trans();
|
||||
float baseToTipLength = glm::length(baseToTip);
|
||||
glm::vec3 baseToTipNormal = baseToTip / baseToTipLength;
|
||||
|
||||
int index = target.getIndex();
|
||||
int endIndex = _skeleton->getParentIndex(base);
|
||||
|
||||
while (index != endIndex) {
|
||||
AnimPose defaultPose = _skeleton->getAbsoluteDefaultPose(index);
|
||||
glm::vec3 baseToCurrentJoint = defaultPose.trans() - basePose.trans();
|
||||
float ratio = glm::dot(baseToCurrentJoint, baseToTipNormal) / baseToTipLength;
|
||||
|
||||
// compute offset from spline to the default pose.
|
||||
float t = spline.arcLengthInverse(ratio * totalArcLength);
|
||||
|
||||
// compute the rotation by using the derivative of the spline as the y-axis, and the defaultPose x-axis
|
||||
glm::vec3 y = glm::normalize(spline.d(t));
|
||||
glm::vec3 x = defaultPose.rot() * Vectors::UNIT_X;
|
||||
glm::vec3 u, v, w;
|
||||
generateBasisVectors(y, x, v, u, w);
|
||||
glm::mat3 m(u, v, glm::cross(u, v));
|
||||
glm::quat rot = glm::normalize(glm::quat_cast(m));
|
||||
|
||||
AnimPose pose(glm::vec3(1.0f), rot, spline(t));
|
||||
AnimPose offsetPose = pose.inverse() * defaultPose;
|
||||
|
||||
SplineJointInfo splineJointInfo = { index, ratio, offsetPose };
|
||||
splineJointInfoVec.push_back(splineJointInfo);
|
||||
index = _skeleton->getParentIndex(index);
|
||||
}
|
||||
_splineJointInfoMap[target.getIndex()] = splineJointInfoVec;
|
||||
}
|
||||
|
||||
void AnimSplineIK::beginInterp(InterpType interpType, const AnimChain& chain) {
|
||||
// capture the current poses in a snapshot.
|
||||
_snapshotChain = chain;
|
||||
|
||||
_interpType = interpType;
|
||||
_interpAlphaVel = FRAMES_PER_SECOND / _interpDuration;
|
||||
_interpAlpha = 0.0f;
|
||||
}
|
104
libraries/animation/src/AnimSplineIK.h
Normal file
104
libraries/animation/src/AnimSplineIK.h
Normal file
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// AnimSplineIK.h
|
||||
//
|
||||
// Created by Angus Antley on 1/7/19.
|
||||
// Copyright (c) 2019 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_AnimSplineIK_h
|
||||
#define hifi_AnimSplineIK_h
|
||||
|
||||
#include "AnimNode.h"
|
||||
#include "IKTarget.h"
|
||||
#include "AnimChain.h"
|
||||
|
||||
static const int MAX_NUMBER_FLEX_VARIABLES = 10;
|
||||
|
||||
// Spline IK for the spine
|
||||
class AnimSplineIK : public AnimNode {
|
||||
public:
|
||||
AnimSplineIK(const QString& id, float alpha, bool enabled, float interpDuration,
|
||||
const QString& baseJointName, const QString& midJointName, const QString& tipJointName,
|
||||
const QString& basePositionVar, const QString& baseRotationVar,
|
||||
const QString& midPositionVar, const QString& midRotationVar,
|
||||
const QString& tipPositionVar, const QString& tipRotationVar,
|
||||
const QString& alphaVar, const QString& enabledVar,
|
||||
const std::vector<float> tipTargetFlexCoefficients,
|
||||
const std::vector<float> midTargetFlexCoefficients);
|
||||
|
||||
virtual ~AnimSplineIK() override;
|
||||
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) override;
|
||||
|
||||
protected:
|
||||
|
||||
enum class InterpType {
|
||||
None = 0,
|
||||
SnapshotToUnderPoses,
|
||||
SnapshotToSolve,
|
||||
NumTypes
|
||||
};
|
||||
|
||||
void computeAbsolutePoses(AnimPoseVec& absolutePoses) const;
|
||||
void loadPoses(const AnimPoseVec& poses);
|
||||
|
||||
// for AnimDebugDraw rendering
|
||||
virtual const AnimPoseVec& getPosesInternal() const override;
|
||||
virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override;
|
||||
|
||||
void lookUpIndices();
|
||||
void beginInterp(InterpType interpType, const AnimChain& chain);
|
||||
|
||||
AnimPoseVec _poses;
|
||||
|
||||
float _alpha;
|
||||
bool _enabled;
|
||||
float _interpDuration;
|
||||
QString _baseJointName;
|
||||
QString _midJointName;
|
||||
QString _tipJointName;
|
||||
QString _basePositionVar;
|
||||
QString _baseRotationVar;
|
||||
QString _midPositionVar;
|
||||
QString _midRotationVar;
|
||||
QString _tipPositionVar;
|
||||
QString _tipRotationVar;
|
||||
QString _alphaVar; // float - (0, 1) 0 means underPoses only, 1 means IK only.
|
||||
QString _enabledVar;
|
||||
|
||||
float _tipTargetFlexCoefficients[MAX_NUMBER_FLEX_VARIABLES];
|
||||
float _midTargetFlexCoefficients[MAX_NUMBER_FLEX_VARIABLES];
|
||||
int _numTipTargetFlexCoefficients { 0 };
|
||||
int _numMidTargetFlexCoefficients { 0 };
|
||||
|
||||
int _baseJointIndex { -1 };
|
||||
int _midJointIndex { -1 };
|
||||
int _tipJointIndex { -1 };
|
||||
|
||||
bool _previousEnableDebugIKTargets { false };
|
||||
|
||||
InterpType _interpType{ InterpType::None };
|
||||
float _interpAlphaVel{ 0.0f };
|
||||
float _interpAlpha{ 0.0f };
|
||||
AnimChain _snapshotChain;
|
||||
|
||||
// used to pre-compute information about each joint influenced by a spline IK target.
|
||||
struct SplineJointInfo {
|
||||
int jointIndex; // joint in the skeleton that this information pertains to.
|
||||
float ratio; // percentage (0..1) along the spline for this joint.
|
||||
AnimPose offsetPose; // local offset from the spline to the joint.
|
||||
};
|
||||
|
||||
void solveTargetWithSpline(const AnimContext& context, int base, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug, AnimChain& chainInfoOut) const;
|
||||
void computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, int base, const IKTarget& target) const;
|
||||
const std::vector<SplineJointInfo>* findOrCreateSplineJointInfo(const AnimContext& context, int base, const IKTarget& target) const;
|
||||
mutable std::map<int, std::vector<SplineJointInfo>> _splineJointInfoMap;
|
||||
|
||||
// no copies
|
||||
AnimSplineIK(const AnimSplineIK&) = delete;
|
||||
AnimSplineIK& operator=(const AnimSplineIK&) = delete;
|
||||
|
||||
};
|
||||
#endif // hifi_AnimSplineIK_h
|
|
@ -22,7 +22,6 @@ AnimStateMachine::~AnimStateMachine() {
|
|||
}
|
||||
|
||||
const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) {
|
||||
|
||||
float parentDebugAlpha = context.getDebugAlpha(_id);
|
||||
|
||||
QString desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID());
|
||||
|
|
|
@ -35,6 +35,8 @@ public:
|
|||
bool getPoleVectorEnabled() const { return _poleVectorEnabled; }
|
||||
int getIndex() const { return _index; }
|
||||
Type getType() const { return _type; }
|
||||
int getNumFlexCoefficients() const { return (int)_numFlexCoefficients; }
|
||||
float getFlexCoefficient(size_t chainDepth) const;
|
||||
|
||||
void setPose(const glm::quat& rotation, const glm::vec3& translation);
|
||||
void setPoleVector(const glm::vec3& poleVector) { _poleVector = poleVector; }
|
||||
|
@ -43,7 +45,6 @@ public:
|
|||
void setIndex(int index) { _index = index; }
|
||||
void setType(int);
|
||||
void setFlexCoefficients(size_t numFlexCoefficientsIn, const float* flexCoefficientsIn);
|
||||
float getFlexCoefficient(size_t chainDepth) const;
|
||||
|
||||
void setWeight(float weight) { _weight = weight; }
|
||||
float getWeight() const { return _weight; }
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
#include "IKTarget.h"
|
||||
#include "PathUtils.h"
|
||||
|
||||
|
||||
static int nextRigId = 1;
|
||||
static std::map<int, Rig*> rigRegistry;
|
||||
static std::mutex rigRegistryMutex;
|
||||
|
@ -74,6 +73,20 @@ static const QString RIGHT_FOOT_IK_ROTATION_VAR("rightFootIKRotationVar");
|
|||
static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_ROTATION("mainStateMachineRightFootRotation");
|
||||
static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_POSITION("mainStateMachineRightFootPosition");
|
||||
|
||||
static const QString LEFT_HAND_POSITION("leftHandPosition");
|
||||
static const QString LEFT_HAND_ROTATION("leftHandRotation");
|
||||
static const QString LEFT_HAND_IK_POSITION_VAR("leftHandIKPositionVar");
|
||||
static const QString LEFT_HAND_IK_ROTATION_VAR("leftHandIKRotationVar");
|
||||
static const QString MAIN_STATE_MACHINE_LEFT_HAND_POSITION("mainStateMachineLeftHandPosition");
|
||||
static const QString MAIN_STATE_MACHINE_LEFT_HAND_ROTATION("mainStateMachineLeftHandRotation");
|
||||
|
||||
static const QString RIGHT_HAND_POSITION("rightHandPosition");
|
||||
static const QString RIGHT_HAND_ROTATION("rightHandRotation");
|
||||
static const QString RIGHT_HAND_IK_POSITION_VAR("rightHandIKPositionVar");
|
||||
static const QString RIGHT_HAND_IK_ROTATION_VAR("rightHandIKRotationVar");
|
||||
static const QString MAIN_STATE_MACHINE_RIGHT_HAND_ROTATION("mainStateMachineRightHandRotation");
|
||||
static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRightHandPosition");
|
||||
|
||||
|
||||
Rig::Rig() {
|
||||
// Ensure thread-safe access to the rigRegistry.
|
||||
|
@ -1051,16 +1064,29 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
|
|||
|
||||
t += deltaTime;
|
||||
|
||||
if (_enableInverseKinematics != _lastEnableInverseKinematics) {
|
||||
if (_enableInverseKinematics) {
|
||||
_animVars.set("ikOverlayAlpha", 1.0f);
|
||||
} else {
|
||||
_animVars.set("ikOverlayAlpha", 0.0f);
|
||||
}
|
||||
if (_enableInverseKinematics) {
|
||||
_animVars.set("ikOverlayAlpha", 1.0f);
|
||||
_animVars.set("splineIKEnabled", true);
|
||||
_animVars.set("leftHandIKEnabled", true);
|
||||
_animVars.set("rightHandIKEnabled", true);
|
||||
_animVars.set("leftFootIKEnabled", true);
|
||||
_animVars.set("rightFootIKEnabled", true);
|
||||
_animVars.set("leftFootPoleVectorEnabled", true);
|
||||
_animVars.set("rightFootPoleVectorEnabled", true);
|
||||
} else {
|
||||
_animVars.set("ikOverlayAlpha", 0.0f);
|
||||
_animVars.set("splineIKEnabled", false);
|
||||
_animVars.set("leftHandIKEnabled", false);
|
||||
_animVars.set("rightHandIKEnabled", false);
|
||||
_animVars.set("leftFootIKEnabled", false);
|
||||
_animVars.set("rightFootIKEnabled", false);
|
||||
_animVars.set("leftHandPoleVectorEnabled", false);
|
||||
_animVars.set("rightHandPoleVectorEnabled", false);
|
||||
_animVars.set("leftFootPoleVectorEnabled", false);
|
||||
_animVars.set("rightFootPoleVectorEnabled", false);
|
||||
}
|
||||
_lastEnableInverseKinematics = _enableInverseKinematics;
|
||||
}
|
||||
|
||||
_lastForward = forward;
|
||||
_lastPosition = worldPosition;
|
||||
_lastVelocity = workingVelocity;
|
||||
|
@ -1251,6 +1277,7 @@ void Rig::computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut
|
|||
void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPose) {
|
||||
if (_animSkeleton) {
|
||||
if (headEnabled) {
|
||||
_animVars.set("splineIKEnabled", true);
|
||||
_animVars.set("headPosition", headPose.trans());
|
||||
_animVars.set("headRotation", headPose.rot());
|
||||
if (hipsEnabled) {
|
||||
|
@ -1265,6 +1292,7 @@ void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPos
|
|||
_animVars.set("headWeight", 8.0f);
|
||||
}
|
||||
} else {
|
||||
_animVars.set("splineIKEnabled", false);
|
||||
_animVars.unset("headPosition");
|
||||
_animVars.set("headRotation", headPose.rot());
|
||||
_animVars.set("headType", (int)IKTarget::Type::RotationOnly);
|
||||
|
@ -1396,8 +1424,22 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab
|
|||
|
||||
const bool ENABLE_POLE_VECTORS = true;
|
||||
|
||||
if (headEnabled) {
|
||||
// always do IK if head is enabled
|
||||
_animVars.set("leftHandIKEnabled", true);
|
||||
_animVars.set("rightHandIKEnabled", true);
|
||||
} else {
|
||||
// only do IK if we have a valid foot.
|
||||
_animVars.set("leftHandIKEnabled", leftHandEnabled);
|
||||
_animVars.set("rightHandIKEnabled", rightHandEnabled);
|
||||
}
|
||||
|
||||
if (leftHandEnabled) {
|
||||
|
||||
// we need this for twoBoneIK version of hands.
|
||||
_animVars.set(LEFT_HAND_IK_POSITION_VAR, LEFT_HAND_POSITION);
|
||||
_animVars.set(LEFT_HAND_IK_ROTATION_VAR, LEFT_HAND_ROTATION);
|
||||
|
||||
glm::vec3 handPosition = leftHandPose.trans();
|
||||
glm::quat handRotation = leftHandPose.rot();
|
||||
|
||||
|
@ -1430,8 +1472,11 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab
|
|||
_animVars.set("leftHandPoleVectorEnabled", false);
|
||||
}
|
||||
} else {
|
||||
_animVars.set("leftHandPoleVectorEnabled", false);
|
||||
// need this for two bone ik
|
||||
_animVars.set(LEFT_HAND_IK_POSITION_VAR, MAIN_STATE_MACHINE_LEFT_HAND_POSITION);
|
||||
_animVars.set(LEFT_HAND_IK_ROTATION_VAR, MAIN_STATE_MACHINE_LEFT_HAND_ROTATION);
|
||||
|
||||
_animVars.set("leftHandPoleVectorEnabled", false);
|
||||
_animVars.unset("leftHandPosition");
|
||||
_animVars.unset("leftHandRotation");
|
||||
|
||||
|
@ -1445,6 +1490,10 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab
|
|||
|
||||
if (rightHandEnabled) {
|
||||
|
||||
// need this for two bone IK
|
||||
_animVars.set(RIGHT_HAND_IK_POSITION_VAR, RIGHT_HAND_POSITION);
|
||||
_animVars.set(RIGHT_HAND_IK_ROTATION_VAR, RIGHT_HAND_ROTATION);
|
||||
|
||||
glm::vec3 handPosition = rightHandPose.trans();
|
||||
glm::quat handRotation = rightHandPose.rot();
|
||||
|
||||
|
@ -1478,8 +1527,12 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab
|
|||
_animVars.set("rightHandPoleVectorEnabled", false);
|
||||
}
|
||||
} else {
|
||||
_animVars.set("rightHandPoleVectorEnabled", false);
|
||||
|
||||
// need this for two bone IK
|
||||
_animVars.set(RIGHT_HAND_IK_POSITION_VAR, MAIN_STATE_MACHINE_RIGHT_HAND_POSITION);
|
||||
_animVars.set(RIGHT_HAND_IK_ROTATION_VAR, MAIN_STATE_MACHINE_RIGHT_HAND_ROTATION);
|
||||
|
||||
_animVars.set("rightHandPoleVectorEnabled", false);
|
||||
_animVars.unset("rightHandPosition");
|
||||
_animVars.unset("rightHandRotation");
|
||||
|
||||
|
@ -1697,6 +1750,7 @@ bool Rig::calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex,
|
|||
correctionVector = forwardAmount * frontVector;
|
||||
}
|
||||
poleVector = glm::normalize(attenuationVector + fullPoleVector + correctionVector);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1819,7 +1873,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
|
|||
std::shared_ptr<AnimInverseKinematics> ikNode = getAnimInverseKinematicsNode();
|
||||
for (int i = 0; i < (int)NumSecondaryControllerTypes; i++) {
|
||||
int index = indexOfJoint(secondaryControllerJointNames[i]);
|
||||
if (index >= 0) {
|
||||
if ((index >= 0) && (ikNode)) {
|
||||
if (params.secondaryControllerFlags[i] & (uint8_t)ControllerFlags::Enabled) {
|
||||
ikNode->setSecondaryTargetInRigFrame(index, params.secondaryControllerPoses[i]);
|
||||
} else {
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#include "RenderableModelEntityItem.h"
|
||||
|
||||
#include <graphics-scripting/Forward.h>
|
||||
#include <CubicHermiteSpline.h>
|
||||
|
||||
#include "Logging.h"
|
||||
|
||||
|
@ -1535,11 +1536,13 @@ void Avatar::setModelURLFinished(bool success) {
|
|||
void Avatar::rigReady() {
|
||||
buildUnscaledEyeHeightCache();
|
||||
computeMultiSphereShapes();
|
||||
buildSpine2SplineRatioCache();
|
||||
}
|
||||
|
||||
// rig has been reset.
|
||||
void Avatar::rigReset() {
|
||||
clearUnscaledEyeHeightCache();
|
||||
clearSpine2SplineRatioCache();
|
||||
}
|
||||
|
||||
void Avatar::computeMultiSphereShapes() {
|
||||
|
@ -1994,10 +1997,43 @@ void Avatar::buildUnscaledEyeHeightCache() {
|
|||
}
|
||||
}
|
||||
|
||||
void Avatar::buildSpine2SplineRatioCache() {
|
||||
if (_skeletonModel) {
|
||||
auto& rig = _skeletonModel->getRig();
|
||||
AnimPose hipsRigDefaultPose = rig.getAbsoluteDefaultPose(rig.indexOfJoint("Hips"));
|
||||
AnimPose headRigDefaultPose(rig.getAbsoluteDefaultPose(rig.indexOfJoint("Head")));
|
||||
glm::vec3 basePosition = hipsRigDefaultPose.trans();
|
||||
glm::vec3 tipPosition = headRigDefaultPose.trans();
|
||||
glm::vec3 spine2Position = rig.getAbsoluteDefaultPose(rig.indexOfJoint("Spine2")).trans();
|
||||
|
||||
glm::vec3 baseToTip = tipPosition - basePosition;
|
||||
float baseToTipLength = glm::length(baseToTip);
|
||||
glm::vec3 baseToTipNormal = baseToTip / baseToTipLength;
|
||||
glm::vec3 baseToSpine2 = spine2Position - basePosition;
|
||||
|
||||
_spine2SplineRatio = glm::dot(baseToSpine2, baseToTipNormal) / baseToTipLength;
|
||||
|
||||
CubicHermiteSplineFunctorWithArcLength defaultSpline(headRigDefaultPose.rot(), headRigDefaultPose.trans(), hipsRigDefaultPose.rot(), hipsRigDefaultPose.trans());
|
||||
|
||||
// measure the total arc length along the spline
|
||||
float totalDefaultArcLength = defaultSpline.arcLength(1.0f);
|
||||
float t = defaultSpline.arcLengthInverse(_spine2SplineRatio * totalDefaultArcLength);
|
||||
glm::vec3 defaultSplineSpine2Translation = defaultSpline(t);
|
||||
|
||||
_spine2SplineOffset = spine2Position - defaultSplineSpine2Translation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Avatar::clearUnscaledEyeHeightCache() {
|
||||
_unscaledEyeHeightCache.set(DEFAULT_AVATAR_EYE_HEIGHT);
|
||||
}
|
||||
|
||||
void Avatar::clearSpine2SplineRatioCache() {
|
||||
_spine2SplineRatio = DEFAULT_AVATAR_EYE_HEIGHT;
|
||||
_spine2SplineOffset = glm::vec3();
|
||||
}
|
||||
|
||||
float Avatar::getUnscaledEyeHeightFromSkeleton() const {
|
||||
|
||||
// TODO: if performance becomes a concern we can cache this value rather then computing it everytime.
|
||||
|
|
|
@ -236,6 +236,7 @@ public:
|
|||
virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; }
|
||||
virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; }
|
||||
|
||||
<<<<<<< HEAD
|
||||
// world-space to avatar-space rigconversion functions
|
||||
/**jsdoc
|
||||
* @function MyAvatar.worldToJointPoint
|
||||
|
@ -285,6 +286,10 @@ public:
|
|||
* @returns {Quat}
|
||||
*/
|
||||
Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const;
|
||||
=======
|
||||
virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; }
|
||||
virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; }
|
||||
>>>>>>> cache the spine2 spline default offset and ratio
|
||||
|
||||
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
|
||||
virtual void setAttachmentData(const QVector<AttachmentData>& attachmentData) override;
|
||||
|
@ -563,7 +568,9 @@ public slots:
|
|||
protected:
|
||||
float getUnscaledEyeHeightFromSkeleton() const;
|
||||
void buildUnscaledEyeHeightCache();
|
||||
void buildSpine2SplineRatioCache();
|
||||
void clearUnscaledEyeHeightCache();
|
||||
void clearSpine2SplineRatioCache();
|
||||
virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send.
|
||||
QString _empty{};
|
||||
virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter!
|
||||
|
@ -669,6 +676,8 @@ protected:
|
|||
float _displayNameAlpha { 1.0f };
|
||||
|
||||
ThreadSafeValueCache<float> _unscaledEyeHeightCache { DEFAULT_AVATAR_EYE_HEIGHT };
|
||||
float _spine2SplineRatio { DEFAULT_SPINE2_SPLINE_PROPORTION };
|
||||
glm::vec3 _spine2SplineOffset;
|
||||
|
||||
std::unordered_map<std::string, graphics::MultiMaterial> _materials;
|
||||
std::mutex _materialsLock;
|
||||
|
|
|
@ -1288,6 +1288,20 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
|
|||
}
|
||||
joint.inverseBindRotation = joint.inverseDefaultRotation;
|
||||
joint.name = fbxModel.name;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
if (hfmModel.hfmToHifiJointNameMapping.contains(hfmModel.hfmToHifiJointNameMapping.key(joint.name))) {
|
||||
joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name);
|
||||
}
|
||||
|
||||
foreach (const QString& childID, _connectionChildMap.values(modelID)) {
|
||||
QString type = typeFlags.value(childID);
|
||||
if (!type.isEmpty()) {
|
||||
hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
>>>>>>> implemented the splineIK in animSplineIK.cpp, todo: disable animinversekinematic.cpp
|
||||
|
||||
joint.bindTransformFoundInCluster = false;
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ const float DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD = 0.11f; // meters
|
|||
const float DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD = 0.185f; // meters
|
||||
const float DEFAULT_AVATAR_NECK_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD;
|
||||
const float DEFAULT_AVATAR_EYE_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
|
||||
const float DEFAULT_SPINE2_SPLINE_PROPORTION = 0.71f;
|
||||
const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f;
|
||||
const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f;
|
||||
const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f;
|
||||
|
|
|
@ -66,19 +66,19 @@ public:
|
|||
memset(_values, 0, sizeof(float) * (NUM_SUBDIVISIONS + 1));
|
||||
}
|
||||
CubicHermiteSplineFunctorWithArcLength(const glm::vec3& p0, const glm::vec3& m0, const glm::vec3& p1, const glm::vec3& m1) : CubicHermiteSplineFunctor(p0, m0, p1, m1) {
|
||||
// initialize _values with the accumulated arcLength along the spline.
|
||||
const float DELTA = 1.0f / NUM_SUBDIVISIONS;
|
||||
float alpha = 0.0f;
|
||||
float accum = 0.0f;
|
||||
_values[0] = 0.0f;
|
||||
glm::vec3 prevValue = this->operator()(alpha);
|
||||
for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) {
|
||||
glm::vec3 nextValue = this->operator()(alpha + DELTA);
|
||||
accum += glm::distance(prevValue, nextValue);
|
||||
alpha += DELTA;
|
||||
_values[i] = accum;
|
||||
prevValue = nextValue;
|
||||
}
|
||||
|
||||
initValues();
|
||||
}
|
||||
|
||||
CubicHermiteSplineFunctorWithArcLength(const glm::quat& tipRot, const glm::vec3& tipTrans, const glm::quat& baseRot, const glm::vec3& baseTrans, float baseGain = 1.0f, float tipGain = 1.0f) : CubicHermiteSplineFunctor() {
|
||||
|
||||
float linearDistance = glm::length(baseTrans - tipTrans);
|
||||
_p0 = baseTrans;
|
||||
_m0 = baseGain * linearDistance * (baseRot * Vectors::UNIT_Y);
|
||||
_p1 = tipTrans;
|
||||
_m1 = tipGain * linearDistance * (tipRot * Vectors::UNIT_Y);
|
||||
|
||||
initValues();
|
||||
}
|
||||
|
||||
CubicHermiteSplineFunctorWithArcLength(const CubicHermiteSplineFunctorWithArcLength& orig) : CubicHermiteSplineFunctor(orig) {
|
||||
|
@ -110,6 +110,21 @@ public:
|
|||
}
|
||||
protected:
|
||||
float _values[NUM_SUBDIVISIONS + 1];
|
||||
|
||||
void initValues() {
|
||||
// initialize _values with the accumulated arcLength along the spline.
|
||||
const float DELTA = 1.0f / NUM_SUBDIVISIONS;
|
||||
float alpha = 0.0f;
|
||||
float accum = 0.0f;
|
||||
_values[0] = 0.0f;
|
||||
for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) {
|
||||
accum += glm::distance(this->operator()(alpha),
|
||||
this->operator()(alpha + DELTA));
|
||||
alpha += DELTA;
|
||||
_values[i] = accum;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
#endif // hifi_CubicHermiteSpline_h
|
||||
|
|
|
@ -2,6 +2,7 @@ High Fidelity, Inc.
|
|||
Avatar Exporter
|
||||
Version 0.2
|
||||
|
||||
|
||||
Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter.
|
||||
|
||||
To create a new avatar project:
|
||||
|
|
Loading…
Reference in a new issue