From 55ffb6d7d2ec6201ad78ab1df58f37f63bfc10a7 Mon Sep 17 00:00:00 2001 From: Max Yari Date: Thu, 4 Apr 2024 01:30:27 +0100 Subject: [PATCH] Animation blending implementation for NIF and osgAnimation through YAML files Signed-off-by: Sam Hellawell --- apps/openmw/mwrender/animation.cpp | 190 ++++++++++- apps/openmw/mwrender/animation.hpp | 29 +- apps/openmw/mwrender/animblendcontroller.cpp | 318 ++++++++++++++++++ apps/openmw/mwrender/animblendcontroller.hpp | 109 ++++++ components/CMakeLists.txt | 4 +- components/nif/niftypes.hpp | 11 + components/nifosg/controller.cpp | 39 ++- components/nifosg/controller.hpp | 2 + components/nifosg/matrixtransform.cpp | 12 +- components/nifosg/matrixtransform.hpp | 3 +- components/resource/animblendrulesmanager.cpp | 94 ++++++ components/resource/animblendrulesmanager.hpp | 36 ++ components/resource/resourcesystem.cpp | 8 + components/resource/resourcesystem.hpp | 3 + components/sceneutil/animblendrules.cpp | 177 ++++++++++ components/sceneutil/animblendrules.hpp | 49 +++ components/sceneutil/keyframe.hpp | 12 + components/settings/categories/game.hpp | 1 + files/data/CMakeLists.txt | 3 + files/data/animations/animation-config.yaml | 70 ++++ files/settings-default.cfg | 4 + 21 files changed, 1143 insertions(+), 31 deletions(-) create mode 100644 apps/openmw/mwrender/animblendcontroller.cpp create mode 100644 apps/openmw/mwrender/animblendcontroller.hpp create mode 100644 components/resource/animblendrulesmanager.cpp create mode 100644 components/resource/animblendrulesmanager.hpp create mode 100644 components/sceneutil/animblendrules.cpp create mode 100644 components/sceneutil/animblendrules.hpp create mode 100644 files/data/animations/animation-config.yaml diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 38aa8b9f07..9afc5baafa 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -13,8 +13,11 @@ #include #include +#include + #include +#include #include #include @@ -56,6 +59,7 @@ #include "../mwmechanics/weapontype.hpp" #include "actorutil.hpp" +#include "animblendcontroller.cpp" #include "rotatecontroller.hpp" #include "util.hpp" #include "vismask.hpp" @@ -400,6 +404,61 @@ namespace namespace MWRender { + void assignBoneBlendCallbackRecursive( + BoneAnimBlendController* controller, ActiveControllersVector& activeControllers, osg::Node* parent, bool isRoot) + { + // Attempt to cast node to an osgAnimation::Bone + osgAnimation::Bone* bone = dynamic_cast(parent); + if (!isRoot && bone) + { + // Wrapping in a custom callback object allows for nested callback chaining, otherwise it has link to self + // issues we need to share the base BoneAnimBlendController as that contains blending information and is + // guaranteed to update before + osg::ref_ptr cb = new BoneAnimBlendControllerWrapper(controller, bone); + + // Ensure there is no other AnimBlendController - this can happen if using + // multiple animations with different roots, such as NPC animation + osg::Callback* updateCb = bone->getUpdateCallback(); + while (updateCb) + { + if (updateCb->className() == std::string(controller->className())) + { + osg::ref_ptr nextCb = updateCb->getNestedCallback(); + bone->removeUpdateCallback(updateCb); + updateCb = nextCb; + } + else + { + updateCb = updateCb->getNestedCallback(); + } + } + + // Find UpdateBone callback and bind to just after that (order is important) + // NOTE: if it doesnt have an UpdateBone callback, we shouldnt be doing blending! + updateCb = bone->getUpdateCallback(); + while (updateCb) + { + if (updateCb->className() == std::string("UpdateBone")) + { + // Override the immediate callback after the UpdateBone + osg::ref_ptr lastCb = updateCb->getNestedCallback(); + updateCb->setNestedCallback(cb); + if (lastCb) + cb->setNestedCallback(lastCb); + break; + } + + updateCb = updateCb->getNestedCallback(); + } + } + + // Traverse childrne if this is a group + osg::Group* group = parent->asGroup(); + if (group) + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + assignBoneBlendCallbackRecursive(controller, activeControllers, group->getChild(i), false); + } + class TransparencyUpdater : public SceneUtil::StateSetUpdater { public: @@ -449,6 +508,8 @@ namespace MWRender ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; + + osg::ref_ptr mAnimBlendRules; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) @@ -605,8 +666,11 @@ namespace MWRender for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { + if (Misc::getFileExtension(name) == "kf") + { addSingleAnimSource(name, baseModel); + } } } @@ -623,17 +687,18 @@ namespace MWRender loadAllAnimationsInFolder(kfname, baseModel); } - void Animation::addSingleAnimSource(const std::string& kfname, const std::string& baseModel) + std::shared_ptr Animation::addSingleAnimSource( + const std::string& kfname, const std::string& baseModel) { if (!mResourceSystem->getVFS()->exists(kfname)) - return; + return nullptr; auto animsrc = std::make_shared(); animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname); if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) - return; + return nullptr; const NodeMap& nodeMap = getNodeMap(); const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers; @@ -661,7 +726,7 @@ namespace MWRender animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); } - mAnimSources.push_back(std::move(animsrc)); + mAnimSources.push_back(animsrc); for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups()) mSupportedAnimations.insert(group); @@ -693,6 +758,30 @@ namespace MWRender break; } } + + // Get the blending rules + if (Settings::game().mSmoothAnimTransitions) + { + // Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the + // manager will check for .json if it will not find a specified .yaml file. + auto yamlpath = kfname; + Misc::StringUtils::replaceLast(yamlpath, ".kf", ".yaml"); + Misc::StringUtils::replaceLast(yamlpath, ".dae", ".yaml"); + + // globalBlendConfigPath is only used with actors! Objects have no default blending. + std::string_view globalBlendConfigPath = "animations/animation-config.yaml"; + + osg::ref_ptr blendRules; + if (mPtr.getClass().isActor()) + blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(globalBlendConfigPath, yamlpath); + else + blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(yamlpath); + + // At this point blendRules will either be nullptr or an AnimBlendRules instance with > 0 rules inside. + animsrc->mAnimBlendRules = blendRules; + } + + return animsrc; } void Animation::clearAnimSources() @@ -811,25 +900,33 @@ namespace MWRender if (!mObjectRoot || mAnimSources.empty()) return; + // Log(Debug::Info) << "Please play: " << groupname << ":" << start << "..." << stop << " mask: " << blendMask; + if (groupname.empty()) { resetActiveGroups(); return; } + AnimStateMap::iterator foundstateiter = mStates.find(groupname); + if (foundstateiter != mStates.end()) + { + foundstateiter->second.mPriority = priority; + } + AnimStateMap::iterator stateiter = mStates.begin(); while (stateiter != mStates.end()) { - if (stateiter->second.mPriority == priority) + if (stateiter->second.mPriority == priority && stateiter->first != groupname) + // This MIGH be a problem since we want old states to be still running so the AnimBlendingController can + // blend them properly mStates.erase(stateiter++); else ++stateiter; } - stateiter = mStates.find(groupname); - if (stateiter != mStates.end()) + if (foundstateiter != mStates.end()) { - stateiter->second.mPriority = priority; resetActiveGroups(); return; } @@ -849,6 +946,8 @@ namespace MWRender state.mPriority = priority; state.mBlendMask = blendMask; state.mAutoDisable = autodisable; + state.mGroupname = groupname; + state.mStartKey = start; mStates[std::string{ groupname }] = state; if (state.mPlaying) @@ -1004,7 +1103,7 @@ namespace MWRender AnimStateMap::const_iterator state = mStates.begin(); for (; state != mStates.end(); ++state) { - if (!(state->second.mBlendMask & (1 << blendMask))) + if (!state->second.blendMaskContains(blendMask)) continue; if (active == mStates.end() @@ -1019,6 +1118,7 @@ namespace MWRender if (active != mStates.end()) { std::shared_ptr animsrc = active->second.mSource; + AnimBlendStateData stateData = active->second.asAnimBlendStateData(); for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) @@ -1026,7 +1126,72 @@ namespace MWRender osg::ref_ptr node = getNodeMap().at( it->first); // this should not throw, we already checked for the node existing in addAnimSource - osg::Callback* callback = it->second->getAsCallback(); + osg::Callback* callback; + const bool useSmoothAnims = Settings::game().mSmoothAnimTransitions; + if (useSmoothAnims && dynamic_cast(node.get())) + { + // Update an existing animation blending controller or create a new one for NIF animations + osg::ref_ptr animController; + + if (mAnimBlendControllers.contains(node)) + { + animController = mAnimBlendControllers[node]; + animController->setKeyframeTrack(it->second, stateData, animsrc->mAnimBlendRules); + } + else + { + animController = osg::ref_ptr( + new AnimBlendController(it->second, stateData, animsrc->mAnimBlendRules)); + + mAnimBlendControllers[node] = animController; + } + + it->second->mTime = active->second.mTime; + + callback = animController->getAsCallback(); + } + else if (useSmoothAnims && dynamic_cast(node.get())) + { + // Update an existing animation blending controller or create a new one for osgAnimation + osg::ref_ptr animController; + + if (mBoneAnimBlendControllers.contains(node)) + { + animController = mBoneAnimBlendControllers[node]; + animController->setKeyframeTrack(it->second, stateData, animsrc->mAnimBlendRules); + } + else + { + animController = osg::ref_ptr( + new BoneAnimBlendController(it->second, stateData, animsrc->mAnimBlendRules)); + + mBoneAnimBlendControllers[node] = animController; + + assignBoneBlendCallbackRecursive(animController, mActiveControllers, node, true); + } + + // IMPORTANT: we must gather all transforms at point of change before next update + // instead of at the root update callback because the root bone may need blending + if (animController->getBlendTrigger()) + animController->gatherRecursiveBoneTransforms(static_cast(node.get())); + + it->second->mTime = active->second.mTime; + + // Register blend callback after the initial animation callback + callback = animController->getAsCallback(); + + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); + + // Ensure the original animation update callback is still applied + // this is because we need this to happen first to get the latest transform to blend to + callback = it->second->getAsCallback(); + } + else + { + callback = it->second->getAsCallback(); + } + node->addUpdateCallback(callback); mActiveControllers.emplace_back(node, callback); @@ -1046,6 +1211,7 @@ namespace MWRender } } } + addControllers(); } @@ -1790,13 +1956,15 @@ namespace MWRender osg::Callback* cb = node->getUpdateCallback(); while (cb) { - if (dynamic_cast(cb)) + if (dynamic_cast(cb) || dynamic_cast(cb) + || dynamic_cast(cb)) { foundKeyframeCtrl = true; break; } cb = cb->getNestedCallback(); } + // Note: AnimBlendController also does the reset so if one is present - we should add the rotation node // Without KeyframeController the orientation will not be reseted each frame, so // RotateController shouldn't be used for such nodes. if (!foundKeyframeCtrl) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 2d2031b13f..e40277a454 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -8,13 +8,17 @@ #include "../mwworld/movementdirection.hpp" #include "../mwworld/ptr.hpp" +#include "animblendcontroller.hpp" #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -47,6 +51,8 @@ namespace MWRender class RotateController; class TransparencyUpdater; + typedef std::vector, osg::ref_ptr>> ActiveControllersVector; + class EffectAnimationTime : public SceneUtil::ControllerSource { private: @@ -158,8 +164,17 @@ namespace MWRender int mBlendMask = 0; bool mAutoDisable = true; + std::string mGroupname; + std::string mStartKey; + float getTime() const { return *mTime; } void setTime(float time) { *mTime = time; } + bool blendMaskContains(size_t blendMask) const { return (mBlendMask & (1 << blendMask)); } + AnimBlendStateData asAnimBlendStateData() const + { + AnimBlendStateData stateData = { .mGroupname = mGroupname, .mStartKey = mStartKey }; + return stateData; + } bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; } }; @@ -189,7 +204,11 @@ namespace MWRender // Keep track of controllers that we added to our scene graph. // We may need to rebuild these controllers when the active animation groups / sources change. - std::vector, osg::ref_ptr>> mActiveControllers; + ActiveControllersVector mActiveControllers; + + // Keep track of the animation controllers for easy access + std::map, osg::ref_ptr> mAnimBlendControllers; + std::map, osg::ref_ptr> mBoneAnimBlendControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; @@ -233,7 +252,9 @@ namespace MWRender const NodeMap& getNodeMap() const; - /* Sets the appropriate animations on the bone groups based on priority. + /* Sets the appropriate animations on the bone groups based on priority by finding + * the highest priority AnimationStates and linking the appropriate controllers stored + * in the AnimationState to the corresponding nodes. */ void resetActiveGroups(); @@ -275,7 +296,7 @@ namespace MWRender * @param baseModel The filename of the mObjectRoot, only used for error messages. */ void addAnimSource(std::string_view model, const std::string& baseModel); - void addSingleAnimSource(const std::string& model, const std::string& baseModel); + std::shared_ptr addSingleAnimSource(const std::string& model, const std::string& baseModel); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const SceneUtil::LightCommon& light); @@ -343,6 +364,7 @@ namespace MWRender void setAccumulation(const osg::Vec3f& accum); /** Plays an animation. + * Creates or updates AnimationStates to represent and manage animation playback. * \param groupname Name of the animation group to play. * \param priority Priority of the animation. The animation will play on * bone groups that don't have another animation set of a @@ -491,6 +513,5 @@ namespace MWRender private: double mStartingTime; }; - } #endif diff --git a/apps/openmw/mwrender/animblendcontroller.cpp b/apps/openmw/mwrender/animblendcontroller.cpp new file mode 100644 index 0000000000..06c18c9ee9 --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.cpp @@ -0,0 +1,318 @@ +#include "animblendcontroller.hpp" + +#include + +#include +#include + +namespace MWRender +{ + /// Animation Easing/Blending functions + namespace Easings + { + float linear(float x) + { + return x; + } + float sineOut(float x) + { + return sin((x * 3.14) / 2); + } + float sineIn(float x) + { + return 1 - cos((x * 3.14) / 2); + } + float sineInOut(float x) + { + return -(cos(3.14 * x) - 1) / 2; + } + float cubicOut(float t) + { + return 1 - powf(1 - t, 3); + } + float cubicIn(float x) + { + return powf(x, 3); + } + float cubicInOut(float x) + { + return x < 0.5 ? 4 * x * x * x : 1 - powf(-2 * x + 2, 3) / 2; + } + float quartOut(float t) + { + return 1 - powf(1 - t, 4); + } + float quartIn(float t) + { + return powf(t, 4); + } + float quartInOut(float x) + { + return x < 0.5 ? 8 * x * x * x * x : 1 - powf(-2 * x + 2, 4) / 2; + } + float springOutGeneric(float x, float lambda, float w) + { + // Higher lambda = lower swing amplitude. 1 = 150% swing amplitude. + // W corresponds to the amount of overswings, more = more. 4.71 = 1 overswing, 7.82 = 2 + return 1 - expf(-lambda * x) * cos(w * x); + } + float springOutWeak(float x) + { + return springOutGeneric(x, 4, 4.71); + } + float springOutMed(float x) + { + return springOutGeneric(x, 3, 4.71); + } + float springOutStrong(float x) + { + return springOutGeneric(x, 2, 4.71); + } + float springOutTooMuch(float x) + { + return springOutGeneric(x, 1, 4.71); + } + std::unordered_map easingsMap = { { "linear", Easings::linear }, + { "sineOut", Easings::sineOut }, { "sineIn", Easings::sineIn }, { "sineInOut", Easings::sineInOut }, + { "cubicOut", Easings::cubicOut }, { "cubicIn", Easings::cubicIn }, { "cubicInOut", Easings::cubicInOut }, + { "quartOut", Easings::quartOut }, { "quartIn", Easings::quartIn }, { "quartInOut", Easings::quartInOut }, + { "springOutWeak", Easings::springOutWeak }, { "springOutMed", Easings::springOutMed }, + { "springOutStrong", Easings::springOutStrong }, { "springOutTooMuch", Easings::springOutTooMuch } }; + } + + namespace + { + // Helper methods + osg::Vec3f vec3fLerp(float t, const osg::Vec3f& A, const osg::Vec3f& B) + { + return A + (B - A) * t; + } + } + + template + AnimBlendControllerBase::AnimBlendControllerBase( + osg::ref_ptr keyframeTrack, AnimBlendStateData newState, + osg::ref_ptr blendRules) + : mTimeFactor(0.0f) + , mInterpFactor(0.0f) + { + setKeyframeTrack(keyframeTrack, newState, blendRules); + } + + template + void AnimBlendControllerBase::setKeyframeTrack(osg::ref_ptr kft, + AnimBlendStateData newState, osg::ref_ptr blendRules) + { + if (newState.mGroupname != mAnimState.mGroupname || newState.mStartKey != mAnimState.mStartKey + || kft != mKeyframeTrack) + { + // Animation have changed, start blending! + // Log(Debug::Info) << "Animation change to: " << newState.mGroupname << ":" << newState.mStartKey; + + // Default blend settings + mBlendDuration = 0; + mEasingFn = &Easings::sineOut; + + if (blendRules) + { + // Finds a matching blend rule either in this or previous ruleset + auto blendRule = blendRules->findBlendingRule( + mAnimState.mGroupname, mAnimState.mStartKey, newState.mGroupname, newState.mStartKey); + // This will also check the previous ruleset, not sure it's a good idea though, commenting out + // for now. + /*if (!blendRule && mAnimBlendRules) + blendRule = mAnimBlendRules->findBlendingRule( + mAnimState.mGroupname, mAnimState.mStartKey, newState.mGroupname, newState.mStartKey);*/ + if (blendRule) + { + if (Easings::easingsMap.contains(blendRule->mEasing)) + { + mBlendDuration = blendRule->mDuration; + mEasingFn = Easings::easingsMap[blendRule->mEasing]; + } + else + { + Log(Debug::Warning) + << "Warning: animation blending rule contains invalid easing type: " << blendRule->mEasing; + } + } + } + + mAnimBlendRules = blendRules; + mKeyframeTrack = kft; + mAnimState = newState; + mBlendTrigger = true; + } + } + + template + void AnimBlendControllerBase::gatherRecursiveBoneTransforms(osgAnimation::Bone* bone, bool isRoot) + { + // Incase group traversal encountered something that isnt a bone + if (!bone) + return; + + mBlendBoneTransforms[bone] = bone->getMatrix(); + + osg::Group* group = bone->asGroup(); + if (group) + { + for (unsigned int i = 0; i < group->getNumChildren(); ++i) + gatherRecursiveBoneTransforms(dynamic_cast(group->getChild(i)), false); + } + } + + template + void AnimBlendControllerBase::applyBoneBlend(osgAnimation::Bone* bone) + { + // If we are done with interpolation then we can safely skip this as the bones are correct + if (!mInterpActive) + return; + + // Shouldnt happen, but potentially an edge case where a new bone was added + // between gatherRecursiveBoneTransforms and this update + // so far OpenMW will never do this, so this check shouldn't be needed in production + assert(mBlendBoneTransforms.find(bone) != mBlendBoneTransforms.end()); + + // every frame the osgAnimation controller updates this + // so it is ok that we update it directly below + osg::Matrixf currentSampledMatrix = bone->getMatrix(); + const osg::Matrixf& lastSampledMatrix = mBlendBoneTransforms.at(bone); + + const osg::Vec3f scale = currentSampledMatrix.getScale(); + const osg::Quat rotation = currentSampledMatrix.getRotate(); + const osg::Vec3f translation = currentSampledMatrix.getTrans(); + + const osg::Quat blendRotation = lastSampledMatrix.getRotate(); + const osg::Vec3f blendTrans = lastSampledMatrix.getTrans(); + + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, blendRotation, rotation); + + osg::Matrixf lerpedMatrix; + lerpedMatrix.makeRotate(lerpedRot); + lerpedMatrix.setTrans(vec3fLerp(mInterpFactor, blendTrans, translation)); + + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + lerpedMatrix = osg::Matrixd::scale(scale) * lerpedMatrix; + + // Apply new blended matrix + osgAnimation::Bone* boneParent = bone->getBoneParent(); + bone->setMatrix(lerpedMatrix); + if (boneParent) + bone->setMatrixInSkeletonSpace(lerpedMatrix * boneParent->getMatrixInSkeletonSpace()); + else + bone->setMatrixInSkeletonSpace(lerpedMatrix); + } + + template + void AnimBlendControllerBase::calculateInterpFactor(float time) + { + if (mBlendDuration != 0) + mTimeFactor = std::min((time - mBlendStartTime) / mBlendDuration, 1.0f); + else + mTimeFactor = 1; + + mInterpActive = mTimeFactor < 1.0; + + if (mInterpActive) + mInterpFactor = mEasingFn(mTimeFactor); + else + mInterpFactor = 1.0f; + } + + template + void AnimBlendControllerBase::operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: This callback method is called only for bones with attached keyframe controllers + // such as bip01, bip01 spine1 etc. The child bones of these controllers have their own callback wrapper + // which will call this instance's applyBoneBlend for each child bone. The order of update is important + // as the blending calculations expect the bone's skeleton matrix to be at the sample point + float time = nv->getFrameStamp()->getSimulationTime(); + assert(node != nullptr); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + } + + calculateInterpFactor(time); + + if (mInterpActive) + applyBoneBlend(node); + + SceneUtil::NodeCallback, osgAnimation::Bone*>::traverse(node, nv); + } + + template + void AnimBlendControllerBase::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) + { + // HOW THIS WORKS: The actual retrieval of the bone transformation based on animation is done by the + // KeyframeController (mKeyframeTrack). The KeyframeController retreives time data (playback position) every + // frame from controller's input (getInputValue(nv)) which is bound to an appropriate AnimationState time value + // in Animation.cpp. Animation.cpp ultimately manages animation playback via updating AnimationState objects and + // determines when and what should be playing. + // This controller exploits KeyframeController to get transformations and upon animation change blends from + // the last known position to the new animated one. + + auto [translation, rotation, scale] = mKeyframeTrack->getCurrentTransformation(nv); + + float time = nv->getFrameStamp()->getSimulationTime(); + + if (mBlendTrigger) + { + mBlendTrigger = false; + mBlendStartTime = time; + // Nif mRotation is used here because it's unaffected by the side-effects of RotationController + mBlendStartRot = node->mRotation.toOsgMatrix().getRotate(); + mBlendStartTrans = node->getMatrix().getTrans(); + mBlendStartScale = node->mScale; + } + + calculateInterpFactor(time); + + if (mInterpActive) + { + // Interpolate node's rotation + if (rotation) + { + osg::Quat lerpedRot; + lerpedRot.slerp(mInterpFactor, mBlendStartRot, *rotation); + node->setRotation(lerpedRot); + } + else + { + // This is necessary to prevent first person animation glitching out + node->setRotation(node->mRotation); + } + + // Update node's translation + if (translation) + { + osg::Vec3f lerpedTrans = vec3fLerp(mInterpFactor, mBlendStartTrans, *translation); + node->setTranslation(lerpedTrans); + } + } + else + { + // Update node's translation + if (translation) + node->setTranslation(*translation); + + if (rotation) + node->setRotation(*rotation); + else + node->setRotation(node->mRotation); + } + + // Update node's scale + if (scale) + // Scale is not lerped based on the idea that it is much more likely that scale animation will be used to + // instantly hide/show objects in which case the scale interpolation is undesirable. + node->setScale(*scale); + + SceneUtil::NodeCallback, NifOsg::MatrixTransform*>::traverse(node, nv); + } +} diff --git a/apps/openmw/mwrender/animblendcontroller.hpp b/apps/openmw/mwrender/animblendcontroller.hpp new file mode 100644 index 0000000000..0ccbc43a5c --- /dev/null +++ b/apps/openmw/mwrender/animblendcontroller.hpp @@ -0,0 +1,109 @@ +#ifndef OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H +#define OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace MWRender +{ + namespace Easings + { + typedef float (*EasingFn)(float); + } + + struct AnimBlendStateData + { + std::string mGroupname; + std::string mStartKey; + }; + + template + class AnimBlendControllerBase : public SceneUtil::NodeCallback, NodeClass*>, + public SceneUtil::Controller + { + public: + AnimBlendControllerBase(osg::ref_ptr keyframeTrack, AnimBlendStateData animState, + osg::ref_ptr blendRules); + + void operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv); + void operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv); + + void setKeyframeTrack(osg::ref_ptr kft, AnimBlendStateData animState, + osg::ref_ptr blendRules); + + osg::Callback* getAsCallback() { return this; } + + bool getBlendTrigger() const { return mBlendTrigger; } + + void gatherRecursiveBoneTransforms(osgAnimation::Bone* parent, bool isRoot = true); + void applyBoneBlend(osgAnimation::Bone* parent); + + const char* libraryName() const override { return "openmw"; } + const char* className() const override { return "AnimBlendController"; } + + protected: + osg::ref_ptr mKeyframeTrack; + + inline void calculateInterpFactor(float time); + + private: + Easings::EasingFn mEasingFn; + float mBlendDuration; + + bool mBlendTrigger = false; + float mBlendStartTime; + osg::Quat mBlendStartRot; + osg::Vec3f mBlendStartTrans; + float mBlendStartScale; + + float mTimeFactor; + float mInterpFactor; + bool mInterpActive; + + AnimBlendStateData mAnimState; + osg::ref_ptr mAnimBlendRules; + + std::unordered_map mBlendBoneTransforms; + }; + + typedef AnimBlendControllerBase AnimBlendController; + typedef AnimBlendControllerBase BoneAnimBlendController; + + // Assigned to child bones with an instance of AnimBlendControllerBase + class BoneAnimBlendControllerWrapper : public osg::Callback + { + public: + BoneAnimBlendControllerWrapper(osg::ref_ptr rootCallback, osg::Node* node) + { + mRootCallback = rootCallback; + mNode = dynamic_cast(node); + } + + bool run(osg::Object* object, osg::Object* data) override + { + mRootCallback->applyBoneBlend(mNode); + traverse(object, data); + return true; + } + + const char* libraryName() const override { return "openmw"; } + const char* className() const override { return "AnimBlendController"; } + + private: + osg::ref_ptr mRootCallback; + osgAnimation::Bone* mNode; + }; +} + +#endif diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index fb5718a979..67cb881ee5 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -127,7 +127,7 @@ add_component_dir (vfs ) add_component_dir (resource - scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem + scenemanager keyframemanager imagemanager animblendrulesmanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem resourcemanager stats animation foreachbulletobject errormarker cachestats bgsmfilemanager ) @@ -138,7 +138,7 @@ add_component_dir (shader add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer - detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt + detourdebugdraw navmesh agentpath animblendrules shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt screencapture depth color riggeometryosgaextension extradata unrefqueue lightcommon lightingmethod clearcolor cullsafeboundsvisitor keyframe nodecallback textkeymap glextensions ) diff --git a/components/nif/niftypes.hpp b/components/nif/niftypes.hpp index b294756f69..f1a131743d 100644 --- a/components/nif/niftypes.hpp +++ b/components/nif/niftypes.hpp @@ -52,6 +52,17 @@ namespace Nif return false; return true; } + + osg::Matrixf toOsgMatrix() const + { + osg::Matrixf osgMat; + + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + osgMat(i, j) = mValues[j][i]; // NB: column/row major difference + + return osgMat; + } }; struct NiTransform diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 60f2e9c355..80fec6e39d 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -175,25 +177,48 @@ namespace NifOsg void KeyframeController::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv) { + auto [translation, rotation, scale] = getCurrentTransformation(nv); + + if (rotation) + { + node->setRotation(*rotation); + } + else + { + // This is necessary to prevent first person animations glitching out due to RotationController + node->setRotation(node->mRotation); + } + + if (translation) + node->setTranslation(*translation); + + if (scale) + node->setScale(*scale); + + traverse(node, nv); + } + + KeyframeController::KfTransform KeyframeController::getCurrentTransformation(osg::NodeVisitor* nv) + { + KfTransform out; + if (hasInput()) { float time = getInputValue(nv); if (!mRotations.empty()) - node->setRotation(mRotations.interpKey(time)); + out.mRotation = mRotations.interpKey(time); else if (!mXRotations.empty() || !mYRotations.empty() || !mZRotations.empty()) - node->setRotation(getXYZRotation(time)); - else - node->setRotation(node->mRotationScale); + out.mRotation = getXYZRotation(time); if (!mTranslations.empty()) - node->setTranslation(mTranslations.interpKey(time)); + out.mTranslation = mTranslations.interpKey(time); if (!mScales.empty()) - node->setScale(mScales.interpKey(time)); + out.mScale = mScales.interpKey(time); } - traverse(node, nv); + return out; } GeomMorpherController::GeomMorpherController() {} diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 51cf1cd428..99d3df9545 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -238,6 +238,8 @@ namespace NifOsg osg::Vec3f getTranslation(float time) const override; osg::Callback* getAsCallback() override { return this; } + KfTransform getCurrentTransformation(osg::NodeVisitor* nv) override; + void operator()(NifOsg::MatrixTransform*, osg::NodeVisitor*); private: diff --git a/components/nifosg/matrixtransform.cpp b/components/nifosg/matrixtransform.cpp index a59f10360a..caf0fa6bfb 100644 --- a/components/nifosg/matrixtransform.cpp +++ b/components/nifosg/matrixtransform.cpp @@ -5,14 +5,14 @@ namespace NifOsg MatrixTransform::MatrixTransform(const Nif::NiTransform& transform) : osg::MatrixTransform(transform.toMatrix()) , mScale(transform.mScale) - , mRotationScale(transform.mRotation) + , mRotation(transform.mRotation) { } MatrixTransform::MatrixTransform(const MatrixTransform& copy, const osg::CopyOp& copyop) : osg::MatrixTransform(copy, copyop) , mScale(copy.mScale) - , mRotationScale(copy.mRotationScale) + , mRotation(copy.mRotation) { } @@ -24,7 +24,7 @@ namespace NifOsg // Rescale the node using the known components. for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) - _matrix(i, j) = mRotationScale.mValues[j][i] * mScale; // NB: column/row major difference + _matrix(i, j) = mRotation.mValues[j][i] * mScale; // NB: column/row major difference _inverseDirty = true; dirtyBound(); @@ -40,7 +40,7 @@ namespace NifOsg for (int j = 0; j < 3; ++j) { // Update the current decomposed rotation and restore the known scale. - mRotationScale.mValues[j][i] = _matrix(i, j); // NB: column/row major difference + mRotation.mValues[j][i] = _matrix(i, j); // NB: column/row major difference _matrix(i, j) *= mScale; } } @@ -52,12 +52,12 @@ namespace NifOsg void MatrixTransform::setRotation(const Nif::Matrix3& rotation) { // Update the decomposed rotation. - mRotationScale = rotation; + mRotation = rotation; // Reorient the node using the known components. for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) - _matrix(i, j) = mRotationScale.mValues[j][i] * mScale; // NB: column/row major difference + _matrix(i, j) = mRotation.mValues[j][i] * mScale; // NB: column/row major difference _inverseDirty = true; dirtyBound(); diff --git a/components/nifosg/matrixtransform.hpp b/components/nifosg/matrixtransform.hpp index 4e42d00787..7f5c908156 100644 --- a/components/nifosg/matrixtransform.hpp +++ b/components/nifosg/matrixtransform.hpp @@ -23,7 +23,8 @@ namespace NifOsg // problems when a KeyframeController wants to change only one of these components. So // we store the scale and rotation components separately here. float mScale{ 0.f }; - Nif::Matrix3 mRotationScale; + + Nif::Matrix3 mRotation; // Utility methods to transform the node and keep these components up-to-date. // The matrix's components should not be overridden manually or using preMult/postMult diff --git a/components/resource/animblendrulesmanager.cpp b/components/resource/animblendrulesmanager.cpp new file mode 100644 index 0000000000..7bfea70974 --- /dev/null +++ b/components/resource/animblendrulesmanager.cpp @@ -0,0 +1,94 @@ +#include "animblendrulesmanager.hpp" + +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include "objectcache.hpp" +#include "scenemanager.hpp" + +namespace Resource +{ + using AnimBlendRules = SceneUtil::AnimBlendRules; + + AnimBlendRulesManager::AnimBlendRulesManager(const VFS::Manager* vfs, double expiryDelay) + : ResourceManager(vfs, expiryDelay) + , mVfs(vfs) + { + } + + osg::ref_ptr AnimBlendRulesManager::getRules( + std::string_view path, std::string_view overridePath) + { + // Note: Providing a non-existing path but an existing overridePath is not supported! + auto tmpl = loadRules(path); + if (!tmpl) + return nullptr; + + // Create an instance based on template and store template reference inside so the template will not be removed + // from cache + osg::ref_ptr blendRules(new AnimBlendRules(*tmpl, osg::CopyOp::SHALLOW_COPY)); + blendRules->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(tmpl)); + + if (!overridePath.empty()) + { + auto blendRuleOverrides = loadRules(overridePath); + if (blendRuleOverrides) + { + blendRules->addOverrideRules(*blendRuleOverrides); + } + blendRules->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(blendRuleOverrides)); + } + + return blendRules; + } + + osg::ref_ptr AnimBlendRulesManager::loadRules(std::string_view path) + { + const VFS::Path::Normalized normalizedPath = VFS::Path::Normalized(path); + std::optional> obj = mCache->getRefFromObjectCacheOrNone(normalizedPath); + if (obj.has_value()) + { + return osg::ref_ptr(static_cast(obj->get())); + } + else + { + osg::ref_ptr blendRules = AnimBlendRules::fromFile(mVfs, normalizedPath); + if (blendRules == nullptr) + { + // No blend rules were found in VFS, cache a nullptr. + osg::ref_ptr nullRules = nullptr; + mCache->addEntryToObjectCache(normalizedPath, nullRules); + // To avoid confusion - never return blend rules with 0 rules + return nullRules; + } + else + { + // Blend rules were found in VFS, cache them. + mCache->addEntryToObjectCache(normalizedPath, blendRules); + return blendRules; + } + } + + return nullptr; + } + + void AnimBlendRulesManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const + { + Resource::reportStats("Blending Rules", frameNumber, mCache->getStats(), *stats); + } + +} diff --git a/components/resource/animblendrulesmanager.hpp b/components/resource/animblendrulesmanager.hpp new file mode 100644 index 0000000000..c8eeee327f --- /dev/null +++ b/components/resource/animblendrulesmanager.hpp @@ -0,0 +1,36 @@ +#ifndef OPENMW_COMPONENTS_ANIMBLENDRULESMANAGER_H +#define OPENMW_COMPONENTS_ANIMBLENDRULESMANAGER_H + +#include +#include + +#include + +#include "resourcemanager.hpp" + +namespace Resource +{ + /// @brief Managing of keyframe resources + /// @note May be used from any thread. + class AnimBlendRulesManager : public ResourceManager + { + public: + explicit AnimBlendRulesManager(const VFS::Manager* vfs, double expiryDelay); + ~AnimBlendRulesManager() = default; + + /// Retrieve a read-only keyframe resource by name (case-insensitive). + /// @note Throws an exception if the resource is not found. + osg::ref_ptr getRules( + std::string_view path, std::string_view overridePath = ""); + + void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; + + private: + osg::ref_ptr loadRules(std::string_view path); + + const VFS::Manager* mVfs; + }; + +} + +#endif diff --git a/components/resource/resourcesystem.cpp b/components/resource/resourcesystem.cpp index f012627efb..f3eb835ddb 100644 --- a/components/resource/resourcesystem.cpp +++ b/components/resource/resourcesystem.cpp @@ -2,6 +2,7 @@ #include +#include "animblendrulesmanager.hpp" #include "bgsmfilemanager.hpp" #include "imagemanager.hpp" #include "keyframemanager.hpp" @@ -21,6 +22,7 @@ namespace Resource mSceneManager = std::make_unique( vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay); mKeyframeManager = std::make_unique(vfs, mSceneManager.get(), expiryDelay, encoder); + mAnimBlendRulesManager = std::make_unique(vfs, expiryDelay); addResourceManager(mNifFileManager.get()); addResourceManager(mBgsmFileManager.get()); @@ -28,6 +30,7 @@ namespace Resource // note, scene references images so add images afterwards for correct implementation of updateCache() addResourceManager(mSceneManager.get()); addResourceManager(mImageManager.get()); + addResourceManager(mAnimBlendRulesManager.get()); } ResourceSystem::~ResourceSystem() @@ -62,6 +65,11 @@ namespace Resource return mKeyframeManager.get(); } + AnimBlendRulesManager* ResourceSystem::getAnimBlendRulesManager() + { + return mAnimBlendRulesManager.get(); + } + void ResourceSystem::setExpiryDelay(double expiryDelay) { for (std::vector::iterator it = mResourceManagers.begin(); it != mResourceManagers.end(); diff --git a/components/resource/resourcesystem.hpp b/components/resource/resourcesystem.hpp index 5609176a89..abc696aae3 100644 --- a/components/resource/resourcesystem.hpp +++ b/components/resource/resourcesystem.hpp @@ -29,6 +29,7 @@ namespace Resource class NifFileManager; class KeyframeManager; class BaseResourceManager; + class AnimBlendRulesManager; /// @brief Wrapper class that constructs and provides access to the most commonly used resource subsystems. /// @par Resource subsystems can be used with multiple OpenGL contexts, just like the OSG equivalents, but @@ -45,6 +46,7 @@ namespace Resource BgsmFileManager* getBgsmFileManager(); NifFileManager* getNifFileManager(); KeyframeManager* getKeyframeManager(); + AnimBlendRulesManager* getAnimBlendRulesManager(); /// Indicates to each resource manager to clear the cache, i.e. to drop cached objects that are no longer /// referenced. @@ -79,6 +81,7 @@ namespace Resource std::unique_ptr mBgsmFileManager; std::unique_ptr mNifFileManager; std::unique_ptr mKeyframeManager; + std::unique_ptr mAnimBlendRulesManager; // Store the base classes separately to get convenient access to the common interface // Here users can register their own resourcemanager as well diff --git a/components/sceneutil/animblendrules.cpp b/components/sceneutil/animblendrules.cpp new file mode 100644 index 0000000000..8289cf0f7d --- /dev/null +++ b/components/sceneutil/animblendrules.cpp @@ -0,0 +1,177 @@ +#include "animblendrules.hpp" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace SceneUtil +{ + namespace + { + std::pair splitRuleName(std::string full) + { + std::string group; + std::string key; + size_t delimiterInd = full.find(":"); + + Misc::StringUtils::lowerCaseInPlace(full); + + if (delimiterInd == std::string::npos) + { + group = full; + Misc::StringUtils::trim(group); + } + else + { + group = full.substr(0, delimiterInd); + key = full.substr(delimiterInd + 1); + Misc::StringUtils::trim(group); + Misc::StringUtils::trim(key); + } + return std::make_pair(group, key); + } + + } + + using BlendRule = AnimBlendRules::BlendRule; + + AnimBlendRules::AnimBlendRules(const AnimBlendRules& copy, const osg::CopyOp& copyop) + : mRules(copy.mRules) + { + } + + AnimBlendRules::AnimBlendRules(const std::vector& rules) + : mRules(rules) + { + } + + osg::ref_ptr AnimBlendRules::fromFile(const VFS::Manager* vfs, VFS::Path::NormalizedView configPath) + { + Log(Debug::Debug) << "Attempting to load animation blending config '" << configPath << "'"; + + if (!vfs->exists(configPath)) + { + Log(Debug::Warning) << "Animation blending files was not found '" << configPath << "'"; + return nullptr; + } + + // Retrieving and parsing animation rules + std::string rawYaml(std::istreambuf_iterator(*vfs->get(configPath)), {}); + + std::vector rules; + + YAML::Node root = YAML::Load(rawYaml); + + if (!root.IsDefined() || root.IsNull() || root.IsScalar()) + { + Log(Debug::Error) << Misc::StringUtils::format( + "Can't parse file '%s'. Check that it's a valid YAML/JSON file.", configPath); + return nullptr; + } + + if (root["blending_rules"]) + { + for (const auto& it : root["blending_rules"]) + { + if (it["from"] && it["to"] && it["duration"] && it["easing"]) + { + auto fromNames = splitRuleName(it["from"].as()); + auto toNames = splitRuleName(it["to"].as()); + + BlendRule ruleObj = { + .mFromGroup = fromNames.first, + .mFromKey = fromNames.second, + .mToGroup = toNames.first, + .mToKey = toNames.second, + .mDuration = it["duration"].as(), + .mEasing = it["easing"].as(), + }; + + rules.emplace_back(ruleObj); + } + else + { + Log(Debug::Warning) << "Warning: Blending rule '" + << (it["from"] ? it["from"].as() : "undefined") << "->" + << (it["to"] ? it["to"].as() : "undefined") + << "' is missing some properties. File: '" << configPath << "'."; + } + } + } + else + { + throw std::domain_error( + Misc::StringUtils::format("'blending_rules' object not found in '%s' file!", configPath)); + } + + // If no rules then dont allocate any instance + if (rules.size() == 0) + return nullptr; + + return new AnimBlendRules(rules); + } + + void AnimBlendRules::addOverrideRules(const AnimBlendRules& overrideRules) + { + auto rules = overrideRules.getRules(); + // Concat the rules together, overrides added at the end since the bottom-most rule has the highest priority. + mRules.insert(mRules.end(), rules.begin(), rules.end()); + } + + inline bool AnimBlendRules::fitsRuleString(const std::string& str, const std::string& ruleStr) const + { + // A wildcard only supported in the beginning or the end of the rule string in hopes that this will be more + // performant. And most likely this kind of support is enough. + return ruleStr == "*" || str == ruleStr || (ruleStr.starts_with("*") && str.ends_with(ruleStr.substr(1))) + || (ruleStr.ends_with("*") && str.starts_with(ruleStr.substr(0, ruleStr.length() - 1))); + } + + std::optional AnimBlendRules::findBlendingRule( + std::string fromGroup, std::string fromKey, std::string toGroup, std::string toKey) const + { + Misc::StringUtils::lowerCaseInPlace(fromGroup); + Misc::StringUtils::lowerCaseInPlace(fromKey); + Misc::StringUtils::lowerCaseInPlace(toGroup); + Misc::StringUtils::lowerCaseInPlace(toKey); + for (auto rule = mRules.rbegin(); rule != mRules.rend(); ++rule) + { + // TO DO: Also allow for partial wildcards at the end of groups and keys via std::string startswith method + bool fromMatch = false; + bool toMatch = false; + + // Pseudocode: + // If not a wildcard and found a wildcard + // starts with substr(0,wildcard) + + if (fitsRuleString(fromGroup, rule->mFromGroup) + && (fitsRuleString(fromKey, rule->mFromKey) || rule->mFromKey == "")) + { + fromMatch = true; + } + + if ((fitsRuleString(toGroup, rule->mToGroup) || (rule->mToGroup == "$" && toGroup == fromGroup)) + && (fitsRuleString(toKey, rule->mToKey) || rule->mToKey == "")) + { + toMatch = true; + } + + if (fromMatch && toMatch) + return std::make_optional(*rule); + } + + return std::nullopt; + } + +} diff --git a/components/sceneutil/animblendrules.hpp b/components/sceneutil/animblendrules.hpp new file mode 100644 index 0000000000..db03c0fd0a --- /dev/null +++ b/components/sceneutil/animblendrules.hpp @@ -0,0 +1,49 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP +#define OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP + +#include +#include +#include + +#include + +#include + +namespace SceneUtil +{ + class AnimBlendRules : public osg::Object + { + public: + struct BlendRule + { + std::string mFromGroup; + std::string mFromKey; + std::string mToGroup; + std::string mToKey; + float mDuration; + std::string mEasing; + }; + + AnimBlendRules() = default; + AnimBlendRules(const std::vector& rules); + AnimBlendRules(const AnimBlendRules& copy, const osg::CopyOp& copyop); + + META_Object(SceneUtil, AnimBlendRules) + + void addOverrideRules(const AnimBlendRules& overrideRules); + + std::optional findBlendingRule( + std::string fromGroup, std::string fromKey, std::string toGroup, std::string toKey) const; + + const std::vector& getRules() const { return mRules; } + + static osg::ref_ptr fromFile(const VFS::Manager* vfs, VFS::Path::NormalizedView yamlpath); + + private: + std::vector mRules; + + inline bool fitsRuleString(const std::string& str, const std::string& ruleStr) const; + }; +} + +#endif diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp index 3ea862d213..e8d53c1876 100644 --- a/components/sceneutil/keyframe.hpp +++ b/components/sceneutil/keyframe.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP #include +#include #include @@ -21,8 +22,19 @@ namespace SceneUtil { } + std::shared_ptr mTime = std::make_shared(0.0f); + + struct KfTransform + { + std::optional mTranslation; + std::optional mRotation; + std::optional mScale; + }; + virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } + virtual KfTransform getCurrentTransformation(osg::NodeVisitor* nv) { return KfTransform(); }; + /// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on. virtual osg::Callback* getAsCallback() = 0; }; diff --git a/components/settings/categories/game.hpp b/components/settings/categories/game.hpp index 4aec92d0b8..ec6f9dc206 100644 --- a/components/settings/categories/game.hpp +++ b/components/settings/categories/game.hpp @@ -39,6 +39,7 @@ namespace Settings SettingValue mCanLootDuringDeathAnimation{ mIndex, "Game", "can loot during death animation" }; SettingValue mRebalanceSoulGemValues{ mIndex, "Game", "rebalance soul gem values" }; SettingValue mUseAdditionalAnimSources{ mIndex, "Game", "use additional anim sources" }; + SettingValue mSmoothAnimTransitions{ mIndex, "Game", "smooth animation transitions" }; SettingValue mBarterDispositionChangeIsPermanent{ mIndex, "Game", "barter disposition change is permanent" }; SettingValue mStrengthInfluencesHandToHand{ mIndex, "Game", "strength influences hand to hand", diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 2e7b98762b..d03d7e634a 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -21,6 +21,9 @@ set(BUILTIN_DATA_FILES fonts/MysticCards.omwfont fonts/MysticCardsFontLicense.txt + # Default animation blending config + animations/animation-config.yaml + # Month names and date formatting l10n/Calendar/de.yaml l10n/Calendar/en.yaml diff --git a/files/data/animations/animation-config.yaml b/files/data/animations/animation-config.yaml new file mode 100644 index 0000000000..205e4d2145 --- /dev/null +++ b/files/data/animations/animation-config.yaml @@ -0,0 +1,70 @@ +# This is the default OpenMW animation blending config file (global config) , will affect NPCs and creatures but not animated objects. +# If you want to provide an animation blending config for your modded animations - DO NOT override the global config in your mod. +# For details on how to edit and create your own blending rules, see https://openmw.readthedocs.io/en/latest/reference/modding/animation-blending.html +# Author: Maksim Eremenko (Max Yari) + +blending_rules: + # General blending rule, any transition that will not be caught by the rules below - will use this rule + - from: "*" + to: "*" + easing: "sineOut" + duration: 0.25 + # From anything to sneaking + - from: "*" + to: "idlesneak*" + easing: "springOutWeak" + duration: 0.4 + - from: "*" + to: "sneakforward*" + easing: "springOutWeak" + duration: 0.4 + # From any to preparing for an attack swing (e.g "weapononehanded: chop start"). + # Note that Rules like *:chop* will technically match any weapon attack animation with + # an animation key beginning on "chop". This includes attack preparation, attack itself and follow-through. + # Yet since rules below this block take care of more specific transitions - most likely this block will + # only affect "any animation"->"attack swing preparation". + - from: "*" + to: "*:shoot*" + easing: "sineOut" + duration: 0.1 + - from: "*" + to: "*:chop*" + easing: "sineOut" + duration: 0.1 + - from: "*" + to: "*:thrust*" + easing: "sineOut" + duration: 0.1 + - from: "*" + to: "*:slash*" + easing: "sineOut" + duration: 0.1 + # From preparing for an attack swing (e.g "weapononehanded: chop start") to an attack swing (e.g "weapononehanded: chop max attack"). + - from: "*:*start" + to: "*:*attack" + easing: "sineOut" + duration: 0.05 + # From a weapon swing to the final follow-through + - from: "*" + to: "*:*follow start" + easing: "linear" + duration: 0 + # Sharper out of jumping transition, so bunny-hopping looks similar to vanilla + - from: "jump:start" + to: "*" + easing: "sineOut" + duration: 0.1 + # Inventory doll poses don't work with transitions, so 0 duraion. + - from: "*" + to: "inventory*" + easing: "linear" + duration: 0 + - from: "inventory*" + to: "*" + easing: "linear" + duration: 0 + # Transitions from a no-state are always instant + - from: "" + to: "*" + easing: "linear" + duration: 0 diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 325bc57618..70b0b133ab 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -284,6 +284,10 @@ rebalance soul gem values = false # Allow to load per-group KF-files from Animations folder use additional anim sources = false +# Use smooth transitions between animations making them a lot less jarring. Also allows to load modded animation blending +# configs (.yaml/.json config files). +smooth animation transitions = false + # Make the disposition change of merchants caused by barter dealings permanent barter disposition change is permanent = false