mirror of https://github.com/OpenMW/openmw.git
Merge branch 'animationblending' into 'master'
Animation blending implementation. Flexible and moddable through .yaml blending config files. See merge request OpenMW/openmw!3497pull/3236/head
commit
1f4ab3b668
@ -0,0 +1,388 @@
|
||||
#include "animblendcontroller.hpp"
|
||||
#include "rotatecontroller.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace MWRender
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Animation Easing/Blending functions
|
||||
namespace Easings
|
||||
{
|
||||
float linear(float x)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
|
||||
float sineOut(float x)
|
||||
{
|
||||
return std::sin((x * osg::PIf) / 2.f);
|
||||
}
|
||||
|
||||
float sineIn(float x)
|
||||
{
|
||||
return 1.f - std::cos((x * osg::PIf) / 2.f);
|
||||
}
|
||||
|
||||
float sineInOut(float x)
|
||||
{
|
||||
return -(std::cos(osg::PIf * x) - 1.f) / 2.f;
|
||||
}
|
||||
|
||||
float cubicOut(float t)
|
||||
{
|
||||
float t1 = 1.f - t;
|
||||
return 1.f - (t1 * t1 * t1); // (1-t)^3
|
||||
}
|
||||
|
||||
float cubicIn(float x)
|
||||
{
|
||||
return x * x * x; // x^3
|
||||
}
|
||||
|
||||
float cubicInOut(float x)
|
||||
{
|
||||
if (x < 0.5f)
|
||||
{
|
||||
return 4.f * x * x * x; // 4x^3
|
||||
}
|
||||
else
|
||||
{
|
||||
float x2 = -2.f * x + 2.f;
|
||||
return 1.f - (x2 * x2 * x2) / 2.f; // (1 - (-2x + 2)^3)/2
|
||||
}
|
||||
}
|
||||
|
||||
float quartOut(float t)
|
||||
{
|
||||
float t1 = 1.f - t;
|
||||
return 1.f - (t1 * t1 * t1 * t1); // (1-t)^4
|
||||
}
|
||||
|
||||
float quartIn(float t)
|
||||
{
|
||||
return t * t * t * t; // t^4
|
||||
}
|
||||
|
||||
float quartInOut(float x)
|
||||
{
|
||||
if (x < 0.5f)
|
||||
{
|
||||
return 8.f * x * x * x * x; // 8x^4
|
||||
}
|
||||
else
|
||||
{
|
||||
float x2 = -2.f * x + 2.f;
|
||||
return 1.f - (x2 * x2 * x2 * x2) / 2.f; // 1 - ((-2x + 2)^4)/2
|
||||
}
|
||||
}
|
||||
|
||||
float springOutGeneric(float x, float lambda)
|
||||
{
|
||||
// Higher lambda = lower swing amplitude. 1 = 150% swing amplitude.
|
||||
// w is the frequency of oscillation in the easing func, controls the amount of overswing
|
||||
const float w = 1.5f * osg::PIf; // 4.71238
|
||||
return 1.f - expf(-lambda * x) * std::cos(w * x);
|
||||
}
|
||||
|
||||
float springOutWeak(float x)
|
||||
{
|
||||
return springOutGeneric(x, 4.f);
|
||||
}
|
||||
|
||||
float springOutMed(float x)
|
||||
{
|
||||
return springOutGeneric(x, 3.f);
|
||||
}
|
||||
|
||||
float springOutStrong(float x)
|
||||
{
|
||||
return springOutGeneric(x, 2.f);
|
||||
}
|
||||
|
||||
float springOutTooMuch(float x)
|
||||
{
|
||||
return springOutGeneric(x, 1.f);
|
||||
}
|
||||
|
||||
const 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 },
|
||||
};
|
||||
}
|
||||
|
||||
osg::Vec3f vec3fLerp(float t, const osg::Vec3f& start, const osg::Vec3f& end)
|
||||
{
|
||||
return start + (end - start) * t;
|
||||
}
|
||||
}
|
||||
|
||||
AnimBlendController::AnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& newState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules)
|
||||
: mEasingFn(&Easings::sineOut)
|
||||
{
|
||||
setKeyframeTrack(keyframeTrack, newState, blendRules);
|
||||
}
|
||||
|
||||
NifAnimBlendController::NifAnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& newState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules)
|
||||
: AnimBlendController(keyframeTrack, newState, blendRules)
|
||||
{
|
||||
}
|
||||
|
||||
BoneAnimBlendController::BoneAnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& newState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules)
|
||||
: AnimBlendController(keyframeTrack, newState, blendRules)
|
||||
{
|
||||
}
|
||||
|
||||
void AnimBlendController::setKeyframeTrack(const osg::ref_ptr<SceneUtil::KeyframeController>& kft,
|
||||
const AnimBlendStateData& newState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules)
|
||||
{
|
||||
// If animation has changed then start blending
|
||||
if (newState.mGroupname != mAnimState.mGroupname || newState.mStartKey != mAnimState.mStartKey
|
||||
|| kft != mKeyframeTrack)
|
||||
{
|
||||
// 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);
|
||||
|
||||
if (blendRule)
|
||||
{
|
||||
if (const auto it = Easings::easingsMap.find(blendRule->mEasing); it != Easings::easingsMap.end())
|
||||
{
|
||||
mEasingFn = it->second;
|
||||
mBlendDuration = blendRule->mDuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(Debug::Warning)
|
||||
<< "Warning: animation blending rule contains invalid easing type: " << blendRule->mEasing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAnimBlendRules = blendRules;
|
||||
mKeyframeTrack = kft;
|
||||
mAnimState = newState;
|
||||
mBlendTrigger = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimBlendController::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;
|
||||
}
|
||||
|
||||
void BoneAnimBlendController::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);
|
||||
}
|
||||
}
|
||||
|
||||
void BoneAnimBlendController::applyBoneBlend(osgAnimation::Bone* bone)
|
||||
{
|
||||
// If we are done with interpolation then we can safely skip this as the bones are correct
|
||||
if (!mInterpActive)
|
||||
return;
|
||||
|
||||
// Shouldn't happen, but potentially an edge case where a new bone was added
|
||||
// between gatherRecursiveBoneTransforms and this update
|
||||
// currently OpenMW will never do this
|
||||
assert(mBlendBoneTransforms.find(bone) != mBlendBoneTransforms.end());
|
||||
|
||||
// Every frame the osgAnimation controller updates this
|
||||
// so it is ok that we update it directly below
|
||||
const 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);
|
||||
}
|
||||
|
||||
void BoneAnimBlendController::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<BoneAnimBlendController, osgAnimation::Bone*>::traverse(node, nv);
|
||||
}
|
||||
|
||||
void NifAnimBlendController::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 mRotationScale is used here because it's unaffected by the side-effects of RotationController
|
||||
mBlendStartRot = node->mRotationScale.toOsgMatrix().getRotate();
|
||||
mBlendStartTrans = node->getMatrix().getTrans();
|
||||
mBlendStartScale = node->mScale;
|
||||
|
||||
// Subtract any rotate controller's offset from start transform (if it appears after this callback)
|
||||
// this is required otherwise the blend start will be with an offset, then offset could be applied again
|
||||
// fixes an issue with camera jumping during first person sneak jumping camera
|
||||
osg::Callback* updateCb = node->getUpdateCallback()->getNestedCallback();
|
||||
while (updateCb)
|
||||
{
|
||||
MWRender::RotateController* rotateController = dynamic_cast<MWRender::RotateController*>(updateCb);
|
||||
if (rotateController)
|
||||
{
|
||||
const osg::Quat& rotate = rotateController->getRotate();
|
||||
const osg::Vec3f& offset = rotateController->getOffset();
|
||||
|
||||
osg::NodePathList nodepaths = node->getParentalNodePaths(rotateController->getRelativeTo());
|
||||
osg::Quat worldOrient;
|
||||
if (!nodepaths.empty())
|
||||
{
|
||||
osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]);
|
||||
worldOrient = worldMat.getRotate();
|
||||
}
|
||||
|
||||
worldOrient = worldOrient * rotate.inverse();
|
||||
const osg::Quat worldOrientInverse = worldOrient.inverse();
|
||||
|
||||
mBlendStartTrans -= worldOrientInverse * offset;
|
||||
}
|
||||
|
||||
updateCb = updateCb->getNestedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
calculateInterpFactor(time);
|
||||
|
||||
if (mInterpActive)
|
||||
{
|
||||
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->mRotationScale);
|
||||
}
|
||||
|
||||
if (translation)
|
||||
{
|
||||
osg::Vec3f lerpedTrans = vec3fLerp(mInterpFactor, mBlendStartTrans, *translation);
|
||||
node->setTranslation(lerpedTrans);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (translation)
|
||||
node->setTranslation(*translation);
|
||||
|
||||
if (rotation)
|
||||
node->setRotation(*rotation);
|
||||
else
|
||||
node->setRotation(node->mRotationScale);
|
||||
}
|
||||
|
||||
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<NifAnimBlendController, NifOsg::MatrixTransform*>::traverse(node, nv);
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
#ifndef OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H
|
||||
#define OPENMW_MWRENDER_ANIMBLENDCONTROLLER_H
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#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
|
||||
{
|
||||
typedef float (*EasingFn)(float);
|
||||
|
||||
struct AnimBlendStateData
|
||||
{
|
||||
std::string mGroupname;
|
||||
std::string mStartKey;
|
||||
};
|
||||
|
||||
class AnimBlendController : public SceneUtil::Controller
|
||||
{
|
||||
public:
|
||||
AnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& animState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules);
|
||||
|
||||
AnimBlendController() {}
|
||||
|
||||
void setKeyframeTrack(const osg::ref_ptr<SceneUtil::KeyframeController>& kft,
|
||||
const AnimBlendStateData& animState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules);
|
||||
|
||||
bool getBlendTrigger() const { return mBlendTrigger; }
|
||||
|
||||
protected:
|
||||
EasingFn mEasingFn;
|
||||
float mBlendDuration = 0.0f;
|
||||
float mBlendStartTime = 0.0f;
|
||||
float mTimeFactor = 0.0f;
|
||||
float mInterpFactor = 0.0f;
|
||||
|
||||
bool mBlendTrigger = false;
|
||||
bool mInterpActive = false;
|
||||
|
||||
AnimBlendStateData mAnimState;
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> mAnimBlendRules;
|
||||
osg::ref_ptr<SceneUtil::KeyframeController> mKeyframeTrack;
|
||||
|
||||
std::unordered_map<osg::Node*, osg::Matrixf> mBlendBoneTransforms;
|
||||
|
||||
inline void calculateInterpFactor(float time);
|
||||
};
|
||||
|
||||
class NifAnimBlendController : public SceneUtil::NodeCallback<NifAnimBlendController, NifOsg::MatrixTransform*>,
|
||||
public AnimBlendController
|
||||
{
|
||||
public:
|
||||
NifAnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& animState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules);
|
||||
|
||||
NifAnimBlendController() {}
|
||||
|
||||
NifAnimBlendController(const NifAnimBlendController& other, const osg::CopyOp&)
|
||||
: NifAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules)
|
||||
{
|
||||
}
|
||||
|
||||
META_Object(MWRender, NifAnimBlendController)
|
||||
|
||||
void operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv);
|
||||
|
||||
osg::Callback* getAsCallback() { return this; }
|
||||
|
||||
private:
|
||||
osg::Quat mBlendStartRot;
|
||||
osg::Vec3f mBlendStartTrans;
|
||||
float mBlendStartScale = 0.0f;
|
||||
};
|
||||
|
||||
class BoneAnimBlendController : public SceneUtil::NodeCallback<BoneAnimBlendController, osgAnimation::Bone*>,
|
||||
public AnimBlendController
|
||||
{
|
||||
public:
|
||||
BoneAnimBlendController(const osg::ref_ptr<SceneUtil::KeyframeController>& keyframeTrack,
|
||||
const AnimBlendStateData& animState, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules);
|
||||
|
||||
BoneAnimBlendController() {}
|
||||
|
||||
BoneAnimBlendController(const BoneAnimBlendController& other, const osg::CopyOp&)
|
||||
: BoneAnimBlendController(other.mKeyframeTrack, other.mAnimState, other.mAnimBlendRules)
|
||||
{
|
||||
}
|
||||
|
||||
void gatherRecursiveBoneTransforms(osgAnimation::Bone* parent, bool isRoot = true);
|
||||
void applyBoneBlend(osgAnimation::Bone* parent);
|
||||
|
||||
META_Object(MWRender, BoneAnimBlendController)
|
||||
|
||||
void operator()(osgAnimation::Bone* node, osg::NodeVisitor* nv);
|
||||
|
||||
osg::Callback* getAsCallback() { return this; }
|
||||
};
|
||||
|
||||
// Assigned to child bones with an instance of AnimBlendController
|
||||
class BoneAnimBlendControllerWrapper : public osg::Callback
|
||||
{
|
||||
public:
|
||||
BoneAnimBlendControllerWrapper(osg::ref_ptr<BoneAnimBlendController> rootCallback, osgAnimation::Bone* node)
|
||||
: mRootCallback(rootCallback)
|
||||
, mNode(node)
|
||||
{
|
||||
}
|
||||
|
||||
BoneAnimBlendControllerWrapper() {}
|
||||
|
||||
BoneAnimBlendControllerWrapper(const BoneAnimBlendControllerWrapper& copy, const osg::CopyOp&)
|
||||
: mRootCallback(copy.mRootCallback)
|
||||
, mNode(copy.mNode)
|
||||
{
|
||||
}
|
||||
|
||||
META_Object(MWRender, BoneAnimBlendControllerWrapper)
|
||||
|
||||
bool run(osg::Object* object, osg::Object* data) override
|
||||
{
|
||||
mRootCallback->applyBoneBlend(mNode);
|
||||
traverse(object, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
osg::ref_ptr<BoneAnimBlendController> mRootCallback;
|
||||
osgAnimation::Bone* mNode;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,76 @@
|
||||
#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)
|
||||
{
|
||||
}
|
||||
|
||||
osg::ref_ptr<const AnimBlendRules> AnimBlendRulesManager::getRules(
|
||||
const VFS::Path::NormalizedView path, const VFS::Path::NormalizedView 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.value().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(VFS::Path::NormalizedView path)
|
||||
{
|
||||
std::optional<osg::ref_ptr<osg::Object>> obj = mCache->getRefFromObjectCacheOrNone(path);
|
||||
if (obj.has_value())
|
||||
{
|
||||
return osg::ref_ptr<AnimBlendRules>(static_cast<AnimBlendRules*>(obj->get()));
|
||||
}
|
||||
|
||||
osg::ref_ptr<AnimBlendRules> blendRules = AnimBlendRules::fromFile(mVFS, path);
|
||||
mCache->addEntryToObjectCache(path.value(), blendRules);
|
||||
return blendRules;
|
||||
}
|
||||
|
||||
void AnimBlendRulesManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const
|
||||
{
|
||||
Resource::reportStats("Blending Rules", frameNumber, mCache->getStats(), *stats);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
#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(
|
||||
const VFS::Path::NormalizedView path, const VFS::Path::NormalizedView overridePath);
|
||||
|
||||
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
|
||||
|
||||
private:
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> loadRules(VFS::Path::NormalizedView path);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,170 @@
|
||||
#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))
|
||||
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_view str, const std::string_view 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)
|
||||
{
|
||||
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)
|
||||
&& (rule->mFromKey.empty() || fitsRuleString(fromKey, rule->mFromKey)))
|
||||
{
|
||||
fromMatch = true;
|
||||
}
|
||||
|
||||
if ((fitsRuleString(toGroup, rule->mToGroup) || (rule->mToGroup == "$" && toGroup == fromGroup))
|
||||
&& (rule->mToKey.empty() || fitsRuleString(toKey, rule->mToKey)))
|
||||
{
|
||||
toMatch = true;
|
||||
}
|
||||
|
||||
if (fromMatch && toMatch)
|
||||
return std::make_optional<BlendRule>(*rule);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
@ -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_view str, const std::string_view ruleStr) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
@ -0,0 +1,90 @@
|
||||
Animation blending
|
||||
##################
|
||||
|
||||
Animation blending introduces smooth animation transitions between essentially every animation in the game without affecting gameplay. Effective if ``smooth animation transitions`` setting is enabled in the launcher or the config files.
|
||||
|
||||
Animation developers can bundle ``.yaml``/``.json`` files together with their ``.kf`` files to specify the blending style of their animations. Those settings will only affect the corresponding animation files.
|
||||
|
||||
The default OpenMW animation blending config file (the global config) affects actors only, that restriction doesn't apply to other animation blending config files; they can affect animated objects too.
|
||||
Do not override the global config file in your mod, instead create a ``your_modded_animation_file_name.yaml`` file and put it in the same folder as your ``.kf`` file.
|
||||
|
||||
For example, if your mod includes a ``newAnimations.kf`` file, you can put a ``newAnimations.yaml`` file beside it and fill it with your blending rules.
|
||||
Animation config files shipped in this fashion will only affect your modded animations and will not meddle with other animations in the game.
|
||||
|
||||
Local (per-kf-file) animation rules will only affect transitions between animations provided in that file and transitions to those animations; they will not affect transitions from the file animation to some other animation.
|
||||
|
||||
Editing animation config files
|
||||
------------------------------
|
||||
|
||||
In examples below ``.yaml`` config file will be used. You can provide ``.json`` files instead of ``.yaml`` if you adhere to the same overall structures and field names.
|
||||
|
||||
Animation blending config file is a list of blending rules that look like this:
|
||||
|
||||
::
|
||||
|
||||
blending_rules:
|
||||
- from: "*"
|
||||
to: "*"
|
||||
easing: "sineOut"
|
||||
duration: 0.25
|
||||
- from: "*"
|
||||
to: "idlesneak*"
|
||||
easing: "springOutWeak"
|
||||
duration: 0.4
|
||||
|
||||
See `files/data/animations/animation-config.yaml <https://gitlab.com/OpenMW/openmw/-/tree/master/files/data/animations/animation-config.yaml>`__ for an example of such a file.
|
||||
|
||||
Every blending rule should include a set of following fields:
|
||||
|
||||
``from`` and ``to`` are rules that will attempt to match animation names; they usually look like ``animationGroupName:keyName`` where ``keyName`` is essentially the name of a specific action within the animation group.
|
||||
Examples: ``"weapononehanded: chop start"``, ``"idle1h"``, ``"jump: start"`` e.t.c.
|
||||
|
||||
.. note::
|
||||
|
||||
``keyName`` is not always present and if omitted - the rule will match any ``keyName``.
|
||||
The different animation names the game uses can be inspected by opening ``.kf`` animation files in Blender.
|
||||
|
||||
|
||||
Both ``animationGroupName`` and ``keyName`` support wildcard characters either at the beginning, the end of the name, or instead of the name:
|
||||
|
||||
- ``"*"`` will match any name.
|
||||
- ``"*idle:sta*"`` will match an animationGroupName ending with ``idle`` and a keyName starting with ``sta``.
|
||||
- ``"weapon*handed: chop*attack"`` will not work since we don't support wildcards in the middle.
|
||||
|
||||
``easing`` is an animation blending function, i.e., a style of transition between animations, look below to see the list of possible easings.
|
||||
|
||||
``duration`` is the transition duration in seconds, 0.2-0.4 are usually reasonable transition times, but this highly depends on your use case.
|
||||
|
||||
.. note::
|
||||
|
||||
The bottom-most rule takes precedence in the animation config files.
|
||||
|
||||
|
||||
List of possible easings
|
||||
------------------------
|
||||
|
||||
- "linear"
|
||||
- "sineOut"
|
||||
- "sineIn"
|
||||
- "sineInOut"
|
||||
- "cubicOut"
|
||||
- "cubicIn"
|
||||
- "cubicInOut"
|
||||
- "quartOut"
|
||||
- "quartIn"
|
||||
- "quartInOut"
|
||||
- "springOutGeneric"
|
||||
- "springOutWeak"
|
||||
- "springOutMed"
|
||||
- "springOutStrong"
|
||||
- "springOutTooMuch"
|
||||
|
||||
``"sineOut"`` easing is usually a safe bet. In general ``"...Out"`` easing functions will yield a transition that is fast at the beginning of the transition but slows down towards the end, that style of transitions usually looks good on organic animations e.g. humanoids and creatures.
|
||||
|
||||
``"...In"`` transitions begin slow but end fast, ``"...InOut"`` begin fast, slowdown in the middle, end fast.
|
||||
|
||||
Its hard to give an example of use cases for the latter 2 types of easing functions, they are there for developers to experiment.
|
||||
|
||||
The possible easings are largely ported from `easings.net <https://easings.net/>`__ and have similar names. Except for the ``springOut`` family, those are similar to ``elasticOut``, with ``springOutWeak`` being almost identical to ``elasticOut``.
|
||||
|
||||
Don't be afraid to experiment with different timing and easing functions!
|
@ -0,0 +1,69 @@
|
||||
# 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
|
||||
|
||||
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
|
Loading…
Reference in New Issue