mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-04-28 21:07:59 +03: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)
|
Cory F. Cohen (cfcohen)
|
||||||
Cris Mihalache (Mirceam)
|
Cris Mihalache (Mirceam)
|
||||||
crussell187
|
crussell187
|
||||||
|
Sam Hellawell (cykoder)
|
||||||
Dan Vukelich (sanchezman)
|
Dan Vukelich (sanchezman)
|
||||||
darkf
|
darkf
|
||||||
Dave Corley (S3ctor)
|
Dave Corley (S3ctor)
|
||||||
|
@ -144,6 +145,7 @@ Programmers
|
||||||
Łukasz Gołębiewski (lukago)
|
Łukasz Gołębiewski (lukago)
|
||||||
Lukasz Gromanowski (lgro)
|
Lukasz Gromanowski (lgro)
|
||||||
Mads Sandvei (Foal)
|
Mads Sandvei (Foal)
|
||||||
|
Maksim Eremenko (Max Yari)
|
||||||
Marc Bouvier (CramitDeFrog)
|
Marc Bouvier (CramitDeFrog)
|
||||||
Marcin Hulist (Gohan)
|
Marcin Hulist (Gohan)
|
||||||
Mark Siewert (mark76)
|
Mark Siewert (mark76)
|
||||||
|
|
|
@ -193,6 +193,7 @@
|
||||||
Feature #5492: Let rain and snow collide with statics
|
Feature #5492: Let rain and snow collide with statics
|
||||||
Feature #5926: Refraction based on water depth
|
Feature #5926: Refraction based on water depth
|
||||||
Feature #5944: Option to use camera as sound listener
|
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 #6152: Playing music via lua scripts
|
||||||
Feature #6188: Specular lighting from point light sources
|
Feature #6188: Specular lighting from point light sources
|
||||||
Feature #6411: Support translations in openmw-launcher
|
Feature #6411: Support translations in openmw-launcher
|
||||||
|
|
|
@ -189,6 +189,7 @@ bool Launcher::SettingsPage::loadSettings()
|
||||||
loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox);
|
loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox);
|
||||||
loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox);
|
loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox);
|
||||||
}
|
}
|
||||||
|
loadSettingBool(Settings::game().mSmoothAnimTransitions, *smoothAnimTransitionsCheckBox);
|
||||||
loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox);
|
loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox);
|
||||||
loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox);
|
loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox);
|
||||||
loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox);
|
loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox);
|
||||||
|
@ -394,6 +395,7 @@ void Launcher::SettingsPage::saveSettings()
|
||||||
saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing);
|
saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing);
|
||||||
saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing);
|
saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing);
|
||||||
saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection);
|
saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection);
|
||||||
|
saveSettingBool(*smoothAnimTransitionsCheckBox, Settings::game().mSmoothAnimTransitions);
|
||||||
saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement);
|
saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement);
|
||||||
saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation);
|
saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation);
|
||||||
|
|
||||||
|
|
|
@ -426,6 +426,16 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -27,7 +27,7 @@ add_openmw_dir (mwrender
|
||||||
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
|
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
|
||||||
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
|
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
|
||||||
postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples
|
postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples
|
||||||
actorutil distortion animationpriority bonegroup blendmask
|
actorutil distortion animationpriority bonegroup blendmask animblendcontroller
|
||||||
)
|
)
|
||||||
|
|
||||||
add_openmw_dir (mwinput
|
add_openmw_dir (mwinput
|
||||||
|
|
|
@ -13,8 +13,12 @@
|
||||||
#include <osgParticle/ParticleProcessor>
|
#include <osgParticle/ParticleProcessor>
|
||||||
#include <osgParticle/ParticleSystem>
|
#include <osgParticle/ParticleSystem>
|
||||||
|
|
||||||
|
#include <osgAnimation/Bone>
|
||||||
|
#include <osgAnimation/UpdateBone>
|
||||||
|
|
||||||
#include <components/debug/debuglog.hpp>
|
#include <components/debug/debuglog.hpp>
|
||||||
|
|
||||||
|
#include <components/resource/animblendrulesmanager.hpp>
|
||||||
#include <components/resource/keyframemanager.hpp>
|
#include <components/resource/keyframemanager.hpp>
|
||||||
#include <components/resource/scenemanager.hpp>
|
#include <components/resource/scenemanager.hpp>
|
||||||
|
|
||||||
|
@ -396,6 +400,60 @@ namespace
|
||||||
|
|
||||||
return lightModel;
|
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
|
namespace MWRender
|
||||||
|
@ -449,6 +507,8 @@ namespace MWRender
|
||||||
ControllerMap mControllerMap[sNumBlendMasks];
|
ControllerMap mControllerMap[sNumBlendMasks];
|
||||||
|
|
||||||
const SceneUtil::TextKeyMap& getTextKeys() const;
|
const SceneUtil::TextKeyMap& getTextKeys() const;
|
||||||
|
|
||||||
|
osg::ref_ptr<const SceneUtil::AnimBlendRules> mAnimBlendRules;
|
||||||
};
|
};
|
||||||
|
|
||||||
void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
|
void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
|
||||||
|
@ -606,7 +666,9 @@ namespace MWRender
|
||||||
for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
|
for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
|
||||||
{
|
{
|
||||||
if (Misc::getFileExtension(name) == "kf")
|
if (Misc::getFileExtension(name) == "kf")
|
||||||
|
{
|
||||||
addSingleAnimSource(name, baseModel);
|
addSingleAnimSource(name, baseModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,17 +685,18 @@ namespace MWRender
|
||||||
loadAllAnimationsInFolder(kfname, baseModel);
|
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))
|
if (!mResourceSystem->getVFS()->exists(kfname))
|
||||||
return;
|
return nullptr;
|
||||||
|
|
||||||
auto animsrc = std::make_shared<AnimSource>();
|
auto animsrc = std::make_shared<AnimSource>();
|
||||||
animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname);
|
animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname);
|
||||||
|
|
||||||
if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty()
|
if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty()
|
||||||
|| animsrc->mKeyframes->mKeyframeControllers.empty())
|
|| animsrc->mKeyframes->mKeyframeControllers.empty())
|
||||||
return;
|
return nullptr;
|
||||||
|
|
||||||
const NodeMap& nodeMap = getNodeMap();
|
const NodeMap& nodeMap = getNodeMap();
|
||||||
const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers;
|
const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers;
|
||||||
|
@ -661,7 +724,7 @@ namespace MWRender
|
||||||
animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned));
|
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())
|
for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups())
|
||||||
mSupportedAnimations.insert(group);
|
mSupportedAnimations.insert(group);
|
||||||
|
@ -693,6 +756,37 @@ namespace MWRender
|
||||||
break;
|
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()
|
void Animation::clearAnimSources()
|
||||||
|
@ -817,19 +911,23 @@ namespace MWRender
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimStateMap::iterator foundstateiter = mStates.find(groupname);
|
||||||
|
if (foundstateiter != mStates.end())
|
||||||
|
{
|
||||||
|
foundstateiter->second.mPriority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
AnimStateMap::iterator stateiter = mStates.begin();
|
AnimStateMap::iterator stateiter = mStates.begin();
|
||||||
while (stateiter != mStates.end())
|
while (stateiter != mStates.end())
|
||||||
{
|
{
|
||||||
if (stateiter->second.mPriority == priority)
|
if (stateiter->second.mPriority == priority && stateiter->first != groupname)
|
||||||
mStates.erase(stateiter++);
|
mStates.erase(stateiter++);
|
||||||
else
|
else
|
||||||
++stateiter;
|
++stateiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
stateiter = mStates.find(groupname);
|
if (foundstateiter != mStates.end())
|
||||||
if (stateiter != mStates.end())
|
|
||||||
{
|
{
|
||||||
stateiter->second.mPriority = priority;
|
|
||||||
resetActiveGroups();
|
resetActiveGroups();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -849,6 +947,8 @@ namespace MWRender
|
||||||
state.mPriority = priority;
|
state.mPriority = priority;
|
||||||
state.mBlendMask = blendMask;
|
state.mBlendMask = blendMask;
|
||||||
state.mAutoDisable = autodisable;
|
state.mAutoDisable = autodisable;
|
||||||
|
state.mGroupname = groupname;
|
||||||
|
state.mStartKey = start;
|
||||||
mStates[std::string{ groupname }] = state;
|
mStates[std::string{ groupname }] = state;
|
||||||
|
|
||||||
if (state.mPlaying)
|
if (state.mPlaying)
|
||||||
|
@ -981,6 +1081,48 @@ namespace MWRender
|
||||||
return mNodeMap;
|
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()
|
void Animation::resetActiveGroups()
|
||||||
{
|
{
|
||||||
// remove all previous external controllers from the scene graph
|
// remove all previous external controllers from the scene graph
|
||||||
|
@ -1004,7 +1146,7 @@ namespace MWRender
|
||||||
AnimStateMap::const_iterator state = mStates.begin();
|
AnimStateMap::const_iterator state = mStates.begin();
|
||||||
for (; state != mStates.end(); ++state)
|
for (; state != mStates.end(); ++state)
|
||||||
{
|
{
|
||||||
if (!(state->second.mBlendMask & (1 << blendMask)))
|
if (!state->second.blendMaskContains(blendMask))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (active == mStates.end()
|
if (active == mStates.end()
|
||||||
|
@ -1019,6 +1161,8 @@ namespace MWRender
|
||||||
if (active != mStates.end())
|
if (active != mStates.end())
|
||||||
{
|
{
|
||||||
std::shared_ptr<AnimSource> animsrc = active->second.mSource;
|
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();
|
for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin();
|
||||||
it != animsrc->mControllerMap[blendMask].end(); ++it)
|
it != animsrc->mControllerMap[blendMask].end(); ++it)
|
||||||
|
@ -1026,7 +1170,23 @@ namespace MWRender
|
||||||
osg::ref_ptr<osg::Node> node = getNodeMap().at(
|
osg::ref_ptr<osg::Node> node = getNodeMap().at(
|
||||||
it->first); // this should not throw, we already checked for the node existing in addAnimSource
|
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();
|
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);
|
node->addUpdateCallback(callback);
|
||||||
mActiveControllers.emplace_back(node, callback);
|
mActiveControllers.emplace_back(node, callback);
|
||||||
|
|
||||||
|
@ -1046,6 +1206,7 @@ namespace MWRender
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addControllers();
|
addControllers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1790,13 +1951,15 @@ namespace MWRender
|
||||||
osg::Callback* cb = node->getUpdateCallback();
|
osg::Callback* cb = node->getUpdateCallback();
|
||||||
while (cb)
|
while (cb)
|
||||||
{
|
{
|
||||||
if (dynamic_cast<SceneUtil::KeyframeController*>(cb))
|
if (dynamic_cast<NifAnimBlendController*>(cb) || dynamic_cast<BoneAnimBlendController*>(cb)
|
||||||
|
|| dynamic_cast<SceneUtil::KeyframeController*>(cb))
|
||||||
{
|
{
|
||||||
foundKeyframeCtrl = true;
|
foundKeyframeCtrl = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cb = cb->getNestedCallback();
|
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
|
// Without KeyframeController the orientation will not be reseted each frame, so
|
||||||
// RotateController shouldn't be used for such nodes.
|
// RotateController shouldn't be used for such nodes.
|
||||||
if (!foundKeyframeCtrl)
|
if (!foundKeyframeCtrl)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define GAME_RENDER_ANIMATION_H
|
#define GAME_RENDER_ANIMATION_H
|
||||||
|
|
||||||
#include "animationpriority.hpp"
|
#include "animationpriority.hpp"
|
||||||
|
#include "animblendcontroller.hpp"
|
||||||
#include "blendmask.hpp"
|
#include "blendmask.hpp"
|
||||||
#include "bonegroup.hpp"
|
#include "bonegroup.hpp"
|
||||||
|
|
||||||
|
@ -9,12 +10,15 @@
|
||||||
#include "../mwworld/ptr.hpp"
|
#include "../mwworld/ptr.hpp"
|
||||||
|
|
||||||
#include <components/misc/strings/algorithm.hpp>
|
#include <components/misc/strings/algorithm.hpp>
|
||||||
|
#include <components/sceneutil/animblendrules.hpp>
|
||||||
#include <components/sceneutil/controller.hpp>
|
#include <components/sceneutil/controller.hpp>
|
||||||
#include <components/sceneutil/nodecallback.hpp>
|
#include <components/sceneutil/nodecallback.hpp>
|
||||||
#include <components/sceneutil/textkeymap.hpp>
|
#include <components/sceneutil/textkeymap.hpp>
|
||||||
#include <components/sceneutil/util.hpp>
|
#include <components/sceneutil/util.hpp>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -47,6 +51,8 @@ namespace MWRender
|
||||||
class RotateController;
|
class RotateController;
|
||||||
class TransparencyUpdater;
|
class TransparencyUpdater;
|
||||||
|
|
||||||
|
using ActiveControllersVector = std::vector<std::pair<osg::ref_ptr<osg::Node>, osg::ref_ptr<osg::Callback>>>;
|
||||||
|
|
||||||
class EffectAnimationTime : public SceneUtil::ControllerSource
|
class EffectAnimationTime : public SceneUtil::ControllerSource
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
@ -158,9 +164,12 @@ namespace MWRender
|
||||||
int mBlendMask = 0;
|
int mBlendMask = 0;
|
||||||
bool mAutoDisable = true;
|
bool mAutoDisable = true;
|
||||||
|
|
||||||
|
std::string mGroupname;
|
||||||
|
std::string mStartKey;
|
||||||
|
|
||||||
float getTime() const { return *mTime; }
|
float getTime() const { return *mTime; }
|
||||||
void setTime(float time) { *mTime = time; }
|
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; }
|
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.
|
// 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.
|
// 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];
|
std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks];
|
||||||
|
|
||||||
|
@ -233,7 +246,9 @@ namespace MWRender
|
||||||
|
|
||||||
const NodeMap& getNodeMap() const;
|
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();
|
void resetActiveGroups();
|
||||||
|
|
||||||
|
@ -275,7 +290,7 @@ namespace MWRender
|
||||||
* @param baseModel The filename of the mObjectRoot, only used for error messages.
|
* @param baseModel The filename of the mObjectRoot, only used for error messages.
|
||||||
*/
|
*/
|
||||||
void addAnimSource(std::string_view model, const std::string& baseModel);
|
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. */
|
/** 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);
|
void addExtraLight(osg::ref_ptr<osg::Group> parent, const SceneUtil::LightCommon& light);
|
||||||
|
@ -291,6 +306,13 @@ namespace MWRender
|
||||||
|
|
||||||
void removeFromSceneImpl();
|
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:
|
public:
|
||||||
Animation(
|
Animation(
|
||||||
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem);
|
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);
|
void setAccumulation(const osg::Vec3f& accum);
|
||||||
|
|
||||||
/** Plays an animation.
|
/** Plays an animation.
|
||||||
|
* Creates or updates AnimationStates to represent and manage animation playback.
|
||||||
* \param groupname Name of the animation group to play.
|
* \param groupname Name of the animation group to play.
|
||||||
* \param priority Priority of the animation. The animation will play on
|
* \param priority Priority of the animation. The animation will play on
|
||||||
* bone groups that don't have another animation set of a
|
* bone groups that don't have another animation set of a
|
||||||
|
@ -491,6 +514,5 @@ namespace MWRender
|
||||||
private:
|
private:
|
||||||
double mStartingTime;
|
double mStartingTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif
|
#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 setOffset(const osg::Vec3f& offset);
|
||||||
void setRotate(const osg::Quat& rotate);
|
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);
|
void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -127,7 +127,7 @@ add_component_dir (vfs
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (resource
|
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
|
resourcemanager stats animation foreachbulletobject errormarker cachestats bgsmfilemanager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ add_component_dir (shader
|
||||||
add_component_dir (sceneutil
|
add_component_dir (sceneutil
|
||||||
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
|
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
|
||||||
lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer
|
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
|
screencapture depth color riggeometryosgaextension extradata unrefqueue lightcommon lightingmethod clearcolor
|
||||||
cullsafeboundsvisitor keyframe nodecallback textkeymap glextensions
|
cullsafeboundsvisitor keyframe nodecallback textkeymap glextensions
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,6 +52,17 @@ namespace Nif
|
||||||
return false;
|
return false;
|
||||||
return true;
|
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
|
struct NiTransform
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
#include <osg/TexMat>
|
#include <osg/TexMat>
|
||||||
#include <osg/Texture2D>
|
#include <osg/Texture2D>
|
||||||
|
|
||||||
|
#include <osgAnimation/Bone>
|
||||||
|
|
||||||
#include <osgParticle/Emitter>
|
#include <osgParticle/Emitter>
|
||||||
|
|
||||||
#include <components/nif/data.hpp>
|
#include <components/nif/data.hpp>
|
||||||
|
@ -175,25 +177,48 @@ namespace NifOsg
|
||||||
|
|
||||||
void KeyframeController::operator()(NifOsg::MatrixTransform* node, osg::NodeVisitor* nv)
|
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())
|
if (hasInput())
|
||||||
{
|
{
|
||||||
float time = getInputValue(nv);
|
float time = getInputValue(nv);
|
||||||
|
|
||||||
if (!mRotations.empty())
|
if (!mRotations.empty())
|
||||||
node->setRotation(mRotations.interpKey(time));
|
out.mRotation = mRotations.interpKey(time);
|
||||||
else if (!mXRotations.empty() || !mYRotations.empty() || !mZRotations.empty())
|
else if (!mXRotations.empty() || !mYRotations.empty() || !mZRotations.empty())
|
||||||
node->setRotation(getXYZRotation(time));
|
out.mRotation = getXYZRotation(time);
|
||||||
else
|
|
||||||
node->setRotation(node->mRotationScale);
|
|
||||||
|
|
||||||
if (!mTranslations.empty())
|
if (!mTranslations.empty())
|
||||||
node->setTranslation(mTranslations.interpKey(time));
|
out.mTranslation = mTranslations.interpKey(time);
|
||||||
|
|
||||||
if (!mScales.empty())
|
if (!mScales.empty())
|
||||||
node->setScale(mScales.interpKey(time));
|
out.mScale = mScales.interpKey(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
traverse(node, nv);
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
GeomMorpherController::GeomMorpherController() {}
|
GeomMorpherController::GeomMorpherController() {}
|
||||||
|
|
|
@ -238,6 +238,8 @@ namespace NifOsg
|
||||||
osg::Vec3f getTranslation(float time) const override;
|
osg::Vec3f getTranslation(float time) const override;
|
||||||
osg::Callback* getAsCallback() override { return this; }
|
osg::Callback* getAsCallback() override { return this; }
|
||||||
|
|
||||||
|
KfTransform getCurrentTransformation(osg::NodeVisitor* nv) override;
|
||||||
|
|
||||||
void operator()(NifOsg::MatrixTransform*, osg::NodeVisitor*);
|
void operator()(NifOsg::MatrixTransform*, osg::NodeVisitor*);
|
||||||
|
|
||||||
private:
|
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 <algorithm>
|
||||||
|
|
||||||
|
#include "animblendrulesmanager.hpp"
|
||||||
#include "bgsmfilemanager.hpp"
|
#include "bgsmfilemanager.hpp"
|
||||||
#include "imagemanager.hpp"
|
#include "imagemanager.hpp"
|
||||||
#include "keyframemanager.hpp"
|
#include "keyframemanager.hpp"
|
||||||
|
@ -21,6 +22,7 @@ namespace Resource
|
||||||
mSceneManager = std::make_unique<SceneManager>(
|
mSceneManager = std::make_unique<SceneManager>(
|
||||||
vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay);
|
vfs, mImageManager.get(), mNifFileManager.get(), mBgsmFileManager.get(), expiryDelay);
|
||||||
mKeyframeManager = std::make_unique<KeyframeManager>(vfs, mSceneManager.get(), expiryDelay, encoder);
|
mKeyframeManager = std::make_unique<KeyframeManager>(vfs, mSceneManager.get(), expiryDelay, encoder);
|
||||||
|
mAnimBlendRulesManager = std::make_unique<AnimBlendRulesManager>(vfs, expiryDelay);
|
||||||
|
|
||||||
addResourceManager(mNifFileManager.get());
|
addResourceManager(mNifFileManager.get());
|
||||||
addResourceManager(mBgsmFileManager.get());
|
addResourceManager(mBgsmFileManager.get());
|
||||||
|
@ -28,6 +30,7 @@ namespace Resource
|
||||||
// note, scene references images so add images afterwards for correct implementation of updateCache()
|
// note, scene references images so add images afterwards for correct implementation of updateCache()
|
||||||
addResourceManager(mSceneManager.get());
|
addResourceManager(mSceneManager.get());
|
||||||
addResourceManager(mImageManager.get());
|
addResourceManager(mImageManager.get());
|
||||||
|
addResourceManager(mAnimBlendRulesManager.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceSystem::~ResourceSystem()
|
ResourceSystem::~ResourceSystem()
|
||||||
|
@ -62,6 +65,11 @@ namespace Resource
|
||||||
return mKeyframeManager.get();
|
return mKeyframeManager.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimBlendRulesManager* ResourceSystem::getAnimBlendRulesManager()
|
||||||
|
{
|
||||||
|
return mAnimBlendRulesManager.get();
|
||||||
|
}
|
||||||
|
|
||||||
void ResourceSystem::setExpiryDelay(double expiryDelay)
|
void ResourceSystem::setExpiryDelay(double expiryDelay)
|
||||||
{
|
{
|
||||||
for (std::vector<BaseResourceManager*>::iterator it = mResourceManagers.begin(); it != mResourceManagers.end();
|
for (std::vector<BaseResourceManager*>::iterator it = mResourceManagers.begin(); it != mResourceManagers.end();
|
||||||
|
|
|
@ -29,6 +29,7 @@ namespace Resource
|
||||||
class NifFileManager;
|
class NifFileManager;
|
||||||
class KeyframeManager;
|
class KeyframeManager;
|
||||||
class BaseResourceManager;
|
class BaseResourceManager;
|
||||||
|
class AnimBlendRulesManager;
|
||||||
|
|
||||||
/// @brief Wrapper class that constructs and provides access to the most commonly used resource subsystems.
|
/// @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
|
/// @par Resource subsystems can be used with multiple OpenGL contexts, just like the OSG equivalents, but
|
||||||
|
@ -45,6 +46,7 @@ namespace Resource
|
||||||
BgsmFileManager* getBgsmFileManager();
|
BgsmFileManager* getBgsmFileManager();
|
||||||
NifFileManager* getNifFileManager();
|
NifFileManager* getNifFileManager();
|
||||||
KeyframeManager* getKeyframeManager();
|
KeyframeManager* getKeyframeManager();
|
||||||
|
AnimBlendRulesManager* getAnimBlendRulesManager();
|
||||||
|
|
||||||
/// Indicates to each resource manager to clear the cache, i.e. to drop cached objects that are no longer
|
/// Indicates to each resource manager to clear the cache, i.e. to drop cached objects that are no longer
|
||||||
/// referenced.
|
/// referenced.
|
||||||
|
@ -79,6 +81,7 @@ namespace Resource
|
||||||
std::unique_ptr<BgsmFileManager> mBgsmFileManager;
|
std::unique_ptr<BgsmFileManager> mBgsmFileManager;
|
||||||
std::unique_ptr<NifFileManager> mNifFileManager;
|
std::unique_ptr<NifFileManager> mNifFileManager;
|
||||||
std::unique_ptr<KeyframeManager> mKeyframeManager;
|
std::unique_ptr<KeyframeManager> mKeyframeManager;
|
||||||
|
std::unique_ptr<AnimBlendRulesManager> mAnimBlendRulesManager;
|
||||||
|
|
||||||
// Store the base classes separately to get convenient access to the common interface
|
// Store the base classes separately to get convenient access to the common interface
|
||||||
// Here users can register their own resourcemanager as well
|
// Here users can register their own resourcemanager as well
|
||||||
|
|
|
@ -93,6 +93,7 @@ namespace Resource
|
||||||
"Terrain Chunk",
|
"Terrain Chunk",
|
||||||
"Terrain Texture",
|
"Terrain Texture",
|
||||||
"Land",
|
"Land",
|
||||||
|
"Blending Rules",
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::string_view cellPreloader[] = {
|
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
|
#define OPENMW_COMPONENTS_SCENEUTIL_KEYFRAME_HPP
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include <osg/Object>
|
#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 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.
|
/// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on.
|
||||||
virtual osg::Callback* getAsCallback() = 0;
|
virtual osg::Callback* getAsCallback() = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -187,19 +187,48 @@ namespace SceneUtil
|
||||||
mgr->addWrapper(new GeometrySerializer);
|
mgr->addWrapper(new GeometrySerializer);
|
||||||
|
|
||||||
// ignore the below for now to avoid warning spam
|
// ignore the below for now to avoid warning spam
|
||||||
const char* ignore[]
|
const char* ignore[] = {
|
||||||
= { "Debug::DebugDrawer", "MWRender::PtrHolder", "Resource::TemplateRef", "Resource::TemplateMultiRef",
|
"Debug::DebugDrawer",
|
||||||
"SceneUtil::CompositeStateSetUpdater", "SceneUtil::UBOManager", "SceneUtil::LightListCallback",
|
"MWRender::NifAnimBlendController",
|
||||||
"SceneUtil::LightManagerUpdateCallback", "SceneUtil::FFPLightStateAttribute",
|
"MWRender::BoneAnimBlendController",
|
||||||
"SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource",
|
"MWRender::BoneAnimBlendControllerWrapper",
|
||||||
"SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder",
|
"MWRender::PtrHolder",
|
||||||
"Shader::AddedState", "Shader::RemovedAlphaFunc", "NifOsg::FlipController",
|
"Resource::TemplateRef",
|
||||||
"NifOsg::KeyframeController", "NifOsg::Emitter", "NifOsg::ParticleColorAffector",
|
"Resource::TemplateMultiRef",
|
||||||
"NifOsg::ParticleSystem", "NifOsg::GravityAffector", "NifOsg::ParticleBomb",
|
"SceneUtil::CompositeStateSetUpdater",
|
||||||
"NifOsg::GrowFadeAffector", "NifOsg::InverseWorldMatrix", "NifOsg::StaticBoundingBoxCallback",
|
"SceneUtil::UBOManager",
|
||||||
"NifOsg::GeomMorpherController", "NifOsg::UpdateMorphGeometry", "NifOsg::UVController",
|
"SceneUtil::LightListCallback",
|
||||||
"NifOsg::VisController", "osgMyGUI::Drawable", "osg::DrawCallback", "osg::UniformBufferObject",
|
"SceneUtil::LightManagerUpdateCallback",
|
||||||
"osgOQ::ClearQueriesCallback", "osgOQ::RetrieveQueriesCallback", "osg::DummyObject" };
|
"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)
|
for (size_t i = 0; i < sizeof(ignore) / sizeof(ignore[0]); ++i)
|
||||||
{
|
{
|
||||||
mgr->addWrapper(makeDummySerializer(ignore[i]));
|
mgr->addWrapper(makeDummySerializer(ignore[i]));
|
||||||
|
|
|
@ -39,6 +39,7 @@ namespace Settings
|
||||||
SettingValue<bool> mCanLootDuringDeathAnimation{ mIndex, "Game", "can loot during death animation" };
|
SettingValue<bool> mCanLootDuringDeathAnimation{ mIndex, "Game", "can loot during death animation" };
|
||||||
SettingValue<bool> mRebalanceSoulGemValues{ mIndex, "Game", "rebalance soul gem values" };
|
SettingValue<bool> mRebalanceSoulGemValues{ mIndex, "Game", "rebalance soul gem values" };
|
||||||
SettingValue<bool> mUseAdditionalAnimSources{ mIndex, "Game", "use additional anim sources" };
|
SettingValue<bool> mUseAdditionalAnimSources{ mIndex, "Game", "use additional anim sources" };
|
||||||
|
SettingValue<bool> mSmoothAnimTransitions{ mIndex, "Game", "smooth animation transitions" };
|
||||||
SettingValue<bool> mBarterDispositionChangeIsPermanent{ mIndex, "Game",
|
SettingValue<bool> mBarterDispositionChangeIsPermanent{ mIndex, "Game",
|
||||||
"barter disposition change is permanent" };
|
"barter disposition change is permanent" };
|
||||||
SettingValue<int> mStrengthInfluencesHandToHand{ mIndex, "Game", "strength influences hand to hand",
|
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
|
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).
|
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
|
Groundcover support
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -31,5 +31,6 @@ about creating new content for OpenMW, please refer to
|
||||||
doors-and-teleports
|
doors-and-teleports
|
||||||
custom-shader-effects
|
custom-shader-effects
|
||||||
extended
|
extended
|
||||||
|
animation-blending
|
||||||
paths
|
paths
|
||||||
localisation
|
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.
|
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.
|
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/MysticCards.omwfont
|
||||||
fonts/MysticCardsFontLicense.txt
|
fonts/MysticCardsFontLicense.txt
|
||||||
|
|
||||||
|
# Default animation blending config
|
||||||
|
animations/animation-config.yaml
|
||||||
|
|
||||||
# Month names and date formatting
|
# Month names and date formatting
|
||||||
l10n/Calendar/de.yaml
|
l10n/Calendar/de.yaml
|
||||||
l10n/Calendar/en.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>
|
<source><html><head/><body><p>Use casting animations for magic items, just as for spells.</p></body></html></source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</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>
|
<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>
|
<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>
|
<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>
|
<source>Browse…</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</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>
|
</context>
|
||||||
</TS>
|
</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>
|
<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>
|
<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>
|
||||||
|
<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>
|
<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>
|
<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>
|
<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>
|
<source>Use Magic Item Animation</source>
|
||||||
<translation>Анимации магических предметов</translation>
|
<translation>Анимации магических предметов</translation>
|
||||||
</message>
|
</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>
|
<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>
|
<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>
|
<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>
|
<source>Run Script After Startup:</source>
|
||||||
<translation>Kör skript efter uppstart:</translation>
|
<translation>Kör skript efter uppstart:</translation>
|
||||||
</message>
|
</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>
|
</context>
|
||||||
</TS>
|
</TS>
|
||||||
|
|
|
@ -284,6 +284,10 @@ rebalance soul gem values = false
|
||||||
# Allow to load per-group KF-files from Animations folder
|
# Allow to load per-group KF-files from Animations folder
|
||||||
use additional anim sources = false
|
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
|
# Make the disposition change of merchants caused by barter dealings permanent
|
||||||
barter disposition change is permanent = false
|
barter disposition change is permanent = false
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ def runTest(name):
|
||||||
"resolution x = 640\n"
|
"resolution x = 640\n"
|
||||||
"resolution y = 480\n"
|
"resolution y = 480\n"
|
||||||
"framerate limit = 60\n"
|
"framerate limit = 60\n"
|
||||||
|
"[Game]\n"
|
||||||
|
"smooth animation transitions = true\n"
|
||||||
)
|
)
|
||||||
stdout_lines = list()
|
stdout_lines = list()
|
||||||
exit_ok = True
|
exit_ok = True
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue