1
0
Fork 0
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:
Max Yari 2024-04-04 01:30:27 +01:00 committed by Sam Hellawell
parent a9281b5246
commit 55ffb6d7d2
21 changed files with 1143 additions and 31 deletions

View file

@ -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)

View file

@ -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

View 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);
}
}

View 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

View file

@ -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
)

View file

@ -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

View file

@ -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() {}

View file

@ -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:

View file

@ -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();

View file

@ -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

View 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);
}
}

View 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

View file

@ -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();

View file

@ -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

View 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;
}
}

View 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

View file

@ -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;
};

View file

@ -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",

View file

@ -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

View 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

View file

@ -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