Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/NifFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2561,6 +2561,29 @@ uint32_t NifFile::GetShapeBoneWeights(NiShape* shape,
return static_cast<uint32_t>(outWeights.size());
}

auto bsGeom = dynamic_cast<BSGeometry*>(shape);
if (bsGeom) {
auto* geomData = dynamic_cast<BSGeometryMeshData*>(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<size_t>(std::numeric_limits<uint16_t>::max()) + 1;
constexpr float maxWeightValue = static_cast<float>(std::numeric_limits<uint16_t>::max());

size_t vertCount = std::min(geomData->skinWeights.size(), maxVerts);
outWeights.reserve(vertCount);
Comment thread
ousnius marked this conversation as resolved.
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<uint16_t>(vid), bw.weight / maxWeightValue);
}
Comment thread
ousnius marked this conversation as resolved.
}
}
return static_cast<uint32_t>(outWeights.size());
}
Comment thread
ousnius marked this conversation as resolved.
}

auto skinInst = hdr.GetBlock<NiSkinInstance>(shape->SkinInstanceRef());
if (!skinInst)
return 0;
Expand Down
54 changes: 54 additions & 0 deletions tests/TestNifFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Comment thread
ousnius marked this conversation as resolved.
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<BSGeometry*>(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();
Comment thread
ousnius marked this conversation as resolved.
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<std::string> bones;
nif.GetShapeBoneList(s, bones);

for (uint32_t boneIndex = 0; boneIndex < bones.size(); boneIndex++) {
std::unordered_map<uint16_t, float> 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);
Expand Down
Loading