#pragma once #include "Log.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include template class std::vector; namespace flutter_filament { using namespace filament; using namespace filament::gltfio; using namespace utils; using namespace std::chrono; typedef std::chrono::time_point time_point_t; enum AnimationType { MORPH, BONE, GLTF }; struct AnimationStatus { time_point_t start = time_point_t::max(); bool loop = false; bool reverse = false; 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; }; // // The status of a morph target animation created dynamically at runtime (not glTF embedded). // struct MorphAnimation : AnimationStatus { utils::Entity meshTarget; int numFrames = -1; float frameLengthInMs = 0; std::vector frameData; std::vector morphIndices; int lengthInFrames; }; // // The status of a skeletal animation created dynamically at runtime (not glTF embedded). // struct BoneAnimation : AnimationStatus { size_t boneIndex; size_t skinIndex = 0; int lengthInFrames; float frameLengthInMs = 0; std::vector frameData; float fadeOutInSecs = 0; float fadeInInSecs = 0; }; struct AnimationComponent { std::variant target; std::vector gltfAnimations; std::vector morphAnimations; std::vector boneAnimations; // the index of the last active glTF animation, // used to cross-fade int fadeGltfAnimationIndex = -1; float fadeDuration = 0.0f; float fadeOutAnimationStart = 0.0f; }; class AnimationComponentManager : public utils::SingleInstanceComponentManager { filament::TransformManager &_transformManager; filament::RenderableManager &_renderableManager; public: AnimationComponentManager( filament::TransformManager &transformManager, filament::RenderableManager &renderableManager) : _transformManager(transformManager), _renderableManager(renderableManager){}; void addAnimationComponent(std::variant target) { AnimationComponent animationComponent; animationComponent.target = target; EntityInstanceBase::Type componentInstance; if (std::holds_alternative(target)) { auto instance = std::get(target); if(!hasComponent(instance->getRoot())) { componentInstance = addComponent(instance->getRoot()); this->elementAt<0>(componentInstance) = animationComponent; } } else { auto entity = std::get(target); if(!hasComponent(entity)) { componentInstance = addComponent(entity); this->elementAt<0>(componentInstance) = animationComponent; } } } void removeAnimationComponent(std::variant target) { AnimationComponent animationComponent; animationComponent.target = target; EntityInstanceBase::Type componentInstance; if (std::holds_alternative(target)) { auto instance = std::get(target); if(hasComponent(instance->getRoot())) { removeComponent(instance->getRoot()); } } else { auto entity = std::get(target); if(hasComponent(entity)) { removeComponent(entity); } } } void update() { for (auto it = begin(); it < end(); it++) { const auto &entity = getEntity(it); auto componentInstance = getInstance(entity); auto &animationComponent = elementAt<0>(componentInstance); auto &morphAnimations = animationComponent.morphAnimations; if (std::holds_alternative(animationComponent.target)) { auto target = std::get(animationComponent.target); auto animator = target->getAnimator(); auto &gltfAnimations = animationComponent.gltfAnimations; auto &boneAnimations = animationComponent.boneAnimations; 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]; auto elapsedInSecs = float(std::chrono::duration_cast(now - animationStatus.start).count()) / 1000.0f; if (!animationStatus.loop && elapsedInSecs >= animationStatus.durationInSecs) { animator->applyAnimation(animationStatus.index, animationStatus.durationInSecs - 0.001); animator->updateBoneMatrices(); gltfAnimations.erase(gltfAnimations.begin() + i); animationComponent.fadeGltfAnimationIndex = -1; continue; } animator->applyAnimation(animationStatus.index, elapsedInSecs); if (animationComponent.fadeGltfAnimationIndex != -1 && elapsedInSecs < animationComponent.fadeDuration) { // cross-fade auto fadeFromTime = animationComponent.fadeOutAnimationStart + elapsedInSecs; auto alpha = elapsedInSecs / animationComponent.fadeDuration; animator->applyCrossFade(animationComponent.fadeGltfAnimationIndex, fadeFromTime, alpha); } } 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 now = high_resolution_clock::now(); 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)) { if(!animationStatus.loop) { Log("Bone animation %d finished", i); boneAnimations.erase(boneAnimations.begin() + i); continue; } } // 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 if (animationStatus.reverse) { currFrame = animationStatus.lengthInFrames - currFrame; if (currFrame > 0) { nextFrame = currFrame - 1; } else { nextFrame = 0; } } else { if (currFrame < animationStatus.lengthInFrames - 1) { nextFrame = currFrame + 1; } else { nextFrame = currFrame; } } currFrame = std::clamp(currFrame, 0, animationStatus.lengthInFrames - 1); nextFrame = std::clamp(nextFrame, 0, animationStatus.lengthInFrames - 1); 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); } auto jointTransform = _transformManager.getInstance(joint); // 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, composeMatrix(newTranslation, newRotation, newScale)); animator->updateBoneMatrices(); if (animationStatus.loop && elapsedInSecs >= (animationStatus.durationInSecs + animationStatus.fadeInInSecs + animationStatus.fadeOutInSecs)) { animationStatus.start = now; } } } 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; if (!animationStatus.loop && elapsedInSecs >= animationStatus.durationInSecs) { morphAnimations.erase(morphAnimations.begin() + i); continue; } int frameNumber = static_cast(elapsedInSecs * 1000.0f / animationStatus.frameLengthInMs) % animationStatus.lengthInFrames; // offset from the end if reverse if (animationStatus.reverse) { frameNumber = animationStatus.lengthInFrames - frameNumber; } auto baseOffset = frameNumber * animationStatus.morphIndices.size(); for (int i = 0; i < animationStatus.morphIndices.size(); i++) { auto morphIndex = animationStatus.morphIndices[i]; // set the weights appropriately _renderableManager.setMorphWeights( _renderableManager.getInstance(animationStatus.meshTarget), animationStatus.frameData.data() + baseOffset + i, 1, morphIndex); } } } } }; }