mirror of
https://github.com/OpenMW/openmw.git
synced 2025-02-28 16:39:43 +00:00
Merge branch 'animationblending' into 'master'
Animation blending implementation. Flexible and moddable through .yaml blending config files. See merge request OpenMW/openmw!3497
This commit is contained in:
commit
1f4ab3b668
37 changed files with 1423 additions and 38 deletions
|
@ -61,6 +61,7 @@ Programmers
|
|||
Cory F. Cohen (cfcohen)
|
||||
Cris Mihalache (Mirceam)
|
||||
crussell187
|
||||
Sam Hellawell (cykoder)
|
||||
Dan Vukelich (sanchezman)
|
||||
darkf
|
||||
Dave Corley (S3ctor)
|
||||
|
@ -144,6 +145,7 @@ Programmers
|
|||
Łukasz Gołębiewski (lukago)
|
||||
Lukasz Gromanowski (lgro)
|
||||
Mads Sandvei (Foal)
|
||||
Maksim Eremenko (Max Yari)
|
||||
Marc Bouvier (CramitDeFrog)
|
||||
Marcin Hulist (Gohan)
|
||||
Mark Siewert (mark76)
|
||||
|
|
|
@ -193,6 +193,7 @@
|
|||
Feature #5492: Let rain and snow collide with statics
|
||||
Feature #5926: Refraction based on water depth
|
||||
Feature #5944: Option to use camera as sound listener
|
||||
Feature #6009: Animation blending - smooth animation transitions with modding support
|
||||
Feature #6152: Playing music via lua scripts
|
||||
Feature #6188: Specular lighting from point light sources
|
||||
Feature #6411: Support translations in openmw-launcher
|
||||
|
|
|
@ -189,6 +189,7 @@ bool Launcher::SettingsPage::loadSettings()
|
|||
loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox);
|
||||
loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox);
|
||||
}
|
||||
loadSettingBool(Settings::game().mSmoothAnimTransitions, *smoothAnimTransitionsCheckBox);
|
||||
loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox);
|
||||
loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox);
|
||||
loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox);
|
||||
|
@ -394,6 +395,7 @@ void Launcher::SettingsPage::saveSettings()
|
|||
saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing);
|
||||
saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing);
|
||||
saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection);
|
||||
saveSettingBool(*smoothAnimTransitionsCheckBox, Settings::game().mSmoothAnimTransitions);
|
||||
saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement);
|
||||
saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation);
|
||||
|
||||
|
|
|
@ -426,6 +426,16 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="smoothAnimTransitionsCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Smooth Animation Transitions</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -27,7 +27,7 @@ add_openmw_dir (mwrender
|
|||
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
|
||||
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
|
||||
postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples
|
||||
actorutil distortion animationpriority bonegroup blendmask
|
||||
actorutil distortion animationpriority bonegroup blendmask animblendcontroller
|
||||
)
|
||||
|
||||
add_openmw_dir (mwinput
|
||||
|
|
|
@ -13,8 +13,12 @@
|
|||
#include <osgParticle/ParticleProcessor>
|
||||
#include <osgParticle/ParticleSystem>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
#include <osgAnimation/UpdateBone>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/resource/animblendrulesmanager.hpp>
|
||||
#include <components/resource/keyframemanager.hpp>
|
||||
#include <components/resource/scenemanager.hpp>
|
||||
|
||||
|
@ -396,6 +400,60 @@ namespace
|
|||
|
||||
return lightModel;
|
||||
}
|
||||
|
||||
void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot)
|
||||
{
|
||||
// Attempt to cast node to an osgAnimation::Bone
|
||||
if (!isRoot && dynamic_cast<osgAnimation::Bone*>(parent))
|
||||
{
|
||||
// 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
|
||||
osgAnimation::Bone* bone = static_cast<osgAnimation::Bone*>(parent);
|
||||
osg::ref_ptr<osg::Callback> cb = new MWRender::BoneAnimBlendControllerWrapper(controller, bone);
|
||||
|
||||
// Ensure there is no other AnimBlendController - this can happen when using
|
||||
// multiple animations with different roots, such as NPC animation
|
||||
osg::Callback* updateCb = bone->getUpdateCallback();
|
||||
while (updateCb)
|
||||
{
|
||||
if (dynamic_cast<MWRender::BoneAnimBlendController*>(updateCb))
|
||||
{
|
||||
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 doesn't have an UpdateBone callback, we shouldn't be doing blending!
|
||||
updateCb = bone->getUpdateCallback();
|
||||
while (updateCb)
|
||||
{
|
||||
if (dynamic_cast<osgAnimation::UpdateBone*>(updateCb))
|
||||
{
|
||||
// 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 child bones if this is a group
|
||||
osg::Group* group = parent->asGroup();
|
||||
if (group)
|
||||
for (unsigned int i = 0; i < group->getNumChildren(); ++i)
|
||||
assignBoneBlendCallbackRecursive(controller, group->getChild(i), false);
|
||||
}
|
||||
}
|
||||
|
||||
namespace MWRender
|
||||
|
@ -449,6 +507,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)
|
||||
|
@ -606,7 +666,9 @@ namespace MWRender
|
|||
for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
|
||||
{
|
||||
if (Misc::getFileExtension(name) == "kf")
|
||||
{
|
||||
addSingleAnimSource(name, baseModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -623,17 +685,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 +724,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 +756,37 @@ 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.
|
||||
VFS::Path::Normalized blendConfigPath(kfname);
|
||||
blendConfigPath.changeExtension("yaml");
|
||||
|
||||
// globalBlendConfigPath is only used with actors! Objects have no default blending.
|
||||
constexpr VFS::Path::NormalizedView globalBlendConfigPath("animations/animation-config.yaml");
|
||||
|
||||
osg::ref_ptr<const SceneUtil::AnimBlendRules> blendRules;
|
||||
if (mPtr.getClass().isActor())
|
||||
{
|
||||
blendRules
|
||||
= mResourceSystem->getAnimBlendRulesManager()->getRules(globalBlendConfigPath, blendConfigPath);
|
||||
if (blendRules == nullptr)
|
||||
Log(Debug::Warning) << "Unable to find animation blending rules: '" << blendConfigPath << "' or '"
|
||||
<< globalBlendConfigPath << "'";
|
||||
}
|
||||
else
|
||||
{
|
||||
blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(blendConfigPath, blendConfigPath);
|
||||
}
|
||||
|
||||
// At this point blendRules will either be nullptr or an AnimBlendRules instance with > 0 rules inside.
|
||||
animsrc->mAnimBlendRules = blendRules;
|
||||
}
|
||||
|
||||
return animsrc;
|
||||
}
|
||||
|
||||
void Animation::clearAnimSources()
|
||||
|
@ -817,19 +911,23 @@ namespace MWRender
|
|||
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)
|
||||
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 +947,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)
|
||||
|
@ -981,6 +1081,48 @@ namespace MWRender
|
|||
return mNodeMap;
|
||||
}
|
||||
|
||||
template <typename ControllerType>
|
||||
inline osg::Callback* Animation::handleBlendTransform(const osg::ref_ptr<osg::Node>& node,
|
||||
osg::ref_ptr<SceneUtil::KeyframeController> keyframeController,
|
||||
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<ControllerType>>& blendControllers,
|
||||
const AnimBlendStateData& stateData, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules,
|
||||
const AnimState& active)
|
||||
{
|
||||
osg::ref_ptr<ControllerType> animController;
|
||||
if (blendControllers.contains(node))
|
||||
{
|
||||
animController = blendControllers.at(node);
|
||||
animController->setKeyframeTrack(keyframeController, stateData, blendRules);
|
||||
}
|
||||
else
|
||||
{
|
||||
animController = new ControllerType(keyframeController, stateData, blendRules);
|
||||
blendControllers.emplace(node, animController);
|
||||
|
||||
if constexpr (std::is_same_v<ControllerType, BoneAnimBlendController>)
|
||||
assignBoneBlendCallbackRecursive(animController, node, true);
|
||||
}
|
||||
|
||||
keyframeController->mTime = active.mTime;
|
||||
|
||||
osg::Callback* asCallback = animController->getAsCallback();
|
||||
if constexpr (std::is_same_v<ControllerType, BoneAnimBlendController>)
|
||||
{
|
||||
// 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 require blending.
|
||||
if (animController->getBlendTrigger())
|
||||
animController->gatherRecursiveBoneTransforms(static_cast<osgAnimation::Bone*>(node.get()));
|
||||
|
||||
// Register blend callback after the initial animation callback
|
||||
node->addUpdateCallback(asCallback);
|
||||
mActiveControllers.emplace_back(node, asCallback);
|
||||
|
||||
return keyframeController->getAsCallback();
|
||||
}
|
||||
|
||||
return asCallback;
|
||||
}
|
||||
|
||||
void Animation::resetActiveGroups()
|
||||
{
|
||||
// remove all previous external controllers from the scene graph
|
||||
|
@ -1004,7 +1146,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 +1161,8 @@ namespace MWRender
|
|||
if (active != mStates.end())
|
||||
{
|
||||
std::shared_ptr<AnimSource> animsrc = active->second.mSource;
|
||||
const AnimBlendStateData stateData
|
||||
= { .mGroupname = active->second.mGroupname, .mStartKey = active->second.mStartKey };
|
||||
|
||||
for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin();
|
||||
it != animsrc->mControllerMap[blendMask].end(); ++it)
|
||||
|
@ -1026,7 +1170,23 @@ 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
|
||||
|
||||
const bool useSmoothAnims = Settings::game().mSmoothAnimTransitions;
|
||||
|
||||
osg::Callback* callback = it->second->getAsCallback();
|
||||
if (useSmoothAnims)
|
||||
{
|
||||
if (dynamic_cast<NifOsg::MatrixTransform*>(node.get()))
|
||||
{
|
||||
callback = handleBlendTransform<NifAnimBlendController>(node, it->second,
|
||||
mAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second);
|
||||
}
|
||||
else if (dynamic_cast<osgAnimation::Bone*>(node.get()))
|
||||
{
|
||||
callback = handleBlendTransform<BoneAnimBlendController>(node, it->second,
|
||||
mBoneAnimBlendControllers, stateData, animsrc->mAnimBlendRules, active->second);
|
||||
}
|
||||
}
|
||||
|
||||
node->addUpdateCallback(callback);
|
||||
mActiveControllers.emplace_back(node, callback);
|
||||
|
||||
|
@ -1046,6 +1206,7 @@ namespace MWRender
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
addControllers();
|
||||
}
|
||||
|
||||
|
@ -1790,13 +1951,15 @@ namespace MWRender
|
|||
osg::Callback* cb = node->getUpdateCallback();
|
||||
while (cb)
|
||||
{
|
||||
if (dynamic_cast<SceneUtil::KeyframeController*>(cb))
|
||||
if (dynamic_cast<NifAnimBlendController*>(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)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define GAME_RENDER_ANIMATION_H
|
||||
|
||||
#include "animationpriority.hpp"
|
||||
#include "animblendcontroller.hpp"
|
||||
#include "blendmask.hpp"
|
||||
#include "bonegroup.hpp"
|
||||
|
||||
|
@ -9,12 +10,15 @@
|
|||
#include "../mwworld/ptr.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;
|
||||
|
||||
using ActiveControllersVector = std::vector<std::pair<osg::ref_ptr<osg::Node>, osg::ref_ptr<osg::Callback>>>;
|
||||
|
||||
class EffectAnimationTime : public SceneUtil::ControllerSource
|
||||
{
|
||||
private:
|
||||
|
@ -158,9 +164,12 @@ 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)); }
|
||||
bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; }
|
||||
};
|
||||
|
||||
|
@ -189,7 +198,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<NifAnimBlendController>> mAnimBlendControllers;
|
||||
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<BoneAnimBlendController>> mBoneAnimBlendControllers;
|
||||
|
||||
std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks];
|
||||
|
||||
|
@ -233,7 +246,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 +290,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);
|
||||
|
@ -291,6 +306,13 @@ namespace MWRender
|
|||
|
||||
void removeFromSceneImpl();
|
||||
|
||||
template <typename ControllerType>
|
||||
inline osg::Callback* handleBlendTransform(const osg::ref_ptr<osg::Node>& node,
|
||||
osg::ref_ptr<SceneUtil::KeyframeController> keyframeController,
|
||||
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<ControllerType>>& blendControllers,
|
||||
const AnimBlendStateData& stateData, const osg::ref_ptr<const SceneUtil::AnimBlendRules>& blendRules,
|
||||
const AnimState& active);
|
||||
|
||||
public:
|
||||
Animation(
|
||||
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem);
|
||||
|
@ -343,6 +365,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 +514,5 @@ namespace MWRender
|
|||
private:
|
||||
double mStartingTime;
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
388
apps/openmw/mwrender/animblendcontroller.cpp
Normal file
388
apps/openmw/mwrender/animblendcontroller.cpp
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
142
apps/openmw/mwrender/animblendcontroller.hpp
Normal file
142
apps/openmw/mwrender/animblendcontroller.hpp
Normal file
|
@ -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
|
|
@ -24,6 +24,12 @@ namespace MWRender
|
|||
void setOffset(const osg::Vec3f& offset);
|
||||
void setRotate(const osg::Quat& rotate);
|
||||
|
||||
const osg::Vec3f& getOffset() const { return mOffset; }
|
||||
|
||||
const osg::Quat& getRotate() const { return mRotate; }
|
||||
|
||||
osg::Node* getRelativeTo() const { return mRelativeTo; }
|
||||
|
||||
void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv);
|
||||
|
||||
protected:
|
||||
|
|
|
@ -127,7 +127,7 @@ add_component_dir (vfs
|
|||
)
|
||||
|
||||
add_component_dir (resource
|
||||
scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem
|
||||
scenemanager keyframemanager imagemanager animblendrulesmanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem
|
||||
resourcemanager stats animation foreachbulletobject errormarker cachestats bgsmfilemanager
|
||||
)
|
||||
|
||||
|
@ -138,7 +138,7 @@ add_component_dir (shader
|
|||
add_component_dir (sceneutil
|
||||
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
|
||||
lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer
|
||||
detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt
|
||||
detourdebugdraw navmesh agentpath animblendrules shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt
|
||||
screencapture depth color riggeometryosgaextension extradata unrefqueue lightcommon lightingmethod clearcolor
|
||||
cullsafeboundsvisitor keyframe nodecallback textkeymap glextensions
|
||||
)
|
||||
|
|
|
@ -52,6 +52,17 @@ namespace Nif
|
|||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
osg::Matrixf toOsgMatrix() const
|
||||
{
|
||||
osg::Matrixf osgMat;
|
||||
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
osgMat(i, j) = mValues[j][i]; // NB: column/row major difference
|
||||
|
||||
return osgMat;
|
||||
}
|
||||
};
|
||||
|
||||
struct NiTransform
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#include <osg/TexMat>
|
||||
#include <osg/Texture2D>
|
||||
|
||||
#include <osgAnimation/Bone>
|
||||
|
||||
#include <osgParticle/Emitter>
|
||||
|
||||
#include <components/nif/data.hpp>
|
||||
|
@ -175,25 +177,48 @@ namespace NifOsg
|
|||
|
||||
void KeyframeController::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv)
|
||||
{
|
||||
auto [translation, rotation, scale] = getCurrentTransformation(nv);
|
||||
|
||||
if (rotation)
|
||||
{
|
||||
node->setRotation(*rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is necessary to prevent first person animations glitching out due to RotationController
|
||||
node->setRotation(node->mRotationScale);
|
||||
}
|
||||
|
||||
if (translation)
|
||||
node->setTranslation(*translation);
|
||||
|
||||
if (scale)
|
||||
node->setScale(*scale);
|
||||
|
||||
traverse(node, nv);
|
||||
}
|
||||
|
||||
KeyframeController::KfTransform KeyframeController::getCurrentTransformation(osg::NodeVisitor* nv)
|
||||
{
|
||||
KfTransform out;
|
||||
|
||||
if (hasInput())
|
||||
{
|
||||
float time = getInputValue(nv);
|
||||
|
||||
if (!mRotations.empty())
|
||||
node->setRotation(mRotations.interpKey(time));
|
||||
out.mRotation = mRotations.interpKey(time);
|
||||
else if (!mXRotations.empty() || !mYRotations.empty() || !mZRotations.empty())
|
||||
node->setRotation(getXYZRotation(time));
|
||||
else
|
||||
node->setRotation(node->mRotationScale);
|
||||
out.mRotation = getXYZRotation(time);
|
||||
|
||||
if (!mTranslations.empty())
|
||||
node->setTranslation(mTranslations.interpKey(time));
|
||||
out.mTranslation = mTranslations.interpKey(time);
|
||||
|
||||
if (!mScales.empty())
|
||||
node->setScale(mScales.interpKey(time));
|
||||
out.mScale = mScales.interpKey(time);
|
||||
}
|
||||
|
||||
traverse(node, nv);
|
||||
return out;
|
||||
}
|
||||
|
||||
GeomMorpherController::GeomMorpherController() {}
|
||||
|
|
|
@ -238,6 +238,8 @@ namespace NifOsg
|
|||
osg::Vec3f getTranslation(float time) const override;
|
||||
osg::Callback* getAsCallback() override { return this; }
|
||||
|
||||
KfTransform getCurrentTransformation(osg::NodeVisitor* nv) override;
|
||||
|
||||
void operator()(NifOsg::MatrixTransform*, osg::NodeVisitor*);
|
||||
|
||||
private:
|
||||
|
|
76
components/resource/animblendrulesmanager.cpp
Normal file
76
components/resource/animblendrulesmanager.cpp
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
34
components/resource/animblendrulesmanager.hpp
Normal file
34
components/resource/animblendrulesmanager.hpp
Normal file
|
@ -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
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
|
||||
#include "animblendrulesmanager.hpp"
|
||||
#include "bgsmfilemanager.hpp"
|
||||
#include "imagemanager.hpp"
|
||||
#include "keyframemanager.hpp"
|
||||
|
@ -21,6 +22,7 @@ namespace Resource
|
|||
mSceneManager = std::make_unique<SceneManager>(
|
||||
vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay);
|
||||
mKeyframeManager = std::make_unique<KeyframeManager>(vfs, mSceneManager.get(), expiryDelay, encoder);
|
||||
mAnimBlendRulesManager = std::make_unique<AnimBlendRulesManager>(vfs, expiryDelay);
|
||||
|
||||
addResourceManager(mNifFileManager.get());
|
||||
addResourceManager(mBgsmFileManager.get());
|
||||
|
@ -28,6 +30,7 @@ namespace Resource
|
|||
// note, scene references images so add images afterwards for correct implementation of updateCache()
|
||||
addResourceManager(mSceneManager.get());
|
||||
addResourceManager(mImageManager.get());
|
||||
addResourceManager(mAnimBlendRulesManager.get());
|
||||
}
|
||||
|
||||
ResourceSystem::~ResourceSystem()
|
||||
|
@ -62,6 +65,11 @@ namespace Resource
|
|||
return mKeyframeManager.get();
|
||||
}
|
||||
|
||||
AnimBlendRulesManager* ResourceSystem::getAnimBlendRulesManager()
|
||||
{
|
||||
return mAnimBlendRulesManager.get();
|
||||
}
|
||||
|
||||
void ResourceSystem::setExpiryDelay(double expiryDelay)
|
||||
{
|
||||
for (std::vector<BaseResourceManager*>::iterator it = mResourceManagers.begin(); it != mResourceManagers.end();
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace Resource
|
|||
class NifFileManager;
|
||||
class KeyframeManager;
|
||||
class BaseResourceManager;
|
||||
class AnimBlendRulesManager;
|
||||
|
||||
/// @brief Wrapper class that constructs and provides access to the most commonly used resource subsystems.
|
||||
/// @par Resource subsystems can be used with multiple OpenGL contexts, just like the OSG equivalents, but
|
||||
|
@ -45,6 +46,7 @@ namespace Resource
|
|||
BgsmFileManager* getBgsmFileManager();
|
||||
NifFileManager* getNifFileManager();
|
||||
KeyframeManager* getKeyframeManager();
|
||||
AnimBlendRulesManager* getAnimBlendRulesManager();
|
||||
|
||||
/// Indicates to each resource manager to clear the cache, i.e. to drop cached objects that are no longer
|
||||
/// referenced.
|
||||
|
@ -79,6 +81,7 @@ namespace Resource
|
|||
std::unique_ptr<BgsmFileManager> mBgsmFileManager;
|
||||
std::unique_ptr<NifFileManager> mNifFileManager;
|
||||
std::unique_ptr<KeyframeManager> mKeyframeManager;
|
||||
std::unique_ptr<AnimBlendRulesManager> mAnimBlendRulesManager;
|
||||
|
||||
// Store the base classes separately to get convenient access to the common interface
|
||||
// Here users can register their own resourcemanager as well
|
||||
|
|
|
@ -93,6 +93,7 @@ namespace Resource
|
|||
"Terrain Chunk",
|
||||
"Terrain Texture",
|
||||
"Land",
|
||||
"Blending Rules",
|
||||
};
|
||||
|
||||
constexpr std::string_view cellPreloader[] = {
|
||||
|
|
170
components/sceneutil/animblendrules.cpp
Normal file
170
components/sceneutil/animblendrules.cpp
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
49
components/sceneutil/animblendrules.hpp
Normal file
49
components/sceneutil/animblendrules.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#ifndef OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP
|
||||
#define OPENMW_COMPONENTS_SCENEUTIL_ANIMBLENDRULES_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <osg/Object>
|
||||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
namespace SceneUtil
|
||||
{
|
||||
class AnimBlendRules : public osg::Object
|
||||
{
|
||||
public:
|
||||
struct BlendRule
|
||||
{
|
||||
std::string mFromGroup;
|
||||
std::string mFromKey;
|
||||
std::string mToGroup;
|
||||
std::string mToKey;
|
||||
float mDuration;
|
||||
std::string mEasing;
|
||||
};
|
||||
|
||||
AnimBlendRules() = default;
|
||||
AnimBlendRules(const std::vector<BlendRule>& rules);
|
||||
AnimBlendRules(const AnimBlendRules& copy, const osg::CopyOp& copyop);
|
||||
|
||||
META_Object(SceneUtil, AnimBlendRules)
|
||||
|
||||
void addOverrideRules(const AnimBlendRules& overrideRules);
|
||||
|
||||
std::optional<BlendRule> findBlendingRule(
|
||||
std::string fromGroup, std::string fromKey, std::string toGroup, std::string toKey) const;
|
||||
|
||||
const std::vector<BlendRule>& getRules() const { return mRules; }
|
||||
|
||||
static osg::ref_ptr<AnimBlendRules> fromFile(const VFS::Manager* vfs, VFS::Path::NormalizedView yamlpath);
|
||||
|
||||
private:
|
||||
std::vector<BlendRule> mRules;
|
||||
|
||||
inline bool fitsRuleString(const std::string_view str, const std::string_view ruleStr) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
|
@ -2,6 +2,7 @@
|
|||
#define OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include <osg/Object>
|
||||
|
||||
|
@ -21,8 +22,19 @@ namespace SceneUtil
|
|||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<float> mTime = std::make_shared<float>(0.0f);
|
||||
|
||||
struct KfTransform
|
||||
{
|
||||
std::optional<osg::Vec3f> mTranslation;
|
||||
std::optional<osg::Quat> mRotation;
|
||||
std::optional<float> mScale;
|
||||
};
|
||||
|
||||
virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); }
|
||||
|
||||
virtual KfTransform getCurrentTransformation(osg::NodeVisitor* nv) { return KfTransform(); };
|
||||
|
||||
/// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on.
|
||||
virtual osg::Callback* getAsCallback() = 0;
|
||||
};
|
||||
|
|
|
@ -187,19 +187,48 @@ namespace SceneUtil
|
|||
mgr->addWrapper(new GeometrySerializer);
|
||||
|
||||
// ignore the below for now to avoid warning spam
|
||||
const char* ignore[]
|
||||
= { "Debug::DebugDrawer", "MWRender::PtrHolder", "Resource::TemplateRef", "Resource::TemplateMultiRef",
|
||||
"SceneUtil::CompositeStateSetUpdater", "SceneUtil::UBOManager", "SceneUtil::LightListCallback",
|
||||
"SceneUtil::LightManagerUpdateCallback", "SceneUtil::FFPLightStateAttribute",
|
||||
"SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource",
|
||||
"SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder",
|
||||
"Shader::AddedState", "Shader::RemovedAlphaFunc", "NifOsg::FlipController",
|
||||
"NifOsg::KeyframeController", "NifOsg::Emitter", "NifOsg::ParticleColorAffector",
|
||||
"NifOsg::ParticleSystem", "NifOsg::GravityAffector", "NifOsg::ParticleBomb",
|
||||
"NifOsg::GrowFadeAffector", "NifOsg::InverseWorldMatrix", "NifOsg::StaticBoundingBoxCallback",
|
||||
"NifOsg::GeomMorpherController", "NifOsg::UpdateMorphGeometry", "NifOsg::UVController",
|
||||
"NifOsg::VisController", "osgMyGUI::Drawable", "osg::DrawCallback", "osg::UniformBufferObject",
|
||||
"osgOQ::ClearQueriesCallback", "osgOQ::RetrieveQueriesCallback", "osg::DummyObject" };
|
||||
const char* ignore[] = {
|
||||
"Debug::DebugDrawer",
|
||||
"MWRender::NifAnimBlendController",
|
||||
"MWRender::BoneAnimBlendController",
|
||||
"MWRender::BoneAnimBlendControllerWrapper",
|
||||
"MWRender::PtrHolder",
|
||||
"Resource::TemplateRef",
|
||||
"Resource::TemplateMultiRef",
|
||||
"SceneUtil::CompositeStateSetUpdater",
|
||||
"SceneUtil::UBOManager",
|
||||
"SceneUtil::LightListCallback",
|
||||
"SceneUtil::LightManagerUpdateCallback",
|
||||
"SceneUtil::FFPLightStateAttribute",
|
||||
"SceneUtil::UpdateRigBounds",
|
||||
"SceneUtil::UpdateRigGeometry",
|
||||
"SceneUtil::LightSource",
|
||||
"SceneUtil::DisableLight",
|
||||
"SceneUtil::MWShadowTechnique",
|
||||
"SceneUtil::TextKeyMapHolder",
|
||||
"Shader::AddedState",
|
||||
"Shader::RemovedAlphaFunc",
|
||||
"NifOsg::FlipController",
|
||||
"NifOsg::KeyframeController",
|
||||
"NifOsg::Emitter",
|
||||
"NifOsg::ParticleColorAffector",
|
||||
"NifOsg::ParticleSystem",
|
||||
"NifOsg::GravityAffector",
|
||||
"NifOsg::ParticleBomb",
|
||||
"NifOsg::GrowFadeAffector",
|
||||
"NifOsg::InverseWorldMatrix",
|
||||
"NifOsg::StaticBoundingBoxCallback",
|
||||
"NifOsg::GeomMorpherController",
|
||||
"NifOsg::UpdateMorphGeometry",
|
||||
"NifOsg::UVController",
|
||||
"NifOsg::VisController",
|
||||
"osgMyGUI::Drawable",
|
||||
"osg::DrawCallback",
|
||||
"osg::UniformBufferObject",
|
||||
"osgOQ::ClearQueriesCallback",
|
||||
"osgOQ::RetrieveQueriesCallback",
|
||||
"osg::DummyObject",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(ignore) / sizeof(ignore[0]); ++i)
|
||||
{
|
||||
mgr->addWrapper(makeDummySerializer(ignore[i]));
|
||||
|
|
|
@ -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",
|
||||
|
|
90
docs/source/reference/modding/animation-blending.rst
Normal file
90
docs/source/reference/modding/animation-blending.rst
Normal file
|
@ -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!
|
|
@ -276,6 +276,14 @@ Also it is possible to add a "Bip01 Arrow" bone to actor skeletons. In this case
|
|||
Such approach allows to implement better shooting animations (for example, beast races have tail, so quivers should be attached under different angle and
|
||||
default arrow fetching animation does not look good).
|
||||
|
||||
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.
|
||||
For more details see :doc:`animation-blending`.
|
||||
|
||||
Groundcover support
|
||||
-------------------
|
||||
|
||||
|
|
|
@ -31,5 +31,6 @@ about creating new content for OpenMW, please refer to
|
|||
doors-and-teleports
|
||||
custom-shader-effects
|
||||
extended
|
||||
animation-blending
|
||||
paths
|
||||
localisation
|
||||
|
|
|
@ -541,3 +541,14 @@ In third person, the camera will sway along with the movement animations of the
|
|||
Enabling this option disables this swaying by having the player character move independently of its animation.
|
||||
|
||||
This setting can be controlled in the Settings tab of the launcher.
|
||||
|
||||
smooth animation transitions
|
||||
----------------------------
|
||||
|
||||
:Type: boolean
|
||||
:Range: True/False
|
||||
:Default: False
|
||||
|
||||
Enabling this option uses smooth transitions between animations making them a lot less jarring. Also allows to load modded animation blending.
|
||||
|
||||
This setting can be controlled in the Settings tab of the launcher.
|
||||
|
|
|
@ -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
|
||||
|
|
69
files/data/animations/animation-config.yaml
Normal file
69
files/data/animations/animation-config.yaml
Normal file
|
@ -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
|
|
@ -698,6 +698,14 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source><html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Smooth Animation Transitions</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html></source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
|
@ -1427,5 +1427,13 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source>Browse…</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Smooth Animation Transitions</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -698,6 +698,14 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source><html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Anime l'utilisation d'objet magique, de façon similaire à l'utilisation des sorts.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></source>
|
||||
<translation><html><body><p>Lorsque cette option est désactivée, le moteur de jeu n'effectue aucune transition entre les différentes poses/animations.</p><p>Lorsque cette option est activée, le moteur de jeu adoucit la transition entre les différentes poses/animations.</p><p>Cette option prend en charge les fichiers de configuration YAML pour les transitions entre animations, ceux-ci peuvent être inclus avec les lots d'animations afin de configurer le type de transition entre les diverses animations fournies.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Smooth Animation Transitions</source>
|
||||
<translation>Adoucir la transition entre animations</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Cette option rend les mouvements des PNJ et du joueur plus souple. Recommandé si l'option "Se tourner en direction du mouvement" est activée.</p></body></html></translation>
|
||||
|
|
|
@ -772,6 +772,14 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source>Use Magic Item Animation</source>
|
||||
<translation>Анимации магических предметов</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Если настройка включена, она делает переходы между различными анимациями/позами намного глаже. Кроме того, она позволяет загружать YAML-файлы конфигураций смешивания анимаций, которые могут быть включены с анимациями, чтобы настроить стили смешивания.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Smooth Animation Transitions</source>
|
||||
<translation>Плавные переходы между анимациями</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Делает перемещение персонажей более плавным. Рекомендуется использовать совместно с настройкой "Поворот в направлении движения".</p></body></html></translation>
|
||||
|
|
|
@ -1446,5 +1446,13 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin
|
|||
<source>Run Script After Startup:</source>
|
||||
<translation>Kör skript efter uppstart:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Smooth Animation Transitions</source>
|
||||
<translation>Mjuka animationsövergångar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config YAML files that can be bundled with animations in order to customise blending styles.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Vid aktivering gör denna funktion att övergångarna mellan olika animationer och poser blir mycket mjukare. Funktionen gör det också möjligt att konfigurera animationsövergångarna i YAML-filer. Dessa filer kan buntas ihop tillsammans med nya animationsfiler.</p></body></html></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -62,6 +62,8 @@ def runTest(name):
|
|||
"resolution x = 640\n"
|
||||
"resolution y = 480\n"
|
||||
"framerate limit = 60\n"
|
||||
"[Game]\n"
|
||||
"smooth animation transitions = true\n"
|
||||
)
|
||||
stdout_lines = list()
|
||||
exit_ok = True
|
||||
|
|
Loading…
Reference in a new issue