1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-29 18:32:36 +00:00
OpenMW/apps/openmw/mwrender/npcanimation.cpp
elsid ce263af393
Use existing functions and objects to call correctMeshPath etc
Remove WindowManager wrappers.

It's not safe to use WindowManager in all places and it's not required.
Environment stores resource system providing VFS required to call these
functions. In the case of ObjectPaging it's available from the member variable.
Also ObjectPaging::createChunk may access WindowManager when it's already
destructed when exiting the game because it's destructed before CellPreloader
finishes all background jobs. Engine::mResourceSystem is destructed after all
other systems so it's safe to use it.
2022-06-29 00:58:49 +02:00

1285 lines
46 KiB
C++

#include "npcanimation.hpp"
#include <osg/UserDataContainer>
#include <osg/MatrixTransform>
#include <osg/Depth>
#include <osgUtil/RenderBin>
#include <osgUtil/CullVisitor>
#include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/sceneutil/actorutil.hpp>
#include <components/sceneutil/attach.hpp>
#include <components/sceneutil/visitor.hpp>
#include <components/sceneutil/skeleton.hpp>
#include <components/sceneutil/keyframe.hpp>
#include <components/sceneutil/depth.hpp>
#include <components/settings/settings.hpp>
#include <components/vfs/manager.hpp>
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/player.hpp"
#include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/weapontype.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/soundmanager.hpp"
#include "camera.hpp"
#include "rotatecontroller.hpp"
#include "renderbin.hpp"
#include "vismask.hpp"
#include "util.hpp"
#include "postprocessor.hpp"
namespace
{
std::string getVampireHead(const std::string& race, bool female, const VFS::Manager& vfs)
{
static std::map <std::pair<std::string,int>, const ESM::BodyPart* > sVampireMapping;
std::pair<std::string, int> thisCombination = std::make_pair(race, int(female));
if (sVampireMapping.find(thisCombination) == sVampireMapping.end())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
for (const ESM::BodyPart& bodypart : store.get<ESM::BodyPart>())
{
if (!bodypart.mData.mVampire)
continue;
if (bodypart.mData.mType != ESM::BodyPart::MT_Skin)
continue;
if (bodypart.mData.mPart != ESM::BodyPart::MP_Head)
continue;
if (female != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female))
continue;
if (!Misc::StringUtils::ciEqual(bodypart.mRace, race))
continue;
sVampireMapping[thisCombination] = &bodypart;
}
}
sVampireMapping.emplace(thisCombination, nullptr);
const ESM::BodyPart* bodyPart = sVampireMapping[thisCombination];
if (!bodyPart)
return std::string();
return Misc::ResourceHelpers::correctMeshPath(bodyPart->mModel, &vfs);
}
}
namespace MWRender
{
class HeadAnimationTime : public SceneUtil::ControllerSource
{
private:
MWWorld::Ptr mReference;
float mTalkStart;
float mTalkStop;
float mBlinkStart;
float mBlinkStop;
float mBlinkTimer;
bool mEnabled;
float mValue;
private:
void resetBlinkTimer();
public:
HeadAnimationTime(const MWWorld::Ptr& reference);
void updatePtr(const MWWorld::Ptr& updated);
void update(float dt);
void setEnabled(bool enabled);
void setTalkStart(float value);
void setTalkStop(float value);
void setBlinkStart(float value);
void setBlinkStop(float value);
float getValue(osg::NodeVisitor* nv) override;
};
// --------------------------------------------------------------------------------------------------------------
HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference)
: mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0), mEnabled(true), mValue(0)
{
resetBlinkTimer();
}
void HeadAnimationTime::updatePtr(const MWWorld::Ptr &updated)
{
mReference = updated;
}
void HeadAnimationTime::setEnabled(bool enabled)
{
mEnabled = enabled;
}
void HeadAnimationTime::resetBlinkTimer()
{
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
mBlinkTimer = -(2.0f + Misc::Rng::rollDice(6, prng));
}
void HeadAnimationTime::update(float dt)
{
if (!mEnabled)
return;
if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference))
{
mBlinkTimer += dt;
float duration = mBlinkStop - mBlinkStart;
if (mBlinkTimer >= 0 && mBlinkTimer <= duration)
{
mValue = mBlinkStart + mBlinkTimer;
}
else
mValue = mBlinkStop;
if (mBlinkTimer > duration)
resetBlinkTimer();
}
else
{
// FIXME: would be nice to hold on to the SoundPtr so we don't have to retrieve it every frame
mValue = mTalkStart +
(mTalkStop - mTalkStart) *
std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*2); // Rescale a bit (most voices are not very loud)
}
}
float HeadAnimationTime::getValue(osg::NodeVisitor*)
{
return mValue;
}
void HeadAnimationTime::setTalkStart(float value)
{
mTalkStart = value;
}
void HeadAnimationTime::setTalkStop(float value)
{
mTalkStop = value;
}
void HeadAnimationTime::setBlinkStart(float value)
{
mBlinkStart = value;
}
void HeadAnimationTime::setBlinkStop(float value)
{
mBlinkStop = value;
}
// ----------------------------------------------------
NpcAnimation::NpcType NpcAnimation::getNpcType() const
{
const MWWorld::Class &cls = mPtr.getClass();
// Dead vampires should typically stay vampires.
if (mNpcType == Type_Vampire && cls.getNpcStats(mPtr).isDead() && !cls.getNpcStats(mPtr).isWerewolf())
return mNpcType;
return getNpcType(mPtr);
}
NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr)
{
const MWWorld::Class &cls = ptr.getClass();
NpcAnimation::NpcType curType = Type_Normal;
if (cls.getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0)
curType = Type_Vampire;
if (cls.getNpcStats(ptr).isWerewolf())
curType = Type_Werewolf;
return curType;
}
static const inline NpcAnimation::PartBoneMap createPartListMap()
{
return {
{ESM::PRT_Head, "Head"},
{ESM::PRT_Hair, "Head"}, // note it uses "Head" as attach bone, but "Hair" as filter
{ESM::PRT_Neck, "Neck"},
{ESM::PRT_Cuirass, "Chest"},
{ESM::PRT_Groin, "Groin"},
{ESM::PRT_Skirt, "Groin"},
{ESM::PRT_RHand, "Right Hand"},
{ESM::PRT_LHand, "Left Hand"},
{ESM::PRT_RWrist, "Right Wrist"},
{ESM::PRT_LWrist, "Left Wrist"},
{ESM::PRT_Shield, "Shield Bone"},
{ESM::PRT_RForearm, "Right Forearm"},
{ESM::PRT_LForearm, "Left Forearm"},
{ESM::PRT_RUpperarm, "Right Upper Arm"},
{ESM::PRT_LUpperarm, "Left Upper Arm"},
{ESM::PRT_RFoot, "Right Foot"},
{ESM::PRT_LFoot, "Left Foot"},
{ESM::PRT_RAnkle, "Right Ankle"},
{ESM::PRT_LAnkle, "Left Ankle"},
{ESM::PRT_RKnee, "Right Knee"},
{ESM::PRT_LKnee, "Left Knee"},
{ESM::PRT_RLeg, "Right Upper Leg"},
{ESM::PRT_LLeg, "Left Upper Leg"},
{ESM::PRT_RPauldron, "Right Clavicle"},
{ESM::PRT_LPauldron, "Left Clavicle"},
{ESM::PRT_Weapon, "Weapon Bone"}, // Fallback. The real node name depends on the current weapon type.
{ESM::PRT_Tail, "Tail"}};
}
const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap();
NpcAnimation::~NpcAnimation()
{
mAmmunition.reset();
}
NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem,
bool disableSounds, ViewMode viewMode, float firstPersonFieldOfView)
: ActorAnimation(ptr, parentNode, resourceSystem),
mViewMode(viewMode),
mShowWeapons(false),
mShowCarriedLeft(true),
mNpcType(getNpcType(ptr)),
mFirstPersonFieldOfView(firstPersonFieldOfView),
mSoundsDisabled(disableSounds),
mAccurateAiming(false),
mAimingFactor(0.f)
{
mNpc = mPtr.get<ESM::NPC>()->mBase;
mHeadAnimationTime = std::make_shared<HeadAnimationTime>(mPtr);
mWeaponAnimationTime = std::make_shared<WeaponAnimationTime>(this);
for(size_t i = 0;i < ESM::PRT_Count;i++)
{
mPartslots[i] = -1; //each slot is empty
mPartPriorities[i] = 0;
}
std::fill(mSounds.begin(), mSounds.end(), nullptr);
updateNpcBase();
}
void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode)
{
assert(viewMode != VM_HeadOnly);
if(mViewMode == viewMode)
return;
mViewMode = viewMode;
MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale(), true); // apply race height after view change
mAmmunition.reset();
rebuild();
setRenderBin();
}
/// @brief A RenderBin callback to clear the depth buffer before rendering.
/// Switches depth attachments to a proxy renderbuffer, reattaches original depth then redraws first person root.
/// This gives a complete depth buffer which can be used for postprocessing, buffer resolves as if depth was never cleared.
class DepthClearCallback : public osgUtil::RenderBin::DrawCallback
{
public:
DepthClearCallback()
{
mDepth = new SceneUtil::AutoDepth;
mDepth->setWriteMask(true);
mStateSet = new osg::StateSet;
mStateSet->setAttributeAndModes(new osg::ColorMask(false, false, false, false), osg::StateAttribute::ON);
mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE);
}
void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override
{
osg::State* state = renderInfo.getState();
PostProcessor* postProcessor = dynamic_cast<PostProcessor*>(renderInfo.getCurrentCamera()->getUserData());
state->applyAttribute(mDepth);
unsigned int frameId = state->getFrameStamp()->getFrameNumber() % 2;
if (postProcessor && postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId))
{
postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// color accumulation pass
bin->drawImplementation(renderInfo, previous);
auto primaryFBO = postProcessor->getPrimaryFbo(frameId);
if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId))
postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state);
else
primaryFBO->apply(*state);
// depth accumulation pass
osg::ref_ptr<osg::StateSet> restore = bin->getStateSet();
bin->setStateSet(mStateSet);
bin->drawImplementation(renderInfo, previous);
bin->setStateSet(restore);
if (postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId))
primaryFBO->apply(*state);
}
else
{
// fallback to standard depth clear when we are not rendering our main scene via an intermediate FBO
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
bin->drawImplementation(renderInfo, previous);
}
state->checkGLErrors("after DepthClearCallback::drawImplementation");
}
osg::ref_ptr<osg::Depth> mDepth;
osg::ref_ptr<osg::StateSet> mStateSet;
};
/// Overrides Field of View to given value for rendering the subgraph.
/// Must be added as cull callback.
class OverrideFieldOfViewCallback : public osg::NodeCallback
{
public:
OverrideFieldOfViewCallback(float fov)
: mFov(fov)
{
}
void operator()(osg::Node* node, osg::NodeVisitor* nv) override
{
osgUtil::CullVisitor* cv = static_cast<osgUtil::CullVisitor*>(nv);
float fov, aspect, zNear, zFar;
if (cv->getProjectionMatrix()->getPerspective(fov, aspect, zNear, zFar) && std::abs(fov-mFov) > 0.001)
{
fov = mFov;
osg::ref_ptr<osg::RefMatrix> newProjectionMatrix = new osg::RefMatrix();
newProjectionMatrix->makePerspective(fov, aspect, zNear, zFar);
osg::ref_ptr<osg::RefMatrix> invertedOldMatrix = cv->getProjectionMatrix();
invertedOldMatrix = new osg::RefMatrix(osg::RefMatrix::inverse(*invertedOldMatrix));
osg::ref_ptr<osg::RefMatrix> viewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix());
viewMatrix->postMult(*newProjectionMatrix);
viewMatrix->postMult(*invertedOldMatrix);
cv->pushModelViewMatrix(viewMatrix, osg::Transform::ReferenceFrame::ABSOLUTE_RF);
traverse(node, nv);
cv->popModelViewMatrix();
}
else
traverse(node, nv);
}
private:
float mFov;
};
void NpcAnimation::setRenderBin()
{
if (mViewMode == VM_FirstPerson)
{
static bool prototypeAdded = false;
if (!prototypeAdded)
{
osg::ref_ptr<osgUtil::RenderBin> depthClearBin (new osgUtil::RenderBin);
depthClearBin->setDrawCallback(new DepthClearCallback);
osgUtil::RenderBin::addRenderBinPrototype("DepthClear", depthClearBin);
prototypeAdded = true;
}
mObjectRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_FirstPerson, "DepthClear", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS);
}
else if (osg::StateSet* stateset = mObjectRoot->getStateSet())
stateset->setRenderBinToInherit();
}
void NpcAnimation::rebuild()
{
mScabbard.reset();
mHolsteredShield.reset();
updateNpcBase();
MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr);
}
int NpcAnimation::getSlot(const osg::NodePath &path) const
{
for (int i=0; i<ESM::PRT_Count; ++i)
{
const PartHolder* const part = mObjectParts[i].get();
if (part == nullptr)
continue;
if (std::find(path.begin(), path.end(), part->getNode().get()) != path.end())
{
return mPartslots[i];
}
}
return -1;
}
void NpcAnimation::updateNpcBase()
{
clearAnimSources();
for(size_t i = 0;i < ESM::PRT_Count;i++)
removeIndividualPart((ESM::PartReferenceType)i);
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const ESM::Race *race = store.get<ESM::Race>().find(mNpc->mRace);
NpcType curType = getNpcType();
bool isWerewolf = (curType == Type_Werewolf);
bool isVampire = (curType == Type_Vampire);
bool isFemale = !mNpc->isMale();
mHeadModel.clear();
mHairModel.clear();
std::string headName = isWerewolf ? "WerewolfHead" : mNpc->mHead;
std::string hairName = isWerewolf ? "WerewolfHair" : mNpc->mHair;
if (!headName.empty())
{
const ESM::BodyPart* bp = store.get<ESM::BodyPart>().search(headName);
if (bp)
mHeadModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS());
else
Log(Debug::Warning) << "Warning: Failed to load body part '" << headName << "'";
}
if (!hairName.empty())
{
const ESM::BodyPart* bp = store.get<ESM::BodyPart>().search(hairName);
if (bp)
mHairModel = Misc::ResourceHelpers::correctMeshPath(bp->mModel, mResourceSystem->getVFS());
else
Log(Debug::Warning) << "Warning: Failed to load body part '" << hairName << "'";
}
const std::string vampireHead = getVampireHead(mNpc->mRace, isFemale, *mResourceSystem->getVFS());
if (!isWerewolf && isVampire && !vampireHead.empty())
mHeadModel = vampireHead;
bool is1stPerson = mViewMode == VM_FirstPerson;
bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0;
std::string defaultSkeleton = SceneUtil::getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf);
defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath(defaultSkeleton, mResourceSystem->getVFS());
std::string smodel = defaultSkeleton;
if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty())
smodel = Misc::ResourceHelpers::correctActorModelPath(
Misc::ResourceHelpers::correctMeshPath(mNpc->mModel, mResourceSystem->getVFS()), mResourceSystem->getVFS());
setObjectRoot(smodel, true, true, false);
updateParts();
if(!is1stPerson)
{
const std::string base = Settings::Manager::getString("xbaseanim", "Models");
if (smodel != base && !isWerewolf)
addAnimSource(base, smodel);
if (smodel != defaultSkeleton && base != defaultSkeleton)
addAnimSource(defaultSkeleton, smodel);
addAnimSource(smodel, smodel);
if(!isWerewolf && Misc::StringUtils::lowerCase(mNpc->mRace).find("argonian") != std::string::npos)
addAnimSource("meshes\\xargonian_swimkna.nif", smodel);
}
else
{
const std::string base = Settings::Manager::getString("xbaseanim1st", "Models");
if (smodel != base && !isWerewolf)
addAnimSource(base, smodel);
addAnimSource(smodel, smodel);
mObjectRoot->setNodeMask(Mask_FirstPerson);
mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView));
}
mWeaponAnimationTime->updateStartTime();
}
std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const
{
std::string mesh = getShieldMesh(shield, !mNpc->isMale());
if (mesh.empty())
return std::string();
std::string holsteredName = mesh;
holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif");
if(mResourceSystem->getVFS()->exists(holsteredName))
{
osg::ref_ptr<osg::Node> shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName);
SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath");
shieldTemplate->accept(findVisitor);
osg::ref_ptr<osg::Node> sheathNode = findVisitor.mFoundNode;
if(!sheathNode)
return std::string();
}
return mesh;
}
void NpcAnimation::updateParts()
{
if (!mObjectRoot.get())
return;
NpcType curType = getNpcType();
if (curType != mNpcType)
{
mNpcType = curType;
rebuild();
return;
}
static const struct {
int mSlot;
int mBasePriority;
} slotlist[] = {
// FIXME: Priority is based on the number of reserved slots. There should be a better way.
{ MWWorld::InventoryStore::Slot_Robe, 11 },
{ MWWorld::InventoryStore::Slot_Skirt, 3 },
{ MWWorld::InventoryStore::Slot_Helmet, 0 },
{ MWWorld::InventoryStore::Slot_Cuirass, 0 },
{ MWWorld::InventoryStore::Slot_Greaves, 0 },
{ MWWorld::InventoryStore::Slot_LeftPauldron, 0 },
{ MWWorld::InventoryStore::Slot_RightPauldron, 0 },
{ MWWorld::InventoryStore::Slot_Boots, 0 },
{ MWWorld::InventoryStore::Slot_LeftGauntlet, 0 },
{ MWWorld::InventoryStore::Slot_RightGauntlet, 0 },
{ MWWorld::InventoryStore::Slot_Shirt, 0 },
{ MWWorld::InventoryStore::Slot_Pants, 0 },
{ MWWorld::InventoryStore::Slot_CarriedLeft, 0 },
{ MWWorld::InventoryStore::Slot_CarriedRight, 0 }
};
static const size_t slotlistsize = sizeof(slotlist)/sizeof(slotlist[0]);
bool wasArrowAttached = isArrowAttached();
mAmmunition.reset();
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
for(size_t i = 0;i < slotlistsize && mViewMode != VM_HeadOnly;i++)
{
MWWorld::ConstContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot);
removePartGroup(slotlist[i].mSlot);
if(store == inv.end())
continue;
if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Helmet)
removeIndividualPart(ESM::PRT_Hair);
int prio = 1;
bool enchantedGlow = !store->getClass().getEnchantment(*store).empty();
osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store);
if(store->getType() == ESM::Clothing::sRecordId)
{
prio = ((slotlist[i].mBasePriority+1)<<1) + 0;
const ESM::Clothing *clothes = store->get<ESM::Clothing>()->mBase;
addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor);
}
else if(store->getType() == ESM::Armor::sRecordId)
{
prio = ((slotlist[i].mBasePriority+1)<<1) + 1;
const ESM::Armor *armor = store->get<ESM::Armor>()->mBase;
addPartGroup(slotlist[i].mSlot, prio, armor->mParts.mParts, enchantedGlow, &glowColor);
}
if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Robe)
{
ESM::PartReferenceType parts[] = {
ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg,
ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee,
ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_Cuirass
};
size_t parts_size = sizeof(parts)/sizeof(parts[0]);
for(size_t p = 0;p < parts_size;++p)
reserveIndividualPart(parts[p], slotlist[i].mSlot, prio);
}
else if(slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Skirt)
{
reserveIndividualPart(ESM::PRT_Groin, slotlist[i].mSlot, prio);
reserveIndividualPart(ESM::PRT_RLeg, slotlist[i].mSlot, prio);
reserveIndividualPart(ESM::PRT_LLeg, slotlist[i].mSlot, prio);
}
}
if(mViewMode != VM_FirstPerson)
{
if(mPartPriorities[ESM::PRT_Head] < 1 && !mHeadModel.empty())
addOrReplaceIndividualPart(ESM::PRT_Head, -1,1, mHeadModel);
if(mPartPriorities[ESM::PRT_Hair] < 1 && mPartPriorities[ESM::PRT_Head] <= 1 && !mHairModel.empty())
addOrReplaceIndividualPart(ESM::PRT_Hair, -1,1, mHairModel);
}
if(mViewMode == VM_HeadOnly)
return;
if(mPartPriorities[ESM::PRT_Shield] < 1)
{
MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
MWWorld::ConstPtr part;
if(store != inv.end() && (part=*store).getType() == ESM::Light::sRecordId)
{
const ESM::Light *light = part.get<ESM::Light>()->mBase;
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1,
Misc::ResourceHelpers::correctMeshPath(light->mModel, vfs), false, nullptr, true);
if (mObjectParts[ESM::PRT_Shield])
addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light);
}
}
showWeapons(mShowWeapons);
showCarriedLeft(mShowCarriedLeft);
bool isWerewolf = (getNpcType() == Type_Werewolf);
std::string race = (isWerewolf ? "werewolf" : Misc::StringUtils::lowerCase(mNpc->mRace));
const std::vector<const ESM::BodyPart*> &parts = getBodyParts(race, !mNpc->isMale(), mViewMode == VM_FirstPerson, isWerewolf);
for(int part = ESM::PRT_Neck; part < ESM::PRT_Count; ++part)
{
if(mPartPriorities[part] < 1)
{
const ESM::BodyPart* bodypart = parts[part];
if(bodypart)
{
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
addOrReplaceIndividualPart(static_cast<ESM::PartReferenceType>(part), -1, 1,
Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs));
}
}
}
if (wasArrowAttached)
attachArrow();
}
PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight)
{
osg::ref_ptr<osg::Node> attached = attach(model, bonename, bonefilter, isLight);
if (enchantedGlow)
mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor);
return PartHolderPtr(new PartHolder(attached));
}
osg::Vec3f NpcAnimation::runAnimation(float timepassed)
{
osg::Vec3f ret = Animation::runAnimation(timepassed);
mHeadAnimationTime->update(timepassed);
if (mFirstPersonNeckController)
{
if (mAccurateAiming)
mAimingFactor = 1.f;
else
mAimingFactor = std::max(0.f, mAimingFactor - timepassed * 0.5f);
float rotateFactor = 0.75f + 0.25f * mAimingFactor;
mFirstPersonNeckController->setRotate(osg::Quat(mPtr.getRefData().getPosition().rot[0] * rotateFactor, osg::Vec3f(-1,0,0)));
mFirstPersonNeckController->setOffset(mFirstPersonOffset);
}
WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians());
return ret;
}
void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type)
{
mPartPriorities[type] = 0;
mPartslots[type] = -1;
mObjectParts[type].reset();
if (mSounds[type] != nullptr && !mSoundsDisabled)
{
MWBase::Environment::get().getSoundManager()->stopSound(mSounds[type]);
mSounds[type] = nullptr;
}
}
void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority)
{
if(priority > mPartPriorities[type])
{
removeIndividualPart(type);
mPartPriorities[type] = priority;
mPartslots[type] = group;
}
}
void NpcAnimation::removePartGroup(int group)
{
for(int i = 0; i < ESM::PRT_Count; i++)
{
if(mPartslots[i] == group)
removeIndividualPart((ESM::PartReferenceType)i);
}
}
bool NpcAnimation::isFirstPersonPart(const ESM::BodyPart* bodypart)
{
return bodypart->mId.size() >= 3 && bodypart->mId.substr(bodypart->mId.size()-3, 3) == "1st";
}
bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart)
{
return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female;
}
bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight)
{
if(priority <= mPartPriorities[type])
return false;
removeIndividualPart(type);
mPartslots[type] = group;
mPartPriorities[type] = priority;
try
{
std::string bonename = sPartList.at(type);
if (type == ESM::PRT_Weapon)
{
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
if(weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId)
{
int weaponType = weapon->get<ESM::Weapon>()->mBase->mData.mType;
const std::string weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone;
if (weaponBonename != bonename)
{
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(weaponBonename);
if (found != nodeMap.end())
bonename = weaponBonename;
}
}
}
// PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone
const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename;
mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight);
}
catch (std::exception& e)
{
Log(Debug::Error) << "Error adding NPC part: " << e.what();
return false;
}
if (!mSoundsDisabled)
{
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator csi = inv.getSlot(group < 0 ? MWWorld::InventoryStore::Slot_Helmet : group);
if (csi != inv.end())
{
const auto soundId = csi->getClass().getSound(*csi);
if (!soundId.empty())
{
mSounds[type] = MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, soundId,
1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop
);
}
}
}
osg::Node* node = mObjectParts[type]->getNode();
if (node->getNumChildrenRequiringUpdateTraversal() > 0)
{
std::shared_ptr<SceneUtil::ControllerSource> src;
if (type == ESM::PRT_Head)
{
src = mHeadAnimationTime;
if (node->getUserDataContainer())
{
for (unsigned int i=0; i<node->getUserDataContainer()->getNumUserObjects(); ++i)
{
osg::Object* obj = node->getUserDataContainer()->getUserObject(i);
if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast<SceneUtil::TextKeyMapHolder*>(obj))
{
for (const auto &key : keys->mTextKeys)
{
if (Misc::StringUtils::ciEqual(key.second, "talk: start"))
mHeadAnimationTime->setTalkStart(key.first);
if (Misc::StringUtils::ciEqual(key.second, "talk: stop"))
mHeadAnimationTime->setTalkStop(key.first);
if (Misc::StringUtils::ciEqual(key.second, "blink: start"))
mHeadAnimationTime->setBlinkStart(key.first);
if (Misc::StringUtils::ciEqual(key.second, "blink: stop"))
mHeadAnimationTime->setBlinkStop(key.first);
}
break;
}
}
}
SceneUtil::ForceControllerSourcesVisitor assignVisitor(src);
node->accept(assignVisitor);
}
else
{
if (type == ESM::PRT_Weapon)
src = mWeaponAnimationTime;
else
src = std::make_shared<NullAnimationTime>();
SceneUtil::AssignControllerSourcesVisitor assignVisitor(src);
node->accept(assignVisitor);
}
}
return true;
}
void NpcAnimation::addPartGroup(int group, int priority, const std::vector<ESM::PartReference> &parts, bool enchantedGlow, osg::Vec4f* glowColor)
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>();
const char *ext = (mViewMode == VM_FirstPerson) ? ".1st" : "";
for(const ESM::PartReference& part : parts)
{
const ESM::BodyPart *bodypart = nullptr;
if(!mNpc->isMale() && !part.mFemale.empty())
{
bodypart = partStore.search(part.mFemale+ext);
if(!bodypart && mViewMode == VM_FirstPerson)
{
bodypart = partStore.search(part.mFemale);
if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand ||
bodypart->mData.mPart == ESM::BodyPart::MP_Wrist ||
bodypart->mData.mPart == ESM::BodyPart::MP_Forearm ||
bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm))
bodypart = nullptr;
}
else if (!bodypart)
Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mFemale << "'";
}
if(!bodypart && !part.mMale.empty())
{
bodypart = partStore.search(part.mMale+ext);
if(!bodypart && mViewMode == VM_FirstPerson)
{
bodypart = partStore.search(part.mMale);
if(bodypart && !(bodypart->mData.mPart == ESM::BodyPart::MP_Hand ||
bodypart->mData.mPart == ESM::BodyPart::MP_Wrist ||
bodypart->mData.mPart == ESM::BodyPart::MP_Forearm ||
bodypart->mData.mPart == ESM::BodyPart::MP_Upperarm))
bodypart = nullptr;
}
else if (!bodypart)
Log(Debug::Warning) << "Warning: Failed to find body part '" << part.mMale << "'";
}
if(bodypart)
{
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
addOrReplaceIndividualPart(static_cast<ESM::PartReferenceType>(part.mPart), group, priority,
Misc::ResourceHelpers::correctMeshPath(bodypart->mModel, vfs), enchantedGlow, glowColor);
}
else
reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority);
}
}
void NpcAnimation::addControllers()
{
Animation::addControllers();
mFirstPersonNeckController = nullptr;
WeaponAnimation::deleteControllers();
if (mViewMode == VM_FirstPerson)
{
NodeMap::iterator found = mNodeMap.find("bip01 neck");
if (found != mNodeMap.end())
{
osg::MatrixTransform* node = found->second.get();
mFirstPersonNeckController = new RotateController(mObjectRoot.get());
node->addUpdateCallback(mFirstPersonNeckController);
mActiveControllers.emplace_back(node, mFirstPersonNeckController);
}
}
else if (mViewMode == VM_Normal)
{
WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get());
}
}
void NpcAnimation::showWeapons(bool showWeapon)
{
mShowWeapons = showWeapon;
mAmmunition.reset();
if(showWeapon)
{
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
if(weapon != inv.end())
{
osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon);
std::string mesh = weapon->getClass().getModel(*weapon);
addOrReplaceIndividualPart(ESM::PRT_Weapon, MWWorld::InventoryStore::Slot_CarriedRight, 1,
mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor);
// Crossbows start out with a bolt attached
if (weapon->getType() == ESM::Weapon::sRecordId &&
weapon->get<ESM::Weapon>()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow)
{
int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType;
MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition);
if (ammo != inv.end() && ammo->get<ESM::Weapon>()->mBase->mData.mType == ammotype)
attachArrow();
}
}
}
else
{
removeIndividualPart(ESM::PRT_Weapon);
// If we remove/hide weapon from player, we should reset attack animation as well
if (mPtr == MWMechanics::getPlayer())
mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(false);
}
updateHolsteredWeapon(!mShowWeapons);
updateQuiver();
}
void NpcAnimation::showCarriedLeft(bool show)
{
mShowCarriedLeft = show;
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if(show && iter != inv.end())
{
osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter);
std::string mesh = iter->getClass().getModel(*iter);
// For shields we must try to use the body part model
if (iter->getType() == ESM::Armor::sRecordId)
{
mesh = getShieldMesh(*iter, !mNpc->isMale());
}
if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1,
mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId))
{
if (mesh.empty())
reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1);
if (iter->getType() == ESM::Light::sRecordId && mObjectParts[ESM::PRT_Shield])
addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get<ESM::Light>()->mBase);
}
}
else
removeIndividualPart(ESM::PRT_Shield);
updateHolsteredShield(mShowCarriedLeft);
}
void NpcAnimation::attachArrow()
{
WeaponAnimation::attachArrow(mPtr);
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition);
if (ammo != inv.end() && !ammo->getClass().getEnchantment(*ammo).empty())
{
osg::Group* bone = getArrowBone();
if (bone != nullptr && bone->getNumChildren())
SceneUtil::addEnchantedGlow(bone->getChild(0), mResourceSystem, ammo->getClass().getEnchantmentColor(*ammo));
}
updateQuiver();
}
void NpcAnimation::detachArrow()
{
WeaponAnimation::detachArrow(mPtr);
updateQuiver();
}
void NpcAnimation::releaseArrow(float attackStrength)
{
WeaponAnimation::releaseArrow(mPtr, attackStrength);
updateQuiver();
}
osg::Group* NpcAnimation::getArrowBone()
{
const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get();
if (part == nullptr)
return nullptr;
const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId)
return nullptr;
int type = weapon->get<ESM::Weapon>()->mBase->mData.mType;
int ammoType = MWMechanics::getWeaponType(type)->mAmmoType;
if (ammoType == ESM::Weapon::None)
return nullptr;
// Try to find and attachment bone in actor's skeleton, otherwise fall back to the ArrowBone in weapon's mesh
osg::Group* bone = getBoneByName(MWMechanics::getWeaponType(ammoType)->mAttachBone);
if (bone == nullptr)
{
SceneUtil::FindByNameVisitor findVisitor ("ArrowBone");
part->getNode()->accept(findVisitor);
bone = findVisitor.mFoundNode;
}
return bone;
}
osg::Node* NpcAnimation::getWeaponNode()
{
const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get();
if (part == nullptr)
return nullptr;
return part->getNode();
}
Resource::ResourceSystem* NpcAnimation::getResourceSystem()
{
return mResourceSystem;
}
void NpcAnimation::enableHeadAnimation(bool enable)
{
mHeadAnimationTime->setEnabled(enable);
}
void NpcAnimation::setWeaponGroup(const std::string &group, bool relativeDuration)
{
mWeaponAnimationTime->setGroup(group, relativeDuration);
}
void NpcAnimation::equipmentChanged()
{
static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game");
if (shieldSheathing)
{
int weaptype = ESM::Weapon::None;
MWMechanics::getActiveWeapon(mPtr, &weaptype);
showCarriedLeft(updateCarriedLeftVisible(weaptype));
}
updateParts();
}
void NpcAnimation::setVampire(bool vampire)
{
if (mNpcType == Type_Werewolf) // we can't have werewolf vampires, can we
return;
if ((mNpcType == Type_Vampire) != vampire)
{
if (mPtr == MWMechanics::getPlayer())
MWBase::Environment::get().getWorld()->reattachPlayerCamera();
else
rebuild();
}
}
void NpcAnimation::setFirstPersonOffset(const osg::Vec3f &offset)
{
mFirstPersonOffset = offset;
}
void NpcAnimation::updatePtr(const MWWorld::Ptr &updated)
{
Animation::updatePtr(updated);
mHeadAnimationTime->updatePtr(updated);
}
// Remember body parts so we only have to search through the store once for each race/gender/viewmode combination
typedef std::map< std::pair<std::string,int>,std::vector<const ESM::BodyPart*> > RaceMapping;
static RaceMapping sRaceMapping;
const std::vector<const ESM::BodyPart *>& NpcAnimation::getBodyParts(const std::string &race, bool female, bool firstPerson, bool werewolf)
{
static const int Flag_FirstPerson = 1<<1;
static const int Flag_Female = 1<<0;
int flags = (werewolf ? -1 : 0);
if(female)
flags |= Flag_Female;
if(firstPerson)
flags |= Flag_FirstPerson;
RaceMapping::iterator found = sRaceMapping.find(std::make_pair(race, flags));
if (found != sRaceMapping.end())
return found->second;
else
{
std::vector<const ESM::BodyPart*>& parts = sRaceMapping[std::make_pair(race, flags)];
typedef std::multimap<ESM::BodyPart::MeshPart,ESM::PartReferenceType> BodyPartMapType;
static const BodyPartMapType sBodyPartMap =
{
{ESM::BodyPart::MP_Neck, ESM::PRT_Neck},
{ESM::BodyPart::MP_Chest, ESM::PRT_Cuirass},
{ESM::BodyPart::MP_Groin, ESM::PRT_Groin},
{ESM::BodyPart::MP_Hand, ESM::PRT_RHand},
{ESM::BodyPart::MP_Hand, ESM::PRT_LHand},
{ESM::BodyPart::MP_Wrist, ESM::PRT_RWrist},
{ESM::BodyPart::MP_Wrist, ESM::PRT_LWrist},
{ESM::BodyPart::MP_Forearm, ESM::PRT_RForearm},
{ESM::BodyPart::MP_Forearm, ESM::PRT_LForearm},
{ESM::BodyPart::MP_Upperarm, ESM::PRT_RUpperarm},
{ESM::BodyPart::MP_Upperarm, ESM::PRT_LUpperarm},
{ESM::BodyPart::MP_Foot, ESM::PRT_RFoot},
{ESM::BodyPart::MP_Foot, ESM::PRT_LFoot},
{ESM::BodyPart::MP_Ankle, ESM::PRT_RAnkle},
{ESM::BodyPart::MP_Ankle, ESM::PRT_LAnkle},
{ESM::BodyPart::MP_Knee, ESM::PRT_RKnee},
{ESM::BodyPart::MP_Knee, ESM::PRT_LKnee},
{ESM::BodyPart::MP_Upperleg, ESM::PRT_RLeg},
{ESM::BodyPart::MP_Upperleg, ESM::PRT_LLeg},
{ESM::BodyPart::MP_Tail, ESM::PRT_Tail}
};
parts.resize(ESM::PRT_Count, nullptr);
if (werewolf)
return parts;
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
for(const ESM::BodyPart& bodypart : store.get<ESM::BodyPart>())
{
if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable)
continue;
if (bodypart.mData.mType != ESM::BodyPart::MT_Skin)
continue;
if (!Misc::StringUtils::ciEqual(bodypart.mRace, race))
continue;
bool partFirstPerson = isFirstPersonPart(&bodypart);
bool isHand = bodypart.mData.mPart == ESM::BodyPart::MP_Hand ||
bodypart.mData.mPart == ESM::BodyPart::MP_Wrist ||
bodypart.mData.mPart == ESM::BodyPart::MP_Forearm ||
bodypart.mData.mPart == ESM::BodyPart::MP_Upperarm;
bool isSameGender = isFemalePart(&bodypart) == female;
/* A fallback for the arms if 1st person is missing:
1. Try to use 3d person skin for same gender
2. Try to use 1st person skin for male, if female == true
3. Try to use 3d person skin for male, if female == true
A fallback in another cases: allow to use male bodyparts, if female == true
*/
if (firstPerson && isHand && !partFirstPerson)
{
// Allow 3rd person skins as a fallback for the arms if 1st person is missing
BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart));
while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart)
{
// If we have no fallback bodypart now and bodypart is for same gender (1)
if(!parts[bIt->second] && isSameGender)
parts[bIt->second] = &bodypart;
// If we have fallback bodypart for other gender and found fallback for current gender (1)
else if(isSameGender && isFemalePart(parts[bIt->second]) != female)
parts[bIt->second] = &bodypart;
// If we have no fallback bodypart and searching for female bodyparts (3)
else if(!parts[bIt->second] && female)
parts[bIt->second] = &bodypart;
++bIt;
}
continue;
}
// Don't allow to use podyparts for a different view
if (partFirstPerson != firstPerson)
continue;
if (female && !isFemalePart(&bodypart))
{
// Allow male parts as fallback for females if female parts are missing
BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart));
while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart)
{
// If we have no fallback bodypart now
if(!parts[bIt->second])
parts[bIt->second] = &bodypart;
// If we have 3d person fallback bodypart for hand and 1st person fallback found (2)
else if(isHand && !isFirstPersonPart(parts[bIt->second]) && partFirstPerson)
parts[bIt->second] = &bodypart;
++bIt;
}
continue;
}
// Don't allow to use podyparts for another gender
if (female != isFemalePart(&bodypart))
continue;
// Use properly found bodypart, replacing fallbacks
BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart));
while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart)
{
parts[bIt->second] = &bodypart;
++bIt;
}
}
return parts;
}
}
void NpcAnimation::setAccurateAiming(bool enabled)
{
mAccurateAiming = enabled;
}
bool NpcAnimation::isArrowAttached() const
{
return mAmmunition != nullptr;
}
}