#include "animation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(&node)) mToRemove.emplace_back(&node); traverse(node); } void apply(osg::Drawable& drw) override { if (osgParticle::ParticleSystem* partsys = dynamic_cast(&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> mToRemove; }; class DayNightCallback : public SceneUtil::NodeCallback { 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::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> 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(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(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 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(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 getVFXLightModelInstance() { static osg::ref_ptr 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(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); } private: float mAlpha; }; struct Animation::AnimSource { osg::ref_ptr mKeyframes; typedef std::map> 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 { 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 parentNode, Resource::ResourceSystem* resourceSystem) : mInsert(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(); mLightListCallback = new SceneUtil::LightListCallback; } Animation::~Animation() { removeFromSceneImpl(); } void Animation::setActive(int active) { if (mSkeleton) mSkeleton->setActive(static_cast(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(); 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 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 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()); 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::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() : active->second.mTime); // add external controllers for the AnimSource active in this blend mask if (active != mStates.end()) { std::shared_ptr animsrc = active->second.mSource; for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) { osg::ref_ptr 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::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& baseNode, const std::string& model, Resource::ResourceSystem* resourceSystem) { const osg::Node* node = resourceSystem->getSceneManager()->getTemplate(model).get(); osg::ref_ptr sheathSkeleton( const_cast(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(nodePair.first->clone(osg::CopyOp::DEEP_COPY_NODES)); sheathParent->addChild(copy); } } } void injectCustomBones( osg::ref_ptr& 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 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> Cache; static Cache cache; Cache::iterator found = cache.find(model); if (found == cache.end()) { osg::ref_ptr 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 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 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* ref = mPtr.get(); if (ref->mBase->mFlags & ESM::Creature::Bipedal) { defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); inject = true; } } else { inject = true; MWWorld::LiveCellRef* ref = mPtr.get(); 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().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 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 skel = dynamic_cast(mObjectRoot.get()); if (skel) mSkeleton = skel.get(); } else { osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); osg::ref_ptr skel = dynamic_cast(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 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::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 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 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 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(); trans->addUpdateCallback(new UpdateVfxCallback(params)); SceneUtil::AssignControllerSourcesVisitor assignVisitor( std::shared_ptr(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& out) const { if (!mHasMagicEffects) return; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); for (std::vector::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 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 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(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 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); } // ------------------------------------------------------ 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(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()->mBase)); if (ptr.getType() == ESM4::Light::sRecordId && allowLight) addExtraLight(getOrCreateObjectRoot(), SceneUtil::LightCommon(*ptr.get()->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* ref = mPtr.get(); if (!(ref->mBase->mFlags & ESM::Container::Organic)) return false; return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } // ------------------------------ PartHolder::PartHolder(osg::ref_ptr node) : mNode(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); } } }