feature!:
This is a breaking change needed to fully implement instancing and stencil highlighting.
Previously, users would work directly with entities (on the Dart side, ThermionEntity), e.g.
final entity = await viewer.loadGlb("some.glb");
However, Filament "entities" are a lower-level abstraction.
Loading a glTF file, for example, inserts multiple entities into the scene.
For example, each mesh, light, and camera within a glTF asset will be assigned an entity. A top-level (non-renderable) entity will also be created for the glTF asset, which can be used to transform the entire hierarchy.
"Asset" is a better representation for loading/inserting objects into the scene; think of this as a bundle of entities.
Unless you need to work directly with transforms, instancing, materials and renderables, you can work directly with ThermionAsset.
This commit is contained in:
521
thermion_dart/native/src/scene/AnimationManager.cpp
Normal file
521
thermion_dart/native/src/scene/AnimationManager.cpp
Normal file
@@ -0,0 +1,521 @@
|
||||
#include <memory>
|
||||
#include <stack>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <filament/Engine.h>
|
||||
#include <filament/TransformManager.h>
|
||||
#include <filament/RenderableManager.h>
|
||||
|
||||
#include <gltfio/Animator.h>
|
||||
|
||||
#include "Log.hpp"
|
||||
|
||||
#include "scene/AnimationManager.hpp"
|
||||
#include "scene/SceneAsset.hpp"
|
||||
#include "scene/GltfSceneAssetInstance.hpp"
|
||||
|
||||
namespace thermion
|
||||
{
|
||||
|
||||
using namespace filament;
|
||||
using namespace utils;
|
||||
|
||||
AnimationManager::AnimationManager(Engine *engine, Scene *scene) : _engine(engine), _scene(scene)
|
||||
{
|
||||
auto &transformManager = _engine->getTransformManager();
|
||||
auto &renderableManager = _engine->getRenderableManager();
|
||||
_animationComponentManager = std::make_unique<AnimationComponentManager>(transformManager, renderableManager);
|
||||
}
|
||||
|
||||
AnimationManager::~AnimationManager()
|
||||
{
|
||||
_animationComponentManager = std::nullptr_t();
|
||||
}
|
||||
|
||||
bool AnimationManager::setMorphAnimationBuffer(
|
||||
utils::Entity entity,
|
||||
const float *const morphData,
|
||||
const uint32_t *const morphIndices,
|
||||
int numMorphTargets,
|
||||
int numFrames,
|
||||
float frameLengthInMs)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
if (!_animationComponentManager->hasComponent(entity))
|
||||
{
|
||||
_animationComponentManager->addAnimationComponent(entity);
|
||||
}
|
||||
|
||||
MorphAnimation morphAnimation;
|
||||
|
||||
morphAnimation.meshTarget = entity;
|
||||
morphAnimation.frameData.clear();
|
||||
morphAnimation.frameData.insert(
|
||||
morphAnimation.frameData.begin(),
|
||||
morphData,
|
||||
morphData + (numFrames * numMorphTargets));
|
||||
morphAnimation.frameLengthInMs = frameLengthInMs;
|
||||
morphAnimation.morphIndices.resize(numMorphTargets);
|
||||
for (int i = 0; i < numMorphTargets; i++)
|
||||
{
|
||||
morphAnimation.morphIndices[i] = morphIndices[i];
|
||||
}
|
||||
morphAnimation.durationInSecs = (frameLengthInMs * numFrames) / 1000.0f;
|
||||
|
||||
morphAnimation.start = high_resolution_clock::now();
|
||||
morphAnimation.lengthInFrames = static_cast<int>(
|
||||
morphAnimation.durationInSecs * 1000.0f /
|
||||
frameLengthInMs);
|
||||
|
||||
auto animationComponentInstance = _animationComponentManager->getInstance(entity);
|
||||
auto &animationComponent = _animationComponentManager->elementAt<0>(animationComponentInstance);
|
||||
auto &morphAnimations = animationComponent.morphAnimations;
|
||||
|
||||
morphAnimations.emplace_back(morphAnimation);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimationManager::clearMorphAnimationBuffer(
|
||||
utils::Entity entity)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
auto animationComponentInstance = _animationComponentManager->getInstance(entity);
|
||||
auto &animationComponent = _animationComponentManager->elementAt<0>(animationComponentInstance);
|
||||
auto &morphAnimations = animationComponent.morphAnimations;
|
||||
morphAnimations.clear();
|
||||
}
|
||||
|
||||
void AnimationManager::resetToRestPose(GltfSceneAssetInstance *instance)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
auto filamentInstance = instance->getInstance();
|
||||
auto skinCount = filamentInstance->getSkinCount();
|
||||
|
||||
TransformManager &transformManager = _engine->getTransformManager();
|
||||
|
||||
//
|
||||
// To reset the skeleton to its rest pose, we could just call animator->resetBoneMatrices(),
|
||||
// which sets all bone matrices to the identity matrix. However, any subsequent calls to animator->updateBoneMatrices()
|
||||
// may result in unexpected poses, because that method uses each bone's transform to calculate
|
||||
// the bone matrices (and resetBoneMatrices does not affect this transform).
|
||||
// To "fully" reset the bone, we need to set its local transform (i.e. relative to its parent)
|
||||
// to its original orientation in rest pose.
|
||||
//
|
||||
// This can be calculated as:
|
||||
//
|
||||
// auto rest = inverse(parentTransformInModelSpace) * bindMatrix
|
||||
//
|
||||
// (where bindMatrix is the inverse of the inverseBindMatrix).
|
||||
//
|
||||
// The only requirement is that parent bone transforms are reset before child bone transforms.
|
||||
// glTF/Filament does not guarantee that parent bones are listed before child bones under a FilamentInstance.
|
||||
// We ensure that parents are reset before children by:
|
||||
// - pushing all bones onto a stack
|
||||
// - iterate over the stack
|
||||
// - look at the bone at the top of the stack
|
||||
// - if the bone already been reset, pop and continue iterating over the stack
|
||||
// - otherwise
|
||||
// - if the bone has a parent that has not been reset, push the parent to the top of the stack and continue iterating
|
||||
// - otherwise
|
||||
// - pop the bone, reset its transform and mark it as completed
|
||||
for (int skinIndex = 0; skinIndex < skinCount; skinIndex++)
|
||||
{
|
||||
std::unordered_set<Entity, Entity::Hasher> joints;
|
||||
std::unordered_set<Entity, Entity::Hasher> completed;
|
||||
std::stack<Entity> stack;
|
||||
|
||||
auto transforms = getBoneRestTranforms(instance, skinIndex);
|
||||
|
||||
for (int i = 0; i < filamentInstance->getJointCountAt(skinIndex); i++)
|
||||
{
|
||||
auto restTransform = transforms[i];
|
||||
const auto &joint = filamentInstance->getJointsAt(skinIndex)[i];
|
||||
auto transformInstance = transformManager.getInstance(joint);
|
||||
transformManager.setTransform(transformInstance, restTransform);
|
||||
}
|
||||
}
|
||||
filamentInstance->getAnimator()->updateBoneMatrices();
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<math::mat4f> AnimationManager::getBoneRestTranforms(GltfSceneAssetInstance *instance, int skinIndex)
|
||||
{
|
||||
|
||||
std::vector<math::mat4f> transforms;
|
||||
|
||||
auto filamentInstance = instance->getInstance();
|
||||
auto skinCount = filamentInstance->getSkinCount();
|
||||
|
||||
TransformManager &transformManager = _engine->getTransformManager();
|
||||
|
||||
transforms.resize(filamentInstance->getJointCountAt(skinIndex));
|
||||
|
||||
//
|
||||
// To reset the skeleton to its rest pose, we could just call animator->resetBoneMatrices(),
|
||||
// which sets all bone matrices to the identity matrix. However, any subsequent calls to animator->updateBoneMatrices()
|
||||
// may result in unexpected poses, because that method uses each bone's transform to calculate
|
||||
// the bone matrices (and resetBoneMatrices does not affect this transform).
|
||||
// To "fully" reset the bone, we need to set its local transform (i.e. relative to its parent)
|
||||
// to its original orientation in rest pose.
|
||||
//
|
||||
// This can be calculated as:
|
||||
//
|
||||
// auto rest = inverse(parentTransformInModelSpace) * bindMatrix
|
||||
//
|
||||
// (where bindMatrix is the inverse of the inverseBindMatrix).
|
||||
//
|
||||
// The only requirement is that parent bone transforms are reset before child bone transforms.
|
||||
// glTF/Filament does not guarantee that parent bones are listed before child bones under a FilamentInstance.
|
||||
// We ensure that parents are reset before children by:
|
||||
// - pushing all bones onto a stack
|
||||
// - iterate over the stack
|
||||
// - look at the bone at the top of the stack
|
||||
// - if the bone already been reset, pop and continue iterating over the stack
|
||||
// - otherwise
|
||||
// - if the bone has a parent that has not been reset, push the parent to the top of the stack and continue iterating
|
||||
// - otherwise
|
||||
// - pop the bone, reset its transform and mark it as completed
|
||||
std::vector<Entity> joints;
|
||||
std::unordered_set<Entity, Entity::Hasher> completed;
|
||||
std::stack<Entity> stack;
|
||||
|
||||
for (int i = 0; i < filamentInstance->getJointCountAt(skinIndex); i++)
|
||||
{
|
||||
const auto &joint = filamentInstance->getJointsAt(skinIndex)[i];
|
||||
joints.push_back(joint);
|
||||
stack.push(joint);
|
||||
}
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
const auto &joint = stack.top();
|
||||
|
||||
// if we've already handled this node previously (e.g. when we encountered it as a parent), then skip
|
||||
if (completed.find(joint) != completed.end())
|
||||
{
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto transformInstance = transformManager.getInstance(joint);
|
||||
auto parent = transformManager.getParent(transformInstance);
|
||||
|
||||
// we need to handle parent joints before handling their children
|
||||
// therefore, if this joint has a parent that hasn't been handled yet,
|
||||
// push the parent to the top of the stack and start the loop again
|
||||
const auto &jointIter = std::find(joints.begin(), joints.end(), joint);
|
||||
auto parentIter = std::find(joints.begin(), joints.end(), parent);
|
||||
|
||||
if (parentIter != joints.end() && completed.find(parent) == completed.end())
|
||||
{
|
||||
stack.push(parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise let's get the inverse bind matrix for the joint
|
||||
math::mat4f inverseBindMatrix;
|
||||
bool found = false;
|
||||
for (int i = 0; i < filamentInstance->getJointCountAt(skinIndex); i++)
|
||||
{
|
||||
if (filamentInstance->getJointsAt(skinIndex)[i] == joint)
|
||||
{
|
||||
inverseBindMatrix = filamentInstance->getInverseBindMatricesAt(skinIndex)[i];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_PRECONDITION(found, "Failed to find inverse bind matrix for joint %d", joint);
|
||||
|
||||
// now we need to ascend back up the hierarchy to calculate the modelSpaceTransform
|
||||
math::mat4f modelSpaceTransform;
|
||||
while (parentIter != joints.end())
|
||||
{
|
||||
const auto transformInstance = transformManager.getInstance(parent);
|
||||
const auto parentIndex = distance(joints.begin(), parentIter);
|
||||
const auto transform = transforms[parentIndex];
|
||||
modelSpaceTransform = transform * modelSpaceTransform;
|
||||
parent = transformManager.getParent(transformInstance);
|
||||
parentIter = std::find(joints.begin(), joints.end(), parent);
|
||||
}
|
||||
|
||||
const auto bindMatrix = inverse(inverseBindMatrix);
|
||||
|
||||
const auto inverseModelSpaceTransform = inverse(modelSpaceTransform);
|
||||
|
||||
const auto jointIndex = distance(joints.begin(), jointIter);
|
||||
transforms[jointIndex] = inverseModelSpaceTransform * bindMatrix;
|
||||
completed.insert(joint);
|
||||
stack.pop();
|
||||
}
|
||||
return transforms;
|
||||
}
|
||||
|
||||
void AnimationManager::updateBoneMatrices(GltfSceneAssetInstance *instance)
|
||||
{
|
||||
instance->getInstance()->getAnimator()->updateBoneMatrices();
|
||||
}
|
||||
|
||||
bool AnimationManager::addBoneAnimation(GltfSceneAssetInstance *instance,
|
||||
int skinIndex,
|
||||
int boneIndex,
|
||||
const float *const frameData,
|
||||
int numFrames,
|
||||
float frameLengthInMs,
|
||||
float fadeOutInSecs,
|
||||
float fadeInInSecs,
|
||||
float maxDelta)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
BoneAnimation animation;
|
||||
animation.boneIndex = boneIndex;
|
||||
animation.frameData.clear();
|
||||
|
||||
const auto &inverseBindMatrix = instance->getInstance()->getInverseBindMatricesAt(skinIndex)[boneIndex];
|
||||
for (int i = 0; i < numFrames; i++)
|
||||
{
|
||||
math::mat4f frame(
|
||||
frameData[i * 16],
|
||||
frameData[(i * 16) + 1],
|
||||
frameData[(i * 16) + 2],
|
||||
frameData[(i * 16) + 3],
|
||||
frameData[(i * 16) + 4],
|
||||
frameData[(i * 16) + 5],
|
||||
frameData[(i * 16) + 6],
|
||||
frameData[(i * 16) + 7],
|
||||
frameData[(i * 16) + 8],
|
||||
frameData[(i * 16) + 9],
|
||||
frameData[(i * 16) + 10],
|
||||
frameData[(i * 16) + 11],
|
||||
frameData[(i * 16) + 12],
|
||||
frameData[(i * 16) + 13],
|
||||
frameData[(i * 16) + 14],
|
||||
frameData[(i * 16) + 15]);
|
||||
|
||||
animation.frameData.push_back(frame);
|
||||
}
|
||||
|
||||
animation.frameLengthInMs = frameLengthInMs;
|
||||
animation.start = std::chrono::high_resolution_clock::now();
|
||||
animation.reverse = false;
|
||||
animation.durationInSecs = (frameLengthInMs * numFrames) / 1000.0f;
|
||||
animation.lengthInFrames = numFrames;
|
||||
animation.frameLengthInMs = frameLengthInMs;
|
||||
animation.fadeOutInSecs = fadeOutInSecs;
|
||||
animation.fadeInInSecs = fadeInInSecs;
|
||||
animation.maxDelta = maxDelta;
|
||||
animation.skinIndex = skinIndex;
|
||||
if (!_animationComponentManager->hasComponent(instance->getInstance()->getRoot()))
|
||||
{
|
||||
Log("ERROR: specified entity is not animatable (has no animation component attached).");
|
||||
return false;
|
||||
}
|
||||
auto animationComponentInstance = _animationComponentManager->getInstance(instance->getInstance()->getRoot());
|
||||
|
||||
auto &animationComponent = _animationComponentManager->elementAt<0>(animationComponentInstance);
|
||||
auto &boneAnimations = animationComponent.boneAnimations;
|
||||
|
||||
boneAnimations.emplace_back(animation);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimationManager::playGltfAnimation(GltfSceneAssetInstance *instance, int index, bool loop, bool reverse, bool replaceActive, float crossfade, float startOffset)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
Log("ERROR: glTF animation index must be greater than zero.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_animationComponentManager->hasComponent(instance->getEntity()))
|
||||
{
|
||||
Log("ERROR: specified entity is not animatable (has no animation component attached).");
|
||||
return;
|
||||
}
|
||||
|
||||
auto animationComponentInstance = _animationComponentManager->getInstance(instance->getEntity());
|
||||
|
||||
auto &animationComponent = _animationComponentManager->elementAt<0>(animationComponentInstance);
|
||||
|
||||
if (replaceActive)
|
||||
{
|
||||
if (animationComponent.gltfAnimations.size() > 0)
|
||||
{
|
||||
auto &last = animationComponent.gltfAnimations.back();
|
||||
animationComponent.fadeGltfAnimationIndex = last.index;
|
||||
animationComponent.fadeDuration = crossfade;
|
||||
auto now = high_resolution_clock::now();
|
||||
auto elapsedInSecs = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - last.start).count()) / 1000.0f;
|
||||
animationComponent.fadeOutAnimationStart = elapsedInSecs;
|
||||
animationComponent.gltfAnimations.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
animationComponent.fadeGltfAnimationIndex = -1;
|
||||
animationComponent.fadeDuration = 0.0f;
|
||||
}
|
||||
}
|
||||
else if (crossfade > 0)
|
||||
{
|
||||
Log("ERROR: crossfade only supported when replaceActive is true.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
animationComponent.fadeGltfAnimationIndex = -1;
|
||||
animationComponent.fadeDuration = 0.0f;
|
||||
}
|
||||
|
||||
GltfAnimation animation;
|
||||
animation.startOffset = startOffset;
|
||||
animation.index = index;
|
||||
animation.start = std::chrono::high_resolution_clock::now();
|
||||
animation.loop = loop;
|
||||
animation.reverse = reverse;
|
||||
animation.durationInSecs = instance->getInstance()->getAnimator()->getAnimationDuration(index);
|
||||
|
||||
bool found = false;
|
||||
|
||||
// don't play the animation if it's already running
|
||||
for (int i = 0; i < animationComponent.gltfAnimations.size(); i++)
|
||||
{
|
||||
if (animationComponent.gltfAnimations[i].index == index)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
animationComponent.gltfAnimations.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationManager::stopGltfAnimation(GltfSceneAssetInstance *instance, int index)
|
||||
{
|
||||
|
||||
auto animationComponentInstance = _animationComponentManager->getInstance(instance->getEntity());
|
||||
auto &animationComponent = _animationComponentManager->elementAt<0>(animationComponentInstance);
|
||||
|
||||
auto erased = std::remove_if(animationComponent.gltfAnimations.begin(),
|
||||
animationComponent.gltfAnimations.end(),
|
||||
[=](GltfAnimation &anim)
|
||||
{ return anim.index == index; });
|
||||
animationComponent.gltfAnimations.erase(erased,
|
||||
animationComponent.gltfAnimations.end());
|
||||
return;
|
||||
}
|
||||
|
||||
void AnimationManager::setMorphTargetWeights(utils::Entity entity, const float *const weights, const int count)
|
||||
{
|
||||
RenderableManager &rm = _engine->getRenderableManager();
|
||||
auto renderableInstance = rm.getInstance(entity);
|
||||
rm.setMorphWeights(
|
||||
renderableInstance,
|
||||
weights,
|
||||
count);
|
||||
}
|
||||
|
||||
void AnimationManager::setGltfAnimationFrame(GltfSceneAssetInstance *instance, int animationIndex, int animationFrame)
|
||||
{
|
||||
auto offset = 60 * animationFrame * 1000; // TODO - don't hardcore 60fps framerate
|
||||
instance->getInstance()->getAnimator()->applyAnimation(animationIndex, offset);
|
||||
instance->getInstance()->getAnimator()->updateBoneMatrices();
|
||||
return;
|
||||
}
|
||||
|
||||
float AnimationManager::getGltfAnimationDuration(GltfSceneAssetInstance *instance, int animationIndex)
|
||||
{
|
||||
return instance->getInstance()->getAnimator()->getAnimationDuration(animationIndex);
|
||||
}
|
||||
|
||||
std::vector<std::string> AnimationManager::getGltfAnimationNames(GltfSceneAssetInstance *instance)
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
|
||||
size_t count = instance->getInstance()->getAnimator()->getAnimationCount();
|
||||
|
||||
for (size_t i = 0; i < count; i++)
|
||||
{
|
||||
names.push_back(instance->getInstance()->getAnimator()->getAnimationName(i));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
std::vector<std::string> AnimationManager::getMorphTargetNames(GltfSceneAsset *asset, EntityId childEntity)
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
|
||||
auto filamentAsset = asset->getAsset();
|
||||
|
||||
const utils::Entity targetEntity = utils::Entity::import(childEntity);
|
||||
|
||||
size_t count = filamentAsset->getMorphTargetCountAt(targetEntity);
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
const char *morphName = filamentAsset->getMorphTargetNameAt(targetEntity, j);
|
||||
names.push_back(morphName);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
vector<Entity> AnimationManager::getBoneEntities(GltfSceneAssetInstance *instance, int skinIndex)
|
||||
{
|
||||
auto *joints = instance->getInstance()->getJointsAt(skinIndex);
|
||||
auto jointCount = instance->getInstance()->getJointCountAt(skinIndex);
|
||||
std::vector<Entity> boneEntities(joints, joints + jointCount);
|
||||
return boneEntities;
|
||||
}
|
||||
|
||||
void AnimationManager::update()
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
_animationComponentManager->update();
|
||||
}
|
||||
|
||||
math::mat4f AnimationManager::getInverseBindMatrix(GltfSceneAssetInstance *instance, int skinIndex, int boneIndex)
|
||||
{
|
||||
return instance->getInstance()->getInverseBindMatricesAt(skinIndex)[boneIndex];
|
||||
}
|
||||
|
||||
bool AnimationManager::setBoneTransform(GltfSceneAssetInstance *instance, int32_t skinIndex, int boneIndex, math::mat4f transform)
|
||||
{
|
||||
std::lock_guard lock(_mutex);
|
||||
|
||||
RenderableManager &rm = _engine->getRenderableManager();
|
||||
|
||||
const auto &renderableInstance = rm.getInstance(instance->getEntity());
|
||||
|
||||
if (!renderableInstance.isValid())
|
||||
{
|
||||
Log("Specified entity is not a renderable. You probably provided the ultimate parent entity of a glTF asset, which is non-renderable. ");
|
||||
return false;
|
||||
}
|
||||
|
||||
rm.setBones(
|
||||
renderableInstance,
|
||||
&transform,
|
||||
1,
|
||||
boneIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AnimationManager::addAnimationComponent(EntityId entity)
|
||||
{
|
||||
_animationComponentManager->addAnimationComponent(utils::Entity::import(entity));
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimationManager::removeAnimationComponent(EntityId entity)
|
||||
{
|
||||
_animationComponentManager->removeComponent(utils::Entity::import(entity));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user