diff --git a/android/gltfio-android/src/main/cpp/Animator.cpp b/android/gltfio-android/src/main/cpp/Animator.cpp index 304c717409..800aed68c8 100644 --- a/android/gltfio-android/src/main/cpp/Animator.cpp +++ b/android/gltfio-android/src/main/cpp/Animator.cpp @@ -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; diff --git a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/Animator.java b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/Animator.java index 6b077ac8c2..cf2e86f071 100644 --- a/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/Animator.java +++ b/android/gltfio-android/src/main/java/com/google/android/filament/gltfio/Animator.java @@ -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); diff --git a/libs/gltfio/include/gltfio/Animator.h b/libs/gltfio/include/gltfio/Animator.h index 2a693a0038..bc15bac2f7 100644 --- a/libs/gltfio/include/gltfio/Animator.h +++ b/libs/gltfio/include/gltfio/Animator.h @@ -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. * diff --git a/libs/gltfio/src/Animator.cpp b/libs/gltfio/src/Animator.cpp index a74677fe5d..defdae3574 100644 --- a/libs/gltfio/src/Animator.cpp +++ b/libs/gltfio/src/Animator.cpp @@ -46,7 +46,7 @@ namespace gltfio { using TimeValues = map; using SourceValues = vector; -using BoneVector = vector; +using BoneVector = vector; struct Sampler { TimeValues times; @@ -75,8 +75,11 @@ struct AnimatorImpl { RenderableManager* renderableManager; TransformManager* transformManager; vector weights; + FixedCapacityVector 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) { diff --git a/libs/viewer/include/viewer/ViewerGui.h b/libs/viewer/include/viewer/ViewerGui.h index 0fb07994be..348b0870ab 100644 --- a/libs/viewer/include/viewer/ViewerGui.h +++ b/libs/viewer/include/viewer/ViewerGui.h @@ -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]; diff --git a/libs/viewer/src/ViewerGui.cpp b/libs/viewer/src/ViewerGui.cpp index 68f8c490ba..66e6ca2771 100644 --- a/libs/viewer/src/ViewerGui.cpp +++ b/libs/viewer/src/ViewerGui.cpp @@ -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; } diff --git a/web/filament-js/filament.d.ts b/web/filament-js/filament.d.ts index 4200c74902..d138b996a1 100644 --- a/web/filament-js/filament.d.ts +++ b/web/filament-js/filament.d.ts @@ -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; diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index 5f5defaf11..00ea8d0d54 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -1707,6 +1707,7 @@ class_("SurfaceOrientation") class_("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)