1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-02-11 06:40:34 +00:00
OpenMW/apps/openmw/mwrender/animblendcontroller.cpp
2024-07-19 00:04:16 +01:00

314 lines
12 KiB
C++

#include "animblendcontroller.hpp"
#include <components/debug/debuglog.hpp>
#include <osgAnimation/Bone>
#include <cassert>
#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
{
osg::Vec3f vec3fLerp(float t, const osg::Vec3f& A, const osg::Vec3f& B)
{
return A + (B - A) * 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 (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;
}
}
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;
// Shouldnt 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 mRotation 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;
}
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);
}
}