From 878e43f983cb70f61f634313fb037a722786a84a Mon Sep 17 00:00:00 2001 From: mo Date: Sat, 17 Oct 2026 11:42:17 +0100 Subject: [PATCH] fix: initialize XModel bounds (#1035) --- .../XModel/LoaderXModel.cpp.template | 8 ++- test/SystemTests/Game/IW4/XModelBoundsIW4.cpp | 67 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/SystemTests/Game/IW4/XModelBoundsIW4.cpp diff --git a/src/ObjLoading/XModel/LoaderXModel.cpp.template b/src/ObjLoading/XModel/LoaderXModel.cpp.template index c07106a5..3f8477f2 100644 --- a/src/ObjLoading/XModel/LoaderXModel.cpp.template +++ b/src/ObjLoading/XModel/LoaderXModel.cpp.template @@ -61,6 +61,7 @@ #include #include #include +#include #include #include @@ -1062,7 +1063,9 @@ namespace return; const auto* surfs = xmodel.lodInfo[0].modelSurfs->surfs; - vec3_t minCoordinate, maxCoordinate; + constexpr auto coordinateLimit = std::numeric_limits::max(); + vec3_t minCoordinate{{coordinateLimit, coordinateLimit, coordinateLimit}}; + vec3_t maxCoordinate{{-coordinateLimit, -coordinateLimit, -coordinateLimit}}; #else if (!xmodel.surfs) return; @@ -1096,6 +1099,9 @@ namespace } #if defined(FEATURE_IW4) || defined(FEATURE_IW5) + if (minCoordinate.x > maxCoordinate.x) + return; + const Eigen::Vector3f minEigen(minCoordinate.x, minCoordinate.y, minCoordinate.z); const Eigen::Vector3f maxEigen(maxCoordinate.x, maxCoordinate.y, maxCoordinate.z); const Eigen::Vector3f boundsCenter = (minEigen + maxEigen) * 0.5f; diff --git a/test/SystemTests/Game/IW4/XModelBoundsIW4.cpp b/test/SystemTests/Game/IW4/XModelBoundsIW4.cpp new file mode 100644 index 00000000..45fa89bf --- /dev/null +++ b/test/SystemTests/Game/IW4/XModelBoundsIW4.cpp @@ -0,0 +1,67 @@ +#include "Game/IW4/IW4.h" +#include "Gdt/GdtLookup.h" +#include "IObjLoader.h" +#include "SearchPath/MockSearchPath.h" +#include "XModel/Gltf/GltfBinOutput.h" +#include "XModel/Gltf/GltfWriter.h" +#include "XModel/XModelCommon.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + TEST_CASE("IW4 model bounds enclose geometry away from the origin", "[iw4][system][xmodel][bounds]") + { + const auto offset = GENERATE(10.0f, -30.0f); + CAPTURE(offset); + XModelCommon common; + common.m_name = "triangle"; + common.m_vertices = { + {.coordinates = {offset, offset, offset}, .normal = {0, 0, 1}, .color = {1, 1, 1, 1}, .uv = {0, 0}}, + {.coordinates = {offset + 2, offset, offset}, .normal = {0, 0, 1}, .color = {1, 1, 1, 1}, .uv = {1, 0}}, + {.coordinates = {offset, offset + 4, offset}, .normal = {0, 0, 1}, .color = {1, 1, 1, 1}, .uv = {0, 1}}, + }; + XModelMaterial material{}; + material.ApplyDefaults(); + material.name = "material"; + common.m_materials.push_back(material); + common.m_objects.push_back({.name = "triangle", .materialIndex = 0, .m_faces = {XModelFace{{0, 1, 2}}}}); + std::ostringstream modelData(std::ios::binary); + const gltf::BinOutput modelOutput(modelData); + gltf::Writer::CreateWriter(&modelOutput, "iw4", "test")->Write(common); + const nlohmann::json metadata{ + {"_game", "iw4" }, + {"_type", "xmodel" }, + {"_version", 2 }, + {"flags", 0 }, + {"lods", {{{"file", "triangle.glb"}, {"distance", 100}}}}, + }; + MockSearchPath searchPath; + searchPath.AddFileData("triangle.glb", modelData.str()); + searchPath.AddFileData("xmodel/triangle.json", metadata.dump()); + searchPath.AddFileData("partclassification.csv", ""); + Zone zone("bounds", 0, GameId::IW4, GamePlatform::PC); + AssetCreatorCollection creators(zone); + GdtLookup gdt; + IObjLoader::GetObjLoaderForGame(GameId::IW4)->ConfigureCreatorCollection(creators, zone, searchPath, gdt); + IgnoredAssetLookup ignored; + ignored.m_ignored_asset_lookup.emplace("material", IW4::ASSET_TYPE_MATERIAL); + AssetCreationContext context(zone, &creators, &ignored); + REQUIRE(creators.CreateAsset(IW4::ASSET_TYPE_XMODEL, "triangle", context).HasBeenSuccessful()); + const auto* asset = zone.m_pools.GetAsset("triangle"); + REQUIRE(asset); + const auto& model = *asset->Asset(); + CHECK_THAT(model.bounds.midPoint.x, Catch::Matchers::WithinAbs(offset + 1.0f, 0.0001f)); + CHECK_THAT(model.bounds.midPoint.y, Catch::Matchers::WithinAbs(offset + 2.0f, 0.0001f)); + CHECK_THAT(model.bounds.midPoint.z, Catch::Matchers::WithinAbs(offset, 0.0001f)); + CHECK_THAT(model.bounds.halfSize.x, Catch::Matchers::WithinAbs(1.0f, 0.0001f)); + CHECK_THAT(model.bounds.halfSize.y, Catch::Matchers::WithinAbs(2.0f, 0.0001f)); + CHECK(model.bounds.halfSize.z == 0.0f); + CHECK_THAT(model.radius, Catch::Matchers::WithinAbs(std::sqrt(5.0f), 0.0001f)); + } +} // namespace