Skip to content

Commit

Permalink
Merge remote-tracking branch 'remotes/maxyari/animationblending' into…
Browse files Browse the repository at this point in the history
… animationblending
  • Loading branch information
dwing4g committed Jan 31, 2024
2 parents e9c567e + 70dc118 commit be07b6e
Show file tree
Hide file tree
Showing 32 changed files with 1,032 additions and 32 deletions.
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ Programmers
Łukasz Gołębiewski (lukago)
Lukasz Gromanowski (lgro)
Mads Sandvei (Foal)
Maksim Eremenko (Max Yari)
Marc Bouvier (CramitDeFrog)
Marcin Hulist (Gohan)
Mark Siewert (mark76)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
Feature #3537: Shader-based water ripples
Feature #5173: Support for NiFogProperty
Feature #5492: Let rain and snow collide with statics
Feature #6009: Animation blending - smooth animation transitions with modding support
Feature #6149: Dehardcode Lua API_REVISION
Feature #6152: Playing music via lua scripts
Feature #6188: Specular lighting from point light sources
Expand Down
2 changes: 2 additions & 0 deletions apps/launcher/settingspage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ bool Launcher::SettingsPage::loadSettings()
loadSettingBool(Settings::game().mWeaponSheathing, *weaponSheathingCheckBox);
loadSettingBool(Settings::game().mShieldSheathing, *shieldSheathingCheckBox);
}
loadSettingBool(Settings::game().mSmoothAnimTransitions, *smoothAnimTransitionsCheckBox);
loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox);
loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox);
loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox);
Expand Down Expand Up @@ -397,6 +398,7 @@ void Launcher::SettingsPage::saveSettings()
saveSettingBool(*weaponSheathingCheckBox, Settings::game().mWeaponSheathing);
saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing);
saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection);
saveSettingBool(*smoothAnimTransitionsCheckBox, Settings::game().mSmoothAnimTransitions);
saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement);
saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation);

Expand Down
10 changes: 10 additions & 0 deletions apps/launcher/ui/settingspage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,16 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="smoothAnimTransitionsCheckBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If enabled - makes transitions between different animations/poses much smoother. Also allows to load animation blending config .yaml files that can be bundled with the modded animations in order to customise blending styles.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Smooth animation transitions</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
Expand Down
2 changes: 1 addition & 1 deletion apps/openmw/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ set(GAME_HEADER
source_group(game FILES ${GAME} ${GAME_HEADER})

add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask
actors objects renderingmanager animation animblendcontroller rotatecontroller sky skyutil npcanimation esm4npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
Expand Down
105 changes: 93 additions & 12 deletions apps/openmw/mwrender/animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <components/debug/debuglog.hpp>

#include <components/resource/animblendrulesmanager.hpp>
#include <components/resource/keyframemanager.hpp>
#include <components/resource/scenemanager.hpp>

Expand Down Expand Up @@ -450,6 +451,8 @@ namespace MWRender
ControllerMap mControllerMap[sNumBlendMasks];

const SceneUtil::TextKeyMap& getTextKeys() const;

osg::ref_ptr<const SceneUtil::AnimBlendRules> mAnimBlendRules;
};

void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
Expand Down Expand Up @@ -605,8 +608,11 @@ namespace MWRender

for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
{

if (Misc::getFileExtension(name) == "kf")
{
addSingleAnimSource(name, baseModel);
}
}
}

Expand All @@ -623,17 +629,18 @@ namespace MWRender
loadAllAnimationsInFolder(kfname, baseModel);
}

void Animation::addSingleAnimSource(const std::string& kfname, const std::string& baseModel)
std::shared_ptr<Animation::AnimSource> Animation::addSingleAnimSource(
const std::string& kfname, const std::string& baseModel)
{
if (!mResourceSystem->getVFS()->exists(kfname))
return;
return nullptr;

auto animsrc = std::make_shared<AnimSource>();
animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname);

if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty()
|| animsrc->mKeyframes->mKeyframeControllers.empty())
return;
return nullptr;

const NodeMap& nodeMap = getNodeMap();
const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers;
Expand Down Expand Up @@ -661,7 +668,7 @@ namespace MWRender
animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned));
}

mAnimSources.push_back(std::move(animsrc));
mAnimSources.push_back(animsrc);

for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups())
mSupportedAnimations.insert(group);
Expand Down Expand Up @@ -693,6 +700,29 @@ namespace MWRender
break;
}
}

// Get the blending rules
if (Settings::game().mSmoothAnimTransitions)
{
// Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the
// manager will check for .json if it will not find a specified .yaml file.
auto yamlpath = kfname;
Misc::StringUtils::replaceLast(yamlpath, ".kf", ".yaml");

// globalBlendConfigPath is only used with actors! Objects have no default blending.
std::string_view globalBlendConfigPath = "animations/animation-config.yaml";

osg::ref_ptr<const SceneUtil::AnimBlendRules> blendRules;
if (mPtr.getClass().isActor())
blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(globalBlendConfigPath, yamlpath);
else
blendRules = mResourceSystem->getAnimBlendRulesManager()->getRules(yamlpath);

// At this point blendRules will either be nullptr or an AnimBlendRules instance with > 0 rules inside.
animsrc->mAnimBlendRules = blendRules;
}

return animsrc;
}

void Animation::clearAnimSources()
Expand Down Expand Up @@ -811,31 +841,40 @@ namespace MWRender
if (!mObjectRoot || mAnimSources.empty())
return;

// Log(Debug::Info) << "Please play: " << groupname << ":" << start << "..." << stop << " mask: " << blendMask;

if (groupname.empty())
{
resetActiveGroups();
return;
}

AnimStateMap::iterator foundstateiter = mStates.find(groupname);
if (foundstateiter != mStates.end())
{
foundstateiter->second.mPriority = priority;
}

AnimStateMap::iterator stateiter = mStates.begin();
while (stateiter != mStates.end())
{
if (stateiter->second.mPriority == priority)
if (stateiter->second.mPriority == priority && stateiter->first != groupname)
// This MIGH be a problem since we want old states to be still running so the AnimBlendingController can
// blend them properly
mStates.erase(stateiter++);
else
++stateiter;
}

stateiter = mStates.find(groupname);
if (stateiter != mStates.end())
if (foundstateiter != mStates.end())
{
stateiter->second.mPriority = priority;
resetActiveGroups();
return;
}

/* Look in reverse; last-inserted source has priority. */
AnimState state;

/* Look in reverse; last-inserted source has priority. */
AnimSourceList::reverse_iterator iter(mAnimSources.rbegin());
for (; iter != mAnimSources.rend(); ++iter)
{
Expand All @@ -849,6 +888,8 @@ namespace MWRender
state.mPriority = priority;
state.mBlendMask = blendMask;
state.mAutoDisable = autodisable;
state.mGroupname = groupname;
state.mStartKey = start;
mStates[std::string{ groupname }] = state;

if (state.mPlaying)
Expand Down Expand Up @@ -995,7 +1036,7 @@ namespace MWRender
AnimStateMap::const_iterator state = mStates.begin();
for (; state != mStates.end(); ++state)
{
if (!(state->second.mBlendMask & (1 << blendMask)))
if (!state->second.blendMaskContains(blendMask))
continue;

if (active == mStates.end()
Expand All @@ -1010,14 +1051,53 @@ namespace MWRender
if (active != mStates.end())
{
std::shared_ptr<AnimSource> animsrc = active->second.mSource;
AnimBlendController::AnimStateData stateData = active->second.asAnimStateData();

for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin();
it != animsrc->mControllerMap[blendMask].end(); ++it)
{
osg::ref_ptr<osg::Node> node = getNodeMap().at(
it->first); // this should not throw, we already checked for the node existing in addAnimSource

osg::Callback* callback = it->second->getAsCallback();
auto mtx = dynamic_cast<NifOsg::MatrixTransform*>(node.get());
osg::Callback* callback;

if (mtx && Settings::game().mSmoothAnimTransitions)
{
// Note: AnimBlendController currently works only with nifOsg::MatrixTransform. Due to
// the side-effect of RotationController (and probably RollController) applying undesired
// rotations to the osg::MatrixTransofrm - the AnimBlendController or KeyframeController need
// to have access to mRotation of NifOsg::MatrixTransform (which is unaffected by the
// side-effect) so they can use it to override the undesired rotations.
//
// For now if the node is not a NifOsg::MatrixTransform - animation will fallback
// to KeyframeControllers and won't do any blending

// Update an existing animation blending controller or create a new one
osg::ref_ptr<AnimBlendController> animController;

if (mAnimBlendControllers.contains(node))
{
animController = mAnimBlendControllers[node];
animController->setKeyframeTrack(it->second, stateData, animsrc->mAnimBlendRules);
}
else
{
animController = osg::ref_ptr<AnimBlendController>(
new AnimBlendController(it->second, stateData, animsrc->mAnimBlendRules));

mAnimBlendControllers[node] = animController;
}

it->second->mTime = active->second.mTime;

callback = animController->getAsCallback();
}
else
{
callback = it->second->getAsCallback();
}

node->addUpdateCallback(callback);
mActiveControllers.emplace_back(node, callback);

Expand Down Expand Up @@ -1779,13 +1859,14 @@ namespace MWRender
osg::Callback* cb = node->getUpdateCallback();
while (cb)
{
if (dynamic_cast<SceneUtil::KeyframeController*>(cb))
if (dynamic_cast<AnimBlendController*>(cb) || dynamic_cast<SceneUtil::KeyframeController*>(cb))
{
foundKeyframeCtrl = true;
break;
}
cb = cb->getNestedCallback();
}
// Note: AnimBlendController also does the reset so if one is present - we should add the rotation node
// Without KeyframeController the orientation will not be reseted each frame, so
// RotateController shouldn't be used for such nodes.
if (!foundKeyframeCtrl)
Expand Down
24 changes: 22 additions & 2 deletions apps/openmw/mwrender/animation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
#include "../mwworld/movementdirection.hpp"
#include "../mwworld/ptr.hpp"

#include "animblendcontroller.hpp"
#include <components/misc/strings/algorithm.hpp>
#include <components/sceneutil/animblendrules.hpp>
#include <components/sceneutil/controller.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/sceneutil/textkeymap.hpp>
#include <components/sceneutil/util.hpp>

#include <map>
#include <span>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
Expand Down Expand Up @@ -159,6 +163,9 @@ namespace MWRender
int mBlendMask;
bool mAutoDisable;

std::string mGroupname;
std::string mStartKey;

AnimState()
: mStartTime(0.0f)
, mLoopStartTime(0.0f)
Expand All @@ -178,9 +185,16 @@ namespace MWRender

float getTime() const { return *mTime; }
void setTime(float time) { *mTime = time; }
bool blendMaskContains(size_t blendMask) const { return (mBlendMask & (1 << blendMask)); }
AnimBlendController::AnimStateData asAnimStateData() const
{
AnimBlendController::AnimStateData stateData = { .mGroupname = mGroupname, .mStartKey = mStartKey };
return stateData;
}

bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; }
};

typedef std::map<std::string, AnimState, std::less<>> AnimStateMap;
AnimStateMap mStates;

Expand Down Expand Up @@ -208,6 +222,9 @@ namespace MWRender
// 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;

// Keep track of the animation controllers for easy access
std::map<osg::ref_ptr<osg::Node>, osg::ref_ptr<AnimBlendController>> mAnimBlendControllers;

std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks];

mutable NodeMap mNodeMap;
Expand Down Expand Up @@ -249,7 +266,9 @@ namespace MWRender

const NodeMap& getNodeMap() const;

/* Sets the appropriate animations on the bone groups based on priority.
/* Sets the appropriate animations on the bone groups based on priority by finding
* the highest priority AnimationStates and linking the appropriate controllers stored
* in the AnimationState to the corresponding nodes.
*/
void resetActiveGroups();

Expand Down Expand Up @@ -291,7 +310,7 @@ namespace MWRender
* @param baseModel The filename of the mObjectRoot, only used for error messages.
*/
void addAnimSource(std::string_view model, const std::string& baseModel);
void addSingleAnimSource(const std::string& model, const std::string& baseModel);
std::shared_ptr<AnimSource> addSingleAnimSource(const std::string& model, const std::string& baseModel);

/** Adds an additional light to the given node using the specified ESM record. */
void addExtraLight(osg::ref_ptr<osg::Group> parent, const SceneUtil::LightCommon& light);
Expand Down Expand Up @@ -359,6 +378,7 @@ namespace MWRender
void setAccumulation(const osg::Vec3f& accum);

/** Plays an animation.
* Creates or updates AnimationStates to represent and manage animation playback.
* \param groupname Name of the animation group to play.
* \param priority Priority of the animation. The animation will play on
* bone groups that don't have another animation set of a
Expand Down
Loading

0 comments on commit be07b6e

Please sign in to comment.