#include "nifloader.hpp" #include #include #include #include #include #include #include #include #include #include // resource #include #include #include #include #include #include #include #include // particle #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "matrixtransform.hpp" #include "particle.hpp" namespace { struct DisableOptimizer : osg::NodeVisitor { DisableOptimizer(osg::NodeVisitor::TraversalMode mode = TRAVERSE_ALL_CHILDREN) : osg::NodeVisitor(mode) {} void apply(osg::Node &node) override { node.setDataVariance(osg::Object::DYNAMIC); traverse(node); } void apply(osg::Drawable &node) override { traverse(node); } }; void getAllNiNodes(const Nif::Node* node, std::vector& outIndices) { if (const Nif::NiNode* ninode = dynamic_cast(node)) { outIndices.push_back(ninode->recIndex); for (unsigned int i=0; ichildren.length(); ++i) if (!ninode->children[i].empty()) getAllNiNodes(ninode->children[i].getPtr(), outIndices); } } bool isTypeGeometry(int type) { switch (type) { case Nif::RC_NiTriShape: case Nif::RC_NiTriStrips: case Nif::RC_NiLines: case Nif::RC_BSLODTriShape: return true; } return false; } // Collect all properties affecting the given drawable that should be handled on drawable basis rather than on the node hierarchy above it. void collectDrawableProperties(const Nif::Node* nifNode, const Nif::Parent* parent, std::vector& out) { if (parent != nullptr) collectDrawableProperties(&parent->mNiNode, parent->mParent, out); const Nif::PropertyList& props = nifNode->props; for (size_t i = 0; i recType) { case Nif::RC_NiMaterialProperty: case Nif::RC_NiVertexColorProperty: case Nif::RC_NiSpecularProperty: case Nif::RC_NiAlphaProperty: out.push_back(props[i].getPtr()); break; default: break; } } } auto geometry = dynamic_cast(nifNode); if (geometry) { if (!geometry->shaderprop.empty()) out.emplace_back(geometry->shaderprop.getPtr()); if (!geometry->alphaprop.empty()) out.emplace_back(geometry->alphaprop.getPtr()); } } // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale // set just like a regular MatrixTransform, but the rotation set will be overridden in order to face the camera. class BillboardCallback : public SceneUtil::NodeCallback { public: BillboardCallback() { } BillboardCallback(const BillboardCallback& copy, const osg::CopyOp& copyop) : SceneUtil::NodeCallback(copy, copyop) { } META_Object(NifOsg, BillboardCallback) void operator()(osg::Node* node, osgUtil::CullVisitor* cv) { osg::Matrix modelView = *cv->getModelViewMatrix(); // attempt to preserve scale float mag[3]; for (int i=0;i<3;++i) { mag[i] = std::sqrt(modelView(0,i) * modelView(0,i) + modelView(1,i) * modelView(1,i) + modelView(2,i) * modelView(2,i)); } modelView.setRotate(osg::Quat()); modelView(0,0) = mag[0]; modelView(1,1) = mag[1]; modelView(2,2) = mag[2]; cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); traverse(node, cv); cv->popModelViewMatrix(); } }; void extractTextKeys(const Nif::NiTextKeyExtraData *tk, SceneUtil::TextKeyMap &textkeys) { for(size_t i = 0;i < tk->list.size();i++) { std::vector results; Misc::StringUtils::split(tk->list[i].text, results, "\r\n"); for (std::string &result : results) { Misc::StringUtils::trim(result); Misc::StringUtils::lowerCaseInPlace(result); if (!result.empty()) textkeys.emplace(tk->list[i].time, std::move(result)); } } } } namespace NifOsg { bool Loader::sShowMarkers = false; void Loader::setShowMarkers(bool show) { sShowMarkers = show; } bool Loader::getShowMarkers() { return sShowMarkers; } unsigned int Loader::sHiddenNodeMask = 0; void Loader::setHiddenNodeMask(unsigned int mask) { sHiddenNodeMask = mask; } unsigned int Loader::getHiddenNodeMask() { return sHiddenNodeMask; } unsigned int Loader::sIntersectionDisabledNodeMask = ~0u; void Loader::setIntersectionDisabledNodeMask(unsigned int mask) { sIntersectionDisabledNodeMask = mask; } unsigned int Loader::getIntersectionDisabledNodeMask() { return sIntersectionDisabledNodeMask; } class LoaderImpl { public: /// @param filename used for warning messages. LoaderImpl(const std::filesystem::path& filename, unsigned int ver, unsigned int userver, unsigned int bethver) : mFilename(filename), mVersion(ver), mUserVersion(userver), mBethVersion(bethver) { } std::filesystem::path mFilename; unsigned int mVersion, mUserVersion, mBethVersion; size_t mFirstRootTextureIndex{~0u}; bool mFoundFirstRootTexturingProperty = false; bool mHasNightDayLabel = false; bool mHasHerbalismLabel = false; bool mHasStencilProperty = false; const Nif::NiSortAdjustNode* mPushedSorter = nullptr; const Nif::NiSortAdjustNode* mLastAppliedNoInheritSorter = nullptr; // This is used to queue emitters that weren't attached to their node yet. std::vector>> mEmitterQueue; void loadKf(Nif::NIFFilePtr nif, SceneUtil::KeyframeHolder& target) const { const Nif::NiSequenceStreamHelper *seq = nullptr; const size_t numRoots = nif->numRoots(); for (size_t i = 0; i < numRoots; ++i) { const Nif::Record *r = nif->getRoot(i); if (r && r->recType == Nif::RC_NiSequenceStreamHelper) { seq = static_cast(r); break; } } if (!seq) { nif->warn("Found no NiSequenceStreamHelper root record"); return; } Nif::ExtraPtr extra = seq->extra; if(extra.empty() || extra->recType != Nif::RC_NiTextKeyExtraData) { nif->warn("First extra data was not a NiTextKeyExtraData, but a "+ (extra.empty() ? std::string("nil") : extra->recName)+"."); return; } extractTextKeys(static_cast(extra.getPtr()), target.mTextKeys); extra = extra->next; Nif::ControllerPtr ctrl = seq->controller; for(;!extra.empty() && !ctrl.empty();(extra=extra->next),(ctrl=ctrl->next)) { if(extra->recType != Nif::RC_NiStringExtraData || ctrl->recType != Nif::RC_NiKeyframeController) { nif->warn("Unexpected extra data "+extra->recName+" with controller "+ctrl->recName); continue; } // Vanilla seems to ignore the "active" flag for NiKeyframeController, // so we don't want to skip inactive controllers here. const Nif::NiStringExtraData *strdata = static_cast(extra.getPtr()); const Nif::NiKeyframeController *key = static_cast(ctrl.getPtr()); if (key->mData.empty() && key->mInterpolator.empty()) continue; if (!key->mInterpolator.empty() && key->mInterpolator->recType != Nif::RC_NiTransformInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiKeyframeController " << key->recIndex << " in " << mFilename; continue; } osg::ref_ptr callback = new NifOsg::KeyframeController(key); setupController(key, callback, /*animflags*/0); if (!target.mKeyframeControllers.emplace(strdata->string, callback).second) Log(Debug::Verbose) << "Controller " << strdata->string << " present more than once in " << nif->getFilename() << ", ignoring later version"; } } osg::ref_ptr load(Nif::NIFFilePtr nif, Resource::ImageManager* imageManager) { const size_t numRoots = nif->numRoots(); std::vector roots; for (size_t i = 0; i < numRoots; ++i) { const Nif::Record* r = nif->getRoot(i); if (!r) continue; const Nif::Node* nifNode = dynamic_cast(r); if (nifNode) roots.emplace_back(nifNode); } if (roots.empty()) nif->fail("Found no root nodes"); osg::ref_ptr textkeys (new SceneUtil::TextKeyMapHolder); osg::ref_ptr created(new osg::Group); created->setDataVariance(osg::Object::STATIC); for (const Nif::Node* root : roots) { auto node = handleNode(root, nullptr, nullptr, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); created->addChild(node); } if (mHasNightDayLabel) created->getOrCreateUserDataContainer()->addDescription(Constants::NightDayLabel); if (mHasHerbalismLabel) created->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel); // Attach particle emitters to their nodes which should all be loaded by now. handleQueuedParticleEmitters(created, nif); if (nif->getUseSkinning()) { osg::ref_ptr skel = new SceneUtil::Skeleton; skel->setStateSet(created->getStateSet()); skel->setName(created->getName()); for (unsigned int i=0; i < created->getNumChildren(); ++i) skel->addChild(created->getChild(i)); created->removeChildren(0, created->getNumChildren()); created = skel; } if (!textkeys->mTextKeys.empty()) created->getOrCreateUserDataContainer()->addUserObject(textkeys); created->setUserValue(Misc::OsgUserValues::sFileHash, nif->getHash()); return created; } void applyNodeProperties(const Nif::Node *nifNode, osg::Node *applyTo, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { const Nif::PropertyList& props = nifNode->props; bool hasStencilProperty = false; for (size_t i = 0; i recType == Nif::RC_NiStencilProperty) { const Nif::NiStencilProperty* stencilprop = static_cast(props[i].getPtr()); if (stencilprop->data.enabled != 0) { hasStencilProperty = true; break; } } } for (size_t i = 0; i parents.empty() && !mFoundFirstRootTexturingProperty && props[i].getPtr()->recType == Nif::RC_NiTexturingProperty) { mFirstRootTextureIndex = props[i].getPtr()->recIndex; mFoundFirstRootTexturingProperty = true; } else if (props[i].getPtr()->recType == Nif::RC_NiTexturingProperty) { if (props[i].getPtr()->recIndex == mFirstRootTextureIndex) applyTo->setUserValue("overrideFx", 1); } handleProperty(props[i].getPtr(), applyTo, composite, imageManager, boundTextures, animflags, hasStencilProperty); } } auto geometry = dynamic_cast(nifNode); // NiGeometry's NiAlphaProperty doesn't get handled here because it's a drawable property if (geometry && !geometry->shaderprop.empty()) handleProperty(geometry->shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags, hasStencilProperty); } static void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags) { bool autoPlay = animflags & Nif::NiNode::AnimFlag_AutoPlay; if (autoPlay) toSetup->setSource(std::make_shared()); toSetup->setFunction(std::make_shared(ctrl)); } static osg::ref_ptr handleLodNode(const Nif::NiLODNode* niLodNode) { osg::ref_ptr lod (new osg::LOD); lod->setName(niLodNode->name); lod->setCenterMode(osg::LOD::USER_DEFINED_CENTER); lod->setCenter(niLodNode->lodCenter); for (unsigned int i=0; ilodLevels.size(); ++i) { const Nif::NiLODNode::LODRange& range = niLodNode->lodLevels[i]; lod->setRange(i, range.minRange, range.maxRange); } lod->setRangeMode(osg::LOD::DISTANCE_FROM_EYE_POINT); return lod; } static osg::ref_ptr handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode) { osg::ref_ptr switchNode (new osg::Switch); switchNode->setName(niSwitchNode->name); switchNode->setNewChildDefaultValue(false); switchNode->setSingleChildOn(niSwitchNode->initialIndex); return switchNode; } static osg::ref_ptr prepareSequenceNode(const Nif::Node* nifNode) { const Nif::NiFltAnimationNode* niFltAnimationNode = static_cast(nifNode); osg::ref_ptr sequenceNode (new osg::Sequence); sequenceNode->setName(niFltAnimationNode->name); if (niFltAnimationNode->children.length()!=0) { if (niFltAnimationNode->swing()) sequenceNode->setDefaultTime(niFltAnimationNode->mDuration/(niFltAnimationNode->children.length()*2)); else sequenceNode->setDefaultTime(niFltAnimationNode->mDuration/niFltAnimationNode->children.length()); } return sequenceNode; } static void activateSequenceNode(osg::Group* osgNode, const Nif::Node* nifNode) { const Nif::NiFltAnimationNode* niFltAnimationNode = static_cast(nifNode); osg::Sequence* sequenceNode = static_cast(osgNode); if (niFltAnimationNode->swing()) sequenceNode->setInterval(osg::Sequence::SWING, 0,-1); else sequenceNode->setInterval(osg::Sequence::LOOP, 0,-1); sequenceNode->setDuration(1.0f, -1); sequenceNode->setMode(osg::Sequence::START); } osg::ref_ptr handleSourceTexture(const Nif::NiSourceTexture* st, Resource::ImageManager* imageManager) { if (!st) return nullptr; osg::ref_ptr image; if (!st->external && !st->data.empty()) { image = handleInternalTexture(st->data.getPtr()); } else { std::string filename = Misc::ResourceHelpers::correctTexturePath(st->filename, imageManager->getVFS()); image = imageManager->getImage(filename); } return image; } void handleEffect(const Nif::Node* nifNode, osg::Node* node, Resource::ImageManager* imageManager) { if (nifNode->recType != Nif::RC_NiTextureEffect) { Log(Debug::Info) << "Unhandled effect " << nifNode->recName << " in " << mFilename; return; } const Nif::NiTextureEffect* textureEffect = static_cast(nifNode); if (textureEffect->textureType != Nif::NiTextureEffect::Environment_Map) { Log(Debug::Info) << "Unhandled NiTextureEffect type " << textureEffect->textureType << " in " << mFilename; return; } if (textureEffect->texture.empty()) { Log(Debug::Info) << "NiTextureEffect missing source texture in " << mFilename; return; } osg::ref_ptr texGen (new osg::TexGen); switch (textureEffect->coordGenType) { case Nif::NiTextureEffect::World_Parallel: texGen->setMode(osg::TexGen::OBJECT_LINEAR); break; case Nif::NiTextureEffect::World_Perspective: texGen->setMode(osg::TexGen::EYE_LINEAR); break; case Nif::NiTextureEffect::Sphere_Map: texGen->setMode(osg::TexGen::SPHERE_MAP); break; default: Log(Debug::Info) << "Unhandled NiTextureEffect coordGenType " << textureEffect->coordGenType << " in " << mFilename; return; } osg::ref_ptr image (handleSourceTexture(textureEffect->texture.getPtr(), imageManager)); osg::ref_ptr texture2d (new osg::Texture2D(image)); if (image) texture2d->setTextureSize(image->s(), image->t()); texture2d->setName("envMap"); texture2d->setWrap(osg::Texture::WRAP_S, textureEffect->wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, textureEffect->wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); int texUnit = 3; // FIXME osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(texUnit, texGen, osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); } // Get a default dataVariance for this node to be used as a hint by optimization (post)routines osg::ref_ptr createNode(const Nif::Node* nifNode) { osg::ref_ptr node; osg::Object::DataVariance dataVariance = osg::Object::UNSPECIFIED; switch (nifNode->recType) { case Nif::RC_NiBillboardNode: dataVariance = osg::Object::DYNAMIC; break; default: // The Root node can be created as a Group if no transformation is required. // This takes advantage of the fact root nodes can't have additional controllers // loaded from an external .kf file (original engine just throws "can't find node" errors if you try). if (nifNode->parents.empty() && nifNode->controller.empty() && nifNode->trafo.isIdentity()) node = new osg::Group; dataVariance = nifNode->isBone ? osg::Object::DYNAMIC : osg::Object::STATIC; break; } if (!node) node = new NifOsg::MatrixTransform(nifNode->trafo); node->setDataVariance(dataVariance); return node; } osg::ref_ptr handleNode(const Nif::Node* nifNode, const Nif::Parent* parent, osg::Group* parentNode, Resource::ImageManager* imageManager, std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool hasAnimatedParents, SceneUtil::TextKeyMap* textKeys, osg::Node* rootNode=nullptr) { if (rootNode != nullptr && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box")) return nullptr; osg::ref_ptr node = createNode(nifNode); if (nifNode->recType == Nif::RC_NiBillboardNode) { node->addCullCallback(new BillboardCallback); } node->setName(nifNode->name); if (parentNode) parentNode->addChild(node); if (!rootNode) rootNode = node; // The original NIF record index is used for a variety of features: // - finding the correct emitter node for a particle system // - establishing connections to the animated collision shapes, which are handled in a separate loader // - finding a random child NiNode in NiBspArrayController node->setUserValue("recIndex", nifNode->recIndex); std::vector extraCollection; for (Nif::ExtraPtr e = nifNode->extra; !e.empty(); e = e->next) extraCollection.emplace_back(e); for (size_t i = 0; i < nifNode->extralist.length(); ++i) { Nif::ExtraPtr e = nifNode->extralist[i]; if (!e.empty()) extraCollection.emplace_back(e); } for (const auto& e : extraCollection) { if(e->recType == Nif::RC_NiTextKeyExtraData && textKeys) { const Nif::NiTextKeyExtraData *tk = static_cast(e.getPtr()); extractTextKeys(tk, *textKeys); } else if(e->recType == Nif::RC_NiStringExtraData) { const Nif::NiStringExtraData *sd = static_cast(e.getPtr()); constexpr std::string_view extraDataIdentifer = "omw:data"; // String markers may contain important information // affecting the entire subtree of this obj if (sd->string == "MRK" && !Loader::getShowMarkers()) { // Marker objects. These meshes are only visible in the editor. hasMarkers = true; } else if (sd->string == "BONE") { node->getOrCreateUserDataContainer()->addDescription("CustomBone"); } else if (sd->string.rfind(extraDataIdentifer, 0) == 0) { node->setUserValue(Misc::OsgUserValues::sExtraData, sd->string.substr(extraDataIdentifer.length())); } } } if (nifNode->recType == Nif::RC_NiBSAnimationNode || nifNode->recType == Nif::RC_NiBSParticleNode) animflags = nifNode->flags; if (nifNode->recType == Nif::RC_NiSortAdjustNode) { auto sortNode = static_cast(nifNode); if (sortNode->mSubSorter.empty()) { Log(Debug::Warning) << "Empty accumulator found in '" << nifNode->recName << "' node " << nifNode->recIndex; } else { if (mPushedSorter && !mPushedSorter->mSubSorter.empty() && mPushedSorter->mMode != Nif::NiSortAdjustNode::SortingMode_Inherit) mLastAppliedNoInheritSorter = mPushedSorter; mPushedSorter = sortNode; } } // Hide collision shapes, but don't skip the subgraph // We still need to animate the hidden bones so the physics system can access them if (nifNode->recType == Nif::RC_RootCollisionNode) { skipMeshes = true; node->setNodeMask(Loader::getHiddenNodeMask()); } // We can skip creating meshes for hidden nodes if they don't have a VisController that // might make them visible later if (nifNode->isHidden()) { bool hasVisController = false; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { hasVisController |= (ctrl->recType == Nif::RC_NiVisController); if (hasVisController) break; } if (!hasVisController) skipMeshes = true; // skip child meshes, but still create the child node hierarchy for animating collision shapes node->setNodeMask(Loader::getHiddenNodeMask()); } if (nifNode->recType == Nif::RC_NiCollisionSwitch && !nifNode->collisionActive()) node->setNodeMask(Loader::getIntersectionDisabledNodeMask()); osg::ref_ptr composite = new SceneUtil::CompositeStateSetUpdater; applyNodeProperties(nifNode, node, composite, imageManager, boundTextures, animflags); const bool isGeometry = isTypeGeometry(nifNode->recType); if (isGeometry && !skipMeshes) { const std::string nodeName = Misc::StringUtils::lowerCase(nifNode->name); static const std::string markerName = "tri editormarker"; static const std::string shadowName = "shadow"; static const std::string shadowName2 = "tri shadow"; const bool isMarker = hasMarkers && !nodeName.compare(0, markerName.size(), markerName); if (!isMarker && nodeName.compare(0, shadowName.size(), shadowName) && nodeName.compare(0, shadowName2.size(), shadowName2)) { Nif::NiSkinInstancePtr skin = static_cast(nifNode)->skin; if (skin.empty()) handleGeometry(nifNode, parent, node, composite, boundTextures, animflags); else handleSkinnedGeometry(nifNode, parent, node, composite, boundTextures, animflags); if (!nifNode->controller.empty()) handleMeshControllers(nifNode, node, composite, boundTextures, animflags); } } if (nifNode->recType == Nif::RC_NiParticles) handleParticleSystem(nifNode, parent, node, composite, animflags); if (composite->getNumControllers() > 0) { osg::Callback *cb = composite; if (composite->getNumControllers() == 1) cb = composite->getController(0); if (animflags & Nif::NiNode::AnimFlag_AutoPlay) node->addCullCallback(cb); else node->addUpdateCallback(cb); // have to remain as UpdateCallback so AssignControllerSourcesVisitor can find it. } bool isAnimated = false; handleNodeControllers(nifNode, node, animflags, isAnimated); hasAnimatedParents |= isAnimated; // Make sure empty nodes and animated shapes are not optimized away so the physics system can find them. if (isAnimated || (hasAnimatedParents && ((skipMeshes || hasMarkers) || isGeometry))) node->setDataVariance(osg::Object::DYNAMIC); // LOD and Switch nodes must be wrapped by a transform (the current node) to support transformations properly // and we need to attach their children to the osg::LOD/osg::Switch nodes // but we must return that transform to the caller of handleNode instead of the actual LOD/Switch nodes. osg::ref_ptr currentNode = node; if (nifNode->recType == Nif::RC_NiSwitchNode) { const Nif::NiSwitchNode* niSwitchNode = static_cast(nifNode); osg::ref_ptr switchNode = handleSwitchNode(niSwitchNode); node->addChild(switchNode); if (niSwitchNode->name == Constants::NightDayLabel) mHasNightDayLabel = true; else if (niSwitchNode->name == Constants::HerbalismLabel) mHasHerbalismLabel = true; currentNode = switchNode; } else if (nifNode->recType == Nif::RC_NiLODNode) { const Nif::NiLODNode* niLodNode = static_cast(nifNode); osg::ref_ptr lodNode = handleLodNode(niLodNode); node->addChild(lodNode); currentNode = lodNode; } else if (nifNode->recType == Nif::RC_NiFltAnimationNode) { osg::ref_ptr sequenceNode = prepareSequenceNode(nifNode); node->addChild(sequenceNode); currentNode = sequenceNode; } const Nif::NiNode *ninode = dynamic_cast(nifNode); if(ninode) { const Nif::NodeList &effects = ninode->effects; for (size_t i = 0; i < effects.length(); ++i) { if (!effects[i].empty()) handleEffect(effects[i].getPtr(), currentNode, imageManager); } const Nif::NodeList &children = ninode->children; const Nif::Parent currentParent {*ninode, parent}; for(size_t i = 0;i < children.length();++i) { if(!children[i].empty()) handleNode(children[i].getPtr(), ¤tParent, currentNode, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, hasAnimatedParents, textKeys, rootNode); } } if (nifNode->recType == Nif::RC_NiFltAnimationNode) activateSequenceNode(currentNode,nifNode); return node; } void handleMeshControllers(const Nif::Node *nifNode, osg::Node* node, SceneUtil::CompositeStateSetUpdater* composite, const std::vector &boundTextures, int animflags) { for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiUVController) { const Nif::NiUVController *niuvctrl = static_cast(ctrl.getPtr()); if (niuvctrl->data.empty()) continue; const unsigned int uvSet = niuvctrl->uvSet; std::set texUnits; // UVController should work only for textures which use a given UV Set, usually 0. for (unsigned int i=0; i uvctrl = new UVController(niuvctrl->data.getPtr(), texUnits); setupController(niuvctrl, uvctrl, animflags); composite->addController(uvctrl); } } } void handleNodeControllers(const Nif::Node* nifNode, osg::Node* node, int animflags, bool& isAnimated) { for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiKeyframeController) { const Nif::NiKeyframeController *key = static_cast(ctrl.getPtr()); if (key->mData.empty() && key->mInterpolator.empty()) continue; if (!key->mInterpolator.empty() && key->mInterpolator->recType != Nif::RC_NiTransformInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiKeyframeController " << key->recIndex << " in " << mFilename; continue; } osg::ref_ptr callback = new KeyframeController(key); setupController(key, callback, animflags); node->addUpdateCallback(callback); isAnimated = true; } else if (ctrl->recType == Nif::RC_NiPathController) { const Nif::NiPathController *path = static_cast(ctrl.getPtr()); if (path->posData.empty() || path->floatData.empty()) continue; osg::ref_ptr callback(new PathController(path)); setupController(path, callback, animflags); node->addUpdateCallback(callback); isAnimated = true; } else if (ctrl->recType == Nif::RC_NiVisController) { const Nif::NiVisController *visctrl = static_cast(ctrl.getPtr()); if (visctrl->mData.empty() && visctrl->mInterpolator.empty()) continue; if (!visctrl->mInterpolator.empty() && visctrl->mInterpolator->recType != Nif::RC_NiBoolInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiVisController " << visctrl->recIndex << " in " << mFilename; continue; } osg::ref_ptr callback(new VisController(visctrl, Loader::getHiddenNodeMask())); setupController(visctrl, callback, animflags); node->addUpdateCallback(callback); } else if (ctrl->recType == Nif::RC_NiRollController) { const Nif::NiRollController *rollctrl = static_cast(ctrl.getPtr()); if (rollctrl->mData.empty() && rollctrl->mInterpolator.empty()) continue; if (!rollctrl->mInterpolator.empty() && rollctrl->mInterpolator->recType != Nif::RC_NiFloatInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiRollController " << rollctrl->recIndex << " in " << mFilename; continue; } osg::ref_ptr callback = new RollController(rollctrl); setupController(rollctrl, callback, animflags); node->addUpdateCallback(callback); isAnimated = true; } else if (ctrl->recType == Nif::RC_NiGeomMorpherController || ctrl->recType == Nif::RC_NiParticleSystemController || ctrl->recType == Nif::RC_NiBSPArrayController || ctrl->recType == Nif::RC_NiUVController) { // These controllers are handled elsewhere } else Log(Debug::Info) << "Unhandled controller " << ctrl->recName << " on node " << nifNode->recIndex << " in " << mFilename; } } void handleMaterialControllers(const Nif::Property *materialProperty, SceneUtil::CompositeStateSetUpdater* composite, int animflags, const osg::Material* baseMaterial) { for (Nif::ControllerPtr ctrl = materialProperty->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiAlphaController) { const Nif::NiAlphaController* alphactrl = static_cast(ctrl.getPtr()); if (alphactrl->mData.empty() && alphactrl->mInterpolator.empty()) continue; if (!alphactrl->mInterpolator.empty() && alphactrl->mInterpolator->recType != Nif::RC_NiFloatInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiAlphaController " << alphactrl->recIndex << " in " << mFilename; continue; } osg::ref_ptr osgctrl = new AlphaController(alphactrl, baseMaterial); setupController(alphactrl, osgctrl, animflags); composite->addController(osgctrl); } else if (ctrl->recType == Nif::RC_NiMaterialColorController) { const Nif::NiMaterialColorController* matctrl = static_cast(ctrl.getPtr()); if (matctrl->mData.empty() && matctrl->mInterpolator.empty()) continue; auto targetColor = static_cast(matctrl->mTargetColor); if (mVersion <= Nif::NIFFile::NIFVersion::VER_MW && targetColor == MaterialColorController::TargetColor::Specular) continue; if (!matctrl->mInterpolator.empty() && matctrl->mInterpolator->recType != Nif::RC_NiPoint3Interpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiMaterialColorController " << matctrl->recIndex << " in " << mFilename; continue; } osg::ref_ptr osgctrl = new MaterialColorController(matctrl, baseMaterial); setupController(matctrl, osgctrl, animflags); composite->addController(osgctrl); } else Log(Debug::Info) << "Unexpected material controller " << ctrl->recType << " in " << mFilename; } } void handleTextureControllers(const Nif::Property *texProperty, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, osg::StateSet *stateset, int animflags) { for (Nif::ControllerPtr ctrl = texProperty->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiFlipController) { const Nif::NiFlipController* flipctrl = static_cast(ctrl.getPtr()); if (!flipctrl->mInterpolator.empty() && flipctrl->mInterpolator->recType != Nif::RC_NiFloatInterpolator) { Log(Debug::Error) << "Unsupported interpolator type for NiFlipController " << flipctrl->recIndex << " in " << mFilename; continue; } std::vector > textures; // inherit wrap settings from the target slot osg::Texture2D* inherit = dynamic_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXTURE)); osg::Texture2D::WrapMode wrapS = osg::Texture2D::REPEAT; osg::Texture2D::WrapMode wrapT = osg::Texture2D::REPEAT; if (inherit) { wrapS = inherit->getWrap(osg::Texture2D::WRAP_S); wrapT = inherit->getWrap(osg::Texture2D::WRAP_T); } for (unsigned int i=0; imSources.length(); ++i) { Nif::NiSourceTexturePtr st = flipctrl->mSources[i]; if (st.empty()) continue; osg::ref_ptr image (handleSourceTexture(st.getPtr(), imageManager)); osg::ref_ptr texture (new osg::Texture2D(image)); if (image) texture->setTextureSize(image->s(), image->t()); texture->setWrap(osg::Texture::WRAP_S, wrapS); texture->setWrap(osg::Texture::WRAP_T, wrapT); textures.push_back(texture); } osg::ref_ptr callback(new FlipController(flipctrl, textures)); setupController(ctrl.getPtr(), callback, animflags); composite->addController(callback); } else Log(Debug::Info) << "Unexpected texture controller " << ctrl->recName << " in " << mFilename; } } void handleParticlePrograms(Nif::NiParticleModifierPtr affectors, Nif::NiParticleModifierPtr colliders, osg::Group *attachTo, osgParticle::ParticleSystem* partsys, osgParticle::ParticleProcessor::ReferenceFrame rf) { osgParticle::ModularProgram* program = new osgParticle::ModularProgram; attachTo->addChild(program); program->setParticleSystem(partsys); program->setReferenceFrame(rf); for (; !affectors.empty(); affectors = affectors->next) { if (affectors->recType == Nif::RC_NiParticleGrowFade) { const Nif::NiParticleGrowFade *gf = static_cast(affectors.getPtr()); program->addOperator(new GrowFadeAffector(gf->growTime, gf->fadeTime)); } else if (affectors->recType == Nif::RC_NiGravity) { const Nif::NiGravity* gr = static_cast(affectors.getPtr()); program->addOperator(new GravityAffector(gr)); } else if (affectors->recType == Nif::RC_NiParticleColorModifier) { const Nif::NiParticleColorModifier *cl = static_cast(affectors.getPtr()); if (cl->data.empty()) continue; const Nif::NiColorData *clrdata = cl->data.getPtr(); program->addOperator(new ParticleColorAffector(clrdata)); } else if (affectors->recType == Nif::RC_NiParticleRotation) { // unused } else Log(Debug::Info) << "Unhandled particle modifier " << affectors->recName << " in " << mFilename; } for (; !colliders.empty(); colliders = colliders->next) { if (colliders->recType == Nif::RC_NiPlanarCollider) { const Nif::NiPlanarCollider* planarcollider = static_cast(colliders.getPtr()); program->addOperator(new PlanarCollider(planarcollider)); } else if (colliders->recType == Nif::RC_NiSphericalCollider) { const Nif::NiSphericalCollider* sphericalcollider = static_cast(colliders.getPtr()); program->addOperator(new SphericalCollider(sphericalcollider)); } else Log(Debug::Info) << "Unhandled particle collider " << colliders->recName << " in " << mFilename; } } // Load the initial state of the particle system, i.e. the initial particles and their positions, velocity and colors. void handleParticleInitialState(const Nif::Node* nifNode, ParticleSystem* partsys, const Nif::NiParticleSystemController* partctrl) { auto particleNode = static_cast(nifNode); if (particleNode->data.empty() || particleNode->data->recType != Nif::RC_NiParticlesData) { partsys->setQuota(partctrl->numParticles); return; } auto particledata = static_cast(particleNode->data.getPtr()); partsys->setQuota(particledata->numParticles); osg::BoundingBox box; int i=0; for (const auto& particle : partctrl->particles) { if (i++ >= particledata->activeCount) break; if (particle.lifespan <= 0) continue; if (particle.vertex >= particledata->vertices.size()) continue; ParticleAgeSetter particletemplate(std::max(0.f, particle.lifetime)); osgParticle::Particle* created = partsys->createParticle(&particletemplate); created->setLifeTime(particle.lifespan); // Note this position and velocity is not correct for a particle system with absolute reference frame, // which can not be done in this loader since we are not attached to the scene yet. Will be fixed up post-load in the SceneManager. created->setVelocity(particle.velocity); const osg::Vec3f& position = particledata->vertices[particle.vertex]; created->setPosition(position); created->setColorRange(osgParticle::rangev4(partctrl->color, partctrl->color)); created->setAlphaRange(osgParticle::rangef(1.f, 1.f)); float size = partctrl->size; if (particle.vertex < particledata->sizes.size()) size *= particledata->sizes[particle.vertex]; created->setSizeRange(osgParticle::rangef(size, size)); box.expandBy(osg::BoundingSphere(position, size)); } // radius may be used to force a larger bounding box box.expandBy(osg::BoundingSphere(osg::Vec3(0,0,0), particledata->radius)); partsys->setInitialBound(box); } osg::ref_ptr handleParticleEmitter(const Nif::NiParticleSystemController* partctrl) { std::vector targets; if (partctrl->recType == Nif::RC_NiBSPArrayController && !partctrl->emitAtVertex()) { getAllNiNodes(partctrl->emitter.getPtr(), targets); } osg::ref_ptr emitter = new Emitter(targets); osgParticle::ConstantRateCounter* counter = new osgParticle::ConstantRateCounter; if (partctrl->noAutoAdjust()) counter->setNumberOfParticlesPerSecondToCreate(partctrl->emitRate); else if (partctrl->lifetime == 0 && partctrl->lifetimeRandom == 0) counter->setNumberOfParticlesPerSecondToCreate(0); else counter->setNumberOfParticlesPerSecondToCreate(partctrl->numParticles / (partctrl->lifetime + partctrl->lifetimeRandom/2)); emitter->setCounter(counter); ParticleShooter* shooter = new ParticleShooter(partctrl->velocity - partctrl->velocityRandom*0.5f, partctrl->velocity + partctrl->velocityRandom*0.5f, partctrl->horizontalDir, partctrl->horizontalAngle, partctrl->verticalDir, partctrl->verticalAngle, partctrl->lifetime, partctrl->lifetimeRandom); emitter->setShooter(shooter); emitter->setFlags(partctrl->flags); if (partctrl->recType == Nif::RC_NiBSPArrayController && partctrl->emitAtVertex()) { emitter->setGeometryEmitterTarget(partctrl->emitter->recIndex); } else { osgParticle::BoxPlacer* placer = new osgParticle::BoxPlacer; placer->setXRange(-partctrl->offsetRandom.x() / 2.f, partctrl->offsetRandom.x() / 2.f); placer->setYRange(-partctrl->offsetRandom.y() / 2.f, partctrl->offsetRandom.y() / 2.f); placer->setZRange(-partctrl->offsetRandom.z() / 2.f, partctrl->offsetRandom.z() / 2.f); emitter->setPlacer(placer); } return emitter; } void handleQueuedParticleEmitters(osg::Group* rootNode, Nif::NIFFilePtr nif) { for (const auto& emitterPair : mEmitterQueue) { size_t recIndex = emitterPair.first; FindGroupByRecIndex findEmitterNode(recIndex); rootNode->accept(findEmitterNode); osg::Group* emitterNode = findEmitterNode.mFound; if (!emitterNode) { nif->warn("Failed to find particle emitter emitter node (node record index " + std::to_string(recIndex) + ")"); continue; } // Emitter attached to the emitter node. Note one side effect of the emitter using the CullVisitor is that hiding its node // actually causes the emitter to stop firing. Convenient, because MW behaves this way too! emitterNode->addChild(emitterPair.second); DisableOptimizer disableOptimizer; emitterNode->accept(disableOptimizer); } mEmitterQueue.clear(); } void handleParticleSystem(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags) { osg::ref_ptr partsys (new ParticleSystem); partsys->setSortMode(osgParticle::ParticleSystem::SORT_BACK_TO_FRONT); const Nif::NiParticleSystemController* partctrl = nullptr; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if(ctrl->recType == Nif::RC_NiParticleSystemController || ctrl->recType == Nif::RC_NiBSPArrayController) partctrl = static_cast(ctrl.getPtr()); } if (!partctrl) { Log(Debug::Info) << "No particle controller found in " << mFilename; return; } osgParticle::ParticleProcessor::ReferenceFrame rf = (animflags & Nif::NiNode::ParticleFlag_LocalSpace) ? osgParticle::ParticleProcessor::RELATIVE_RF : osgParticle::ParticleProcessor::ABSOLUTE_RF; // HACK: ParticleSystem has no setReferenceFrame method if (rf == osgParticle::ParticleProcessor::ABSOLUTE_RF) { partsys->getOrCreateUserDataContainer()->addDescription("worldspace"); } partsys->setParticleScaleReferenceFrame(osgParticle::ParticleSystem::LOCAL_COORDINATES); handleParticleInitialState(nifNode, partsys, partctrl); partsys->getDefaultParticleTemplate().setSizeRange(osgParticle::rangef(partctrl->size, partctrl->size)); partsys->getDefaultParticleTemplate().setColorRange(osgParticle::rangev4(partctrl->color, partctrl->color)); partsys->getDefaultParticleTemplate().setAlphaRange(osgParticle::rangef(1.f, 1.f)); if (!partctrl->emitter.empty()) { osg::ref_ptr emitter = handleParticleEmitter(partctrl); emitter->setParticleSystem(partsys); emitter->setReferenceFrame(osgParticle::ParticleProcessor::RELATIVE_RF); // The emitter node may not actually be handled yet, so let's delay attaching the emitter to a later moment. // If the emitter node is placed later than the particle node, it'll have a single frame delay in particle processing. // But that shouldn't be a game-breaking issue. mEmitterQueue.emplace_back(partctrl->emitter->recIndex, emitter); osg::ref_ptr callback(new ParticleSystemController(partctrl)); setupController(partctrl, callback, animflags); emitter->setUpdateCallback(callback); if (!(animflags & Nif::NiNode::ParticleFlag_AutoPlay)) { partsys->setFrozen(true); } // Due to odd code in the ParticleSystemUpdater, particle systems will not be updated in the first frame // So do that update manually osg::NodeVisitor nv; partsys->update(0.0, nv); } // affectors should be attached *after* the emitter in the scene graph for correct update order // attach to same node as the ParticleSystem, we need osgParticle Operators to get the correct // localToWorldMatrix for transforming to particle space handleParticlePrograms(partctrl->affectors, partctrl->colliders, parentNode, partsys.get(), rf); std::vector drawableProps; collectDrawableProperties(nifNode, parent, drawableProps); applyDrawableProperties(parentNode, drawableProps, composite, true, animflags); // particle system updater (after the emitters and affectors in the scene graph) // I think for correct culling needs to be *before* the ParticleSystem, though osg examples do it the other way osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; updater->addParticleSystem(partsys); parentNode->addChild(updater); osg::Node* toAttach = partsys.get(); if (rf == osgParticle::ParticleProcessor::RELATIVE_RF) parentNode->addChild(toAttach); else { osg::MatrixTransform* trans = new osg::MatrixTransform; trans->setUpdateCallback(new InverseWorldMatrix); trans->addChild(toAttach); parentNode->addChild(trans); } } void handleNiGeometryData(osg::Geometry *geometry, const Nif::NiGeometryData* data, const std::vector& boundTextures, const std::string& name) { const auto& vertices = data->vertices; const auto& normals = data->normals; const auto& colors = data->colors; if (!vertices.empty()) geometry->setVertexArray(new osg::Vec3Array(vertices.size(), vertices.data())); if (!normals.empty()) geometry->setNormalArray(new osg::Vec3Array(normals.size(), normals.data()), osg::Array::BIND_PER_VERTEX); if (!colors.empty()) geometry->setColorArray(new osg::Vec4Array(colors.size(), colors.data()), osg::Array::BIND_PER_VERTEX); const auto& uvlist = data->uvlist; int textureStage = 0; for (std::vector::const_iterator it = boundTextures.begin(); it != boundTextures.end(); ++it, ++textureStage) { unsigned int uvSet = *it; if (uvSet >= uvlist.size()) { Log(Debug::Verbose) << "Out of bounds UV set " << uvSet << " on shape \"" << name << "\" in " << mFilename; if (uvlist.empty()) continue; uvSet = 0; } geometry->setTexCoordArray(textureStage, new osg::Vec2Array(uvlist[uvSet].size(), uvlist[uvSet].data()), osg::Array::BIND_PER_VERTEX); } } void handleNiGeometry(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { const Nif::NiGeometry* niGeometry = static_cast(nifNode); if (niGeometry->data.empty()) return; const Nif::NiGeometryData* niGeometryData = niGeometry->data.getPtr(); if (niGeometry->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_BSLODTriShape) { if (niGeometryData->recType != Nif::RC_NiTriShapeData) return; auto triangles = static_cast(niGeometryData)->triangles; if (triangles.empty()) return; geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, triangles.size(), (unsigned short*)triangles.data())); } else if (niGeometry->recType == Nif::RC_NiTriStrips) { if (niGeometryData->recType != Nif::RC_NiTriStripsData) return; auto data = static_cast(niGeometryData); bool hasGeometry = false; for (const auto& strip : data->strips) { if (strip.size() < 3) continue; geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), reinterpret_cast(strip.data()))); hasGeometry = true; } if (!hasGeometry) return; } else if (niGeometry->recType == Nif::RC_NiLines) { if (niGeometryData->recType != Nif::RC_NiLinesData) return; auto data = static_cast(niGeometryData); const auto& line = data->lines; if (line.empty()) return; geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), reinterpret_cast(line.data()))); } handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->name); // osg::Material properties are handled here for two reasons: // - if there are no vertex colors, we need to disable colorMode. // - there are 3 "overlapping" nif properties that all affect the osg::Material, handling them // above the actual renderable would be tedious. std::vector drawableProps; collectDrawableProperties(nifNode, parent, drawableProps); applyDrawableProperties(parentNode, drawableProps, composite, !niGeometryData->colors.empty(), animflags); } void handleGeometry(const Nif::Node* nifNode, const Nif::Parent* parent, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { assert(isTypeGeometry(nifNode->recType)); osg::ref_ptr geom (new osg::Geometry); handleNiGeometry(nifNode, parent, geom, parentNode, composite, boundTextures, animflags); // If the record had no valid geometry data in it, early-out if (geom->empty()) return; osg::ref_ptr drawable; for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next) { if (!ctrl->isActive()) continue; if(ctrl->recType == Nif::RC_NiGeomMorpherController) { const Nif::NiGeomMorpherController* nimorphctrl = static_cast(ctrl.getPtr()); if (nimorphctrl->data.empty()) continue; drawable = handleMorphGeometry(nimorphctrl, geom, parentNode, composite, boundTextures, animflags); osg::ref_ptr morphctrl = new GeomMorpherController(nimorphctrl); setupController(ctrl.getPtr(), morphctrl, animflags); drawable->setUpdateCallback(morphctrl); break; } } if (!drawable.get()) drawable = geom; drawable->setName(nifNode->name); parentNode->addChild(drawable); } osg::ref_ptr handleMorphGeometry(const Nif::NiGeomMorpherController* morpher, osg::ref_ptr sourceGeometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { osg::ref_ptr morphGeom = new SceneUtil::MorphGeometry; morphGeom->setSourceGeometry(sourceGeometry); const std::vector& morphs = morpher->data.getPtr()->mMorphs; if (morphs.empty()) return morphGeom; if (morphs[0].mVertices.size() != static_cast(sourceGeometry->getVertexArray())->size()) return morphGeom; for (unsigned int i = 0; i < morphs.size(); ++i) morphGeom->addMorphTarget(new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f); return morphGeom; } void handleSkinnedGeometry(const Nif::Node *nifNode, const Nif::Parent* parent, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { assert(isTypeGeometry(nifNode->recType)); osg::ref_ptr geometry (new osg::Geometry); handleNiGeometry(nifNode, parent, geometry, parentNode, composite, boundTextures, animflags); if (geometry->empty()) return; osg::ref_ptr rig(new SceneUtil::RigGeometry); rig->setSourceGeometry(geometry); rig->setName(nifNode->name); // Assign bone weights osg::ref_ptr map (new SceneUtil::RigGeometry::InfluenceMap); const Nif::NiSkinInstance *skin = static_cast(nifNode)->skin.getPtr(); const Nif::NiSkinData *data = skin->data.getPtr(); const Nif::NodeList &bones = skin->bones; for(size_t i = 0;i < bones.length();i++) { std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->name); SceneUtil::RigGeometry::BoneInfluence influence; const std::vector &weights = data->bones[i].weights; for(size_t j = 0;j < weights.size();j++) { influence.mWeights.emplace_back(weights[j].vertex, weights[j].weight); } influence.mInvBindMatrix = data->bones[i].trafo.toMatrix(); influence.mBoundSphere = osg::BoundingSpheref(data->bones[i].boundSphereCenter, data->bones[i].boundSphereRadius); map->mData.emplace_back(boneName, influence); } rig->setInfluenceMap(map); parentNode->addChild(rig); } osg::BlendFunc::BlendFuncMode getBlendMode(int mode) { switch(mode) { case 0: return osg::BlendFunc::ONE; case 1: return osg::BlendFunc::ZERO; case 2: return osg::BlendFunc::SRC_COLOR; case 3: return osg::BlendFunc::ONE_MINUS_SRC_COLOR; case 4: return osg::BlendFunc::DST_COLOR; case 5: return osg::BlendFunc::ONE_MINUS_DST_COLOR; case 6: return osg::BlendFunc::SRC_ALPHA; case 7: return osg::BlendFunc::ONE_MINUS_SRC_ALPHA; case 8: return osg::BlendFunc::DST_ALPHA; case 9: return osg::BlendFunc::ONE_MINUS_DST_ALPHA; case 10: return osg::BlendFunc::SRC_ALPHA_SATURATE; default: Log(Debug::Info) << "Unexpected blend mode: "<< mode << " in " << mFilename; return osg::BlendFunc::SRC_ALPHA; } } osg::AlphaFunc::ComparisonFunction getTestMode(int mode) { switch (mode) { case 0: return osg::AlphaFunc::ALWAYS; case 1: return osg::AlphaFunc::LESS; case 2: return osg::AlphaFunc::EQUAL; case 3: return osg::AlphaFunc::LEQUAL; case 4: return osg::AlphaFunc::GREATER; case 5: return osg::AlphaFunc::NOTEQUAL; case 6: return osg::AlphaFunc::GEQUAL; case 7: return osg::AlphaFunc::NEVER; default: Log(Debug::Info) << "Unexpected blend mode: " << mode << " in " << mFilename; return osg::AlphaFunc::LEQUAL; } } osg::Stencil::Function getStencilFunction(int func) { switch (func) { case 0: return osg::Stencil::NEVER; case 1: return osg::Stencil::LESS; case 2: return osg::Stencil::EQUAL; case 3: return osg::Stencil::LEQUAL; case 4: return osg::Stencil::GREATER; case 5: return osg::Stencil::NOTEQUAL; case 6: return osg::Stencil::GEQUAL; case 7: return osg::Stencil::ALWAYS; default: Log(Debug::Info) << "Unexpected stencil function: " << func << " in " << mFilename; return osg::Stencil::NEVER; } } osg::Stencil::Operation getStencilOperation(int op) { switch (op) { case 0: return osg::Stencil::KEEP; case 1: return osg::Stencil::ZERO; case 2: return osg::Stencil::REPLACE; case 3: return osg::Stencil::INCR; case 4: return osg::Stencil::DECR; case 5: return osg::Stencil::INVERT; default: Log(Debug::Info) << "Unexpected stencil operation: " << op << " in " << mFilename; return osg::Stencil::KEEP; } } osg::ref_ptr handleInternalTexture(const Nif::NiPixelData* pixelData) { osg::ref_ptr image (new osg::Image); // Pixel row alignment, defining it to be consistent with OSG DDS plugin int packing = 1; GLenum pixelformat = 0; switch (pixelData->fmt) { case Nif::NiPixelData::NIPXFMT_RGB8: pixelformat = GL_RGB; break; case Nif::NiPixelData::NIPXFMT_RGBA8: pixelformat = GL_RGBA; break; case Nif::NiPixelData::NIPXFMT_PAL8: case Nif::NiPixelData::NIPXFMT_PALA8: pixelformat = GL_RED; // Each color is defined by a byte. break; case Nif::NiPixelData::NIPXFMT_BGR8: pixelformat = GL_BGR; break; case Nif::NiPixelData::NIPXFMT_BGRA8: pixelformat = GL_BGRA; break; case Nif::NiPixelData::NIPXFMT_DXT1: pixelformat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; packing = 2; break; case Nif::NiPixelData::NIPXFMT_DXT3: pixelformat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; packing = 4; break; case Nif::NiPixelData::NIPXFMT_DXT5: pixelformat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; packing = 4; break; default: Log(Debug::Info) << "Unhandled internal pixel format " << pixelData->fmt << " in " << mFilename; return nullptr; } if (pixelData->mipmaps.empty()) return nullptr; int width = 0; int height = 0; std::vector mipmapVector; for (unsigned int i=0; imipmaps.size(); ++i) { const Nif::NiPixelData::Mipmap& mip = pixelData->mipmaps[i]; size_t mipSize = osg::Image::computeImageSizeInBytes(mip.width, mip.height, 1, pixelformat, GL_UNSIGNED_BYTE, packing); if (mipSize + mip.dataOffset > pixelData->data.size()) { Log(Debug::Info) << "Internal texture's mipmap data out of bounds, ignoring texture"; return nullptr; } if (i != 0) mipmapVector.push_back(mip.dataOffset); else { width = mip.width; height = mip.height; } } if (width <= 0 || height <= 0) { Log(Debug::Info) << "Internal Texture Width and height must be non zero, ignoring texture"; return nullptr; } const std::vector& pixels = pixelData->data; switch (pixelData->fmt) { case Nif::NiPixelData::NIPXFMT_RGB8: case Nif::NiPixelData::NIPXFMT_RGBA8: case Nif::NiPixelData::NIPXFMT_BGR8: case Nif::NiPixelData::NIPXFMT_BGRA8: case Nif::NiPixelData::NIPXFMT_DXT1: case Nif::NiPixelData::NIPXFMT_DXT3: case Nif::NiPixelData::NIPXFMT_DXT5: { unsigned char* data = new unsigned char[pixels.size()]; memcpy(data, pixels.data(), pixels.size()); image->setImage(width, height, 1, pixelformat, pixelformat, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE, packing); break; } case Nif::NiPixelData::NIPXFMT_PAL8: case Nif::NiPixelData::NIPXFMT_PALA8: { if (pixelData->palette.empty() || pixelData->bpp != 8) { Log(Debug::Info) << "Palettized texture in " << mFilename << " is invalid, ignoring"; return nullptr; } pixelformat = pixelData->fmt == Nif::NiPixelData::NIPXFMT_PAL8 ? GL_RGB : GL_RGBA; // We're going to convert the indices that pixel data contains // into real colors using the palette. const auto& palette = pixelData->palette->colors; const int numChannels = pixelformat == GL_RGBA ? 4 : 3; unsigned char* data = new unsigned char[pixels.size() * numChannels]; unsigned char* pixel = data; for (unsigned char index : pixels) { memcpy(pixel, &palette[index], sizeof(unsigned char) * numChannels); pixel += numChannels; } for (unsigned int& offset : mipmapVector) offset *= numChannels; image->setImage(width, height, 1, pixelformat, pixelformat, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE, packing); break; } default: return nullptr; } image->setMipmapLevels(mipmapVector); image->flipVertical(); return image; } osg::ref_ptr createEmissiveTexEnv() { osg::ref_ptr texEnv(new osg::TexEnvCombine); // Sum the previous colour and the emissive colour. texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); // Keep the previous alpha. texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); return texEnv; } void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags) { if (!boundTextures.empty()) { // overriding a parent NiTexturingProperty, so remove what was previously bound for (unsigned int i=0; isetTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); boundTextures.clear(); } // If this loop is changed such that the base texture isn't guaranteed to end up in texture unit 0, the shadow casting shader will need to be updated accordingly. for (size_t i=0; itextures.size(); ++i) { if (texprop->textures[i].inUse || (i == Nif::NiTexturingProperty::BaseTexture && !texprop->controller.empty())) { switch(i) { //These are handled later on case Nif::NiTexturingProperty::BaseTexture: case Nif::NiTexturingProperty::GlowTexture: case Nif::NiTexturingProperty::DarkTexture: case Nif::NiTexturingProperty::BumpTexture: case Nif::NiTexturingProperty::DetailTexture: case Nif::NiTexturingProperty::DecalTexture: case Nif::NiTexturingProperty::GlossTexture: break; default: { Log(Debug::Info) << "Unhandled texture stage " << i << " on shape \"" << nodeName << "\" in " << mFilename; continue; } } unsigned int uvSet = 0; // create a new texture, will later attempt to share using the SharedStateManager osg::ref_ptr texture2d; if (texprop->textures[i].inUse) { const Nif::NiTexturingProperty::Texture& tex = texprop->textures[i]; if(tex.texture.empty() && texprop->controller.empty()) { if (i == 0) Log(Debug::Warning) << "Base texture is in use but empty on shape \"" << nodeName << "\" in " << mFilename; continue; } if (!tex.texture.empty()) { const Nif::NiSourceTexture *st = tex.texture.getPtr(); osg::ref_ptr image = handleSourceTexture(st, imageManager); texture2d = new osg::Texture2D(image); if (image) texture2d->setTextureSize(image->s(), image->t()); } else texture2d = new osg::Texture2D; texture2d->setWrap(osg::Texture::WRAP_S, tex.wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, tex.wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); uvSet = tex.uvSet; } else { // Texture only comes from NiFlipController, so tex is ignored, set defaults texture2d = new osg::Texture2D; texture2d->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); texture2d->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); uvSet = 0; } unsigned int texUnit = boundTextures.size(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); if (i == Nif::NiTexturingProperty::GlowTexture) { stateset->setTextureAttributeAndModes(texUnit, createEmissiveTexEnv(), osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DarkTexture) { osg::TexEnv* texEnv = new osg::TexEnv; // Modulate both the colour and the alpha with the dark map. texEnv->setMode(osg::TexEnv::MODULATE); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DetailTexture) { osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; // Modulate previous colour... texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); // with the detail map's colour, texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); // and a twist: texEnv->setScale_RGB(2.f); // Keep the previous alpha. texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::BumpTexture) { // Bump maps offset the environment map. // Set this texture to Off by default since we can't render it with the fixed-function pipeline stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); osg::Matrix2 bumpMapMatrix(texprop->bumpMapMatrix.x(), texprop->bumpMapMatrix.y(), texprop->bumpMapMatrix.z(), texprop->bumpMapMatrix.w()); stateset->addUniform(new osg::Uniform("bumpMapMatrix", bumpMapMatrix)); stateset->addUniform(new osg::Uniform("envMapLumaBias", texprop->envMapLumaBias)); } else if (i == Nif::NiTexturingProperty::GlossTexture) { // A gloss map is an environment map mask. // Gloss maps are only implemented in the object shaders as well. stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); } else if (i == Nif::NiTexturingProperty::DecalTexture) { // This is only an inaccurate imitation of the original implementation, // see https://github.com/niftools/nifskope/issues/184 osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; // Interpolate to the decal texture's colour... texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE); texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE); texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); // ...from the previous colour... texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS); texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); // using the decal texture's alpha as the factor. texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE); texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA); // Keep the previous alpha. texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } switch (i) { case Nif::NiTexturingProperty::BaseTexture: texture2d->setName("diffuseMap"); break; case Nif::NiTexturingProperty::BumpTexture: texture2d->setName("bumpMap"); break; case Nif::NiTexturingProperty::GlowTexture: texture2d->setName("emissiveMap"); break; case Nif::NiTexturingProperty::DarkTexture: texture2d->setName("darkMap"); break; case Nif::NiTexturingProperty::DetailTexture: texture2d->setName("detailMap"); break; case Nif::NiTexturingProperty::DecalTexture: texture2d->setName("decalMap"); break; case Nif::NiTexturingProperty::GlossTexture: texture2d->setName("glossMap"); break; default: break; } boundTextures.push_back(uvSet); } } handleTextureControllers(texprop, composite, imageManager, stateset, animflags); } void handleTextureSet(const Nif::BSShaderTextureSet* textureSet, unsigned int clamp, const std::string& nodeName, osg::StateSet* stateset, Resource::ImageManager* imageManager, std::vector& boundTextures) { if (!boundTextures.empty()) { for (unsigned int i = 0; i < boundTextures.size(); ++i) stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); boundTextures.clear(); } const unsigned int uvSet = 0; for (size_t i = 0; i < textureSet->textures.size(); ++i) { if (textureSet->textures[i].empty()) continue; switch(i) { case Nif::BSShaderTextureSet::TextureType_Base: case Nif::BSShaderTextureSet::TextureType_Normal: case Nif::BSShaderTextureSet::TextureType_Glow: break; default: { Log(Debug::Info) << "Unhandled texture stage " << i << " on shape \"" << nodeName << "\" in " << mFilename; continue; } } std::string filename = Misc::ResourceHelpers::correctTexturePath(textureSet->textures[i], imageManager->getVFS()); osg::ref_ptr image = imageManager->getImage(filename); osg::ref_ptr texture2d = new osg::Texture2D(image); if (image) texture2d->setTextureSize(image->s(), image->t()); bool wrapT = clamp & 0x1; bool wrapS = (clamp >> 1) & 0x1; texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); unsigned int texUnit = boundTextures.size(); stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); // BSShaderTextureSet presence means there's no need for FFP support for the affected node switch (i) { case Nif::BSShaderTextureSet::TextureType_Base: texture2d->setName("diffuseMap"); break; case Nif::BSShaderTextureSet::TextureType_Normal: texture2d->setName("normalMap"); break; case Nif::BSShaderTextureSet::TextureType_Glow: texture2d->setName("emissiveMap"); break; } boundTextures.emplace_back(uvSet); } } std::string_view getBSShaderPrefix(unsigned int type) const { switch (static_cast(type)) { case Nif::BSShaderType::ShaderType_Default: return "nv_default"; case Nif::BSShaderType::ShaderType_NoLighting: return "nv_nolighting"; case Nif::BSShaderType::ShaderType_TallGrass: case Nif::BSShaderType::ShaderType_Sky: case Nif::BSShaderType::ShaderType_Skin: case Nif::BSShaderType::ShaderType_Water: case Nif::BSShaderType::ShaderType_Lighting30: case Nif::BSShaderType::ShaderType_Tile: Log(Debug::Warning) << "Unhandled BSShaderType " << type << " in " << mFilename; return std::string_view(); } Log(Debug::Warning) << "Unknown BSShaderType " << type << " in " << mFilename; return std::string_view(); } std::string_view getBSLightingShaderPrefix(unsigned int type) const { switch (static_cast(type)) { case Nif::BSLightingShaderType::ShaderType_Default: return "nv_default"; case Nif::BSLightingShaderType::ShaderType_EnvMap: case Nif::BSLightingShaderType::ShaderType_Glow: case Nif::BSLightingShaderType::ShaderType_Parallax: case Nif::BSLightingShaderType::ShaderType_FaceTint: case Nif::BSLightingShaderType::ShaderType_SkinTint: case Nif::BSLightingShaderType::ShaderType_HairTint: case Nif::BSLightingShaderType::ShaderType_ParallaxOcc: case Nif::BSLightingShaderType::ShaderType_MultitexLand: case Nif::BSLightingShaderType::ShaderType_LODLand: case Nif::BSLightingShaderType::ShaderType_Snow: case Nif::BSLightingShaderType::ShaderType_MultiLayerParallax: case Nif::BSLightingShaderType::ShaderType_TreeAnim: case Nif::BSLightingShaderType::ShaderType_LODObjects: case Nif::BSLightingShaderType::ShaderType_SparkleSnow: case Nif::BSLightingShaderType::ShaderType_LODObjectsHD: case Nif::BSLightingShaderType::ShaderType_EyeEnvmap: case Nif::BSLightingShaderType::ShaderType_Cloud: case Nif::BSLightingShaderType::ShaderType_LODNoise: case Nif::BSLightingShaderType::ShaderType_MultitexLandLODBlend: case Nif::BSLightingShaderType::ShaderType_Dismemberment: Log(Debug::Warning) << "Unhandled BSLightingShaderType " << type << " in " << mFilename; return std::string_view(); } Log(Debug::Warning) << "Unknown BSLightingShaderType " << type << " in " << mFilename; return std::string_view(); } void handleProperty(const Nif::Property *property, osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector& boundTextures, int animflags, bool hasStencilProperty) { switch (property->recType) { case Nif::RC_NiStencilProperty: { const Nif::NiStencilProperty* stencilprop = static_cast(property); osg::ref_ptr frontFace = new osg::FrontFace; switch (stencilprop->data.drawMode) { case 2: frontFace->setMode(osg::FrontFace::CLOCKWISE); break; case 0: case 1: default: frontFace->setMode(osg::FrontFace::COUNTER_CLOCKWISE); break; } frontFace = shareAttribute(frontFace); osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setAttribute(frontFace, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, stencilprop->data.drawMode == 3 ? osg::StateAttribute::OFF : osg::StateAttribute::ON); if (stencilprop->data.enabled != 0) { mHasStencilProperty = true; osg::ref_ptr stencil = new osg::Stencil; stencil->setFunction(getStencilFunction(stencilprop->data.compareFunc), stencilprop->data.stencilRef, stencilprop->data.stencilMask); stencil->setStencilFailOperation(getStencilOperation(stencilprop->data.failAction)); stencil->setStencilPassAndDepthFailOperation(getStencilOperation(stencilprop->data.zFailAction)); stencil->setStencilPassAndDepthPassOperation(getStencilOperation(stencilprop->data.zPassAction)); stencil = shareAttribute(stencil); stateset->setAttributeAndModes(stencil, osg::StateAttribute::ON); } break; } case Nif::RC_NiWireframeProperty: { const Nif::NiWireframeProperty* wireprop = static_cast(property); osg::ref_ptr mode = new osg::PolygonMode; mode->setMode(osg::PolygonMode::FRONT_AND_BACK, wireprop->isEnabled() ? osg::PolygonMode::LINE : osg::PolygonMode::FILL); mode = shareAttribute(mode); node->getOrCreateStateSet()->setAttributeAndModes(mode, osg::StateAttribute::ON); break; } case Nif::RC_NiZBufferProperty: { const Nif::NiZBufferProperty* zprop = static_cast(property); osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setMode(GL_DEPTH_TEST, zprop->depthTest() ? osg::StateAttribute::ON : osg::StateAttribute::OFF); osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(zprop->depthWrite()); // Morrowind ignores depth test function, unless a NiStencilProperty is present, in which case it uses a fixed depth function of GL_ALWAYS. if (hasStencilProperty) depth->setFunction(osg::Depth::ALWAYS); depth = shareAttribute(depth); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); break; } // OSG groups the material properties that NIFs have separate, so we have to parse them all again when one changed case Nif::RC_NiMaterialProperty: case Nif::RC_NiVertexColorProperty: case Nif::RC_NiSpecularProperty: { // Handled on drawable level so we know whether vertex colors are available break; } case Nif::RC_NiAlphaProperty: { // Handled on drawable level to prevent RenderBin nesting issues break; } case Nif::RC_NiTexturingProperty: { const Nif::NiTexturingProperty* texprop = static_cast(property); osg::StateSet* stateset = node->getOrCreateStateSet(); handleTextureProperty(texprop, node->getName(), stateset, composite, imageManager, boundTextures, animflags); break; } case Nif::RC_BSShaderPPLightingProperty: { auto texprop = static_cast(property); bool shaderRequired = true; node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->type))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); if (!texprop->textureSet.empty()) { auto textureSet = texprop->textureSet.getPtr(); handleTextureSet(textureSet, texprop->clamp, node->getName(), stateset, imageManager, boundTextures); } handleTextureControllers(texprop, composite, imageManager, stateset, animflags); break; } case Nif::RC_BSShaderNoLightingProperty: { auto texprop = static_cast(property); bool shaderRequired = true; node->setUserValue("shaderPrefix", std::string(getBSShaderPrefix(texprop->type))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); if (!texprop->filename.empty()) { if (!boundTextures.empty()) { for (unsigned int i = 0; i < boundTextures.size(); ++i) stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF); boundTextures.clear(); } std::string filename = Misc::ResourceHelpers::correctTexturePath(texprop->filename, imageManager->getVFS()); osg::ref_ptr image = imageManager->getImage(filename); osg::ref_ptr texture2d = new osg::Texture2D(image); texture2d->setName("diffuseMap"); if (image) texture2d->setTextureSize(image->s(), image->t()); texture2d->setWrap(osg::Texture::WRAP_S, texprop->wrapS() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); texture2d->setWrap(osg::Texture::WRAP_T, texprop->wrapT() ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); const unsigned int texUnit = 0; const unsigned int uvSet = 0; stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON); boundTextures.push_back(uvSet); } if (mBethVersion >= 27) { stateset->addUniform(new osg::Uniform("useFalloff", true)); stateset->addUniform(new osg::Uniform("falloffParams", texprop->falloffParams)); } else { stateset->addUniform(new osg::Uniform("useFalloff", false)); } handleTextureControllers(texprop, composite, imageManager, stateset, animflags); break; } case Nif::RC_BSLightingShaderProperty: { auto texprop = static_cast(property); bool shaderRequired = true; node->setUserValue("shaderPrefix", std::string(getBSLightingShaderPrefix(texprop->type))); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); if (!texprop->mTextureSet.empty()) handleTextureSet(texprop->mTextureSet.getPtr(), texprop->mClamp, node->getName(), stateset, imageManager, boundTextures); handleTextureControllers(texprop, composite, imageManager, stateset, animflags); break; } // unused by mw case Nif::RC_NiShadeProperty: case Nif::RC_NiDitherProperty: case Nif::RC_NiFogProperty: { break; } default: Log(Debug::Info) << "Unhandled " << property->recName << " in " << mFilename; break; } } struct CompareStateAttribute { bool operator() (const osg::ref_ptr& left, const osg::ref_ptr& right) const { return left->compare(*right) < 0; } }; // global sharing of State Attributes will reduce the number of GL calls as the osg::State will check by pointer to see if state is the same template Attribute* shareAttribute(const osg::ref_ptr& attr) { typedef std::set, CompareStateAttribute> Cache; static Cache sCache; static std::mutex sMutex; std::lock_guard lock(sMutex); typename Cache::iterator found = sCache.find(attr); if (found == sCache.end()) found = sCache.insert(attr).first; return *found; } void applyDrawableProperties(osg::Node* node, const std::vector& properties, SceneUtil::CompositeStateSetUpdater* composite, bool hasVertexColors, int animflags) { // Specular lighting is enabled by default, but there's a quirk... bool specEnabled = true; osg::ref_ptr mat (new osg::Material); mat->setColorMode(hasVertexColors ? osg::Material::AMBIENT_AND_DIFFUSE : osg::Material::OFF); // NIF material defaults don't match OpenGL defaults mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); bool hasMatCtrl = false; bool hasSortAlpha = false; osg::StateSet* blendFuncStateSet = nullptr; auto setBin_Transparent = [] (osg::StateSet* ss) { ss->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); }; auto setBin_BackToFront = [] (osg::StateSet* ss) { ss->setRenderBinDetails(0, "SORT_BACK_TO_FRONT"); }; auto setBin_Traversal = [] (osg::StateSet* ss) { ss->setRenderBinDetails(2, "TraversalOrderBin"); }; auto setBin_Inherit = [] (osg::StateSet* ss) { ss->setRenderBinToInherit(); }; int lightmode = 1; float emissiveMult = 1.f; float specStrength = 1.f; for (const Nif::Property* property : properties) { switch (property->recType) { case Nif::RC_NiSpecularProperty: { // Specular property can turn specular lighting off. // FIXME: NiMaterialColorController doesn't care about this. auto specprop = static_cast(property); specEnabled = specprop->isEnabled(); break; } case Nif::RC_NiMaterialProperty: { const Nif::NiMaterialProperty* matprop = static_cast(property); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.diffuse, matprop->data.alpha)); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.ambient, 1.f)); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.emissive, 1.f)); emissiveMult = matprop->data.emissiveMult; mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.specular, 1.f)); mat->setShininess(osg::Material::FRONT_AND_BACK, matprop->data.glossiness); if (!matprop->controller.empty()) { hasMatCtrl = true; handleMaterialControllers(matprop, composite, animflags, mat); } break; } case Nif::RC_NiVertexColorProperty: { const Nif::NiVertexColorProperty* vertprop = static_cast(property); lightmode = vertprop->data.lightmode; switch (vertprop->data.vertmode) { case 0: mat->setColorMode(osg::Material::OFF); break; case 1: mat->setColorMode(osg::Material::EMISSION); break; case 2: if (lightmode != 0) mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); else mat->setColorMode(osg::Material::OFF); break; } break; } case Nif::RC_NiAlphaProperty: { const Nif::NiAlphaProperty* alphaprop = static_cast(property); if (alphaprop->useAlphaBlending()) { osg::ref_ptr blendFunc (new osg::BlendFunc(getBlendMode(alphaprop->sourceBlendMode()), getBlendMode(alphaprop->destinationBlendMode()))); // on AMD hardware, alpha still seems to be stored with an RGBA framebuffer with OpenGL. // This might be mandated by the OpenGL 2.1 specification section 2.14.9, or might be a bug. // Either way, D3D8.1 doesn't do that, so adapt the destination factor. if (blendFunc->getDestination() == GL_DST_ALPHA) blendFunc->setDestination(GL_ONE); blendFunc = shareAttribute(blendFunc); node->getOrCreateStateSet()->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); if (!alphaprop->noSorter()) { hasSortAlpha = true; if (!mPushedSorter) setBin_Transparent(node->getStateSet()); } else { if (!mPushedSorter) setBin_Inherit(node->getStateSet()); } } else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); stateset->removeMode(GL_BLEND); blendFuncStateSet = stateset; if (!mPushedSorter) blendFuncStateSet->setRenderBinToInherit(); } if (alphaprop->useAlphaTesting()) { osg::ref_ptr alphaFunc (new osg::AlphaFunc(getTestMode(alphaprop->alphaTestMode()), alphaprop->data.threshold/255.f)); alphaFunc = shareAttribute(alphaFunc); node->getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); } else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); stateset->removeMode(GL_ALPHA_TEST); } break; } case Nif::RC_BSLightingShaderProperty: { auto shaderprop = static_cast(property); mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderprop->mAlpha); mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mEmissive, 1.f)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mSpecular, 1.f)); mat->setShininess(osg::Material::FRONT_AND_BACK, shaderprop->mGlossiness); emissiveMult = shaderprop->mEmissiveMult; specStrength = shaderprop->mSpecStrength; break; } default: break; } } // While NetImmerse and Gamebryo support specular lighting, Morrowind has its support disabled. if (mVersion <= Nif::NIFFile::NIFVersion::VER_MW || !specEnabled) mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f,0.f,0.f,0.f)); if (lightmode == 0) { osg::Vec4f diffuse = mat->getDiffuse(osg::Material::FRONT_AND_BACK); diffuse = osg::Vec4f(0,0,0,diffuse.a()); mat->setDiffuse(osg::Material::FRONT_AND_BACK, diffuse); mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f()); } // If we're told to use vertex colors but there are none to use, use a default color instead. if (!hasVertexColors) { switch (mat->getColorMode()) { case osg::Material::AMBIENT: mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); break; case osg::Material::AMBIENT_AND_DIFFUSE: mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); break; case osg::Material::EMISSION: mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); break; default: break; } mat->setColorMode(osg::Material::OFF); } if (!mPushedSorter && !hasSortAlpha && mHasStencilProperty) setBin_Traversal(node->getOrCreateStateSet()); if (!mPushedSorter && !hasMatCtrl && mat->getColorMode() == osg::Material::OFF && mat->getEmission(osg::Material::FRONT_AND_BACK) == osg::Vec4f(0,0,0,1) && mat->getDiffuse(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1) && mat->getAmbient(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1) && mat->getShininess(osg::Material::FRONT_AND_BACK) == 0 && mat->getSpecular(osg::Material::FRONT_AND_BACK) == osg::Vec4f(0.f, 0.f, 0.f, 0.f)) { // default state, skip return; } mat = shareAttribute(mat); osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); if (emissiveMult != 1.f) stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); if (specStrength != 1.f) stateset->addUniform(new osg::Uniform("specStrength", specStrength)); if (!mPushedSorter) return; auto assignBin = [&] (int mode, int type) { if (mode == Nif::NiSortAdjustNode::SortingMode_Off) { setBin_Traversal(stateset); return; } if (type == Nif::RC_NiAlphaAccumulator) { if (hasSortAlpha) setBin_BackToFront(stateset); else setBin_Traversal(stateset); } else if (type == Nif::RC_NiClusterAccumulator) setBin_BackToFront(stateset); else Log(Debug::Error) << "Unrecognized NiAccumulator in " << mFilename; }; switch (mPushedSorter->mMode) { case Nif::NiSortAdjustNode::SortingMode_Inherit: { if (mLastAppliedNoInheritSorter) assignBin(mLastAppliedNoInheritSorter->mMode, mLastAppliedNoInheritSorter->mSubSorter->recType); else assignBin(mPushedSorter->mMode, Nif::RC_NiAlphaAccumulator); break; } case Nif::NiSortAdjustNode::SortingMode_Off: { setBin_Traversal(stateset); break; } case Nif::NiSortAdjustNode::SortingMode_Subsort: { assignBin(mPushedSorter->mMode, mPushedSorter->mSubSorter->recType); break; } } } }; osg::ref_ptr Loader::load(Nif::NIFFilePtr file, Resource::ImageManager* imageManager) { LoaderImpl impl(file->getFilename(), file->getVersion(), file->getUserVersion(), file->getBethVersion()); return impl.load(file, imageManager); } void Loader::loadKf(Nif::NIFFilePtr kf, SceneUtil::KeyframeHolder& target) { LoaderImpl impl(kf->getFilename(), kf->getVersion(), kf->getUserVersion(), kf->getBethVersion()); impl.loadKf(kf, target); } }