mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-26 18:35:20 +00:00
1933 lines
65 KiB
C++
1933 lines
65 KiB
C++
#include "animation.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <limits>
|
|
|
|
#include <osg/BlendFunc>
|
|
#include <osg/ColorMaski>
|
|
#include <osg/LightModel>
|
|
#include <osg/Material>
|
|
#include <osg/MatrixTransform>
|
|
#include <osg/Switch>
|
|
|
|
#include <osgParticle/ParticleProcessor>
|
|
#include <osgParticle/ParticleSystem>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
|
|
#include <components/resource/keyframemanager.hpp>
|
|
#include <components/resource/scenemanager.hpp>
|
|
|
|
#include <components/esm3/loadcont.hpp>
|
|
#include <components/esm3/loadcrea.hpp>
|
|
#include <components/esm3/loadmgef.hpp>
|
|
#include <components/esm3/loadnpc.hpp>
|
|
#include <components/esm3/loadrace.hpp>
|
|
#include <components/esm4/loadligh.hpp>
|
|
#include <components/misc/constants.hpp>
|
|
#include <components/misc/pathhelpers.hpp>
|
|
#include <components/misc/resourcehelpers.hpp>
|
|
#include <components/sceneutil/lightcommon.hpp>
|
|
|
|
#include <components/sceneutil/keyframe.hpp>
|
|
|
|
#include <components/vfs/manager.hpp>
|
|
|
|
#include <components/sceneutil/actorutil.hpp>
|
|
#include <components/sceneutil/lightmanager.hpp>
|
|
#include <components/sceneutil/lightutil.hpp>
|
|
#include <components/sceneutil/positionattitudetransform.hpp>
|
|
#include <components/sceneutil/skeleton.hpp>
|
|
#include <components/sceneutil/statesetupdater.hpp>
|
|
#include <components/sceneutil/util.hpp>
|
|
#include <components/sceneutil/visitor.hpp>
|
|
|
|
#include <components/settings/values.hpp>
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
#include "../mwworld/cellstore.hpp"
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/containerstore.hpp"
|
|
#include "../mwworld/esmstore.hpp"
|
|
|
|
#include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority
|
|
|
|
#include "rotatecontroller.hpp"
|
|
#include "util.hpp"
|
|
#include "vismask.hpp"
|
|
|
|
namespace
|
|
{
|
|
class MarkDrawablesVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
MarkDrawablesVisitor(osg::Node::NodeMask mask)
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
, mMask(mask)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Drawable& drawable) override { drawable.setNodeMask(mMask); }
|
|
|
|
private:
|
|
osg::Node::NodeMask mMask = 0;
|
|
};
|
|
|
|
/// Removes all particle systems and related nodes in a subgraph.
|
|
class RemoveParticlesVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
RemoveParticlesVisitor()
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Node& node) override
|
|
{
|
|
if (dynamic_cast<osgParticle::ParticleProcessor*>(&node))
|
|
mToRemove.emplace_back(&node);
|
|
|
|
traverse(node);
|
|
}
|
|
|
|
void apply(osg::Drawable& drw) override
|
|
{
|
|
if (osgParticle::ParticleSystem* partsys = dynamic_cast<osgParticle::ParticleSystem*>(&drw))
|
|
mToRemove.emplace_back(partsys);
|
|
}
|
|
|
|
void remove()
|
|
{
|
|
for (osg::Node* node : mToRemove)
|
|
{
|
|
// FIXME: a Drawable might have more than one parent
|
|
if (node->getNumParents())
|
|
node->getParent(0)->removeChild(node);
|
|
}
|
|
mToRemove.clear();
|
|
}
|
|
|
|
private:
|
|
std::vector<osg::ref_ptr<osg::Node>> mToRemove;
|
|
};
|
|
|
|
class DayNightCallback : public SceneUtil::NodeCallback<DayNightCallback, osg::Switch*>
|
|
{
|
|
public:
|
|
DayNightCallback()
|
|
: mCurrentState(0)
|
|
{
|
|
}
|
|
|
|
void operator()(osg::Switch* node, osg::NodeVisitor* nv)
|
|
{
|
|
unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode();
|
|
const unsigned int newState = node->getNumChildren() > state ? state : 0;
|
|
|
|
if (newState != mCurrentState)
|
|
{
|
|
mCurrentState = newState;
|
|
node->setSingleChildOn(mCurrentState);
|
|
}
|
|
|
|
traverse(node, nv);
|
|
}
|
|
|
|
private:
|
|
unsigned int mCurrentState;
|
|
};
|
|
|
|
class AddSwitchCallbacksVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
AddSwitchCallbacksVisitor()
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Switch& switchNode) override
|
|
{
|
|
if (switchNode.getName() == Constants::NightDayLabel)
|
|
switchNode.addUpdateCallback(new DayNightCallback());
|
|
|
|
traverse(switchNode);
|
|
}
|
|
};
|
|
|
|
class HarvestVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
HarvestVisitor()
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Switch& node) override
|
|
{
|
|
if (node.getName() == Constants::HerbalismLabel)
|
|
{
|
|
node.setSingleChildOn(1);
|
|
}
|
|
|
|
traverse(node);
|
|
}
|
|
};
|
|
|
|
bool equalsParts(std::string_view value, std::string_view s1, std::string_view s2, std::string_view s3 = {})
|
|
{
|
|
if (value.starts_with(s1))
|
|
{
|
|
value = value.substr(s1.size());
|
|
if (value.starts_with(s2))
|
|
return value.substr(s2.size()) == s3;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float calcAnimVelocity(const SceneUtil::TextKeyMap& keys, SceneUtil::KeyframeController* nonaccumctrl,
|
|
const osg::Vec3f& accum, std::string_view groupname)
|
|
{
|
|
float starttime = std::numeric_limits<float>::max();
|
|
float stoptime = 0.0f;
|
|
|
|
// Pick the last Loop Stop key and the last Loop Start key.
|
|
// This is required because of broken text keys in AshVampire.nif.
|
|
// It has *two* WalkForward: Loop Stop keys at different times, the first one is used for stopping playback
|
|
// but the animation velocity calculation uses the second one.
|
|
// As result the animation velocity calculation is not correct, and this incorrect velocity must be replicated,
|
|
// because otherwise the Creature's Speed (dagoth uthol) would not be sufficient to move fast enough.
|
|
auto keyiter = keys.rbegin();
|
|
while (keyiter != keys.rend())
|
|
{
|
|
if (equalsParts(keyiter->second, groupname, ": start")
|
|
|| equalsParts(keyiter->second, groupname, ": loop start"))
|
|
{
|
|
starttime = keyiter->first;
|
|
break;
|
|
}
|
|
++keyiter;
|
|
}
|
|
keyiter = keys.rbegin();
|
|
while (keyiter != keys.rend())
|
|
{
|
|
if (equalsParts(keyiter->second, groupname, ": stop"))
|
|
stoptime = keyiter->first;
|
|
else if (equalsParts(keyiter->second, groupname, ": loop stop"))
|
|
{
|
|
stoptime = keyiter->first;
|
|
break;
|
|
}
|
|
++keyiter;
|
|
}
|
|
|
|
if (stoptime > starttime)
|
|
{
|
|
osg::Vec3f startpos = osg::componentMultiply(nonaccumctrl->getTranslation(starttime), accum);
|
|
osg::Vec3f endpos = osg::componentMultiply(nonaccumctrl->getTranslation(stoptime), accum);
|
|
|
|
return (startpos - endpos).length() / (stoptime - starttime);
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
class GetExtendedBonesVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
GetExtendedBonesVisitor()
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Node& node) override
|
|
{
|
|
if (SceneUtil::hasUserDescription(&node, "CustomBone"))
|
|
{
|
|
mFoundBones.emplace_back(&node, node.getParent(0));
|
|
return;
|
|
}
|
|
|
|
traverse(node);
|
|
}
|
|
|
|
std::vector<std::pair<osg::Node*, osg::Group*>> mFoundBones;
|
|
};
|
|
|
|
class RemoveFinishedCallbackVisitor : public SceneUtil::RemoveVisitor
|
|
{
|
|
public:
|
|
bool mHasMagicEffects;
|
|
|
|
RemoveFinishedCallbackVisitor()
|
|
: RemoveVisitor()
|
|
, mHasMagicEffects(false)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Node& node) override { traverse(node); }
|
|
|
|
void apply(osg::Group& group) override
|
|
{
|
|
traverse(group);
|
|
|
|
osg::Callback* callback = group.getUpdateCallback();
|
|
if (callback)
|
|
{
|
|
// We should remove empty transformation nodes and finished callbacks here
|
|
MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast<MWRender::UpdateVfxCallback*>(callback);
|
|
if (vfxCallback)
|
|
{
|
|
if (vfxCallback->mFinished)
|
|
mToRemove.emplace_back(group.asNode(), group.getParent(0));
|
|
else
|
|
mHasMagicEffects = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void apply(osg::MatrixTransform& node) override { traverse(node); }
|
|
|
|
void apply(osg::Geometry&) override {}
|
|
};
|
|
|
|
class RemoveCallbackVisitor : public SceneUtil::RemoveVisitor
|
|
{
|
|
public:
|
|
bool mHasMagicEffects;
|
|
|
|
RemoveCallbackVisitor()
|
|
: RemoveVisitor()
|
|
, mHasMagicEffects(false)
|
|
, mEffectId(-1)
|
|
{
|
|
}
|
|
|
|
RemoveCallbackVisitor(int effectId)
|
|
: RemoveVisitor()
|
|
, mHasMagicEffects(false)
|
|
, mEffectId(effectId)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Node& node) override { traverse(node); }
|
|
|
|
void apply(osg::Group& group) override
|
|
{
|
|
traverse(group);
|
|
|
|
osg::Callback* callback = group.getUpdateCallback();
|
|
if (callback)
|
|
{
|
|
MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast<MWRender::UpdateVfxCallback*>(callback);
|
|
if (vfxCallback)
|
|
{
|
|
bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId;
|
|
if (toRemove)
|
|
mToRemove.emplace_back(group.asNode(), group.getParent(0));
|
|
else
|
|
mHasMagicEffects = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void apply(osg::MatrixTransform& node) override { traverse(node); }
|
|
|
|
void apply(osg::Geometry&) override {}
|
|
|
|
private:
|
|
int mEffectId;
|
|
};
|
|
|
|
class FindVfxCallbacksVisitor : public osg::NodeVisitor
|
|
{
|
|
public:
|
|
std::vector<MWRender::UpdateVfxCallback*> mCallbacks;
|
|
|
|
FindVfxCallbacksVisitor()
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
, mEffectId(-1)
|
|
{
|
|
}
|
|
|
|
FindVfxCallbacksVisitor(int effectId)
|
|
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
|
|
, mEffectId(effectId)
|
|
{
|
|
}
|
|
|
|
void apply(osg::Node& node) override { traverse(node); }
|
|
|
|
void apply(osg::Group& group) override
|
|
{
|
|
osg::Callback* callback = group.getUpdateCallback();
|
|
if (callback)
|
|
{
|
|
MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast<MWRender::UpdateVfxCallback*>(callback);
|
|
if (vfxCallback)
|
|
{
|
|
if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId)
|
|
{
|
|
mCallbacks.push_back(vfxCallback);
|
|
}
|
|
}
|
|
}
|
|
traverse(group);
|
|
}
|
|
|
|
void apply(osg::MatrixTransform& node) override { traverse(node); }
|
|
|
|
void apply(osg::Geometry&) override {}
|
|
|
|
private:
|
|
int mEffectId;
|
|
};
|
|
|
|
osg::ref_ptr<osg::LightModel> getVFXLightModelInstance()
|
|
{
|
|
static osg::ref_ptr<osg::LightModel> lightModel = nullptr;
|
|
|
|
if (!lightModel)
|
|
{
|
|
lightModel = new osg::LightModel;
|
|
lightModel->setAmbientIntensity({ 1, 1, 1, 1 });
|
|
}
|
|
|
|
return lightModel;
|
|
}
|
|
}
|
|
|
|
namespace MWRender
|
|
{
|
|
class TransparencyUpdater : public SceneUtil::StateSetUpdater
|
|
{
|
|
public:
|
|
TransparencyUpdater(const float alpha)
|
|
: mAlpha(alpha)
|
|
{
|
|
}
|
|
|
|
void setAlpha(const float alpha) { mAlpha = alpha; }
|
|
|
|
protected:
|
|
void setDefaults(osg::StateSet* stateset) override
|
|
{
|
|
osg::BlendFunc* blendfunc(new osg::BlendFunc);
|
|
stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
|
|
|
stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
|
|
stateset->setRenderBinMode(osg::StateSet::OVERRIDE_RENDERBIN_DETAILS);
|
|
|
|
// FIXME: overriding diffuse/ambient/emissive colors
|
|
osg::Material* material = new osg::Material;
|
|
material->setColorMode(osg::Material::OFF);
|
|
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, mAlpha));
|
|
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1, 1, 1, 1));
|
|
stateset->setAttributeAndModes(material, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
|
stateset->addUniform(
|
|
new osg::Uniform("colorMode", 0), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
|
}
|
|
|
|
void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override
|
|
{
|
|
osg::Material* material
|
|
= static_cast<osg::Material*>(stateset->getAttribute(osg::StateAttribute::MATERIAL));
|
|
material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha);
|
|
}
|
|
|
|
private:
|
|
float mAlpha;
|
|
};
|
|
|
|
struct Animation::AnimSource
|
|
{
|
|
osg::ref_ptr<const SceneUtil::KeyframeHolder> mKeyframes;
|
|
|
|
typedef std::map<std::string, osg::ref_ptr<SceneUtil::KeyframeController>> ControllerMap;
|
|
|
|
ControllerMap mControllerMap[Animation::sNumBlendMasks];
|
|
|
|
const SceneUtil::TextKeyMap& getTextKeys() const;
|
|
};
|
|
|
|
void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
|
|
{
|
|
traverse(node, nv);
|
|
|
|
if (mFinished)
|
|
return;
|
|
|
|
double newTime = nv->getFrameStamp()->getSimulationTime();
|
|
if (mStartingTime == 0)
|
|
{
|
|
mStartingTime = newTime;
|
|
return;
|
|
}
|
|
|
|
double duration = newTime - mStartingTime;
|
|
mStartingTime = newTime;
|
|
|
|
mParams.mAnimTime->addTime(duration);
|
|
if (mParams.mAnimTime->getTime() >= mParams.mMaxControllerLength)
|
|
{
|
|
if (mParams.mLoop)
|
|
{
|
|
// Start from the beginning again; carry over the remainder
|
|
// Not sure if this is actually needed, the controller function might already handle loops
|
|
float remainder = mParams.mAnimTime->getTime() - mParams.mMaxControllerLength;
|
|
mParams.mAnimTime->resetTime(remainder);
|
|
}
|
|
else
|
|
{
|
|
// Hide effect immediately
|
|
node->setNodeMask(0);
|
|
mFinished = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ResetAccumRootCallback : public SceneUtil::NodeCallback<ResetAccumRootCallback, osg::MatrixTransform*>
|
|
{
|
|
public:
|
|
void operator()(osg::MatrixTransform* transform, osg::NodeVisitor* nv)
|
|
{
|
|
osg::Matrix mat = transform->getMatrix();
|
|
osg::Vec3f position = mat.getTrans();
|
|
position = osg::componentMultiply(mResetAxes, position);
|
|
mat.setTrans(position);
|
|
transform->setMatrix(mat);
|
|
|
|
traverse(transform, nv);
|
|
}
|
|
|
|
void setAccumulate(const osg::Vec3f& accumulate)
|
|
{
|
|
// anything that accumulates (1.f) should be reset in the callback to (0.f)
|
|
mResetAxes.x() = accumulate.x() != 0.f ? 0.f : 1.f;
|
|
mResetAxes.y() = accumulate.y() != 0.f ? 0.f : 1.f;
|
|
mResetAxes.z() = accumulate.z() != 0.f ? 0.f : 1.f;
|
|
}
|
|
|
|
private:
|
|
osg::Vec3f mResetAxes;
|
|
};
|
|
|
|
Animation::Animation(
|
|
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem)
|
|
: mInsert(std::move(parentNode))
|
|
, mSkeleton(nullptr)
|
|
, mNodeMapCreated(false)
|
|
, mPtr(ptr)
|
|
, mResourceSystem(resourceSystem)
|
|
, mAccumulate(1.f, 1.f, 0.f)
|
|
, mTextKeyListener(nullptr)
|
|
, mHeadYawRadians(0.f)
|
|
, mHeadPitchRadians(0.f)
|
|
, mUpperBodyYawRadians(0.f)
|
|
, mLegsYawRadians(0.f)
|
|
, mBodyPitchRadians(0.f)
|
|
, mHasMagicEffects(false)
|
|
, mAlpha(1.f)
|
|
{
|
|
for (size_t i = 0; i < sNumBlendMasks; i++)
|
|
mAnimationTimePtr[i] = std::make_shared<AnimationTime>();
|
|
|
|
mLightListCallback = new SceneUtil::LightListCallback;
|
|
}
|
|
|
|
Animation::~Animation()
|
|
{
|
|
removeFromSceneImpl();
|
|
}
|
|
|
|
void Animation::setActive(int active)
|
|
{
|
|
if (mSkeleton)
|
|
mSkeleton->setActive(static_cast<SceneUtil::Skeleton::ActiveType>(active));
|
|
}
|
|
|
|
void Animation::updatePtr(const MWWorld::Ptr& ptr)
|
|
{
|
|
mPtr = ptr;
|
|
}
|
|
|
|
void Animation::setAccumulation(const osg::Vec3f& accum)
|
|
{
|
|
mAccumulate = accum;
|
|
|
|
if (mResetAccumRootCallback)
|
|
mResetAccumRootCallback->setAccumulate(mAccumulate);
|
|
}
|
|
|
|
// controllerName is used for Collada animated deforming models
|
|
size_t Animation::detectBlendMask(const osg::Node* node, const std::string& controllerName) const
|
|
{
|
|
static const std::string_view sBlendMaskRoots[sNumBlendMasks] = {
|
|
"", /* Lower body / character root */
|
|
"Bip01 Spine1", /* Torso */
|
|
"Bip01 L Clavicle", /* Left arm */
|
|
"Bip01 R Clavicle", /* Right arm */
|
|
};
|
|
|
|
while (node != mObjectRoot)
|
|
{
|
|
const std::string& name = node->getName();
|
|
for (size_t i = 1; i < sNumBlendMasks; i++)
|
|
{
|
|
if (name == sBlendMaskRoots[i] || controllerName == sBlendMaskRoots[i])
|
|
return i;
|
|
}
|
|
|
|
assert(node->getNumParents() > 0);
|
|
|
|
node = node->getParent(0);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
const SceneUtil::TextKeyMap& Animation::AnimSource::getTextKeys() const
|
|
{
|
|
return mKeyframes->mTextKeys;
|
|
}
|
|
|
|
void Animation::loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel)
|
|
{
|
|
std::string animationPath = model;
|
|
if (animationPath.find("meshes") == 0)
|
|
{
|
|
animationPath.replace(0, 6, "animations");
|
|
}
|
|
animationPath.replace(animationPath.size() - 3, 3, "/");
|
|
|
|
for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
|
|
{
|
|
if (Misc::getFileExtension(name) == "kf")
|
|
addSingleAnimSource(name, baseModel);
|
|
}
|
|
}
|
|
|
|
void Animation::addAnimSource(std::string_view model, const std::string& baseModel)
|
|
{
|
|
std::string kfname = Misc::StringUtils::lowerCase(model);
|
|
|
|
if (kfname.ends_with(".nif"))
|
|
kfname.replace(kfname.size() - 4, 4, ".kf");
|
|
|
|
addSingleAnimSource(kfname, baseModel);
|
|
|
|
if (Settings::game().mUseAdditionalAnimSources)
|
|
loadAllAnimationsInFolder(kfname, baseModel);
|
|
}
|
|
|
|
void Animation::addSingleAnimSource(const std::string& kfname, const std::string& baseModel)
|
|
{
|
|
if (!mResourceSystem->getVFS()->exists(kfname))
|
|
return;
|
|
|
|
auto animsrc = std::make_shared<AnimSource>();
|
|
animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(kfname);
|
|
|
|
if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty()
|
|
|| animsrc->mKeyframes->mKeyframeControllers.empty())
|
|
return;
|
|
|
|
const NodeMap& nodeMap = getNodeMap();
|
|
const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers;
|
|
for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin();
|
|
it != controllerMap.end(); ++it)
|
|
{
|
|
std::string bonename = Misc::StringUtils::lowerCase(it->first);
|
|
NodeMap::const_iterator found = nodeMap.find(bonename);
|
|
if (found == nodeMap.end())
|
|
{
|
|
Log(Debug::Warning) << "Warning: addAnimSource: can't find bone '" + bonename << "' in " << baseModel
|
|
<< " (referenced by " << kfname << ")";
|
|
continue;
|
|
}
|
|
|
|
osg::Node* node = found->second;
|
|
|
|
size_t blendMask = detectBlendMask(node, it->second->getName());
|
|
|
|
// clone the controller, because each Animation needs its own ControllerSource
|
|
osg::ref_ptr<SceneUtil::KeyframeController> cloned
|
|
= osg::clone(it->second.get(), osg::CopyOp::SHALLOW_COPY);
|
|
cloned->setSource(mAnimationTimePtr[blendMask]);
|
|
|
|
animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned));
|
|
}
|
|
|
|
mAnimSources.push_back(std::move(animsrc));
|
|
|
|
for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups())
|
|
mSupportedAnimations.insert(group);
|
|
|
|
SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]);
|
|
mObjectRoot->accept(assignVisitor);
|
|
|
|
// Determine the movement accumulation bone if necessary
|
|
if (!mAccumRoot)
|
|
{
|
|
// Priority matters! bip01 is preferred.
|
|
static const std::initializer_list<std::string_view> accumRootNames = { "bip01", "root bone" };
|
|
NodeMap::const_iterator found = nodeMap.end();
|
|
for (const std::string_view& name : accumRootNames)
|
|
{
|
|
found = nodeMap.find(name);
|
|
if (found == nodeMap.end())
|
|
continue;
|
|
for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin();
|
|
it != controllerMap.end(); ++it)
|
|
{
|
|
if (Misc::StringUtils::ciEqual(it->first, name))
|
|
{
|
|
mAccumRoot = found->second;
|
|
break;
|
|
}
|
|
}
|
|
if (mAccumRoot)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Animation::clearAnimSources()
|
|
{
|
|
mStates.clear();
|
|
|
|
for (size_t i = 0; i < sNumBlendMasks; i++)
|
|
mAnimationTimePtr[i]->setTimePtr(std::shared_ptr<float>());
|
|
|
|
mAccumCtrl = nullptr;
|
|
|
|
mSupportedAnimations.clear();
|
|
mAnimSources.clear();
|
|
|
|
mAnimVelocities.clear();
|
|
}
|
|
|
|
bool Animation::hasAnimation(std::string_view anim) const
|
|
{
|
|
return mSupportedAnimations.find(anim) != mSupportedAnimations.end();
|
|
}
|
|
|
|
float Animation::getStartTime(const std::string& groupname) const
|
|
{
|
|
for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter)
|
|
{
|
|
const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys();
|
|
|
|
const auto found = keys.findGroupStart(groupname);
|
|
if (found != keys.end())
|
|
return found->first;
|
|
}
|
|
return -1.f;
|
|
}
|
|
|
|
float Animation::getTextKeyTime(std::string_view textKey) const
|
|
{
|
|
for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter)
|
|
{
|
|
const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys();
|
|
|
|
for (auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey)
|
|
{
|
|
if (iterKey->second.starts_with(textKey))
|
|
return iterKey->first;
|
|
}
|
|
}
|
|
|
|
return -1.f;
|
|
}
|
|
|
|
void Animation::handleTextKey(AnimState& state, std::string_view groupname,
|
|
SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map)
|
|
{
|
|
std::string_view evt = key->second;
|
|
|
|
if (evt.starts_with(groupname) && evt.substr(groupname.size()).starts_with(": "))
|
|
{
|
|
size_t off = groupname.size() + 2;
|
|
if (evt.substr(off) == "loop start")
|
|
state.mLoopStartTime = key->first;
|
|
else if (evt.substr(off) == "loop stop")
|
|
state.mLoopStopTime = key->first;
|
|
}
|
|
|
|
if (mTextKeyListener)
|
|
{
|
|
try
|
|
{
|
|
mTextKeyListener->handleTextKey(groupname, key, map);
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Animation::play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable,
|
|
float speedmult, std::string_view start, std::string_view stop, float startpoint, size_t loops,
|
|
bool loopfallback)
|
|
{
|
|
if (!mObjectRoot || mAnimSources.empty())
|
|
return;
|
|
|
|
if (groupname.empty())
|
|
{
|
|
resetActiveGroups();
|
|
return;
|
|
}
|
|
|
|
AnimStateMap::iterator stateiter = mStates.begin();
|
|
while (stateiter != mStates.end())
|
|
{
|
|
if (stateiter->second.mPriority == priority)
|
|
mStates.erase(stateiter++);
|
|
else
|
|
++stateiter;
|
|
}
|
|
|
|
stateiter = mStates.find(groupname);
|
|
if (stateiter != mStates.end())
|
|
{
|
|
stateiter->second.mPriority = priority;
|
|
resetActiveGroups();
|
|
return;
|
|
}
|
|
|
|
/* Look in reverse; last-inserted source has priority. */
|
|
AnimState state;
|
|
AnimSourceList::reverse_iterator iter(mAnimSources.rbegin());
|
|
for (; iter != mAnimSources.rend(); ++iter)
|
|
{
|
|
const SceneUtil::TextKeyMap& textkeys = (*iter)->getTextKeys();
|
|
if (reset(state, textkeys, groupname, start, stop, startpoint, loopfallback))
|
|
{
|
|
state.mSource = *iter;
|
|
state.mSpeedMult = speedmult;
|
|
state.mLoopCount = loops;
|
|
state.mPlaying = (state.getTime() < state.mStopTime);
|
|
state.mPriority = priority;
|
|
state.mBlendMask = blendMask;
|
|
state.mAutoDisable = autodisable;
|
|
mStates[std::string{ groupname }] = state;
|
|
|
|
if (state.mPlaying)
|
|
{
|
|
auto textkey = textkeys.lowerBound(state.getTime());
|
|
while (textkey != textkeys.end() && textkey->first <= state.getTime())
|
|
{
|
|
handleTextKey(state, groupname, textkey, textkeys);
|
|
++textkey;
|
|
}
|
|
}
|
|
|
|
if (state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0)
|
|
{
|
|
state.mLoopCount--;
|
|
state.setTime(state.mLoopStartTime);
|
|
state.mPlaying = true;
|
|
if (state.getTime() >= state.mLoopStopTime)
|
|
break;
|
|
|
|
auto textkey = textkeys.lowerBound(state.getTime());
|
|
while (textkey != textkeys.end() && textkey->first <= state.getTime())
|
|
{
|
|
handleTextKey(state, groupname, textkey, textkeys);
|
|
++textkey;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
resetActiveGroups();
|
|
}
|
|
|
|
bool Animation::reset(AnimState& state, const SceneUtil::TextKeyMap& keys, std::string_view groupname,
|
|
std::string_view start, std::string_view stop, float startpoint, bool loopfallback)
|
|
{
|
|
// Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two
|
|
// separate walkforward keys, and the last one is supposed to be used.
|
|
auto groupend = keys.rbegin();
|
|
for (; groupend != keys.rend(); ++groupend)
|
|
{
|
|
if (groupend->second.starts_with(groupname) && groupend->second.compare(groupname.size(), 2, ": ") == 0)
|
|
break;
|
|
}
|
|
|
|
auto startkey = groupend;
|
|
while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": ", start))
|
|
++startkey;
|
|
if (startkey == keys.rend() && start == "loop start")
|
|
{
|
|
startkey = groupend;
|
|
while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": start"))
|
|
++startkey;
|
|
}
|
|
if (startkey == keys.rend())
|
|
return false;
|
|
|
|
auto stopkey = groupend;
|
|
std::size_t checkLength = groupname.size() + 2 + stop.size();
|
|
while (stopkey != keys.rend()
|
|
// We have to ignore extra garbage at the end.
|
|
// The Scrib's idle3 animation has "Idle3: Stop." instead of "Idle3: Stop".
|
|
// Why, just why? :(
|
|
&& !equalsParts(std::string_view{ stopkey->second }.substr(0, checkLength), groupname, ": ", stop))
|
|
++stopkey;
|
|
if (stopkey == keys.rend())
|
|
return false;
|
|
|
|
if (startkey->first > stopkey->first)
|
|
return false;
|
|
|
|
state.mStartTime = startkey->first;
|
|
if (loopfallback)
|
|
{
|
|
state.mLoopStartTime = startkey->first;
|
|
state.mLoopStopTime = stopkey->first;
|
|
}
|
|
else
|
|
{
|
|
state.mLoopStartTime = startkey->first;
|
|
state.mLoopStopTime = std::numeric_limits<float>::max();
|
|
}
|
|
state.mStopTime = stopkey->first;
|
|
|
|
state.setTime(state.mStartTime + ((state.mStopTime - state.mStartTime) * startpoint));
|
|
|
|
// mLoopStartTime and mLoopStopTime normally get assigned when encountering these keys while playing the
|
|
// animation (see handleTextKey). But if startpoint is already past these keys, or start time is == stop time,
|
|
// we need to assign them now.
|
|
|
|
auto key = groupend;
|
|
for (; key != startkey && key != keys.rend(); ++key)
|
|
{
|
|
if (key->first > state.getTime())
|
|
continue;
|
|
|
|
if (equalsParts(key->second, groupname, ": loop start"))
|
|
state.mLoopStartTime = key->first;
|
|
else if (equalsParts(key->second, groupname, ": loop stop"))
|
|
state.mLoopStopTime = key->first;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Animation::setTextKeyListener(Animation::TextKeyListener* listener)
|
|
{
|
|
mTextKeyListener = listener;
|
|
}
|
|
|
|
const Animation::NodeMap& Animation::getNodeMap() const
|
|
{
|
|
if (!mNodeMapCreated && mObjectRoot)
|
|
{
|
|
SceneUtil::NodeMapVisitor visitor(mNodeMap);
|
|
mObjectRoot->accept(visitor);
|
|
mNodeMapCreated = true;
|
|
}
|
|
return mNodeMap;
|
|
}
|
|
|
|
void Animation::resetActiveGroups()
|
|
{
|
|
// remove all previous external controllers from the scene graph
|
|
for (auto it = mActiveControllers.begin(); it != mActiveControllers.end(); ++it)
|
|
{
|
|
osg::Node* node = it->first;
|
|
node->removeUpdateCallback(it->second);
|
|
|
|
// Should be no longer needed with OSG 3.4
|
|
it->second->setNestedCallback(nullptr);
|
|
}
|
|
|
|
mActiveControllers.clear();
|
|
|
|
mAccumCtrl = nullptr;
|
|
|
|
for (size_t blendMask = 0; blendMask < sNumBlendMasks; blendMask++)
|
|
{
|
|
AnimStateMap::const_iterator active = mStates.end();
|
|
|
|
AnimStateMap::const_iterator state = mStates.begin();
|
|
for (; state != mStates.end(); ++state)
|
|
{
|
|
if (!(state->second.mBlendMask & (1 << blendMask)))
|
|
continue;
|
|
|
|
if (active == mStates.end()
|
|
|| active->second.mPriority[(BoneGroup)blendMask] < state->second.mPriority[(BoneGroup)blendMask])
|
|
active = state;
|
|
}
|
|
|
|
mAnimationTimePtr[blendMask]->setTimePtr(
|
|
active == mStates.end() ? std::shared_ptr<float>() : active->second.mTime);
|
|
|
|
// add external controllers for the AnimSource active in this blend mask
|
|
if (active != mStates.end())
|
|
{
|
|
std::shared_ptr<AnimSource> animsrc = active->second.mSource;
|
|
|
|
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();
|
|
node->addUpdateCallback(callback);
|
|
mActiveControllers.emplace_back(node, callback);
|
|
|
|
if (blendMask == 0 && node == mAccumRoot)
|
|
{
|
|
mAccumCtrl = it->second;
|
|
|
|
// make sure reset is last in the chain of callbacks
|
|
if (!mResetAccumRootCallback)
|
|
{
|
|
mResetAccumRootCallback = new ResetAccumRootCallback;
|
|
mResetAccumRootCallback->setAccumulate(mAccumulate);
|
|
}
|
|
mAccumRoot->addUpdateCallback(mResetAccumRootCallback);
|
|
mActiveControllers.emplace_back(mAccumRoot, mResetAccumRootCallback);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addControllers();
|
|
}
|
|
|
|
void Animation::adjustSpeedMult(const std::string& groupname, float speedmult)
|
|
{
|
|
AnimStateMap::iterator state(mStates.find(groupname));
|
|
if (state != mStates.end())
|
|
state->second.mSpeedMult = speedmult;
|
|
}
|
|
|
|
bool Animation::isPlaying(std::string_view groupname) const
|
|
{
|
|
AnimStateMap::const_iterator state(mStates.find(groupname));
|
|
if (state != mStates.end())
|
|
return state->second.mPlaying;
|
|
return false;
|
|
}
|
|
|
|
bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult) const
|
|
{
|
|
AnimStateMap::const_iterator iter = mStates.find(groupname);
|
|
if (iter == mStates.end())
|
|
{
|
|
if (complete)
|
|
*complete = 0.0f;
|
|
if (speedmult)
|
|
*speedmult = 0.0f;
|
|
return false;
|
|
}
|
|
|
|
if (complete)
|
|
{
|
|
if (iter->second.mStopTime > iter->second.mStartTime)
|
|
*complete = (iter->second.getTime() - iter->second.mStartTime)
|
|
/ (iter->second.mStopTime - iter->second.mStartTime);
|
|
else
|
|
*complete = (iter->second.mPlaying ? 0.0f : 1.0f);
|
|
}
|
|
if (speedmult)
|
|
*speedmult = iter->second.mSpeedMult;
|
|
return true;
|
|
}
|
|
|
|
float Animation::getCurrentTime(const std::string& groupname) const
|
|
{
|
|
AnimStateMap::const_iterator iter = mStates.find(groupname);
|
|
if (iter == mStates.end())
|
|
return -1.f;
|
|
|
|
return iter->second.getTime();
|
|
}
|
|
|
|
size_t Animation::getCurrentLoopCount(const std::string& groupname) const
|
|
{
|
|
AnimStateMap::const_iterator iter = mStates.find(groupname);
|
|
if (iter == mStates.end())
|
|
return 0;
|
|
|
|
return iter->second.mLoopCount;
|
|
}
|
|
|
|
void Animation::disable(std::string_view groupname)
|
|
{
|
|
AnimStateMap::iterator iter = mStates.find(groupname);
|
|
if (iter != mStates.end())
|
|
mStates.erase(iter);
|
|
resetActiveGroups();
|
|
}
|
|
|
|
float Animation::getVelocity(std::string_view groupname) const
|
|
{
|
|
if (!mAccumRoot)
|
|
return 0.0f;
|
|
|
|
std::map<std::string, float>::const_iterator found = mAnimVelocities.find(groupname);
|
|
if (found != mAnimVelocities.end())
|
|
return found->second;
|
|
|
|
// Look in reverse; last-inserted source has priority.
|
|
AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin());
|
|
for (; animsrc != mAnimSources.rend(); ++animsrc)
|
|
{
|
|
const SceneUtil::TextKeyMap& keys = (*animsrc)->getTextKeys();
|
|
if (keys.hasGroupStart(groupname))
|
|
break;
|
|
}
|
|
if (animsrc == mAnimSources.rend())
|
|
return 0.0f;
|
|
|
|
float velocity = 0.0f;
|
|
const SceneUtil::TextKeyMap& keys = (*animsrc)->getTextKeys();
|
|
|
|
const AnimSource::ControllerMap& ctrls = (*animsrc)->mControllerMap[0];
|
|
for (AnimSource::ControllerMap::const_iterator it = ctrls.begin(); it != ctrls.end(); ++it)
|
|
{
|
|
if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName()))
|
|
{
|
|
velocity = calcAnimVelocity(keys, it->second, mAccumulate, groupname);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there's no velocity, keep looking
|
|
if (!(velocity > 1.0f))
|
|
{
|
|
AnimSourceList::const_reverse_iterator animiter = mAnimSources.rbegin();
|
|
while (*animiter != *animsrc)
|
|
++animiter;
|
|
|
|
while (!(velocity > 1.0f) && ++animiter != mAnimSources.rend())
|
|
{
|
|
const SceneUtil::TextKeyMap& keys2 = (*animiter)->getTextKeys();
|
|
|
|
const AnimSource::ControllerMap& ctrls2 = (*animiter)->mControllerMap[0];
|
|
for (AnimSource::ControllerMap::const_iterator it = ctrls2.begin(); it != ctrls2.end(); ++it)
|
|
{
|
|
if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName()))
|
|
{
|
|
velocity = calcAnimVelocity(keys2, it->second, mAccumulate, groupname);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mAnimVelocities.emplace(groupname, velocity);
|
|
|
|
return velocity;
|
|
}
|
|
|
|
void Animation::updatePosition(float oldtime, float newtime, osg::Vec3f& position)
|
|
{
|
|
// Get the difference from the last update, and move the position
|
|
osg::Vec3f off = osg::componentMultiply(mAccumCtrl->getTranslation(newtime), mAccumulate);
|
|
position += off - osg::componentMultiply(mAccumCtrl->getTranslation(oldtime), mAccumulate);
|
|
}
|
|
|
|
osg::Vec3f Animation::runAnimation(float duration)
|
|
{
|
|
// If we have scripted animations, play only them
|
|
bool hasScriptedAnims = false;
|
|
for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++)
|
|
{
|
|
if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent))
|
|
&& stateiter->second.mPlaying)
|
|
{
|
|
hasScriptedAnims = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
osg::Vec3f movement(0.f, 0.f, 0.f);
|
|
AnimStateMap::iterator stateiter = mStates.begin();
|
|
while (stateiter != mStates.end())
|
|
{
|
|
AnimState& state = stateiter->second;
|
|
if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent)))
|
|
{
|
|
++stateiter;
|
|
continue;
|
|
}
|
|
|
|
const SceneUtil::TextKeyMap& textkeys = state.mSource->getTextKeys();
|
|
auto textkey = textkeys.upperBound(state.getTime());
|
|
|
|
float timepassed = duration * state.mSpeedMult;
|
|
while (state.mPlaying)
|
|
{
|
|
if (!state.shouldLoop())
|
|
{
|
|
float targetTime = state.getTime() + timepassed;
|
|
if (textkey == textkeys.end() || textkey->first > targetTime)
|
|
{
|
|
if (mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr())
|
|
updatePosition(state.getTime(), targetTime, movement);
|
|
state.setTime(std::min(targetTime, state.mStopTime));
|
|
}
|
|
else
|
|
{
|
|
if (mAccumCtrl && state.mTime == mAnimationTimePtr[0]->getTimePtr())
|
|
updatePosition(state.getTime(), textkey->first, movement);
|
|
state.setTime(textkey->first);
|
|
}
|
|
|
|
state.mPlaying = (state.getTime() < state.mStopTime);
|
|
timepassed = targetTime - state.getTime();
|
|
|
|
while (textkey != textkeys.end() && textkey->first <= state.getTime())
|
|
{
|
|
handleTextKey(state, stateiter->first, textkey, textkeys);
|
|
++textkey;
|
|
}
|
|
}
|
|
if (state.shouldLoop())
|
|
{
|
|
state.mLoopCount--;
|
|
state.setTime(state.mLoopStartTime);
|
|
state.mPlaying = true;
|
|
|
|
textkey = textkeys.lowerBound(state.getTime());
|
|
while (textkey != textkeys.end() && textkey->first <= state.getTime())
|
|
{
|
|
handleTextKey(state, stateiter->first, textkey, textkeys);
|
|
++textkey;
|
|
}
|
|
|
|
if (state.getTime() >= state.mLoopStopTime)
|
|
break;
|
|
}
|
|
|
|
if (timepassed <= 0.0f)
|
|
break;
|
|
}
|
|
|
|
if (!state.mPlaying && state.mAutoDisable)
|
|
{
|
|
mStates.erase(stateiter++);
|
|
|
|
resetActiveGroups();
|
|
}
|
|
else
|
|
++stateiter;
|
|
}
|
|
|
|
updateEffects();
|
|
|
|
const float epsilon = 0.001f;
|
|
float yawOffset = 0;
|
|
if (mRootController)
|
|
{
|
|
bool enable = std::abs(mLegsYawRadians) > epsilon || std::abs(mBodyPitchRadians) > epsilon;
|
|
mRootController->setEnabled(enable);
|
|
if (enable)
|
|
{
|
|
mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1))
|
|
* osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0)));
|
|
yawOffset = mLegsYawRadians;
|
|
}
|
|
}
|
|
if (mSpineController)
|
|
{
|
|
float yaw = mUpperBodyYawRadians - yawOffset;
|
|
bool enable = std::abs(yaw) > epsilon;
|
|
mSpineController->setEnabled(enable);
|
|
if (enable)
|
|
{
|
|
mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0, 0, 1)));
|
|
yawOffset = mUpperBodyYawRadians;
|
|
}
|
|
}
|
|
if (mHeadController)
|
|
{
|
|
float yaw = mHeadYawRadians - yawOffset;
|
|
bool enable = (std::abs(mHeadPitchRadians) > epsilon || std::abs(yaw) > epsilon);
|
|
mHeadController->setEnabled(enable);
|
|
if (enable)
|
|
mHeadController->setRotate(
|
|
osg::Quat(mHeadPitchRadians, osg::Vec3f(1, 0, 0)) * osg::Quat(yaw, osg::Vec3f(0, 0, 1)));
|
|
}
|
|
|
|
// Scripted animations should not cause movement
|
|
if (hasScriptedAnims)
|
|
return osg::Vec3f(0, 0, 0);
|
|
|
|
return movement;
|
|
}
|
|
|
|
void Animation::setLoopingEnabled(std::string_view groupname, bool enabled)
|
|
{
|
|
AnimStateMap::iterator state(mStates.find(groupname));
|
|
if (state != mStates.end())
|
|
state->second.mLoopingEnabled = enabled;
|
|
}
|
|
|
|
void loadBonesFromFile(
|
|
osg::ref_ptr<osg::Node>& baseNode, const std::string& model, Resource::ResourceSystem* resourceSystem)
|
|
{
|
|
const osg::Node* node = resourceSystem->getSceneManager()->getTemplate(model).get();
|
|
osg::ref_ptr<osg::Node> sheathSkeleton(
|
|
const_cast<osg::Node*>(node)); // const-trickery required because there is no const version of NodeVisitor
|
|
|
|
GetExtendedBonesVisitor getBonesVisitor;
|
|
sheathSkeleton->accept(getBonesVisitor);
|
|
for (auto& nodePair : getBonesVisitor.mFoundBones)
|
|
{
|
|
SceneUtil::FindByNameVisitor findVisitor(nodePair.second->getName());
|
|
baseNode->accept(findVisitor);
|
|
|
|
osg::Group* sheathParent = findVisitor.mFoundNode;
|
|
if (sheathParent)
|
|
{
|
|
osg::Node* copy = static_cast<osg::Node*>(nodePair.first->clone(osg::CopyOp::DEEP_COPY_NODES));
|
|
sheathParent->addChild(copy);
|
|
}
|
|
}
|
|
}
|
|
|
|
void injectCustomBones(
|
|
osg::ref_ptr<osg::Node>& node, const std::string& model, Resource::ResourceSystem* resourceSystem)
|
|
{
|
|
if (model.empty())
|
|
return;
|
|
|
|
std::string animationPath = model;
|
|
if (animationPath.find("meshes") == 0)
|
|
{
|
|
animationPath.replace(0, 6, "animations");
|
|
}
|
|
animationPath.replace(animationPath.size() - 4, 4, "/");
|
|
|
|
for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath))
|
|
{
|
|
if (Misc::getFileExtension(name) == "nif")
|
|
loadBonesFromFile(node, name, resourceSystem);
|
|
}
|
|
}
|
|
|
|
osg::ref_ptr<osg::Node> getModelInstance(Resource::ResourceSystem* resourceSystem, const std::string& model,
|
|
bool baseonly, bool inject, const std::string& defaultSkeleton)
|
|
{
|
|
Resource::SceneManager* sceneMgr = resourceSystem->getSceneManager();
|
|
if (baseonly)
|
|
{
|
|
typedef std::map<std::string, osg::ref_ptr<osg::Node>> Cache;
|
|
static Cache cache;
|
|
Cache::iterator found = cache.find(model);
|
|
if (found == cache.end())
|
|
{
|
|
osg::ref_ptr<osg::Node> created = sceneMgr->getInstance(model);
|
|
|
|
if (inject)
|
|
{
|
|
injectCustomBones(created, defaultSkeleton, resourceSystem);
|
|
injectCustomBones(created, model, resourceSystem);
|
|
}
|
|
|
|
SceneUtil::CleanObjectRootVisitor removeDrawableVisitor;
|
|
created->accept(removeDrawableVisitor);
|
|
removeDrawableVisitor.remove();
|
|
|
|
cache.insert(std::make_pair(model, created));
|
|
|
|
return sceneMgr->getInstance(created);
|
|
}
|
|
else
|
|
return sceneMgr->getInstance(found->second);
|
|
}
|
|
else
|
|
{
|
|
osg::ref_ptr<osg::Node> created = sceneMgr->getInstance(model);
|
|
|
|
if (inject)
|
|
{
|
|
injectCustomBones(created, defaultSkeleton, resourceSystem);
|
|
injectCustomBones(created, model, resourceSystem);
|
|
}
|
|
|
|
return created;
|
|
}
|
|
}
|
|
|
|
void Animation::setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature)
|
|
{
|
|
osg::ref_ptr<osg::StateSet> previousStateset;
|
|
if (mObjectRoot)
|
|
{
|
|
if (mLightListCallback)
|
|
mObjectRoot->removeCullCallback(mLightListCallback);
|
|
if (mTransparencyUpdater)
|
|
mObjectRoot->removeCullCallback(mTransparencyUpdater);
|
|
previousStateset = mObjectRoot->getStateSet();
|
|
mObjectRoot->getParent(0)->removeChild(mObjectRoot);
|
|
}
|
|
mObjectRoot = nullptr;
|
|
mSkeleton = nullptr;
|
|
|
|
mNodeMap.clear();
|
|
mNodeMapCreated = false;
|
|
mActiveControllers.clear();
|
|
mAccumRoot = nullptr;
|
|
mAccumCtrl = nullptr;
|
|
|
|
std::string defaultSkeleton;
|
|
bool inject = false;
|
|
|
|
if (Settings::game().mUseAdditionalAnimSources && mPtr.getClass().isActor())
|
|
{
|
|
if (isCreature)
|
|
{
|
|
MWWorld::LiveCellRef<ESM::Creature>* ref = mPtr.get<ESM::Creature>();
|
|
if (ref->mBase->mFlags & ESM::Creature::Bipedal)
|
|
{
|
|
defaultSkeleton = Settings::models().mXbaseanim;
|
|
inject = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
inject = true;
|
|
MWWorld::LiveCellRef<ESM::NPC>* ref = mPtr.get<ESM::NPC>();
|
|
if (!ref->mBase->mModel.empty())
|
|
{
|
|
// If NPC has a custom animation model attached, we should inject bones from default skeleton for
|
|
// given race and gender as well Since it is a quite rare case, there should not be a noticable
|
|
// performance loss Note: consider that player and werewolves have no custom animation files
|
|
// attached for now
|
|
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
|
|
const ESM::Race* race = store.get<ESM::Race>().find(ref->mBase->mRace);
|
|
|
|
bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0;
|
|
bool isFemale = !ref->mBase->isMale();
|
|
|
|
defaultSkeleton = SceneUtil::getActorSkeleton(false, isFemale, isBeast, false);
|
|
defaultSkeleton
|
|
= Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!forceskeleton)
|
|
{
|
|
osg::ref_ptr<osg::Node> created
|
|
= getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton);
|
|
mInsert->addChild(created);
|
|
mObjectRoot = created->asGroup();
|
|
if (!mObjectRoot)
|
|
{
|
|
mInsert->removeChild(created);
|
|
mObjectRoot = new osg::Group;
|
|
mObjectRoot->addChild(created);
|
|
mInsert->addChild(mObjectRoot);
|
|
}
|
|
osg::ref_ptr<SceneUtil::Skeleton> skel = dynamic_cast<SceneUtil::Skeleton*>(mObjectRoot.get());
|
|
if (skel)
|
|
mSkeleton = skel.get();
|
|
}
|
|
else
|
|
{
|
|
osg::ref_ptr<osg::Node> created
|
|
= getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton);
|
|
osg::ref_ptr<SceneUtil::Skeleton> skel = dynamic_cast<SceneUtil::Skeleton*>(created.get());
|
|
if (!skel)
|
|
{
|
|
skel = new SceneUtil::Skeleton;
|
|
skel->addChild(created);
|
|
}
|
|
mSkeleton = skel.get();
|
|
mObjectRoot = skel;
|
|
mInsert->addChild(mObjectRoot);
|
|
}
|
|
|
|
if (previousStateset)
|
|
mObjectRoot->setStateSet(previousStateset);
|
|
|
|
if (isCreature)
|
|
{
|
|
SceneUtil::RemoveTriBipVisitor removeTriBipVisitor;
|
|
mObjectRoot->accept(removeTriBipVisitor);
|
|
removeTriBipVisitor.remove();
|
|
}
|
|
|
|
if (!mLightListCallback)
|
|
mLightListCallback = new SceneUtil::LightListCallback;
|
|
mObjectRoot->addCullCallback(mLightListCallback);
|
|
if (mTransparencyUpdater)
|
|
mObjectRoot->addCullCallback(mTransparencyUpdater);
|
|
}
|
|
|
|
osg::Group* Animation::getObjectRoot()
|
|
{
|
|
return mObjectRoot.get();
|
|
}
|
|
|
|
osg::Group* Animation::getOrCreateObjectRoot()
|
|
{
|
|
if (mObjectRoot)
|
|
return mObjectRoot.get();
|
|
|
|
mObjectRoot = new osg::Group;
|
|
mInsert->addChild(mObjectRoot);
|
|
return mObjectRoot.get();
|
|
}
|
|
|
|
void Animation::addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration)
|
|
{
|
|
if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true)))
|
|
{
|
|
if (mGlowUpdater && mGlowUpdater->isDone())
|
|
mObjectRoot->removeUpdateCallback(mGlowUpdater);
|
|
|
|
if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater())
|
|
{
|
|
mGlowUpdater->setColor(effect->getColor());
|
|
mGlowUpdater->setDuration(glowDuration);
|
|
}
|
|
else
|
|
mGlowUpdater
|
|
= SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, effect->getColor(), glowDuration);
|
|
}
|
|
}
|
|
|
|
void Animation::addExtraLight(osg::ref_ptr<osg::Group> parent, const SceneUtil::LightCommon& esmLight)
|
|
{
|
|
bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior();
|
|
|
|
mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_Lighting, exterior);
|
|
mExtraLightSource->setActorFade(mAlpha);
|
|
}
|
|
|
|
void Animation::addEffect(
|
|
const std::string& model, int effectId, bool loop, std::string_view bonename, std::string_view texture)
|
|
{
|
|
if (!mObjectRoot.get())
|
|
return;
|
|
|
|
// Early out if we already have this effect
|
|
FindVfxCallbacksVisitor visitor(effectId);
|
|
mInsert->accept(visitor);
|
|
|
|
for (std::vector<UpdateVfxCallback*>::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end();
|
|
++it)
|
|
{
|
|
UpdateVfxCallback* callback = *it;
|
|
|
|
if (loop && !callback->mFinished && callback->mParams.mLoop && callback->mParams.mBoneName == bonename)
|
|
return;
|
|
}
|
|
|
|
EffectParams params;
|
|
params.mModelName = model;
|
|
osg::ref_ptr<osg::Group> parentNode;
|
|
if (bonename.empty())
|
|
parentNode = mInsert;
|
|
else
|
|
{
|
|
NodeMap::const_iterator found = getNodeMap().find(bonename);
|
|
if (found == getNodeMap().end())
|
|
throw std::runtime_error("Can't find bone " + std::string{ bonename });
|
|
|
|
parentNode = found->second;
|
|
}
|
|
|
|
osg::ref_ptr<SceneUtil::PositionAttitudeTransform> trans = new SceneUtil::PositionAttitudeTransform;
|
|
if (!mPtr.getClass().isNpc())
|
|
{
|
|
osg::Vec3f bounds(MWBase::Environment::get().getWorld()->getHalfExtents(mPtr) * 2.f);
|
|
float scale = std::max({ bounds.x(), bounds.y(), bounds.z() / 2.f }) / 64.f;
|
|
if (scale > 1.f)
|
|
trans->setScale(osg::Vec3f(scale, scale, scale));
|
|
float offset = 0.f;
|
|
if (bounds.z() < 128.f)
|
|
offset = bounds.z() - 128.f;
|
|
else if (bounds.z() < bounds.x() + bounds.y())
|
|
offset = 128.f - bounds.z();
|
|
if (MWBase::Environment::get().getWorld()->isFlying(mPtr))
|
|
offset /= 20.f;
|
|
trans->setPosition(osg::Vec3f(0.f, 0.f, offset * scale));
|
|
}
|
|
parentNode->addChild(trans);
|
|
|
|
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, trans);
|
|
|
|
// Morrowind has a white ambient light attached to the root VFX node of the scenegraph
|
|
node->getOrCreateStateSet()->setAttributeAndModes(
|
|
getVFXLightModelInstance(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
|
if (mResourceSystem->getSceneManager()->getSupportsNormalsRT())
|
|
node->getOrCreateStateSet()->setAttribute(new osg::ColorMaski(1, false, false, false, false));
|
|
SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor;
|
|
node->accept(findMaxLengthVisitor);
|
|
|
|
node->setNodeMask(Mask_Effect);
|
|
|
|
MarkDrawablesVisitor markVisitor(Mask_Effect);
|
|
node->accept(markVisitor);
|
|
|
|
params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength();
|
|
params.mLoop = loop;
|
|
params.mEffectId = effectId;
|
|
params.mBoneName = bonename;
|
|
params.mAnimTime = std::make_shared<EffectAnimationTime>();
|
|
trans->addUpdateCallback(new UpdateVfxCallback(params));
|
|
|
|
SceneUtil::AssignControllerSourcesVisitor assignVisitor(
|
|
std::shared_ptr<SceneUtil::ControllerSource>(params.mAnimTime));
|
|
node->accept(assignVisitor);
|
|
|
|
// Notify that this animation has attached magic effects
|
|
mHasMagicEffects = true;
|
|
|
|
overrideFirstRootTexture(texture, mResourceSystem, node);
|
|
}
|
|
|
|
void Animation::removeEffect(int effectId)
|
|
{
|
|
RemoveCallbackVisitor visitor(effectId);
|
|
mInsert->accept(visitor);
|
|
visitor.remove();
|
|
mHasMagicEffects = visitor.mHasMagicEffects;
|
|
}
|
|
|
|
void Animation::removeEffects()
|
|
{
|
|
removeEffect(-1);
|
|
}
|
|
|
|
void Animation::getLoopingEffects(std::vector<int>& out) const
|
|
{
|
|
if (!mHasMagicEffects)
|
|
return;
|
|
|
|
FindVfxCallbacksVisitor visitor;
|
|
mInsert->accept(visitor);
|
|
|
|
for (std::vector<UpdateVfxCallback*>::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end();
|
|
++it)
|
|
{
|
|
UpdateVfxCallback* callback = *it;
|
|
|
|
if (callback->mParams.mLoop && !callback->mFinished)
|
|
out.push_back(callback->mParams.mEffectId);
|
|
}
|
|
}
|
|
|
|
void Animation::updateEffects()
|
|
{
|
|
// We do not need to visit scene every frame.
|
|
// We can use a bool flag to check in spellcasting effect found.
|
|
if (!mHasMagicEffects)
|
|
return;
|
|
|
|
// TODO: objects without animation still will have
|
|
// transformation nodes with finished callbacks
|
|
RemoveFinishedCallbackVisitor visitor;
|
|
mInsert->accept(visitor);
|
|
visitor.remove();
|
|
mHasMagicEffects = visitor.mHasMagicEffects;
|
|
}
|
|
|
|
bool Animation::upperBodyReady() const
|
|
{
|
|
for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter)
|
|
{
|
|
if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Hit))
|
|
|| stateiter->second.mPriority.contains(int(MWMechanics::Priority_Weapon))
|
|
|| stateiter->second.mPriority.contains(int(MWMechanics::Priority_Knockdown))
|
|
|| stateiter->second.mPriority.contains(int(MWMechanics::Priority_Death)))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const osg::Node* Animation::getNode(std::string_view name) const
|
|
{
|
|
NodeMap::const_iterator found = getNodeMap().find(name);
|
|
if (found == getNodeMap().end())
|
|
return nullptr;
|
|
else
|
|
return found->second;
|
|
}
|
|
|
|
void Animation::setAlpha(float alpha)
|
|
{
|
|
if (alpha == mAlpha)
|
|
return;
|
|
mAlpha = alpha;
|
|
|
|
// TODO: we use it to fade actors away too, but it would be nice to have a dithering shader instead.
|
|
if (alpha != 1.f)
|
|
{
|
|
if (mTransparencyUpdater == nullptr)
|
|
{
|
|
mTransparencyUpdater = new TransparencyUpdater(alpha);
|
|
mObjectRoot->addCullCallback(mTransparencyUpdater);
|
|
}
|
|
else
|
|
mTransparencyUpdater->setAlpha(alpha);
|
|
}
|
|
else
|
|
{
|
|
mObjectRoot->removeCullCallback(mTransparencyUpdater);
|
|
mTransparencyUpdater = nullptr;
|
|
}
|
|
if (mExtraLightSource)
|
|
mExtraLightSource->setActorFade(alpha);
|
|
}
|
|
|
|
void Animation::setLightEffect(float effect)
|
|
{
|
|
if (effect == 0)
|
|
{
|
|
if (mGlowLight)
|
|
{
|
|
mInsert->removeChild(mGlowLight);
|
|
mGlowLight = nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 1 pt of Light magnitude corresponds to 1 foot of radius
|
|
float radius = effect * std::ceil(Constants::UnitsPerFoot);
|
|
// Arbitrary multiplier used to make the obvious cut-off less obvious
|
|
float cutoffMult = 3;
|
|
|
|
if (!mGlowLight || (radius * cutoffMult) != mGlowLight->getRadius())
|
|
{
|
|
if (mGlowLight)
|
|
{
|
|
mInsert->removeChild(mGlowLight);
|
|
mGlowLight = nullptr;
|
|
}
|
|
|
|
osg::ref_ptr<osg::Light> light(new osg::Light);
|
|
light->setDiffuse(osg::Vec4f(0, 0, 0, 0));
|
|
light->setSpecular(osg::Vec4f(0, 0, 0, 0));
|
|
light->setAmbient(osg::Vec4f(1.5f, 1.5f, 1.5f, 1.f));
|
|
|
|
bool isExterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior();
|
|
SceneUtil::configureLight(light, radius, isExterior);
|
|
|
|
mGlowLight = new SceneUtil::LightSource;
|
|
mGlowLight->setNodeMask(Mask_Lighting);
|
|
mInsert->addChild(mGlowLight);
|
|
mGlowLight->setLight(light);
|
|
}
|
|
|
|
mGlowLight->setRadius(radius * cutoffMult);
|
|
}
|
|
}
|
|
|
|
void Animation::addControllers()
|
|
{
|
|
mHeadController = addRotateController("bip01 head");
|
|
mSpineController = addRotateController("bip01 spine1");
|
|
mRootController = addRotateController("bip01");
|
|
}
|
|
|
|
osg::ref_ptr<RotateController> Animation::addRotateController(std::string_view bone)
|
|
{
|
|
auto iter = getNodeMap().find(bone);
|
|
if (iter == getNodeMap().end())
|
|
return nullptr;
|
|
osg::MatrixTransform* node = iter->second;
|
|
|
|
bool foundKeyframeCtrl = false;
|
|
osg::Callback* cb = node->getUpdateCallback();
|
|
while (cb)
|
|
{
|
|
if (dynamic_cast<SceneUtil::KeyframeController*>(cb))
|
|
{
|
|
foundKeyframeCtrl = true;
|
|
break;
|
|
}
|
|
cb = cb->getNestedCallback();
|
|
}
|
|
// Without KeyframeController the orientation will not be reseted each frame, so
|
|
// RotateController shouldn't be used for such nodes.
|
|
if (!foundKeyframeCtrl)
|
|
return nullptr;
|
|
|
|
osg::ref_ptr<RotateController> controller(new RotateController(mObjectRoot.get()));
|
|
node->addUpdateCallback(controller);
|
|
mActiveControllers.emplace_back(node, controller);
|
|
return controller;
|
|
}
|
|
|
|
void Animation::setHeadPitch(float pitchRadians)
|
|
{
|
|
mHeadPitchRadians = pitchRadians;
|
|
}
|
|
|
|
void Animation::setHeadYaw(float yawRadians)
|
|
{
|
|
mHeadYawRadians = yawRadians;
|
|
}
|
|
|
|
float Animation::getHeadPitch() const
|
|
{
|
|
return mHeadPitchRadians;
|
|
}
|
|
|
|
float Animation::getHeadYaw() const
|
|
{
|
|
return mHeadYawRadians;
|
|
}
|
|
|
|
void Animation::removeFromScene()
|
|
{
|
|
removeFromSceneImpl();
|
|
}
|
|
|
|
void Animation::removeFromSceneImpl()
|
|
{
|
|
if (mGlowLight != nullptr)
|
|
mInsert->removeChild(mGlowLight);
|
|
|
|
if (mObjectRoot != nullptr)
|
|
mInsert->removeChild(mObjectRoot);
|
|
}
|
|
|
|
MWWorld::MovementDirectionFlags Animation::getSupportedMovementDirections(
|
|
std::span<const std::string_view> prefixes) const
|
|
{
|
|
MWWorld::MovementDirectionFlags result = 0;
|
|
for (const std::string_view animation : mSupportedAnimations)
|
|
{
|
|
if (std::find_if(
|
|
prefixes.begin(), prefixes.end(), [&](std::string_view v) { return animation.starts_with(v); })
|
|
== prefixes.end())
|
|
continue;
|
|
if (animation.ends_with("forward"))
|
|
result |= MWWorld::MovementDirectionFlag_Forward;
|
|
else if (animation.ends_with("back"))
|
|
result |= MWWorld::MovementDirectionFlag_Back;
|
|
else if (animation.ends_with("left"))
|
|
result |= MWWorld::MovementDirectionFlag_Left;
|
|
else if (animation.ends_with("right"))
|
|
result |= MWWorld::MovementDirectionFlag_Right;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ------------------------------------------------------
|
|
|
|
float Animation::AnimationTime::getValue(osg::NodeVisitor*)
|
|
{
|
|
if (mTimePtr)
|
|
return *mTimePtr;
|
|
return 0.f;
|
|
}
|
|
|
|
float EffectAnimationTime::getValue(osg::NodeVisitor*)
|
|
{
|
|
return mTime;
|
|
}
|
|
|
|
void EffectAnimationTime::addTime(float duration)
|
|
{
|
|
mTime += duration;
|
|
}
|
|
|
|
void EffectAnimationTime::resetTime(float time)
|
|
{
|
|
mTime = time;
|
|
}
|
|
|
|
float EffectAnimationTime::getTime() const
|
|
{
|
|
return mTime;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------
|
|
|
|
ObjectAnimation::ObjectAnimation(const MWWorld::Ptr& ptr, const std::string& model,
|
|
Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight)
|
|
: Animation(ptr, osg::ref_ptr<osg::Group>(ptr.getRefData().getBaseNode()), resourceSystem)
|
|
{
|
|
if (!model.empty())
|
|
{
|
|
setObjectRoot(model, false, false, false);
|
|
if (animated)
|
|
addAnimSource(model, model);
|
|
|
|
if (!ptr.getClass().getEnchantment(ptr).empty())
|
|
mGlowUpdater = SceneUtil::addEnchantedGlow(
|
|
mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr));
|
|
}
|
|
if (ptr.getType() == ESM::Light::sRecordId && allowLight)
|
|
addExtraLight(getOrCreateObjectRoot(), SceneUtil::LightCommon(*ptr.get<ESM::Light>()->mBase));
|
|
if (ptr.getType() == ESM4::Light::sRecordId && allowLight)
|
|
addExtraLight(getOrCreateObjectRoot(), SceneUtil::LightCommon(*ptr.get<ESM4::Light>()->mBase));
|
|
|
|
if (!allowLight && mObjectRoot)
|
|
{
|
|
RemoveParticlesVisitor visitor;
|
|
mObjectRoot->accept(visitor);
|
|
visitor.remove();
|
|
}
|
|
|
|
if (Settings::game().mDayNightSwitches && SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel))
|
|
{
|
|
AddSwitchCallbacksVisitor visitor;
|
|
mObjectRoot->accept(visitor);
|
|
}
|
|
|
|
if (ptr.getRefData().getCustomData() != nullptr && ObjectAnimation::canBeHarvested())
|
|
{
|
|
const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
|
|
if (!store.hasVisibleItems())
|
|
{
|
|
HarvestVisitor visitor;
|
|
mObjectRoot->accept(visitor);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ObjectAnimation::canBeHarvested() const
|
|
{
|
|
if (mPtr.getType() != ESM::Container::sRecordId)
|
|
return false;
|
|
|
|
const MWWorld::LiveCellRef<ESM::Container>* ref = mPtr.get<ESM::Container>();
|
|
if (!(ref->mBase->mFlags & ESM::Container::Organic))
|
|
return false;
|
|
|
|
return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel);
|
|
}
|
|
|
|
// ------------------------------
|
|
|
|
PartHolder::PartHolder(osg::ref_ptr<osg::Node> node)
|
|
: mNode(std::move(node))
|
|
{
|
|
}
|
|
|
|
PartHolder::~PartHolder()
|
|
{
|
|
if (mNode.get() && !mNode->getNumParents())
|
|
Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has no parents";
|
|
|
|
if (mNode.get() && mNode->getNumParents())
|
|
{
|
|
if (mNode->getNumParents() > 1)
|
|
Log(Debug::Verbose) << "Part \"" << mNode->getName() << "\" has multiple (" << mNode->getNumParents()
|
|
<< ") parents";
|
|
mNode->getParent(0)->removeChild(mNode);
|
|
}
|
|
}
|
|
}
|