gltfio: introduce cross-fade animation API

Fixes #4754
This commit is contained in:
Philip Rideout
2022-05-23 13:31:59 -07:00
parent 60a6d4a348
commit a01fa21209
8 changed files with 131 additions and 8 deletions

View File

@@ -36,6 +36,13 @@ Java_com_google_android_filament_gltfio_Animator_nUpdateBoneMatrices(JNIEnv*, jc
animator->updateBoneMatrices();
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_gltfio_Animator_nApplyCrossFade(JNIEnv*, jclass, jlong nativeAnimator,
jint previousAnimIndex, jfloat previousAnimTime, jfloat alpha) {
Animator* animator = (Animator*) nativeAnimator;
animator->applyCrossFade(previousAnimIndex, previousAnimTime, alpha);
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_gltfio_Animator_nResetBoneMatrices(JNIEnv*, jclass, jlong nativeAnimator) {
Animator* animator = (Animator*) nativeAnimator;

View File

@@ -66,6 +66,27 @@ public class Animator {
nUpdateBoneMatrices(getNativeObject());
}
/**
* Applies a blended transform to the union of nodes affected by two animations.
* Used for cross-fading from a previous skinning-based animation or rigid body animation.
*
* First, this stashes the current transform hierarchy into a transient memory buffer.
*
* Next, this applies previousAnimIndex / previousAnimTime to the actual asset by internally
* calling applyAnimation().
*
* Finally, the stashed local transforms are lerped (via the scale / translation / rotation
* components) with their live counterparts, and the results are pushed to the asset.
*
* To achieve a cross fade effect with skinned models, clients will typically call animator
* methods in this order: (1) applyAnimation (2) applyCrossFade (3) updateBoneMatrices. The
* animation that clients pass to applyAnimation is the "current" animation corresponding to
* alpha=1, while the "previous" animation passed to applyCrossFade corresponds to alpha=0.
*/
public void applyCrossFade(int previousAnimIndex, float previousAnimTime, float alpha) {
nApplyCrossFade(getNativeObject(), previousAnimIndex, previousAnimTime, alpha);
}
/**
* Pass the identity matrix into all bone nodes, useful for returning to the T pose.
*
@@ -118,6 +139,7 @@ public class Animator {
private static native void nApplyAnimation(long nativeAnimator, int index, float time);
private static native void nUpdateBoneMatrices(long nativeAnimator);
private static native void nApplyCrossFade(long nativeAnimator, int animIndex, float animTime, float alpha);
private static native void nResetBoneMatrices(long nativeAnimator);
private static native int nGetAnimationCount(long nativeAnimator);
private static native float nGetAnimationDuration(long nativeAnimator, int index);

View File

@@ -56,6 +56,25 @@ public:
*/
void updateBoneMatrices();
/**
* Applies a blended transform to the union of nodes affected by two animations.
* Used for cross-fading from a previous skinning-based animation or rigid body animation.
*
* First, this stashes the current transform hierarchy into a transient memory buffer.
*
* Next, this applies previousAnimIndex / previousAnimTime to the actual asset by internally
* calling applyAnimation().
*
* Finally, the stashed local transforms are lerped (via the scale / translation / rotation
* components) with their live counterparts, and the results are pushed to the asset.
*
* To achieve a cross fade effect with skinned models, clients will typically call animator
* methods in this order: (1) applyAnimation (2) applyCrossFade (3) updateBoneMatrices. The
* animation that clients pass to applyAnimation is the "current" animation corresponding to
* alpha=1, while the "previous" animation passed to applyCrossFade corresponds to alpha=0.
*/
void applyCrossFade(size_t previousAnimIndex, float previousAnimTime, float alpha);
/**
* Pass the identity matrix into all bone nodes, useful for returning to the T pose.
*

View File

@@ -46,7 +46,7 @@ namespace gltfio {
using TimeValues = map<float, size_t>;
using SourceValues = vector<float>;
using BoneVector = vector<filament::math::mat4f>;
using BoneVector = vector<mat4f>;
struct Sampler {
TimeValues times;
@@ -75,8 +75,11 @@ struct AnimatorImpl {
RenderableManager* renderableManager;
TransformManager* transformManager;
vector<float> weights;
FixedCapacityVector<mat4f> crossFade;
void addChannels(const NodeMap& nodeMap, const cgltf_animation& srcAnim, Animation& dst);
void applyAnimation(const Channel& channel, float t, size_t prevIndex, size_t nextIndex);
void stashCrossFade();
void applyCrossFade(float alpha);
};
static void createSampler(const cgltf_animation_sampler& src, Sampler& dst) {
@@ -221,6 +224,12 @@ Animator::Animator(FFilamentAsset* asset, FFilamentInstance* instance) {
}
}
void Animator::applyCrossFade(size_t previousAnimIndex, float previousAnimTime, float alpha) {
mImpl->stashCrossFade();
applyAnimation(previousAnimIndex, previousAnimTime);
mImpl->applyCrossFade(alpha);
}
void Animator::addInstance(FFilamentInstance* instance) {
const cgltf_data* srcAsset = mImpl->asset->mSourceAsset->hierarchy;
const cgltf_animation* srcAnims = srcAsset->animations;
@@ -367,6 +376,58 @@ const char* Animator::getAnimationName(size_t animationIndex) const {
return mImpl->animations[animationIndex].name.c_str();
}
void AnimatorImpl::stashCrossFade() {
using Instance = TransformManager::Instance;
auto& tm = *this->transformManager;
auto& stash = this->crossFade;
// Count the total number of transformable nodes to preallocate the stash memory.
// We considered caching this count, but the cache would need to be invalidated when entities
// are added into the hierarchy.
auto recursiveCount = [&tm](Instance node, size_t count, auto& fn) -> size_t {
++count;
for (auto iter = tm.getChildrenBegin(node); iter != tm.getChildrenEnd(node); ++iter) {
count = fn(*iter, count, fn);
}
return count;
};
auto recursiveStash = [&tm, &stash](Instance node, size_t index, auto& fn) -> size_t {
stash[index++] = tm.getTransform(node);
for (auto iter = tm.getChildrenBegin(node); iter != tm.getChildrenEnd(node); ++iter) {
index = fn(*iter, index, fn);
}
return index;
};
const Instance root = tm.getInstance(asset->mRoot);
const size_t count = recursiveCount(root, 0, recursiveCount);
crossFade.reserve(count);
crossFade.resize(count);
recursiveStash(root, 0, recursiveStash);
}
void AnimatorImpl::applyCrossFade(float alpha) {
using Instance = TransformManager::Instance;
auto& tm = *this->transformManager;
auto& stash = this->crossFade;
auto recursiveFn = [&tm, &stash, alpha](Instance node, size_t index, auto& fn) -> size_t {
float3 scale0, scale1;
quatf rotation0, rotation1;
float3 translation0, translation1;
decomposeMatrix(stash[index++], &translation1, &rotation1, &scale1);
decomposeMatrix(tm.getTransform(node), &translation0, &rotation0, &scale0);
const float3 scale = mix(scale0, scale1, alpha);
const quatf rotation = slerp(rotation0, rotation1, alpha);
const float3 translation = mix(translation0, translation1, alpha);
tm.setTransform(node, composeMatrix(translation, rotation, scale));
for (auto iter = tm.getChildrenBegin(node); iter != tm.getChildrenEnd(node); ++iter) {
index = fn(*iter, index, fn);
}
return index;
};
recursiveFn(tm.getInstance(asset->mRoot), 0, recursiveFn);
}
void AnimatorImpl::addChannels(const NodeMap& nodeMap, const cgltf_animation& srcAnim,
Animation& dst) {

View File

@@ -255,7 +255,6 @@ private:
// Properties that can be changed from the UI.
int mCurrentAnimation = 1; // It is a 1-based index and 0 means not playing animation
int mCurrentVariant = 0;
bool mResetAnimation = true;
bool mEnableWireframe = false;
int mVsmMsaaSamplesLog2 = 1;
Settings mSettings;
@@ -269,6 +268,13 @@ private:
// 0 is the default "free camera". Additional cameras come from the gltf file (1-based index).
int mCurrentCamera = 0;
// Cross fade animation parameters.
float mCrossFadeDuration = 0.5f; // number of seconds to transition between animations
int mPreviousAnimation = 0; // one-based index of the previous animation
double mCurrentStartTime = 0.0f; // start time of most recent cross-fade (seconds)
double mPreviousStartTime = 0.0f; // start time of previous cross-fade (seconds)
bool mResetAnimation = true; // set when building ImGui widgets, honored in applyAnimation
// Color grading UI state.
float mToneMapPlot[1024];
float mRangePlot[1024 * 3];

View File

@@ -500,17 +500,20 @@ void ViewerGui::sceneSelectionUI() {
void ViewerGui::applyAnimation(double currentTime) {
assert_invariant(!isRemoteMode());
static double startTime = 0;
const size_t numAnimations = mAnimator->getAnimationCount();
if (mResetAnimation) {
startTime = currentTime;
for (size_t i = 0; i < numAnimations; i++) {
mAnimator->applyAnimation(i, 0);
}
mPreviousStartTime = mCurrentStartTime;
mCurrentStartTime = currentTime;
mResetAnimation = false;
}
const double elapsedSeconds = currentTime - mCurrentStartTime;
if (numAnimations > 0 && mCurrentAnimation > 0) {
mAnimator->applyAnimation(mCurrentAnimation - 1, currentTime - startTime);
mAnimator->applyAnimation(mCurrentAnimation - 1, elapsedSeconds);
if (elapsedSeconds < mCrossFadeDuration && mPreviousAnimation > 0) {
const double previousSeconds = currentTime - mPreviousStartTime;
const float lerpFactor = elapsedSeconds / mCrossFadeDuration;
mAnimator->applyCrossFade(mPreviousAnimation - 1, previousSeconds, lerpFactor);
}
}
if (mShowingRestPose) {
mAnimator->resetBoneMatrices();
@@ -1031,6 +1034,8 @@ void ViewerGui::updateUserInterface() {
ImGui::Indent();
int selectedAnimation = mCurrentAnimation;
ImGui::RadioButton("Disable", &selectedAnimation, 0);
ImGui::SliderFloat("Cross fade", &mCrossFadeDuration, 0.0f, 2.0f,
"%4.2f seconds", ImGuiSliderFlags_AlwaysClamp);
for (size_t i = 0, count = mAnimator->getAnimationCount(); i < count; ++i) {
std::string label = mAnimator->getAnimationName(i);
if (label.empty()) {
@@ -1039,6 +1044,7 @@ void ViewerGui::updateUserInterface() {
ImGui::RadioButton(label.c_str(), &selectedAnimation, i + 1);
}
if (selectedAnimation != mCurrentAnimation) {
mPreviousAnimation = mCurrentAnimation;
mCurrentAnimation = selectedAnimation;
mResetAnimation = true;
}

View File

@@ -598,6 +598,7 @@ export class gltfio$FilamentInstance {
export class gltfio$Animator {
public applyAnimation(index: number): void;
public applyCrossFade(previousAnimIndex: number, previousAnimTime: number, alpha: number): void;
public updateBoneMatrices(): void;
public resetBoneMatrices(): void;
public getAnimationCount(): number;

View File

@@ -1707,6 +1707,7 @@ class_<SurfaceOrientation>("SurfaceOrientation")
class_<Animator>("gltfio$Animator")
.function("applyAnimation", &Animator::applyAnimation)
.function("updateBoneMatrices", &Animator::updateBoneMatrices)
.function("applyCrossFade", &Animator::applyCrossFade)
.function("resetBoneMatrices", &Animator::resetBoneMatrices)
.function("getAnimationCount", &Animator::getAnimationCount)
.function("getAnimationDuration", &Animator::getAnimationDuration)