From f31bbccdc9aacbe81a3002ea669dd7e4ca3fb48a Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Tue, 4 Jun 2024 13:14:53 +0800 Subject: [PATCH] add fade in/out to dynamic bone animations --- .../abstract_filament_viewer.dart | 2 +- .../compatibility/native/dart_filament.g.dart | 17 ++- .../dart_filament/filament_viewer_impl.dart | 5 +- .../native/include/DartFilamentApi.h | 4 +- dart_filament/native/include/SceneManager.hpp | 5 +- .../components/AnimationComponentManager.hpp | 104 ++++++++++++++---- dart_filament/native/src/DartFilamentApi.cpp | 6 +- dart_filament/native/src/SceneManager.cpp | 5 +- 8 files changed, 115 insertions(+), 33 deletions(-) diff --git a/dart_filament/lib/dart_filament/abstract_filament_viewer.dart b/dart_filament/lib/dart_filament/abstract_filament_viewer.dart index 3c8cd926..65e8115e 100644 --- a/dart_filament/lib/dart_filament/abstract_filament_viewer.dart +++ b/dart_filament/lib/dart_filament/abstract_filament_viewer.dart @@ -274,7 +274,7 @@ abstract class AbstractFilamentViewer { /// to transform to another space, you will need to do so manually. /// Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation, - {int skinIndex = 0}); + {int skinIndex = 0, double fadeInInSecs=0.0, double fadeOutInSecs=0.0}); /// /// Gets the entity representing the bone at [boneIndex]/[skinIndex]. diff --git a/dart_filament/lib/dart_filament/compatibility/native/dart_filament.g.dart b/dart_filament/lib/dart_filament/compatibility/native/dart_filament.g.dart index 2a50bba4..3936caa2 100644 --- a/dart_filament/lib/dart_filament/compatibility/native/dart_filament.g.dart +++ b/dart_filament/lib/dart_filament/compatibility/native/dart_filament.g.dart @@ -461,7 +461,7 @@ external void reset_to_rest_pose( @ffi.Native< ffi.Void Function(ffi.Pointer, EntityId, ffi.Int, ffi.Int, - ffi.Pointer, ffi.Int, ffi.Float)>( + ffi.Pointer, ffi.Int, ffi.Float, ffi.Float, ffi.Float)>( symbol: 'add_bone_animation', assetId: 'package:dart_filament/dart_filament.dart') external void add_bone_animation( @@ -472,6 +472,8 @@ external void add_bone_animation( ffi.Pointer frameData, int numFrames, double frameLengthInMs, + double fadeOutInSecs, + double fadeInInSecs, ); @ffi.Native< @@ -485,6 +487,19 @@ external void get_local_transform( ffi.Pointer arg2, ); +@ffi.Native< + ffi.Void Function(ffi.Pointer, EntityId, ffi.Int, + ffi.Pointer, ffi.Int)>( + symbol: 'get_rest_local_transforms', + assetId: 'package:dart_filament/dart_filament.dart') +external void get_rest_local_transforms( + ffi.Pointer sceneManager, + int entityId, + int skinIndex, + ffi.Pointer out, + int numBones, +); + @ffi.Native< ffi.Void Function( ffi.Pointer, EntityId, ffi.Pointer)>( diff --git a/dart_filament/lib/dart_filament/filament_viewer_impl.dart b/dart_filament/lib/dart_filament/filament_viewer_impl.dart index f055c0a1..4ab9dce8 100644 --- a/dart_filament/lib/dart_filament/filament_viewer_impl.dart +++ b/dart_filament/lib/dart_filament/filament_viewer_impl.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:math'; import 'package:animation_tools_dart/animation_tools_dart.dart'; import 'package:dart_filament/dart_filament/compatibility/compatibility.dart'; -import 'package:dart_filament/dart_filament/compatibility/native/compatibility.dart'; import 'package:dart_filament/dart_filament/entities/filament_entity.dart'; import 'package:dart_filament/dart_filament/entities/gizmo.dart'; @@ -676,7 +675,7 @@ class FilamentViewer extends AbstractFilamentViewer { /// @override Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation, - {int skinIndex = 0}) async { + {int skinIndex = 0, double fadeOutInSecs=0.0, double fadeInInSecs=0.0}) async { if (animation.space != Space.Bone && animation.space != Space.ParentWorldRotation) { throw UnimplementedError("TODO - support ${animation.space}"); @@ -739,7 +738,7 @@ class FilamentViewer extends AbstractFilamentViewer { } add_bone_animation(_sceneManager!, entity, skinIndex, entityBoneIndex, - data, numFrames, animation.frameLengthInMs); + data, numFrames, animation.frameLengthInMs, fadeOutInSecs, fadeInInSecs); } allocator.free(data); } diff --git a/dart_filament/native/include/DartFilamentApi.h b/dart_filament/native/include/DartFilamentApi.h index 45ce2c5e..bb98763b 100644 --- a/dart_filament/native/include/DartFilamentApi.h +++ b/dart_filament/native/include/DartFilamentApi.h @@ -147,7 +147,9 @@ extern "C" int boneIndex, const float *const frameData, int numFrames, - float frameLengthInMs); + float frameLengthInMs, + float fadeOutInSecs, + float fadeInInSecs); EMSCRIPTEN_KEEPALIVE void get_local_transform(void *sceneManager, EntityId entityId, float* const); EMSCRIPTEN_KEEPALIVE void get_rest_local_transforms(void *sceneManager, diff --git a/dart_filament/native/include/SceneManager.hpp b/dart_filament/native/include/SceneManager.hpp index 6f9bdf8a..69bf2c6b 100644 --- a/dart_filament/native/include/SceneManager.hpp +++ b/dart_filament/native/include/SceneManager.hpp @@ -116,7 +116,10 @@ namespace flutter_filament int boneIndex, const float *const frameData, int numFrames, - float frameLengthInMs); + float frameLengthInMs, + float fadeOutInSecs, + float fadeInInSecs + ); std::unique_ptr> getBoneRestTranforms(EntityId entityId, int skinIndex); void resetBones(EntityId entityId); diff --git a/dart_filament/native/include/components/AnimationComponentManager.hpp b/dart_filament/native/include/components/AnimationComponentManager.hpp index 0ce6e6dc..7933f25e 100644 --- a/dart_filament/native/include/components/AnimationComponentManager.hpp +++ b/dart_filament/native/include/components/AnimationComponentManager.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include template class std::vector; @@ -47,13 +48,17 @@ namespace flutter_filament float durationInSecs = 0; }; + /// @brief + /// The status of an animation embedded in a glTF object. + /// @param index refers to the index of the animation in the animations property of the underlying object. + /// struct GltfAnimation : AnimationStatus { int index = -1; }; // - // Use this to construct a dynamic (i.e. non-glTF embedded) morph target animation. + // The status of a morph target animation created dynamically at runtime (not glTF embedded). // struct MorphAnimation : AnimationStatus { @@ -66,7 +71,7 @@ namespace flutter_filament }; // - // Use this to construct a dynamic (i.e. non-glTF embedded) bone/joint animation. + // The status of a skeletal animation created dynamically at runtime (not glTF embedded). // struct BoneAnimation : AnimationStatus { @@ -75,6 +80,8 @@ namespace flutter_filament int lengthInFrames; float frameLengthInMs = 0; std::vector frameData; + float fadeOutInSecs = 0; + float fadeInInSecs = 0; }; struct AnimationComponent @@ -149,7 +156,6 @@ namespace flutter_filament void update() { - auto now = high_resolution_clock::now(); for (auto it = begin(); it < end(); it++) { @@ -162,6 +168,7 @@ namespace flutter_filament if (std::holds_alternative(animationComponent.target)) { + auto target = std::get(animationComponent.target); auto animator = target->getAnimator(); auto &gltfAnimations = animationComponent.gltfAnimations; @@ -170,6 +177,7 @@ namespace flutter_filament if(gltfAnimations.size() > 0) { for (int i = ((int)gltfAnimations.size()) - 1; i >= 0; i--) { + auto now = high_resolution_clock::now(); auto animationStatus = animationComponent.gltfAnimations[i]; @@ -197,23 +205,33 @@ namespace flutter_filament animator->updateBoneMatrices(); } + /// + /// When fading in/out, interpolate between the "current" transform (which has possibly been set by the glTF animation loop above) + /// and the first (for fading in) or last (for fading out) frame. + /// for (int i = (int)boneAnimations.size() - 1; i >= 0; i--) { auto animationStatus = boneAnimations[i]; - auto elapsedInSecs = float(std::chrono::duration_cast(now - animationStatus.start).count()) / 1000.0f; + auto now = high_resolution_clock::now(); - if (!animationStatus.loop && elapsedInSecs >= animationStatus.durationInSecs) + auto elapsedInMillis = float(std::chrono::duration_cast(now - animationStatus.start).count()); + auto elapsedInSecs = elapsedInMillis / 1000.0f; + + // if we're not looping and the amount of time elapsed is greater than the animation duration plus the fade-in/out buffer, + // then the animation is completed and we can delete it + if (elapsedInSecs >= (animationStatus.durationInSecs + animationStatus.fadeInInSecs + animationStatus.fadeOutInSecs)) { - Log("Bone animation %d finished", i); - boneAnimations.erase(boneAnimations.begin() + i); - continue; + if(!animationStatus.loop) { + Log("Bone animation %d finished", i); + boneAnimations.erase(boneAnimations.begin() + i); + continue; + } } - float elapsedFrames = elapsedInSecs * 1000.0f / animationStatus.frameLengthInMs; - - int currFrame = static_cast(elapsedFrames) % animationStatus.lengthInFrames; - float delta = elapsedFrames - currFrame; + // if we're fading in, treat elapsedFrames is zero (and fading out, treat elapsedFrames as lengthInFrames) + float elapsedInFrames = (elapsedInMillis - (1000 * animationStatus.fadeInInSecs)) / animationStatus.frameLengthInMs; + int currFrame = std::floor(elapsedInFrames); int nextFrame = currFrame; // offset from the end if reverse @@ -240,27 +258,65 @@ namespace flutter_filament nextFrame = currFrame; } } + currFrame = std::clamp(currFrame, 0, animationStatus.lengthInFrames - 1); + nextFrame = std::clamp(nextFrame, 0, animationStatus.lengthInFrames - 1); - // linear interpolation for this animation - math::mat4f curr = (1 - delta) * animationStatus.frameData[currFrame]; - math::mat4f next = delta * animationStatus.frameData[nextFrame]; - math::mat4f localTransform = curr + next; + float frameDelta = elapsedInFrames - currFrame; + + // linearly interpolate this animation between its last/current frames + // this is to avoid jerky animations when the animation framerate is slower than our tick rate + + math::float3 currScale, newScale; + math::quatf currRotation, newRotation; + math::float3 currTranslation, newTranslation; + math::mat4f curr = animationStatus.frameData[currFrame]; + decomposeMatrix(curr, &currTranslation, &currRotation, &currScale); + + if(frameDelta > 0) { + math::mat4f next = animationStatus.frameData[nextFrame]; + decomposeMatrix(next, &newTranslation, &newRotation, &newScale); + newScale = mix(currScale, newScale, frameDelta); + newRotation = slerp(currRotation, newRotation, frameDelta); + newTranslation = mix(currTranslation, newTranslation, frameDelta); + } else { + newScale = currScale; + newRotation = currRotation; + newTranslation = currTranslation; + } const Entity joint = target->getJointsAt(animationStatus.skinIndex)[animationStatus.boneIndex]; + // now calculate the fade out/in delta + // if we're fading in, this will be 0.0 at the start of the fade and 1.0 at the end + auto fadeDelta = elapsedInSecs / animationStatus.fadeInInSecs; + + // if we're fading out, this will be 1.0 at the start of the fade and 0.0 at the end + if(fadeDelta > 1.0f) { + fadeDelta = 1 - ((elapsedInSecs - animationStatus.durationInSecs - animationStatus.fadeInInSecs) / animationStatus.fadeOutInSecs); + } + + Log("fadeDelta %f ", fadeDelta); + auto jointTransform = _transformManager.getInstance(joint); - auto currentTransform = _transformManager.getTransform(jointTransform); - // linear interpolation between the current transform (e.g. if set by the gltf frame) - math::mat4f curr2 = (1 - delta) * animationStatus.frameData[currFrame]; - math::mat4f next2 = delta * currentTransform; - math::mat4f localTransform2 = curr + next; + // linearly interpolate this animation between its current (interpolated) frame and the current transform (i.e. as set by the gltf frame) + // // if we are fading in or out, apply a delta + if (fadeDelta >= 0.0f && fadeDelta <= 1.0f) { + math::float3 fadeScale; + math::quatf fadeRotation; + math::float3 fadeTranslation; + auto currentTransform = _transformManager.getTransform(jointTransform); + decomposeMatrix(currentTransform, &fadeTranslation, &fadeRotation, &fadeScale); + newScale = mix(fadeScale, newScale, fadeDelta); + newRotation = slerp(fadeRotation, newRotation, fadeDelta); + newTranslation = mix(fadeTranslation, newTranslation, fadeDelta); + } - _transformManager.setTransform(jointTransform, localTransform2); + _transformManager.setTransform(jointTransform, composeMatrix(newTranslation, newRotation, newScale)); animator->updateBoneMatrices(); - if (animationStatus.loop && elapsedInSecs >= animationStatus.durationInSecs) + if (animationStatus.loop && elapsedInSecs >= (animationStatus.durationInSecs + animationStatus.fadeInInSecs + animationStatus.fadeOutInSecs)) { animationStatus.start = now; } @@ -269,6 +325,8 @@ namespace flutter_filament for (int i = (int)morphAnimations.size() - 1; i >= 0; i--) { + auto now = high_resolution_clock::now(); + auto animationStatus = morphAnimations[i]; auto elapsedInSecs = float(std::chrono::duration_cast(now - animationStatus.start).count()) / 1000.0f; diff --git a/dart_filament/native/src/DartFilamentApi.cpp b/dart_filament/native/src/DartFilamentApi.cpp index d3291551..67de0d1b 100644 --- a/dart_filament/native/src/DartFilamentApi.cpp +++ b/dart_filament/native/src/DartFilamentApi.cpp @@ -422,9 +422,11 @@ extern "C" int boneIndex, const float *const frameData, int numFrames, - float frameLengthInMs) + float frameLengthInMs, + float fadeOutInSecs, + float fadeInInSecs) { - ((SceneManager *)sceneManager)->addBoneAnimation(asset, skinIndex, boneIndex, frameData, numFrames, frameLengthInMs); + ((SceneManager *)sceneManager)->addBoneAnimation(asset, skinIndex, boneIndex, frameData, numFrames, frameLengthInMs, fadeOutInSecs, fadeInInSecs); } EMSCRIPTEN_KEEPALIVE void set_post_processing(void *const viewer, bool enabled) diff --git a/dart_filament/native/src/SceneManager.cpp b/dart_filament/native/src/SceneManager.cpp index 755ab3e9..f21fa322 100644 --- a/dart_filament/native/src/SceneManager.cpp +++ b/dart_filament/native/src/SceneManager.cpp @@ -1025,7 +1025,8 @@ namespace flutter_filament int boneIndex, const float *const frameData, int numFrames, - float frameLengthInMs) + float frameLengthInMs, + float fadeOutInSecs, float fadeInInSecs) { std::lock_guard lock(_mutex); @@ -1079,6 +1080,8 @@ namespace flutter_filament animation.durationInSecs = (frameLengthInMs * numFrames) / 1000.0f; animation.lengthInFrames = numFrames; animation.frameLengthInMs = frameLengthInMs; + animation.fadeOutInSecs = fadeOutInSecs; + animation.fadeInInSecs = fadeInInSecs; animation.skinIndex = skinIndex; if(!_animationComponentManager->hasComponent(instance->getRoot())) { Log("ERROR: specified entity is not animatable (has no animation component attached).");