diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 0ae45c6c26..cddcbe463b 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -56,13 +56,26 @@ std::string Misc::ResourceHelpers::correctResourcePath( std::string correctedPath = Misc::StringUtils::lowerCase(resPath); - // Apparently, leading separators are allowed - while (correctedPath.size() && (correctedPath[0] == '/' || correctedPath[0] == '\\')) + // Flatten slashes + std::replace(correctedPath.begin(), correctedPath.end(), '/', '\\'); + auto bothSeparators = [](char a, char b) { return a == '\\' && b == '\\'; }; + correctedPath.erase(std::unique(correctedPath.begin(), correctedPath.end(), bothSeparators), correctedPath.end()); + + // Remove leading separator + if (!correctedPath.empty() && correctedPath[0] == '\\') correctedPath.erase(0, 1); + // Handle top level directory if (!correctedPath.starts_with(topLevelDirectory) || correctedPath.size() <= topLevelDirectory.size() - || (correctedPath[topLevelDirectory.size()] != '/' && correctedPath[topLevelDirectory.size()] != '\\')) - correctedPath = std::string{ topLevelDirectory } + '\\' + correctedPath; + || correctedPath[topLevelDirectory.size()] != '\\') + { + std::string topLevelPrefix = std::string{ topLevelDirectory } + '\\'; + size_t topLevelPos = correctedPath.find('\\' + topLevelPrefix); + if (topLevelPos == std::string::npos) + correctedPath = topLevelPrefix + correctedPath; + else + correctedPath.erase(0, topLevelPos + 1); + } std::string origExt = correctedPath; diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 62a2a9cce3..8b459d015a 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -302,6 +302,33 @@ namespace Nif } } + void BSSkinInstance::read(NIFStream* nif) + { + mRoot.read(nif); + mData.read(nif); + readRecordList(nif, mBones); + nif->readVector(mScales, nif->get()); + } + + void BSSkinInstance::post(Reader& nif) + { + mRoot.post(nif); + mData.post(nif); + postRecordList(nif, mBones); + if (mData.empty() || mRoot.empty()) + throw Nif::Exception("BSSkin::Instance missing root or data", nif.getFilename()); + + if (mBones.size() != mData->mBones.size()) + throw Nif::Exception("Mismatch in BSSkin::BoneData bone count", nif.getFilename()); + + for (auto& bone : mBones) + { + if (bone.empty()) + throw Nif::Exception("Oops: Missing bone! Don't know how to handle this.", nif.getFilename()); + bone->setBone(); + } + } + void NiSkinData::read(NIFStream* nif) { nif->read(mTransform); @@ -344,6 +371,16 @@ namespace Nif mPartitions.post(nif); } + void BSSkinBoneData::read(NIFStream* nif) + { + mBones.resize(nif->get()); + for (BoneInfo& bone : mBones) + { + nif->read(bone.mBoundSphere); + nif->read(bone.mTransform); + } + } + void NiSkinPartition::read(NIFStream* nif) { mPartitions.resize(nif->get()); diff --git a/components/nif/data.hpp b/components/nif/data.hpp index efab514223..75c18d657a 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -223,6 +223,17 @@ namespace Nif void read(NIFStream* nif) override; }; + struct BSSkinInstance : Record + { + NiAVObjectPtr mRoot; + BSSkinBoneDataPtr mData; + NiAVObjectList mBones; + std::vector mScales; + + void read(NIFStream* nif) override; + void post(Reader& nif) override; + }; + struct NiSkinData : public Record { using VertWeight = std::pair; @@ -242,6 +253,19 @@ namespace Nif void post(Reader& nif) override; }; + struct BSSkinBoneData : Record + { + struct BoneInfo + { + osg::BoundingSpheref mBoundSphere; + NiTransform mTransform; + }; + + std::vector mBones; + + void read(NIFStream* nif) override; + }; + struct NiSkinPartition : public Record { struct Partition diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp index 69f66014d6..1fc24e5f89 100644 --- a/components/nif/extra.cpp +++ b/components/nif/extra.cpp @@ -136,4 +136,30 @@ namespace Nif nif->readVector(mData, nif->get()); } + void BSConnectPoint::Point::read(NIFStream* nif) + { + mParent = nif->getSizedString(); + mName = nif->getSizedString(); + nif->read(mTransform.mRotation); + nif->read(mTransform.mTranslation); + nif->read(mTransform.mScale); + } + + void BSConnectPoint::Parents::read(NIFStream* nif) + { + NiExtraData::read(nif); + + mPoints.resize(nif->get()); + for (Point& point : mPoints) + point.read(nif); + } + + void BSConnectPoint::Children::read(NIFStream* nif) + { + NiExtraData::read(nif); + + nif->read(mSkinned); + nif->getSizedStrings(mPointNames, nif->get()); + } + } diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 49c77723fb..0808e91b16 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -45,6 +45,8 @@ namespace Nif using NiFloatsExtraData = TypedVectorExtra; using NiIntegersExtraData = TypedVectorExtra; + using BSEyeCenterExtraData = TypedVectorExtra; + using BSPositionData = TypedVectorExtra; using BSWArray = TypedVectorExtra; // Distinct from NiBinaryExtraData, uses mRecordSize as its size @@ -170,5 +172,32 @@ namespace Nif void read(NIFStream* nif) override; }; + struct BSConnectPoint + { + struct Point + { + std::string mParent; + std::string mName; + NiQuatTransform mTransform; + + void read(NIFStream* nif); + }; + + struct Parents : NiExtraData + { + std::vector mPoints; + + void read(NIFStream* nif) override; + }; + + struct Children : NiExtraData + { + bool mSkinned; + std::vector mPointNames; + + void read(NIFStream* nif) override; + }; + }; + } #endif diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index d5faebb55f..89559a9149 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -158,6 +158,7 @@ namespace Nif &construct }, { "bhkBlendController", &construct }, { "NiBSBoneLODController", &construct }, + { "NiLightRadiusController", &construct }, // Interpolators, Gamebryo { "NiBlendBoolInterpolator", &construct }, @@ -245,11 +246,15 @@ namespace Nif { "BSBehaviorGraphExtraData", &construct }, { "BSBoneLODExtraData", &construct }, { "BSClothExtraData", &construct }, + { "BSConnectPoint::Children", &construct }, + { "BSConnectPoint::Parents", &construct }, { "BSDecalPlacementVectorExtraData", &construct }, { "BSDistantObjectExtraData", &construct }, { "BSDistantObjectLargeRefExtraData", &construct }, + { "BSEyeCenterExtraData", &construct }, + { "BSPositionData", &construct }, { "BSWArray", &construct }, { "BSXFlags", &construct }, @@ -268,6 +273,8 @@ namespace Nif // Bethesda { "BSDismemberSkinInstance", &construct }, + { "BSSkin::Instance", &construct }, + { "BSSkin::BoneData", &construct }, { "BSTriShape", &construct }, { "BSDynamicTriShape", &construct }, { "BSLODTriShape", &construct }, diff --git a/components/nif/node.cpp b/components/nif/node.cpp index 860e75671f..8f5443b28e 100644 --- a/components/nif/node.cpp +++ b/components/nif/node.cpp @@ -426,6 +426,8 @@ namespace Nif mSkin.post(nif); mShaderProperty.post(nif); mAlphaProperty.post(nif); + if (!mSkin.empty()) + nif.setUseSkinning(true); } void BSDynamicTriShape::read(NIFStream* nif) @@ -460,14 +462,13 @@ namespace Nif mLandscapeDataOffset = (data & 0xF00000000) >> 0x20; mEyeDataOffset = (data & 0xF000000000) >> 0x24; mFlags = (data & 0xFFF00000000000) >> 0x2C; + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_SSE) + mFlags |= BSVertexDesc::VertexAttribute::Full_Precision; } void BSVertexData::read(NIFStream* nif, uint16_t flags) { - bool fullPrecision = true; - if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_SSE) - fullPrecision = flags & BSVertexDesc::VertexAttribute::Full_Precision; - + bool fullPrecision = flags & BSVertexDesc::VertexAttribute::Full_Precision; bool hasVertex = flags & BSVertexDesc::VertexAttribute::Vertex; bool hasTangent = flags & BSVertexDesc::VertexAttribute::Tangents; bool hasUV = flags & BSVertexDesc::VertexAttribute::UVs; diff --git a/components/nif/node.hpp b/components/nif/node.hpp index f560932e22..e873fc27dc 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -119,14 +119,7 @@ namespace Nif void post(Reader& nif) override; }; - struct GeometryInterface - { - NiSkinInstancePtr mSkin; - BSShaderPropertyPtr mShaderProperty; - NiAlphaPropertyPtr mAlphaProperty; - }; - - struct NiGeometry : NiAVObject, GeometryInterface + struct NiGeometry : NiAVObject { /* Possible flags: 0x40 - mesh has no vertex normals ? @@ -146,7 +139,10 @@ namespace Nif }; NiGeometryDataPtr mData; + NiSkinInstancePtr mSkin; MaterialData mMaterial; + BSShaderPropertyPtr mShaderProperty; + NiAlphaPropertyPtr mAlphaProperty; void read(NIFStream* nif) override; void post(Reader& nif) override; @@ -365,10 +361,13 @@ namespace Nif void read(NIFStream* nif, uint16_t flags); }; - struct BSTriShape : NiAVObject, GeometryInterface + struct BSTriShape : NiAVObject { osg::BoundingSpheref mBoundingSphere; std::array mBoundMinMax; + RecordPtrT mSkin; + BSShaderPropertyPtr mShaderProperty; + NiAlphaPropertyPtr mAlphaProperty; BSVertexDesc mVertDesc; uint32_t mDataSize; std::vector mVertData; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index 00eec64b24..a06dcfe2a9 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -77,6 +77,8 @@ namespace Nif RC_BSBound, RC_BSBoneLODExtraData, RC_BSClothExtraData, + RC_BSConnectPointChildren, + RC_BSConnectPointParents, RC_BSDecalPlacementVectorExtraData, RC_BSDistantTreeShaderProperty, RC_BSDynamicTriShape, @@ -87,6 +89,7 @@ namespace Nif RC_BSEffectShaderPropertyColorController, RC_BSEffectShaderPropertyFloatController, RC_BSExtraData, + RC_BSEyeCenterExtraData, RC_BSFrustumFOVController, RC_BSFurnitureMarker, RC_BSInvMarker, @@ -105,6 +108,7 @@ namespace Nif RC_BSNiAlphaPropertyTestRefController, RC_BSPackedAdditionalGeometryData, RC_BSParentVelocityModifier, + RC_BSPositionData, RC_BSProceduralLightningController, RC_BSPSysArrayEmitter, RC_BSPSysHavokUpdateModifier, @@ -125,6 +129,8 @@ namespace Nif RC_BSShaderPPLightingProperty, RC_BSShaderProperty, RC_BSShaderTextureSet, + RC_BSSkinBoneData, + RC_BSSkinInstance, RC_BSSkyShaderProperty, RC_BSTriShape, RC_BSWArray, @@ -184,6 +190,7 @@ namespace Nif RC_NiLight, RC_NiLightColorController, RC_NiLightDimmerController, + RC_NiLightRadiusController, RC_NiLines, RC_NiLinesData, RC_NiLODNode, diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index b847f609a4..7056abe6a3 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -162,6 +162,7 @@ namespace Nif struct bhkCompressedMeshShapeData; struct BSMultiBound; struct BSMultiBoundData; + struct BSSkinBoneData; using NiAVObjectPtr = RecordPtrT; using ExtraPtr = RecordPtrT; @@ -211,6 +212,7 @@ namespace Nif using bhkCompressedMeshShapeDataPtr = RecordPtrT; using BSMultiBoundPtr = RecordPtrT; using BSMultiBoundDataPtr = RecordPtrT; + using BSSkinBoneDataPtr = RecordPtrT; using NiAVObjectList = RecordListT; using NiPropertyList = RecordListT; diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index df4155db7d..a72ec36cad 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -32,6 +32,32 @@ namespace return letterPos < path.size() && (path[letterPos] == 'x' || path[letterPos] == 'X'); } + bool isTypeNiGeometry(int type) + { + switch (type) + { + case Nif::RC_NiTriShape: + case Nif::RC_NiTriStrips: + case Nif::RC_BSLODTriShape: + case Nif::RC_BSSegmentedTriShape: + return true; + } + return false; + } + + bool isTypeTriShape(int type) + { + switch (type) + { + case Nif::RC_NiTriShape: + case Nif::RC_BSLODTriShape: + case Nif::RC_BSSegmentedTriShape: + return true; + } + + return false; + } + void prepareTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriBasedGeomData& data) { // FIXME: copying vertices/indices individually is unreasonable @@ -81,7 +107,7 @@ namespace auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) -> decltype(function(static_cast(geometry.mData.get()))) { - if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) + if (isTypeTriShape(geometry.recType)) { auto data = static_cast(geometry.mData.getPtr()); if (data->mTriangles.empty()) @@ -329,12 +355,8 @@ namespace NifBullet // NOTE: a trishape with bounds, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. // (occurs in tr_ex_imp_wall_arch_04.nif) - if (node.mBounds.mType == Nif::BoundingVolume::Type::BASE_BV - && (node.recType == Nif::RC_NiTriShape || node.recType == Nif::RC_NiTriStrips - || node.recType == Nif::RC_BSLODTriShape)) - { + if (node.mBounds.mType == Nif::BoundingVolume::Type::BASE_BV && isTypeNiGeometry(node.recType)) handleNiTriShape(static_cast(node), parent, args); - } } // For NiNodes, loop through children diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index fbcd2f52bd..fdf51486ed 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -88,7 +88,7 @@ namespace } } - bool isTypeGeometry(int type) + bool isTypeNiGeometry(int type) { switch (type) { @@ -96,6 +96,19 @@ namespace case Nif::RC_NiTriStrips: case Nif::RC_NiLines: case Nif::RC_BSLODTriShape: + case Nif::RC_BSSegmentedTriShape: + return true; + } + return false; + } + + bool isTypeBSGeometry(int type) + { + switch (type) + { + case Nif::RC_BSTriShape: + case Nif::RC_BSDynamicTriShape: + case Nif::RC_BSMeshLODTriShape: return true; } return false; @@ -125,15 +138,6 @@ namespace } } } - - auto geometry = dynamic_cast(nifNode); - if (geometry) - { - if (!geometry->mShaderProperty.empty()) - out.emplace_back(geometry->mShaderProperty.getPtr()); - if (!geometry->mAlphaProperty.empty()) - out.emplace_back(geometry->mAlphaProperty.getPtr()); - } } // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale @@ -443,11 +447,16 @@ namespace NifOsg } } - auto geometry = dynamic_cast(nifNode); - // NiGeometry's NiAlphaProperty doesn't get handled here because it's a drawable property - if (geometry && !geometry->mShaderProperty.empty()) - handleProperty(geometry->mShaderProperty.getPtr(), applyTo, composite, imageManager, boundTextures, - animflags, hasStencilProperty); + // NiAlphaProperty is handled as a drawable property + Nif::BSShaderPropertyPtr shaderprop = nullptr; + if (isTypeNiGeometry(nifNode->recType)) + shaderprop = static_cast(nifNode)->mShaderProperty; + else if (isTypeBSGeometry(nifNode->recType)) + shaderprop = static_cast(nifNode)->mShaderProperty; + + if (!shaderprop.empty()) + handleProperty(shaderprop.getPtr(), applyTo, composite, imageManager, boundTextures, animflags, + hasStencilProperty); } static void setupController(const Nif::NiTimeController* ctrl, SceneUtil::Controller* toSetup, int animflags) @@ -747,7 +756,9 @@ namespace NifOsg applyNodeProperties(nifNode, node, composite, args.mImageManager, args.mBoundTextures, args.mAnimFlags); - const bool isGeometry = isTypeGeometry(nifNode->recType); + const bool isNiGeometry = isTypeNiGeometry(nifNode->recType); + const bool isBSGeometry = isTypeBSGeometry(nifNode->recType); + const bool isGeometry = isNiGeometry || isBSGeometry; if (isGeometry && !args.mSkipMeshes) { @@ -762,12 +773,10 @@ namespace NifOsg skip = args.mHasMarkers && Misc::StringUtils::ciStartsWith(nifNode->mName, "EditorMarker"); if (!skip) { - Nif::NiSkinInstancePtr skin = static_cast(nifNode)->mSkin; - - if (skin.empty()) - handleGeometry(nifNode, parent, node, composite, args.mBoundTextures, args.mAnimFlags); - else - handleSkinnedGeometry(nifNode, parent, node, composite, args.mBoundTextures, args.mAnimFlags); + if (isNiGeometry) + handleNiGeometry(nifNode, parent, node, composite, args.mBoundTextures, args.mAnimFlags); + else // isBSGeometry + handleBSGeometry(nifNode, parent, node, composite, args.mBoundTextures, args.mAnimFlags); if (!nifNode->mController.empty()) handleMeshControllers(nifNode, node, composite, args.mBoundTextures, args.mAnimFlags); @@ -1342,41 +1351,7 @@ namespace NifOsg } } - void handleNiGeometryData(osg::Geometry* geometry, const Nif::NiGeometryData* data, - const std::vector& boundTextures, const std::string& name) - { - const auto& vertices = data->mVertices; - const auto& normals = data->mNormals; - const auto& colors = data->mColors; - 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->mUVList; - 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::NiAVObject* nifNode, const Nif::Parent* parent, osg::Geometry* geometry, + void handleNiGeometryData(const Nif::NiAVObject* nifNode, const Nif::Parent* parent, osg::Geometry* geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { @@ -1458,7 +1433,36 @@ namespace NifOsg new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, line.size(), line.data())); } } - handleNiGeometryData(geometry, niGeometryData, boundTextures, nifNode->mName); + + const auto& vertices = niGeometryData->mVertices; + const auto& normals = niGeometryData->mNormals; + const auto& colors = niGeometryData->mColors; + 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 = niGeometryData->mUVList; + 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 \"" << nifNode->mName + << "\" 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); + } // osg::Material properties are handled here for two reasons: // - if there are no vertex colors, we need to disable colorMode. @@ -1466,98 +1470,226 @@ namespace NifOsg // above the actual renderable would be tedious. std::vector drawableProps; collectDrawableProperties(nifNode, parent, drawableProps); + if (!niGeometry->mShaderProperty.empty()) + drawableProps.emplace_back(niGeometry->mShaderProperty.getPtr()); + if (!niGeometry->mAlphaProperty.empty()) + drawableProps.emplace_back(niGeometry->mAlphaProperty.getPtr()); applyDrawableProperties(parentNode, drawableProps, composite, !niGeometryData->mColors.empty(), animflags); } - void handleGeometry(const Nif::NiAVObject* nifNode, const Nif::Parent* parent, osg::Group* parentNode, + void handleNiGeometry(const Nif::NiAVObject* nifNode, const Nif::Parent* parent, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector& boundTextures, int animflags) { - assert(isTypeGeometry(nifNode->recType)); + assert(isTypeNiGeometry(nifNode->recType)); + osg::ref_ptr geom(new osg::Geometry); - handleNiGeometry(nifNode, parent, geom, parentNode, composite, boundTextures, animflags); + handleNiGeometryData(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; + + osg::ref_ptr drawable = geom; + + auto niGeometry = static_cast(nifNode); + if (!niGeometry->mSkin.empty()) + { + osg::ref_ptr rig(new SceneUtil::RigGeometry); + rig->setSourceGeometry(geom); + + // Assign bone weights + osg::ref_ptr map(new SceneUtil::RigGeometry::InfluenceMap); + + const Nif::NiSkinInstance* skin = niGeometry->mSkin.getPtr(); + const Nif::NiSkinData* data = skin->mData.getPtr(); + const Nif::NiAVObjectList& bones = skin->mBones; + for (std::size_t i = 0; i < bones.size(); ++i) + { + std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); + + SceneUtil::RigGeometry::BoneInfluence influence; + influence.mWeights = data->mBones[i].mWeights; + influence.mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); + influence.mBoundSphere = data->mBones[i].mBoundSphere; + + map->mData.emplace_back(boneName, influence); + } + rig->setInfluenceMap(map); + + drawable = rig; + } + for (Nif::NiTimeControllerPtr ctrl = nifNode->mController; !ctrl.empty(); ctrl = ctrl->mNext) { if (!ctrl->isActive()) continue; if (ctrl->recType == Nif::RC_NiGeomMorpherController) { - const Nif::NiGeomMorpherController* nimorphctrl - = static_cast(ctrl.getPtr()); + if (!niGeometry->mSkin.empty()) + continue; + + auto nimorphctrl = static_cast(ctrl.getPtr()); if (nimorphctrl->mData.empty()) continue; - drawable = handleMorphGeometry(nimorphctrl, geom, parentNode, composite, boundTextures, animflags); + + const std::vector& morphs = nimorphctrl->mData.getPtr()->mMorphs; + if (morphs.empty() + || morphs[0].mVertices.size() + != static_cast(geom->getVertexArray())->size()) + continue; + + osg::ref_ptr morphGeom = new SceneUtil::MorphGeometry; + morphGeom->setSourceGeometry(geom); + for (unsigned int i = 0; i < morphs.size(); ++i) + morphGeom->addMorphTarget( + new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f); osg::ref_ptr morphctrl = new GeomMorpherController(nimorphctrl); setupController(ctrl.getPtr(), morphctrl, animflags); - drawable->setUpdateCallback(morphctrl); + morphGeom->setUpdateCallback(morphctrl); + + drawable = morphGeom; break; } } - if (!drawable.get()) - drawable = geom; + drawable->setName(nifNode->mName); parentNode->addChild(drawable); } - osg::ref_ptr handleMorphGeometry(const Nif::NiGeomMorpherController* morpher, - osg::ref_ptr sourceGeometry, osg::Node* parentNode, + void handleBSGeometry(const Nif::NiAVObject* nifNode, const Nif::Parent* parent, osg::Group* 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->mData.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; - } + assert(isTypeBSGeometry(nifNode->recType)); - void handleSkinnedGeometry(const Nif::NiAVObject* 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()) + auto bsTriShape = static_cast(nifNode); + const std::vector& triangles = bsTriShape->mTriangles; + if (triangles.empty()) return; - osg::ref_ptr rig(new SceneUtil::RigGeometry); - rig->setSourceGeometry(geometry); - rig->setName(nifNode->mName); - // Assign bone weights - osg::ref_ptr map(new SceneUtil::RigGeometry::InfluenceMap); + osg::ref_ptr geometry(new osg::Geometry); + geometry->addPrimitiveSet( + new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, triangles.size(), triangles.data())); + + osg::ref_ptr drawable = geometry; + + // Some input geometry may not be used as is so it needs to be converted. + // Normals, tangents and bitangents use a special normal map-like format not equivalent to snorm8 or unorm8 + auto normbyteToFloat = [](uint8_t value) { return value / 255.f * 2.f - 1.f; }; + // Vertices and UV sets may be half-precision. + // OSG doesn't have a way to pass half-precision data at the moment. + auto halfToFloat = [](uint16_t value) { + uint32_t bits = static_cast(value & 0x8000) << 16; + + const uint32_t exp16 = (value & 0x7c00) >> 10; + uint32_t frac16 = value & 0x3ff; + if (exp16) + bits |= (exp16 + 0x70) << 23; + else if (frac16) + { + uint8_t offset = 0; + do + { + ++offset; + frac16 <<= 1; + } while ((frac16 & 0x400) != 0x400); + frac16 &= 0x3ff; + bits |= (0x71 - offset) << 23; + } + bits |= frac16 << 13; - const Nif::NiSkinInstance* skin = static_cast(nifNode)->mSkin.getPtr(); - const Nif::NiSkinData* data = skin->mData.getPtr(); - const Nif::NiAVObjectList& bones = skin->mBones; - for (std::size_t i = 0; i < bones.size(); ++i) + float result; + std::memcpy(&result, &bits, sizeof(float)); + return result; + }; + + const bool fullPrec = bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Full_Precision; + const bool hasVertices = bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Vertex; + const bool hasNormals = bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Normals; + const bool hasColors = bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Vertex_Colors; + const bool hasUV = bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::UVs; + + std::vector vertices; + std::vector normals; + std::vector colors; + std::vector uvlist; + for (auto& elem : bsTriShape->mVertData) { - std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); + if (hasVertices) + { + if (fullPrec) + vertices.emplace_back(elem.mVertex.x(), elem.mVertex.y(), elem.mVertex.z()); + else + vertices.emplace_back(halfToFloat(elem.mHalfVertex[0]), halfToFloat(elem.mHalfVertex[1]), + halfToFloat(elem.mHalfVertex[2])); + } + if (hasNormals) + normals.emplace_back(normbyteToFloat(elem.mNormal[0]), normbyteToFloat(elem.mNormal[1]), + normbyteToFloat(elem.mNormal[2])); + if (hasColors) + colors.emplace_back(elem.mVertColor[0], elem.mVertColor[1], elem.mVertColor[2], elem.mVertColor[3]); + if (hasUV) + uvlist.emplace_back(halfToFloat(elem.mUV[0]), 1.0 - halfToFloat(elem.mUV[1])); + } - SceneUtil::RigGeometry::BoneInfluence influence; - influence.mWeights = data->mBones[i].mWeights; - influence.mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); - influence.mBoundSphere = data->mBones[i].mBoundSphere; + 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::Vec4ubArray(colors.size(), colors.data()), osg::Array::BIND_PER_VERTEX); + if (!uvlist.empty()) + geometry->setTexCoordArray( + 0, new osg::Vec2Array(uvlist.size(), uvlist.data()), osg::Array::BIND_PER_VERTEX); + + // This is the skinning data Fallout 4 provides + // TODO: support Skyrim SE skinning data + if (!bsTriShape->mSkin.empty() && bsTriShape->mSkin->recType == Nif::RC_BSSkinInstance + && bsTriShape->mVertDesc.mFlags & Nif::BSVertexDesc::VertexAttribute::Skinned) + { + osg::ref_ptr rig(new SceneUtil::RigGeometry); + rig->setSourceGeometry(geometry); + + osg::ref_ptr map(new SceneUtil::RigGeometry::InfluenceMap); + + auto skin = static_cast(bsTriShape->mSkin.getPtr()); + const Nif::BSSkinBoneData* data = skin->mData.getPtr(); + const Nif::NiAVObjectList& bones = skin->mBones; + std::vector> vertWeights(data->mBones.size()); + for (size_t i = 0; i < vertices.size(); i++) + for (int j = 0; j < 4; j++) + vertWeights[bsTriShape->mVertData[i].mBoneIndices[j]].emplace_back( + i, halfToFloat(bsTriShape->mVertData[i].mBoneWeights[j])); + + for (std::size_t i = 0; i < bones.size(); ++i) + { + std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->mName); + + SceneUtil::RigGeometry::BoneInfluence influence; + influence.mWeights = vertWeights[i]; + influence.mInvBindMatrix = data->mBones[i].mTransform.toMatrix(); + influence.mBoundSphere = data->mBones[i].mBoundSphere; - map->mData.emplace_back(boneName, influence); + map->mData.emplace_back(boneName, influence); + } + rig->setInfluenceMap(map); + + drawable = rig; } - rig->setInfluenceMap(map); - parentNode->addChild(rig); + std::vector drawableProps; + collectDrawableProperties(nifNode, parent, drawableProps); + if (!bsTriShape->mShaderProperty.empty()) + drawableProps.emplace_back(bsTriShape->mShaderProperty.getPtr()); + if (!bsTriShape->mAlphaProperty.empty()) + drawableProps.emplace_back(bsTriShape->mAlphaProperty.getPtr()); + applyDrawableProperties(parentNode, drawableProps, composite, !colors.empty(), animflags); + + drawable->setName(nifNode->mName); + parentNode->addChild(drawable); } osg::BlendFunc::BlendFuncMode getBlendMode(int mode)