diff --git a/src/NifFile.cpp b/src/NifFile.cpp index 5959190..2408c07 100644 --- a/src/NifFile.cpp +++ b/src/NifFile.cpp @@ -2561,6 +2561,29 @@ uint32_t NifFile::GetShapeBoneWeights(NiShape* shape, return static_cast(outWeights.size()); } + auto bsGeom = dynamic_cast(shape); + if (bsGeom) { + auto* geomData = dynamic_cast(bsGeom->GetGeomData()); + if (geomData && !geomData->skinWeights.empty()) { + // outWeights is keyed by uint16_t, so vertex indices are capped at + // 0xFFFF; iterate with a wide index to avoid overflowing the loop + // counter on meshes with more than 65535 vertices. + constexpr size_t maxVerts = static_cast(std::numeric_limits::max()) + 1; + constexpr float maxWeightValue = static_cast(std::numeric_limits::max()); + + size_t vertCount = std::min(geomData->skinWeights.size(), maxVerts); + outWeights.reserve(vertCount); + for (size_t vid = 0; vid < vertCount; vid++) { + for (auto& bw : geomData->skinWeights[vid]) { + if (bw.boneIndex == boneIndex && bw.weight != 0) { + outWeights.emplace(static_cast(vid), bw.weight / maxWeightValue); + } + } + } + return static_cast(outWeights.size()); + } + } + auto skinInst = hdr.GetBlock(shape->SkinInstanceRef()); if (!skinInst) return 0; diff --git a/tests/TestNifFile.cpp b/tests/TestNifFile.cpp index c5b5783..29bf55f 100644 --- a/tests/TestNifFile.cpp +++ b/tests/TestNifFile.cpp @@ -359,6 +359,60 @@ TEST_CASE("Load and save file (SF)", "[NifFile]") { REQUIRE(CompareBinaryFiles(fileOutput, fileExpected)); } +TEST_CASE("BSGeometry bone weights are normalized (SF)", "[NifFile]") { + constexpr auto fileName = "TestNifFile_SF"; + const auto fileInput = std::get<0>(GetFileTuple(fileName, nifSuffix)); + + NifFile nif; + REQUIRE(nif.Load(fileInput) == 0); + + auto shapes = nif.GetShapes(); + REQUIRE(!shapes.empty()); + + size_t weightedVertices = 0; + + for (auto& s : shapes) { + auto* bsGeom = dynamic_cast(s); + REQUIRE(bsGeom != nullptr); + + auto meshPaths = nif.GetExternalGeometryPathRefs(s); + REQUIRE(!meshPaths.empty()); + + uint8_t meshIndex = 0; + for (auto meshPath : meshPaths) { + std::string meshPathStr = meshPath.get(); + REQUIRE(!meshPathStr.empty()); + + const auto meshFileInput = std::get<0>(GetFileTuple(meshPathStr.c_str(), meshSuffix)); + const std::filesystem::path meshInputPath = std::filesystem::u8path(meshFileInput); + auto meshStream = GetBinaryInputFileStream(meshInputPath); + REQUIRE(meshStream); + REQUIRE(nif.LoadExternalShapeData(s, *meshStream, meshIndex)); + meshStream.reset(); + meshIndex++; + } + + std::vector bones; + nif.GetShapeBoneList(s, bones); + + for (uint32_t boneIndex = 0; boneIndex < bones.size(); boneIndex++) { + std::unordered_map weights; + nif.GetShapeBoneWeights(s, boneIndex, weights); + + for (const auto& [vid, weight] : weights) { + REQUIRE(weight >= 0.0f); + REQUIRE(weight <= 1.0f); + } + + weightedVertices += weights.size(); + } + } + + // The Starfield mesh fixture is skinned, so the BSGeometry path must + // actually return per-vertex weights (regression for the new code path). + REQUIRE(weightedVertices > 0); +} + TEST_CASE("Load external and save as internal mesh data (SF)", "[NifFile]") { constexpr auto fileName = "TestNifFile_ToInternalMesh_SF"; const auto [fileInput, fileOutput, fileExpected] = GetFileTuple(fileName, nifSuffix);