mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-29 17:15:34 +00:00
Animation blending implementation for NIF and osgAnimation through YAML files
Signed-off-by: Sam Hellawell <sshellawell@gmail.com>
This commit is contained in:
parent
a9281b5246
commit
55ffb6d7d2
21 changed files with 1143 additions and 31 deletions
|
@ -13,8 +13,11 @@
|
|||
#include <osgParticle/ParticleProcessor>
|
||||
#include <osgParticle/ParticleSystem>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/resource/animblendrulesmanager.hpp>
|
||||
#include <components/resource/keyframemanager.hpp>
|
||||
#include <components/resource/scenemanager.hpp>
|
||||
|
||||
|
@ -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<osgAnimation::Bone*>(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<osg::Callback> 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<osg::Callback> 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<osg::Callback> 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<const SceneUtil::AnimBlendRules> 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::AnimSource> Animation::addSingleAnimSource(
|
||||
const std::string& kfname, const std::string& baseModel)
|
||||
{
|
||||
if (!mResourceSystem->getVFS()->exists(kfname))
|
||||
return;
|
||||
return nullptr;
|
||||
|
||||
auto animsrc = std::make_shared<AnimSource>();
|
||||
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<const SceneUtil::AnimBlendRules> 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<AnimSource> 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<osg::Node> 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<NifOsg::MatrixTransform*>(node.get()))
|
||||
{
|
||||
// Update an existing animation blending controller or create a new one for NIF animations
|
||||
osg::ref_ptr<AnimBlendController> animController;
|
||||
|
||||
if (mAnimBlendControllers.contains(node))
|
||||
{
|
||||
animController = mAnimBlendControllers[node];
|
||||
animController->setKeyframeTrack(it->second, stateData, animsrc->mAnimBlendRules);
|
||||
}
|
||||
else
|
||||
{
|
||||
animController = osg::ref_ptr<AnimBlendController>(
|
||||
new AnimBlendController(it->second, stateData, animsrc->mAnimBlendRules));
|
||||
|
||||
mAnimBlendControllers[node] = animController;
|
||||
}
|
||||
|
||||
it->second->mTime = active->second.mTime;
|
||||
|
||||
callback = animController->getAsCallback();
|
||||
}
|
||||
else if (useSmoothAnims && dynamic_cast<osgAnimation::Bone*>(node.get()))
|
||||
{
|
||||
// Update an existing animation blending controller or create a new one for osgAnimation
|
||||
osg::ref_ptr<BoneAnimBlendController> animController;
|
||||
|
||||
if (mBoneAnimBlendControllers.contains(node))
|
||||
{
|
||||
animController = mBoneAnimBlendControllers[node];
|
||||
animController->setKeyframeTrack(it->second, stateData, animsrc->mAnimBlendRules);
|
||||
}
|
||||
else
|
||||
{
|
||||
animController = osg::ref_ptr<BoneAnimBlendController>(
|
||||
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<osgAnimation::Bone*>(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<SceneUtil::KeyframeController*>(cb))
|
||||
if (dynamic_cast<AnimBlendController*>(cb) || dynamic_cast<BoneAnimBlendController*>(cb)
|
||||
|| dynamic_cast<SceneUtil::KeyframeController*>(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)
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
#include "../mwworld/movementdirection.hpp"
|
||||
#include "../mwworld/ptr.hpp"
|
||||
|
||||
#include "animblendcontroller.hpp"
|
||||
#include <components/misc/strings/algorithm.hpp>
|
||||
#include <components/sceneutil/animblendrules.hpp>
|
||||
#include <components/sceneutil/controller.hpp>
|
||||
#include <components/sceneutil/nodecallback.hpp>
|
||||
#include <components/sceneutil/textkeymap.hpp>
|
||||
#include <components/sceneutil/util.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
@ -47,6 +51,8 @@ namespace MWRender
|
|||
class RotateController;
|
||||
class TransparencyUpdater;
|
||||
|
||||
typedef std::vector<std::pair<osg::ref_ptr<osg::Node>, osg::ref_ptr<osg::Callback>>> 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<std::pair<osg::ref_ptr<osg::Node>, osg::ref_ptr<osg::Callback>>> mActiveControllers;
|
||||
ActiveControllersVector mActiveControllers;
|
||||
|
||||
// Keep track of the animation controllers for easy access
|
||||
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<AnimBlendController>> mAnimBlendControllers;
|
||||
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<BoneAnimBlendController>> mBoneAnimBlendControllers;
|
||||
|
||||
std::shared_ptr<AnimationTime> 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<AnimSource> 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<osg::Group> 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
|
||||
|
|
318
apps/openmw/mwrender/animblendcontroller.cpp
Normal file
318
apps/openmw/mwrender/animblendcontroller.cpp
Normal file
|
@ -0,0 +1,318 @@
|
|||
#include "animblendcontroller.hpp"
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string, EasingFn> 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 <typename NodeClass>
|
||||
AnimBlendControllerBase<NodeClass>::AnimBlendControllerBase(
|
||||
osg::ref_ptr<SceneUtil::KeyframeController> keyframeTrack, AnimBlendStateData newState,
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> blendRules)
|
||||
: mTimeFactor(0.0f)
|
||||
, mInterpFactor(0.0f)
|
||||
{
|
||||
setKeyframeTrack(keyframeTrack, newState, blendRules);
|
||||
}
|
||||
|
||||
template <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::setKeyframeTrack(osg::ref_ptr<SceneUtil::KeyframeController> kft,
|
||||
AnimBlendStateData newState, osg::ref_ptr<const SceneUtil::AnimBlendRules> 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 <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::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<osgAnimation::Bone*>(group->getChild(i)), false);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::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 <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::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 <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::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<AnimBlendControllerBase<NodeClass>, osgAnimation::Bone*>::traverse(node, nv);
|
||||
}
|
||||
|
||||
template <typename NodeClass>
|
||||
void AnimBlendControllerBase<NodeClass>::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<AnimBlendControllerBase<NodeClass>, NifOsg::MatrixTransform*>::traverse(node, nv);
|
||||
}
|
||||
}
|
109
apps/openmw/mwrender/animblendcontroller.hpp
Normal file
109
apps/openmw/mwrender/animblendcontroller.hpp
Normal file
|
@ -0,0 +1,109 @@
|
|||
#ifndef OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H
|
||||
#define OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/nifosg/matrixtransform.hpp>
|
||||
#include <components/sceneutil/animblendrules.hpp>
|
||||
#include <components/sceneutil/controller.hpp>
|
||||
#include <components/sceneutil/keyframe.hpp>
|
||||
#include <components/sceneutil/nodecallback.hpp>
|
||||
|
||||
namespace MWRender
|
||||
{
|
||||
namespace Easings
|
||||
{
|
||||
typedef float (*EasingFn)(float);
|
||||
}
|
||||
|
||||
struct AnimBlendStateData
|
||||
{
|
||||
std::string mGroupname;
|
||||
std::string mStartKey;
|
||||
};
|
||||
|
||||
template <typename NodeClass>
|
||||
class AnimBlendControllerBase : public SceneUtil::NodeCallback<AnimBlendControllerBase<NodeClass>, NodeClass*>,
|
||||
public SceneUtil::Controller
|
||||
{
|
||||
public:
|
||||
AnimBlendControllerBase(osg::ref_ptr<SceneUtil::KeyframeController> keyframeTrack, AnimBlendStateData animState,
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> blendRules);
|
||||
|
||||
void operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv);
|
||||
void operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv);
|
||||
|
||||
void setKeyframeTrack(osg::ref_ptr<SceneUtil::KeyframeController> kft, AnimBlendStateData animState,
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> 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<SceneUtil::KeyframeController> 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<const SceneUtil::AnimBlendRules> mAnimBlendRules;
|
||||
|
||||
std::unordered_map<osg::Node*, osg::Matrixf> mBlendBoneTransforms;
|
||||
};
|
||||
|
||||
typedef AnimBlendControllerBase<NifOsg::MatrixTransform> AnimBlendController;
|
||||
typedef AnimBlendControllerBase<osgAnimation::Bone> BoneAnimBlendController;
|
||||
|
||||
// Assigned to child bones with an instance of AnimBlendControllerBase
|
||||
class BoneAnimBlendControllerWrapper : public osg::Callback
|
||||
{
|
||||
public:
|
||||
BoneAnimBlendControllerWrapper(osg::ref_ptr<BoneAnimBlendController> rootCallback, osg::Node* node)
|
||||
{
|
||||
mRootCallback = rootCallback;
|
||||
mNode = dynamic_cast<osgAnimation::Bone*>(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<BoneAnimBlendController> mRootCallback;
|
||||
osgAnimation::Bone* mNode;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#include <osg/TexMat>
|
||||
#include <osg/Texture2D>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <osgParticle/Emitter>
|
||||
|
||||
#include <components/nif/data.hpp>
|
||||
|
@ -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() {}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
94
components/resource/animblendrulesmanager.cpp
Normal file
94
components/resource/animblendrulesmanager.cpp
Normal file
|
@ -0,0 +1,94 @@
|
|||
#include "animblendrulesmanager.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
#include <osg/Stats>
|
||||
#include <osgAnimation/Animation>
|
||||
#include <osgAnimation/BasicAnimationManager>
|
||||
#include <osgAnimation/Channel>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/misc/pathhelpers.hpp>
|
||||
|
||||
#include <components/sceneutil/osgacontroller.hpp>
|
||||
#include <components/vfs/pathutil.hpp>
|
||||
|
||||
#include <components/resource/scenemanager.hpp>
|
||||
|
||||
#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<const AnimBlendRules> 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<SceneUtil::AnimBlendRules> 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<const AnimBlendRules> AnimBlendRulesManager::loadRules(std::string_view path)
|
||||
{
|
||||
const VFS::Path::Normalized normalizedPath = VFS::Path::Normalized(path);
|
||||
std::optional<osg::ref_ptr<osg::Object>> obj = mCache->getRefFromObjectCacheOrNone(normalizedPath);
|
||||
if (obj.has_value())
|
||||
{
|
||||
return osg::ref_ptr<AnimBlendRules>(static_cast<AnimBlendRules*>(obj->get()));
|
||||
}
|
||||
else
|
||||
{
|
||||
osg::ref_ptr<AnimBlendRules> blendRules = AnimBlendRules::fromFile(mVfs, normalizedPath);
|
||||
if (blendRules == nullptr)
|
||||
{
|
||||
// No blend rules were found in VFS, cache a nullptr.
|
||||
osg::ref_ptr<AnimBlendRules> 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);
|
||||
}
|
||||
|
||||
}
|
36
components/resource/animblendrulesmanager.hpp
Normal file
36
components/resource/animblendrulesmanager.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#ifndef OPENMW_COMPONENTS_ANIMBLENDRULESMANAGER_H
|
||||
#define OPENMW_COMPONENTS_ANIMBLENDRULESMANAGER_H
|
||||
|
||||
#include <osg/ref_ptr>
|
||||
#include <string>
|
||||
|
||||
#include <components/sceneutil/animblendrules.hpp>
|
||||
|
||||
#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<const SceneUtil::AnimBlendRules> getRules(
|
||||
std::string_view path, std::string_view overridePath = "");
|
||||
|
||||
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
|
||||
|
||||
private:
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> loadRules(std::string_view path);
|
||||
|
||||
const VFS::Manager* mVfs;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
|
||||
#include "animblendrulesmanager.hpp"
|
||||
#include "bgsmfilemanager.hpp"
|
||||
#include "imagemanager.hpp"
|
||||
#include "keyframemanager.hpp"
|
||||
|
@ -21,6 +22,7 @@ namespace Resource
|
|||
mSceneManager = std::make_unique<SceneManager>(
|
||||
vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay);
|
||||
mKeyframeManager = std::make_unique<KeyframeManager>(vfs, mSceneManager.get(), expiryDelay, encoder);
|
||||
mAnimBlendRulesManager = std::make_unique<AnimBlendRulesManager>(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<BaseResourceManager*>::iterator it = mResourceManagers.begin(); it != mResourceManagers.end();
|
||||
|
|
|
@ -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<BgsmFileManager> mBgsmFileManager;
|
||||
std::unique_ptr<NifFileManager> mNifFileManager;
|
||||
std::unique_ptr<KeyframeManager> mKeyframeManager;
|
||||
std::unique_ptr<AnimBlendRulesManager> mAnimBlendRulesManager;
|
||||
|
||||
// Store the base classes separately to get convenient access to the common interface
|
||||
// Here users can register their own resourcemanager as well
|
||||
|
|
177
components/sceneutil/animblendrules.cpp
Normal file
177
components/sceneutil/animblendrules.cpp
Normal file
|
@ -0,0 +1,177 @@
|
|||
#include "animblendrules.hpp"
|
||||
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
|
||||
#include <components/misc/strings/algorithm.hpp>
|
||||
#include <components/misc/strings/format.hpp>
|
||||
#include <components/misc/strings/lower.hpp>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/files/configfileparser.hpp>
|
||||
#include <components/files/conversion.hpp>
|
||||
#include <components/sceneutil/controller.hpp>
|
||||
#include <components/sceneutil/textkeymap.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
namespace SceneUtil
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::pair<std::string, std::string> 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<BlendRule>& rules)
|
||||
: mRules(rules)
|
||||
{
|
||||
}
|
||||
|
||||
osg::ref_ptr<AnimBlendRules> 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<char>(*vfs->get(configPath)), {});
|
||||
|
||||
std::vector<BlendRule> 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<std::string>());
|
||||
auto toNames = splitRuleName(it["to"].as<std::string>());
|
||||
|
||||
BlendRule ruleObj = {
|
||||
.mFromGroup = fromNames.first,
|
||||
.mFromKey = fromNames.second,
|
||||
.mToGroup = toNames.first,
|
||||
.mToKey = toNames.second,
|
||||
.mDuration = it["duration"].as<float>(),
|
||||
.mEasing = it["easing"].as<std::string>(),
|
||||
};
|
||||
|
||||
rules.emplace_back(ruleObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(Debug::Warning) << "Warning: Blending rule '"
|
||||
<< (it["from"] ? it["from"].as<std::string>() : "undefined") << "->"
|
||||
<< (it["to"] ? it["to"].as<std::string>() : "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<BlendRule> 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<BlendRule>(*rule);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
}
|
49
components/sceneutil/animblendrules.hpp
Normal file
49
components/sceneutil/animblendrules.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP
|
||||
#define OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <osg/Object>
|
||||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
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<BlendRule>& rules);
|
||||
AnimBlendRules(const AnimBlendRules& copy, const osg::CopyOp& copyop);
|
||||
|
||||
META_Object(SceneUtil, AnimBlendRules)
|
||||
|
||||
void addOverrideRules(const AnimBlendRules& overrideRules);
|
||||
|
||||
std::optional<BlendRule> findBlendingRule(
|
||||
std::string fromGroup, std::string fromKey, std::string toGroup, std::string toKey) const;
|
||||
|
||||
const std::vector<BlendRule>& getRules() const { return mRules; }
|
||||
|
||||
static osg::ref_ptr<AnimBlendRules> fromFile(const VFS::Manager* vfs, VFS::Path::NormalizedView yamlpath);
|
||||
|
||||
private:
|
||||
std::vector<BlendRule> mRules;
|
||||
|
||||
inline bool fitsRuleString(const std::string& str, const std::string& ruleStr) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
|
@ -2,6 +2,7 @@
|
|||
#define OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include <osg/Object>
|
||||
|
||||
|
@ -21,8 +22,19 @@ namespace SceneUtil
|
|||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<float> mTime = std::make_shared<float>(0.0f);
|
||||
|
||||
struct KfTransform
|
||||
{
|
||||
std::optional<osg::Vec3f> mTranslation;
|
||||
std::optional<osg::Quat> mRotation;
|
||||
std::optional<float> 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;
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ namespace Settings
|
|||
SettingValue<bool> mCanLootDuringDeathAnimation{ mIndex, "Game", "can loot during death animation" };
|
||||
SettingValue<bool> mRebalanceSoulGemValues{ mIndex, "Game", "rebalance soul gem values" };
|
||||
SettingValue<bool> mUseAdditionalAnimSources{ mIndex, "Game", "use additional anim sources" };
|
||||
SettingValue<bool> mSmoothAnimTransitions{ mIndex, "Game", "smooth animation transitions" };
|
||||
SettingValue<bool> mBarterDispositionChangeIsPermanent{ mIndex, "Game",
|
||||
"barter disposition change is permanent" };
|
||||
SettingValue<int> mStrengthInfluencesHandToHand{ mIndex, "Game", "strength influences hand to hand",
|
||||
|
|
|
@ -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
|
||||
|
|
70
files/data/animations/animation-config.yaml
Normal file
70
files/data/animations/animation-config.yaml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue