diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 27c98bbce..5de4c197e 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -418,10 +418,10 @@ namespace MWClass { const MWWorld::LiveCellRef *ref = ptr.get(); - std::string model = "meshes\\base_anim.nif"; + std::string model = Settings::Manager::getString("baseanim", "Models"); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); if(race->mData.mFlags & ESM::Race::Beast) - model = "meshes\\base_animkna.nif"; + model = Settings::Manager::getString("baseanimkna", "Models"); return model; } @@ -431,12 +431,12 @@ namespace MWClass const MWWorld::LiveCellRef *npc = ptr.get(); const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().search(npc->mBase->mRace); if(race && race->mData.mFlags & ESM::Race::Beast) - models.emplace_back("meshes\\base_animkna.nif"); + models.emplace_back(Settings::Manager::getString("baseanimkna", "Models")); // keep these always loaded just in case - models.emplace_back("meshes/xargonian_swimkna.nif"); - models.emplace_back("meshes/xbase_anim_female.nif"); - models.emplace_back("meshes/xbase_anim.nif"); + models.emplace_back(Settings::Manager::getString("xargonianswimkna", "Models")); + models.emplace_back(Settings::Manager::getString("xbaseanimfemale", "Models")); + models.emplace_back(Settings::Manager::getString("xbaseanim", "Models")); if (!npc->mBase->mModel.empty()) models.push_back("meshes/"+npc->mBase->mModel); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index f8ff3780d..d8c759935 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1500,7 +1500,7 @@ namespace MWRender MWWorld::LiveCellRef *ref = mPtr.get(); if(ref->mBase->mFlags & ESM::Creature::Bipedal) { - defaultSkeleton = "meshes\\xbase_anim.nif"; + defaultSkeleton = Settings::Manager::getString("xbaseanim", "Models"); inject = true; } } diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index f1df6c90f..298711162 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -10,7 +10,7 @@ #include #include #include - +#include #include #include "../mwbase/environment.hpp" @@ -35,7 +35,7 @@ CreatureAnimation::CreatureAnimation(const MWWorld::Ptr &ptr, setObjectRoot(model, false, false, true); if((ref->mBase->mFlags&ESM::Creature::Bipedal)) - addAnimSource("meshes\\xbase_anim.nif", model); + addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); addAnimSource(model, model); } } @@ -54,7 +54,7 @@ CreatureWeaponAnimation::CreatureWeaponAnimation(const MWWorld::Ptr &ptr, const if((ref->mBase->mFlags&ESM::Creature::Bipedal)) { - addAnimSource("meshes\\xbase_anim.nif", model); + addAnimSource(Settings::Manager::getString("xbaseanim", "Models"), model); } addAnimSource(model, model); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index d97e57115..b538f0b7b 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -524,7 +524,7 @@ void NpcAnimation::updateNpcBase() if(!is1stPerson) { - const std::string base = "meshes\\xbase_anim.nif"; + const std::string base = Settings::Manager::getString("xbaseanim", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); @@ -538,7 +538,7 @@ void NpcAnimation::updateNpcBase() } else { - const std::string base = "meshes\\xbase_anim.1st.nif"; + const std::string base = Settings::Manager::getString("xbaseanim1st", "Models"); if (smodel != base && !isWerewolf) addAnimSource(base, smodel); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 6ba4baec5..a3aee5c0f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -456,12 +456,15 @@ namespace MWRender mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); - const char* basemodels[] = {"xbase_anim", "xbase_anim.1st", "xbase_anim_female", "xbase_animkna"}; - for (size_t i=0; imModels.push_back(std::string("meshes/") + basemodels[i] + ".nif"); - workItem->mKeyframes.push_back(std::string("meshes/") + basemodels[i] + ".kf"); - } + workItem->mModels.push_back(Settings::Manager::getString("xbaseanim", "Models")); + workItem->mModels.push_back(Settings::Manager::getString("xbaseanim1st", "Models")); + workItem->mModels.push_back(Settings::Manager::getString("xbaseanimfemale", "Models")); + workItem->mModels.push_back(Settings::Manager::getString("xargonianswimkna", "Models")); + + workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimkf", "Models")); + workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanim1stkf", "Models")); + workItem->mKeyframes.push_back(Settings::Manager::getString("xbaseanimfemalekf", "Models")); + workItem->mKeyframes.push_back(Settings::Manager::getString("xargonianswimknakf", "Models")); workItem->mTextures.emplace_back("textures/_land_default.dds"); diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index d1da9090d..ad37eda0d 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -145,11 +146,31 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & osg::ref_ptr constNode (mSceneManager->getTemplate(normalized)); osg::ref_ptr node (const_cast(constNode.get())); // const-trickery required because there is no const version of NodeVisitor - NodeToShapeVisitor visitor; - node->accept(visitor); - shape = visitor.getShape(); + + // Check first if there's a custom collision node + unsigned int visitAllNodesMask = 0xffffffff; + SceneUtil::FindByNameVisitor nameFinder("Collision"); + nameFinder.setTraversalMask(visitAllNodesMask); + nameFinder.setNodeMaskOverride(visitAllNodesMask); + node->accept(nameFinder); + if (nameFinder.mFoundNode) + { + NodeToShapeVisitor visitor; + visitor.setTraversalMask(visitAllNodesMask); + visitor.setNodeMaskOverride(visitAllNodesMask); + nameFinder.mFoundNode->accept(visitor); + shape = visitor.getShape(); + } + + // Generate a collision shape from the mesh if (!shape) - return osg::ref_ptr(); + { + NodeToShapeVisitor visitor; + node->accept(visitor); + shape = visitor.getShape(); + if (!shape) + return osg::ref_ptr(); + } } mCache->addEntryToObjectCache(normalized, shape); diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index d739392e8..0c33428e0 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "animation.hpp" #include "objectcache.hpp" @@ -17,11 +18,13 @@ namespace Resource { - RetrieveAnimationsVisitor::RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mTarget(target), mAnimationManager(animationManager) {} + RetrieveAnimationsVisitor::RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager, + const std::string& normalized, const VFS::Manager* vfs) : + osg::NodeVisitor(TRAVERSE_ALL_CHILDREN), mTarget(target), mAnimationManager(animationManager), mNormalized(normalized), mVFS(vfs) {} void RetrieveAnimationsVisitor::apply(osg::Node& node) { - if (node.libraryName() == std::string("osgAnimation") && node.className() == std::string("Bone") && node.getName() == std::string("bip01")) + if (node.libraryName() == std::string("osgAnimation") && node.className() == std::string("Bone") && Misc::StringUtils::lowerCase(node.getName()) == std::string("bip01")) { osg::ref_ptr callback = new SceneUtil::OsgAnimationController(); @@ -38,27 +41,19 @@ namespace Resource osg::ref_ptr mergedAnimationTrack = new Resource::Animation; std::string animationName = animation->getName(); - std::string start = animationName + std::string(": start"); - std::string stop = animationName + std::string(": stop"); + mergedAnimationTrack->setName(animationName); const osgAnimation::ChannelList& channels = animation->getChannels(); for (const auto& channel: channels) { mergedAnimationTrack->addChannel(channel.get()->clone()); // is ->clone needed? } - mergedAnimationTrack->setName(animation->getName()); + callback->addMergedAnimationTrack(mergedAnimationTrack); float startTime = animation->getStartTime(); float stopTime = startTime + animation->getDuration(); - // mTextKeys is a nif-thing, used by OpenMW's animation system - // Format is likely "AnimationName: [Keyword_optional] [Start OR Stop]" - // AnimationNames are keywords like idle2, idle3... AiPackages and various mechanics control which animations are played - // Keywords can be stuff like Loop, Equip, Unequip, Block, InventoryHandtoHand, InventoryWeaponOneHand, PickProbe, Slash, Thrust, Chop... even "Slash Small Follow" - mTarget.mTextKeys.emplace(startTime, std::move(start)); - mTarget.mTextKeys.emplace(stopTime, std::move(stop)); - SceneUtil::EmulatedAnimation emulatedAnimation; emulatedAnimation.mStartTime = startTime; emulatedAnimation.mStopTime = stopTime; @@ -66,12 +61,61 @@ namespace Resource emulatedAnimations.emplace_back(emulatedAnimation); } } + + // mTextKeys is a nif-thing, used by OpenMW's animation system + // Format is likely "AnimationName: [Keyword_optional] [Start OR Stop]" + // AnimationNames are keywords like idle2, idle3... AiPackages and various mechanics control which animations are played + // Keywords can be stuff like Loop, Equip, Unequip, Block, InventoryHandtoHand, InventoryWeaponOneHand, PickProbe, Slash, Thrust, Chop... even "Slash Small Follow" + // osgAnimation formats should have a .txt file with the same name, each line holding a textkey and whitespace separated time value + // e.g. idle: start 0.0333 + try + { + Files::IStreamPtr textKeysFile = mVFS->get(changeFileExtension(mNormalized, "txt")); + std::string line; + while ( getline (*textKeysFile, line) ) + { + mTarget.mTextKeys.emplace(parseTimeSignature(line), std::move(parseTextKey(line))); + } + } + catch (std::exception& e) + { + Log(Debug::Warning) << "No textkey file found for " << mNormalized; + } + callback->setEmulatedAnimations(emulatedAnimations); mTarget.mKeyframeControllers.emplace(node.getName(), callback); } traverse(node); } + + std::string RetrieveAnimationsVisitor::parseTextKey(const std::string& line) + { + size_t spacePos = line.find_last_of(' '); + if (spacePos != std::string::npos) + return line.substr(0, spacePos); + return ""; + } + + double RetrieveAnimationsVisitor::parseTimeSignature(const std::string& line) + { + size_t spacePos = line.find_last_of(' '); + double time = 0.0; + if (spacePos != std::string::npos && spacePos + 1 < line.size()) + time = std::stod(line.substr(spacePos + 1)); + return time; + } + + std::string RetrieveAnimationsVisitor::changeFileExtension(const std::string file, const std::string ext) + { + size_t extPos = file.find_last_of('.'); + if (extPos != std::string::npos && extPos+1 < file.size()) + { + return file.substr(0, extPos + 1) + ext; + } + return file; + } + } namespace Resource @@ -109,7 +153,7 @@ namespace Resource osg::ref_ptr bam = dynamic_cast (scene->getUpdateCallback()); if (bam) { - Resource::RetrieveAnimationsVisitor rav(*loaded.get(), bam); + Resource::RetrieveAnimationsVisitor rav(*loaded.get(), bam, normalized, mVFS); scene->accept(rav); } } diff --git a/components/resource/keyframemanager.hpp b/components/resource/keyframemanager.hpp index 3e992ac5e..87a20b97a 100644 --- a/components/resource/keyframemanager.hpp +++ b/components/resource/keyframemanager.hpp @@ -15,13 +15,21 @@ namespace Resource class RetrieveAnimationsVisitor : public osg::NodeVisitor { public: - RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager); + RetrieveAnimationsVisitor(SceneUtil::KeyframeHolder& target, osg::ref_ptr animationManager, + const std::string& normalized, const VFS::Manager* vfs); virtual void apply(osg::Node& node) override; private: + + std::string changeFileExtension(const std::string file, const std::string ext); + std::string parseTextKey(const std::string& line); + double parseTimeSignature(const std::string& line); + SceneUtil::KeyframeHolder& mTarget; osg::ref_ptr mAnimationManager; + std::string mNormalized; + const VFS::Manager* mVFS; }; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 71f11e382..19cc96433 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -373,6 +374,14 @@ namespace Resource errormsg << "Error loading " << normalizedFilename << ": " << result.message() << " code " << result.status() << std::endl; throw std::runtime_error(errormsg.str()); } + + // Recognize and hide collision node + unsigned int hiddenNodeMask = 0; + SceneUtil::FindByNameVisitor nameFinder("Collision"); + result.getNode()->accept(nameFinder); + if (nameFinder.mFoundNode) + nameFinder.mFoundNode->setNodeMask(hiddenNodeMask); + return result.getNode(); } } @@ -390,7 +399,8 @@ namespace Resource { const char* reserved[] = {"Head", "Neck", "Chest", "Groin", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield Bone", "Right Forearm", "Left Forearm", "Right Upper Arm", "Left Upper Arm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Upper Leg", "Left Upper Leg", "Right Clavicle", - "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera"}; + "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera", "Collision", "Right_Wrist", "Left_Wrist", + "Shield_Bone", "Right_Forearm", "Left_Forearm", "Right_Upper_Arm", "Left_Clavicle", "Weapon_Bone", "Root_Bone"}; reservedNames = std::vector(reserved, reserved + sizeof(reserved)/sizeof(reserved[0])); diff --git a/components/sceneutil/actorutil.cpp b/components/sceneutil/actorutil.cpp index 988a61f60..a0785e413 100644 --- a/components/sceneutil/actorutil.cpp +++ b/components/sceneutil/actorutil.cpp @@ -1,5 +1,7 @@ #include "actorutil.hpp" +#include + namespace SceneUtil { std::string getActorSkeleton(bool firstPerson, bool isFemale, bool isBeast, bool isWerewolf) @@ -7,24 +9,24 @@ namespace SceneUtil if (!firstPerson) { if (isWerewolf) - return "meshes\\wolf\\skin.nif"; + return Settings::Manager::getString("wolfskin", "Models"); else if (isBeast) - return "meshes\\base_animkna.nif"; + return Settings::Manager::getString("baseanimkna", "Models"); else if (isFemale) - return "meshes\\base_anim_female.nif"; + return Settings::Manager::getString("baseanimfemale", "Models"); else - return "meshes\\base_anim.nif"; + return Settings::Manager::getString("baseanim", "Models"); } else { if (isWerewolf) - return "meshes\\wolf\\skin.1st.nif"; + return Settings::Manager::getString("wolfskin1st", "Models"); else if (isBeast) - return "meshes\\base_animkna.1st.nif"; + return Settings::Manager::getString("baseanimkna1st", "Models"); else if (isFemale) - return "meshes\\base_anim_female.1st.nif"; + return Settings::Manager::getString("baseanimfemale1st", "Models"); else - return "meshes\\base_anim.1st.nif"; + return Settings::Manager::getString("xbaseanim1st", "Models"); } } } diff --git a/components/sceneutil/osgacontroller.cpp b/components/sceneutil/osgacontroller.cpp index 37aa525af..b2d819117 100644 --- a/components/sceneutil/osgacontroller.cpp +++ b/components/sceneutil/osgacontroller.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -83,7 +84,7 @@ namespace SceneUtil { osgAnimation::UpdateMatrixTransform* umt = dynamic_cast(cb); if (umt) - if (node.getName() != "bip01") link(umt); + if (Misc::StringUtils::lowerCase(node.getName()) != "bip01") link(umt); cb = cb->getNestedCallback(); } @@ -102,10 +103,14 @@ namespace SceneUtil } OsgAnimationController::OsgAnimationController(const OsgAnimationController ©, const osg::CopyOp ©op) : SceneUtil::KeyframeController(copy, copyop) - , mMergedAnimationTracks(copy.mMergedAnimationTracks) , mEmulatedAnimations(copy.mEmulatedAnimations) { mLinker = nullptr; + for (const auto& mergedAnimationTrack : copy.mMergedAnimationTracks) + { + Resource::Animation* copiedAnimationTrack = static_cast(mergedAnimationTrack.get()->clone(copyop)); + mMergedAnimationTracks.emplace_back(copiedAnimationTrack); + } } osg::Vec3f OsgAnimationController::getTranslation(float time) const diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index 00d18ffcb..60f99b2fe 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -60,7 +60,20 @@ namespace SceneUtil void NodeMapVisitor::apply(osg::MatrixTransform& trans) { // Take transformation for first found node in file - const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName()); + std::string originalNodeName = Misc::StringUtils::lowerCase(trans.getName()); + + if (trans.libraryName() == std::string("osgAnimation")) + { + // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses whitespace-separated names) + std::string underscore = "_"; + std::size_t foundUnderscore = originalNodeName.find(underscore); + + if (foundUnderscore != std::string::npos) + std::replace(originalNodeName.begin(), originalNodeName.end(), '_', ' '); + } + + const std::string nodeName = originalNodeName; + mMap.emplace(nodeName, &trans); traverse(trans); diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 3660f56f0..67d944d19 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -955,6 +955,51 @@ defer aabb update = true # Loading arbitrary meshes is not advised and may cause instability. load unsupported nif files = false +# 3rd person base animation model that looks also for the corresponding kf-file +xbaseanim = meshes/xbase_anim.nif + +# 3rd person base model with textkeys-data +baseanim = meshes/base_anim.nif + +# 1st person base animation model that looks also for corresponding kf-file +xbaseanim1st = meshes/xbase_anim.1st.nif + +# 3rd person beast race base model with textkeys-data +baseanimkna = meshes/base_animkna.nif + +# 1st person beast race base animation model +baseanimkna1st = meshes/base_animkna.1st.nif + +# 3rd person female base animation model +xbaseanimfemale = meshes/xbase_anim_female.nif + +# 3rd person female base model with textkeys-data +baseanimfemale = meshes/base_anim_female.nif + +# 1st person female base model with textkeys-data +baseanimfemale1st = meshes/base_anim_female.1st.nif + +# 3rd person werewolf skin +wolfskin = meshes/wolf/skin.nif + +# 1st person werewolf skin +wolfskin1st = meshes/wolf/skin.1st.nif + +# Argonian smimkna +xargonianswimkna = meshes/xargonian_swimkna.nif + +# File to load xbaseanim 3rd person animations +xbaseanimkf = meshes/xbase_anim.kf + +# File to load xbaseanim 3rd person animations +xbaseanim1stkf = meshes/xbase_anim.1st.kf + +# File to load xbaseanim animations from +xbaseanimfemalekf = meshes/xbase_anim_female.kf + +# File to load xargonianswimkna animations from +xargonianswimknakf = meshes/xargonian_swimkna.kf + [Groundcover] # enable separate groundcover handling