diff --git a/src/Common/Game/IW3/IW3_Assets.h b/src/Common/Game/IW3/IW3_Assets.h index 0d4062e9..84380acd 100644 --- a/src/Common/Game/IW3/IW3_Assets.h +++ b/src/Common/Game/IW3/IW3_Assets.h @@ -327,8 +327,8 @@ namespace IW3 struct XSurfaceCollisionTree { - float trans[3]; - float scale[3]; + vec3_t trans; + vec3_t scale; unsigned int nodeCount; XSurfaceCollisionNode* nodes; unsigned int leafCount; diff --git a/src/Common/Game/IW4/IW4_Assets.h b/src/Common/Game/IW4/IW4_Assets.h index 40aa6a87..58e6925f 100644 --- a/src/Common/Game/IW4/IW4_Assets.h +++ b/src/Common/Game/IW4/IW4_Assets.h @@ -468,8 +468,8 @@ namespace IW4 struct XSurfaceCollisionTree { - float trans[3]; - float scale[3]; + vec3_t trans; + vec3_t scale; unsigned int nodeCount; XSurfaceCollisionNode* nodes; unsigned int leafCount; diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h index ae694a85..3dcc1ecc 100644 --- a/src/Common/Game/IW5/IW5_Assets.h +++ b/src/Common/Game/IW5/IW5_Assets.h @@ -492,8 +492,8 @@ namespace IW5 struct XSurfaceCollisionTree { - float trans[3]; - float scale[3]; + vec3_t trans; + vec3_t scale; unsigned int nodeCount; XSurfaceCollisionNode* nodes; unsigned int leafCount; @@ -516,6 +516,11 @@ namespace IW5 typedef tdef_align32(16) XSurfaceTri XSurfaceTri16; + enum XSurfaceFlag + { + XSURFACE_FLAG_DEFORMED = 0x40 + }; + struct XSurface { unsigned char tileMode; diff --git a/src/Common/Game/T5/T5_Assets.h b/src/Common/Game/T5/T5_Assets.h index 13f25115..7ffd2c64 100644 --- a/src/Common/Game/T5/T5_Assets.h +++ b/src/Common/Game/T5/T5_Assets.h @@ -488,8 +488,8 @@ namespace T5 struct XSurfaceCollisionTree { - float trans[3]; - float scale[3]; + vec3_t trans; + vec3_t scale; unsigned int nodeCount; XSurfaceCollisionNode* nodes; unsigned int leafCount; diff --git a/src/ObjLoading/XModel/CollisionTreeCreator.cpp b/src/ObjLoading/XModel/CollisionTreeCreator.cpp new file mode 100644 index 00000000..4e9ec646 --- /dev/null +++ b/src/ObjLoading/XModel/CollisionTreeCreator.cpp @@ -0,0 +1,566 @@ +#include "CollisionTreeCreator.h" + +#include "Utils/Logging/Log.h" + +#include +#include + +namespace +{ + class Bounds + { + public: + Bounds() + : m_mins(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()), + m_maxs(-std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::max()) + { + } + + void ExpandFromTri(const std::array& triCoordinates) + { + for (const auto& vert : triCoordinates) + { + m_mins[0] = std::min(m_mins[0], vert.x()); + m_mins[1] = std::min(m_mins[1], vert.y()); + m_mins[2] = std::min(m_mins[2], vert.z()); + m_maxs[0] = std::max(m_maxs[0], vert.x()); + m_maxs[1] = std::max(m_maxs[1], vert.y()); + m_maxs[2] = std::max(m_maxs[2], vert.z()); + } + } + + void ExpandFromBounds(const Bounds& otherBounds) + { + m_mins[0] = std::min(m_mins[0], otherBounds.m_mins[0]); + m_mins[1] = std::min(m_mins[1], otherBounds.m_mins[1]); + m_mins[2] = std::min(m_mins[2], otherBounds.m_mins[2]); + m_maxs[0] = std::max(m_maxs[0], otherBounds.m_maxs[0]); + m_maxs[1] = std::max(m_maxs[1], otherBounds.m_maxs[1]); + m_maxs[2] = std::max(m_maxs[2], otherBounds.m_maxs[2]); + } + + [[nodiscard]] Eigen::Vector3f GetDelta() const + { + return m_maxs - m_mins; + } + + [[nodiscard]] float GetVolume() const + { + return GetDelta().prod(); + } + + [[nodiscard]] float AddedVolume(const Bounds& addedBounds) const + { + Bounds expandedBounds(*this); + expandedBounds.ExpandFromBounds(addedBounds); + + return expandedBounds.GetVolume() - GetVolume(); + } + + Eigen::Vector3f m_mins; + Eigen::Vector3f m_maxs; + }; + + constexpr auto MIN_ITEMS_PER_LEAF = 1u; + constexpr auto MAX_ITEMS_PER_LEAF = 16u; + + struct GenericAabbTree + { + size_t firstItem; + size_t itemCount; + size_t firstChild; + size_t childCount; + }; + + class AabbTreeBuilder + { + public: + AabbTreeBuilder(std::vector& nodes, std::vector& items, std::vector& itemBounds) + : m_nodes(nodes), + m_items(items), + m_item_bounds(itemBounds) + { + } + + size_t BuildAabbTree() + { + if (m_items.empty()) + return 0; + + const auto itemCount = m_items.size(); + + std::vector remap(m_items.size()); + m_sorted_mins = std::vector(m_items.size()); + m_sorted_maxs = std::vector(m_items.size()); + m_sorted_coplanar = std::vector(m_items.size()); + + std::ranges::iota(remap, 0); + + // Insert root node + m_nodes.emplace_back(GenericAabbTree{ + .firstItem = 0, + .itemCount = itemCount, + .firstChild = 0, + .childCount = 0, + }); + + BuildAabbTree_r(0, remap.data()); + + // Reorder items + std::vector itemCopies(itemCount); + memcpy(itemCopies.data(), m_items.data(), sizeof(xmodel::CommonCollisionLeaf) * itemCount); + for (size_t itemIndex = 0; itemIndex < itemCount; ++itemIndex) + m_items[itemIndex] = itemCopies[remap[itemIndex]]; + + // Always trying to maintain valid bounds + std::vector boundCopies(itemCount); + std::ranges::copy(m_item_bounds, boundCopies.begin()); + for (size_t itemIndex = 0; itemIndex < itemCount; ++itemIndex) + m_item_bounds[itemIndex] = boundCopies[remap[itemIndex]]; + + return m_nodes.size(); + } + + private: + void BuildAabbTree_r(size_t treeIndex, size_t* remap) + { + assert(m_nodes[treeIndex].itemCount); + + m_nodes[treeIndex].firstChild = m_nodes.size(); + m_nodes[treeIndex].childCount = 0; + + size_t midStart, lastStart; + if (m_nodes[treeIndex].itemCount > MAX_ITEMS_PER_LEAF && SplitAabbTree(m_nodes[treeIndex].itemCount, remap, &midStart, &lastStart)) + { + const auto subTreeStartIndex = m_nodes.size(); + assert(m_nodes[treeIndex].firstChild == subTreeStartIndex); + + CreateAabbSubTrees(treeIndex, remap, 0, midStart); + + if (midStart < lastStart) + CreateAabbSubTrees(treeIndex, remap, midStart, lastStart - midStart); + + CreateAabbSubTrees(treeIndex, remap, lastStart, m_nodes[treeIndex].itemCount - lastStart); + + m_nodes[treeIndex].childCount = m_nodes.size() - m_nodes[treeIndex].firstChild; + for (size_t childIndex = 0; childIndex < m_nodes[treeIndex].childCount; ++childIndex) + { + const auto subTreeIndex = subTreeStartIndex + childIndex; + BuildAabbTree_r(subTreeIndex, &remap[m_nodes[subTreeIndex].firstItem - m_nodes[treeIndex].firstItem]); + } + } + } + + int SplitAabbTree(const size_t count, size_t* remap, size_t* midStart, size_t* lastStart) + { + size_t swapCache; + + int splitAxis; + float splitDist; + if (!PickAabbSplitPlane(m_item_bounds.data(), remap, count, splitAxis, splitDist)) + return 0; + + int bot = 0; + int top = static_cast(count - 1); + Bounds bounds[2]; + while (bot <= top) + { + while (bot <= top && splitDist >= m_item_bounds[remap[bot]].m_maxs[splitAxis] && splitDist > m_item_bounds[remap[bot]].m_mins[splitAxis]) + { + bounds[0].ExpandFromBounds(m_item_bounds[remap[bot]]); + ++bot; + } + + while (bot <= top && m_item_bounds[remap[top]].m_mins[splitAxis] >= splitDist && m_item_bounds[remap[top]].m_maxs[splitAxis] > splitDist) + { + bounds[1].ExpandFromBounds(m_item_bounds[remap[top]]); + --top; + } + + if (bot > top) + break; + + if ((m_item_bounds[remap[bot]].m_mins[splitAxis] < splitDist || m_item_bounds[remap[bot]].m_maxs[splitAxis] <= splitDist) + && (splitDist < m_item_bounds[remap[top]].m_maxs[splitAxis] || splitDist <= m_item_bounds[remap[top]].m_mins[splitAxis])) + { + int mid; + for (mid = bot; mid < top; ++mid) + { + if (m_item_bounds[remap[mid]].m_mins[splitAxis] >= splitDist && m_item_bounds[remap[mid]].m_maxs[splitAxis] > splitDist) + { + swapCache = remap[mid]; + remap[mid] = remap[top]; + remap[top] = swapCache; + break; + } + + if (splitDist >= m_item_bounds[remap[mid]].m_maxs[splitAxis] && splitDist > m_item_bounds[remap[mid]].m_mins[splitAxis]) + { + swapCache = remap[mid]; + remap[mid] = remap[bot]; + remap[bot] = swapCache; + break; + } + } + + if (mid == top) + break; + } + else + { + swapCache = remap[bot]; + remap[bot] = remap[top]; + remap[top] = swapCache; + } + } + + if (bot <= top && (bot < MIN_ITEMS_PER_LEAF || top - bot + 1 < MIN_ITEMS_PER_LEAF || count - top - 1 < MIN_ITEMS_PER_LEAF)) + { + while (bot <= top) + { + while (bot <= top) + { + if (bounds[1].AddedVolume(m_item_bounds[remap[bot]]) < bounds[0].AddedVolume(m_item_bounds[remap[bot]])) + break; + + bounds[0].ExpandFromBounds(m_item_bounds[remap[bot]]); + ++bot; + } + + while (bot <= top) + { + if (bounds[0].AddedVolume(m_item_bounds[remap[top]]) < bounds[1].AddedVolume(m_item_bounds[remap[top]])) + break; + + bounds[1].ExpandFromBounds(m_item_bounds[remap[top]]); + --top; + } + + if (bot >= top) + { + if (bot == top) + { + if (static_cast(2 * bot) >= count) + --top; + else + ++bot; + } + } + else + { + swapCache = remap[bot]; + remap[bot] = remap[top]; + remap[top] = swapCache; + ++bot; + --top; + } + } + } + + if (!bot || bot == count) + return 0; + + *midStart = bot; + *lastStart = top + 1; + + return 1; + } + + bool PickAabbSplitPlane(const Bounds* bounds, const size_t* remap, const size_t count, int& chosenAxis, float& chosenDist) + { + Bounds globalBounds; + + for (size_t i = 0; i < count; ++i) + globalBounds.ExpandFromBounds(bounds[remap[i]]); + + size_t smallestAxis = globalBounds.m_maxs[0] - globalBounds.m_mins[0] > globalBounds.m_maxs[1] - globalBounds.m_mins[1]; + if (globalBounds.m_maxs[smallestAxis] - globalBounds.m_mins[smallestAxis] > globalBounds.m_maxs[2] - globalBounds.m_mins[2]) + smallestAxis = 2; + + int axisBias[3]; + for (size_t i = 0; i < 3u; ++i) + { + axisBias[i] = static_cast((globalBounds.m_maxs[i] - globalBounds.m_mins[i] + 1.0f) * 10.0f + / (globalBounds.m_maxs[smallestAxis] - globalBounds.m_mins[smallestAxis] + 1.0f) + + 0.4999999990686774); + } + + auto bestHeuristic = -1; + for (int axisIndex = 0; axisIndex < 3; ++axisIndex) + { + size_t minMaxCount = 0; + size_t coplanarCount = 0; + + for (size_t i = 0; i < count; ++i) + { + if (bounds[remap[i]].m_mins[axisIndex] == bounds[remap[i]].m_maxs[axisIndex]) + { + m_sorted_coplanar[coplanarCount++] = bounds[remap[i]].m_mins[axisIndex]; + } + else + { + m_sorted_mins[minMaxCount] = bounds[remap[i]].m_mins[axisIndex]; + m_sorted_maxs[minMaxCount++] = bounds[remap[i]].m_maxs[axisIndex]; + } + } + + std::sort(&m_sorted_mins[0], &m_sorted_mins[minMaxCount], std::greater()); + std::sort(&m_sorted_maxs[0], &m_sorted_maxs[minMaxCount], std::greater()); + std::sort(&m_sorted_coplanar[0], &m_sorted_coplanar[coplanarCount], std::greater()); + + int sideFrontCount = 0; + int sideBackCount = static_cast(count); + int sideSplitCount = 0; + int sideOnCount = 0; + int prevMinCount = 0; + int prevOnCount = 0; + + float nextDist; + if (coplanarCount && m_sorted_coplanar[0] - m_sorted_mins[0] < 0.0f) + nextDist = m_sorted_coplanar[0]; + else + nextDist = m_sorted_mins[0]; + + size_t minIndex = 0; + size_t maxIndex = 0; + size_t onIndex = 0; + while (nextDist < std::numeric_limits::max()) + { + const auto dist = nextDist; + nextDist = std::numeric_limits::max(); + + sideSplitCount += prevMinCount; + sideBackCount -= prevMinCount; + prevMinCount = 0; + + while (minIndex < minMaxCount && m_sorted_mins[minIndex] == dist) + { + ++prevMinCount; + ++minIndex; + } + + if (minIndex < minMaxCount && m_sorted_mins[minIndex] < nextDist) + nextDist = m_sorted_mins[minIndex]; + + while (maxIndex < minMaxCount && m_sorted_maxs[maxIndex] == dist) + { + ++sideFrontCount; + --sideSplitCount; + ++maxIndex; + } + + if (maxIndex < minMaxCount && nextDist > m_sorted_maxs[maxIndex]) + nextDist = m_sorted_maxs[maxIndex]; + + sideFrontCount += prevOnCount; + sideOnCount -= prevOnCount; + prevOnCount = 0; + + while (onIndex < coplanarCount && m_sorted_coplanar[onIndex] == dist) + { + ++prevOnCount; + ++onIndex; + } + + sideOnCount += prevOnCount; + sideBackCount -= prevOnCount; + + if (onIndex < coplanarCount && nextDist > m_sorted_coplanar[onIndex]) + nextDist = m_sorted_coplanar[onIndex]; + + assert(sideFrontCount + sideBackCount + sideSplitCount + sideOnCount == count); + assert(sideFrontCount >= 0); + assert(sideBackCount >= 0); + assert(sideSplitCount >= 0); + assert(sideOnCount >= 0); + + if (sideFrontCount > 1 && sideBackCount > 1) + { + int heuristic = + axisBias[axisIndex] + static_cast(count) - std::abs(sideFrontCount - sideBackCount) - sideOnCount - 4 * sideSplitCount; + + if (!sideOnCount && !sideSplitCount && !prevMinCount) + heuristic += static_cast(nextDist - dist); + + if (heuristic > bestHeuristic) + { + bestHeuristic = heuristic; + chosenAxis = axisIndex; + if (sideOnCount || sideSplitCount || prevMinCount) + chosenDist = dist; + else + chosenDist = (dist + nextDist) * 0.5f; + } + } + } + } + + return bestHeuristic != -1; + } + + void CreateAabbSubTrees(const size_t treeIndex, size_t* remap, const size_t firstIndex, const size_t count) + { + size_t midStart, lastStart; + if (count > MAX_ITEMS_PER_LEAF && SplitAabbTree(count, &remap[firstIndex], &midStart, &lastStart)) + { + m_nodes.emplace_back(GenericAabbTree{ + .firstItem = firstIndex + m_nodes[treeIndex].firstItem, + .itemCount = midStart, + .firstChild = 0, + .childCount = 0, + }); + + if (midStart < lastStart) + { + m_nodes.emplace_back(GenericAabbTree{ + .firstItem = midStart + firstIndex + m_nodes[treeIndex].firstItem, + .itemCount = lastStart - midStart, + .firstChild = 0, + .childCount = 0, + }); + } + + m_nodes.emplace_back(GenericAabbTree{ + .firstItem = lastStart + firstIndex + m_nodes[treeIndex].firstItem, + .itemCount = count - lastStart, + .firstChild = 0, + .childCount = 0, + }); + } + else + { + m_nodes.emplace_back(GenericAabbTree{ + .firstItem = firstIndex + m_nodes[treeIndex].firstItem, + .itemCount = count, + .firstChild = 0, + .childCount = 0, + }); + } + } + + // Options + std::vector& m_nodes; + std::vector& m_items; + std::vector& m_item_bounds; + + // State + std::vector m_sorted_mins; + std::vector m_sorted_maxs; + std::vector m_sorted_coplanar; + }; +} // namespace + +namespace xmodel +{ + std::unique_ptr CreateCollisionTree(const IRigidVertListAccessor& vertList) + { + auto tree = std::make_unique(); + + Bounds globalBounds; + + const auto triOffset = vertList.GetTriOffset(); + const auto triCount = vertList.GetTriCount(); + std::vector leafBounds; + auto lastMergeable = false; + + Bounds prevBounds; + for (size_t triNumber = 0; triNumber < triCount; triNumber++) + { + const auto triIndex = triOffset + triNumber; + Bounds triBounds; + + triBounds.ExpandFromTri(vertList.GetCoordinatesForTri(triIndex)); + globalBounds.ExpandFromBounds(triBounds); + + auto shouldMerge = false; + if (lastMergeable) + { + const auto prevVolume = prevBounds.GetVolume(); + const auto thisVolume = triBounds.GetVolume(); + + prevBounds.ExpandFromBounds(triBounds); + const auto combinedVolume = prevBounds.GetVolume(); + if (combinedVolume <= prevVolume + thisVolume) + shouldMerge = true; + } + + if (shouldMerge) + { + leafBounds.back() = prevBounds; + assert(!tree->leafs.back().twoTriangles); + tree->leafs.back().twoTriangles = 1; + lastMergeable = false; + } + else + { + tree->leafs.emplace_back(CommonCollisionLeaf{ + .triangleBeginIndex = static_cast(triIndex), + .twoTriangles = 0, + }); + leafBounds.emplace_back(triBounds); + lastMergeable = true; + prevBounds = triBounds; + } + } + + tree->trans = -globalBounds.m_mins; + const auto globalDelta = globalBounds.GetDelta(); + tree->scale = Eigen::Vector3f(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()) + .cwiseQuotient(globalDelta); + + std::vector nodes; + AabbTreeBuilder aabbTreeBuilder(nodes, tree->leafs, leafBounds); + aabbTreeBuilder.BuildAabbTree(); + + const auto nodeCount = nodes.size(); + tree->nodes.resize(nodeCount); + for (size_t nodeIndex = 0; nodeIndex < nodeCount; ++nodeIndex) + { + auto& outNode = tree->nodes[nodeIndex]; + const auto& builtNode = nodes[nodeIndex]; + + const auto leafEnd = builtNode.itemCount + builtNode.firstItem; + Bounds nodeBounds; + + for (auto leafIndex = builtNode.firstItem; leafIndex < leafEnd; ++leafIndex) + nodeBounds.ExpandFromBounds(leafBounds[leafIndex]); + + outNode.aabb.mins[0] = static_cast(std::clamp( + (nodeBounds.m_mins.x() + tree->trans[0]) * tree->scale[0] - 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + outNode.aabb.mins[1] = static_cast(std::clamp( + (nodeBounds.m_mins.y() + tree->trans[1]) * tree->scale[1] - 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + outNode.aabb.mins[2] = static_cast(std::clamp( + (nodeBounds.m_mins.z() + tree->trans[2]) * tree->scale[2] - 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + + outNode.aabb.maxs[0] = static_cast(std::clamp( + (nodeBounds.m_maxs.x() + tree->trans[0]) * tree->scale[0] + 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + outNode.aabb.maxs[1] = static_cast(std::clamp( + (nodeBounds.m_maxs.y() + tree->trans[1]) * tree->scale[1] + 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + outNode.aabb.maxs[2] = static_cast(std::clamp( + (nodeBounds.m_maxs.z() + tree->trans[2]) * tree->scale[2] + 0.5f, std::numeric_limits::min(), std::numeric_limits::max())); + + if (builtNode.childCount) + { + assert(builtNode.firstChild <= std::numeric_limits::max()); + outNode.childBeginIndex = static_cast(builtNode.firstChild); + + assert(builtNode.childCount <= std::numeric_limits::max()); + outNode.childCount = static_cast(builtNode.childCount); + } + else + { + assert(builtNode.itemCount); + + assert(builtNode.firstItem <= std::numeric_limits::max()); + outNode.childBeginIndex = static_cast(builtNode.firstItem); + + assert(builtNode.itemCount <= std::numeric_limits::max()); + outNode.childCount = static_cast(builtNode.itemCount); + + outNode.childrenAreLeafs = true; + } + } + + return std::move(tree); + } +} // namespace xmodel diff --git a/src/ObjLoading/XModel/CollisionTreeCreator.h b/src/ObjLoading/XModel/CollisionTreeCreator.h new file mode 100644 index 00000000..169a7caf --- /dev/null +++ b/src/ObjLoading/XModel/CollisionTreeCreator.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace xmodel +{ + struct CommonCollisionAabb + { + std::array mins; + std::array maxs; + }; + + struct CommonCollisionNode + { + CommonCollisionAabb aabb; + uint16_t childBeginIndex; + uint16_t childCount : 15; + uint16_t childrenAreLeafs : 1; + }; + + struct CommonCollisionLeaf + { + uint16_t triangleBeginIndex : 15; + uint16_t twoTriangles : 1; + }; + + struct CommonCollisionTree + { + Eigen::Vector3f trans; + Eigen::Vector3f scale; + std::vector nodes; + std::vector leafs; + }; + + class IRigidVertListAccessor + { + public: + IRigidVertListAccessor() = default; + virtual ~IRigidVertListAccessor() = default; + IRigidVertListAccessor(const IRigidVertListAccessor& other) = default; + IRigidVertListAccessor(IRigidVertListAccessor&& other) noexcept = default; + IRigidVertListAccessor& operator=(const IRigidVertListAccessor& other) = default; + IRigidVertListAccessor& operator=(IRigidVertListAccessor&& other) noexcept = default; + + [[nodiscard]] virtual size_t GetTriOffset() const = 0; + [[nodiscard]] virtual size_t GetTriCount() const = 0; + [[nodiscard]] virtual std::array GetCoordinatesForTri(size_t triIndex) const = 0; + }; + + std::unique_ptr CreateCollisionTree(const IRigidVertListAccessor& vertList); +} // namespace xmodel diff --git a/src/ObjLoading/XModel/LoaderXModel.cpp.template b/src/ObjLoading/XModel/LoaderXModel.cpp.template index 018ada36..c5efdd94 100644 --- a/src/ObjLoading/XModel/LoaderXModel.cpp.template +++ b/src/ObjLoading/XModel/LoaderXModel.cpp.template @@ -9,12 +9,16 @@ #if GAME == "IW3" #define FEATURE_IW3 +#define EVEN_TRI_COUNT #elif GAME == "IW4" #define FEATURE_IW4 +#define EVEN_TRI_COUNT #elif GAME == "IW5" #define FEATURE_IW5 +#define EVEN_TRI_COUNT #elif GAME == "T5" #define FEATURE_T5 +#define EVEN_TRI_COUNT #elif GAME == "T6" #define FEATURE_T6 #endif @@ -26,6 +30,7 @@ #include JSON_HEADER #include "Asset/AssetRegistration.h" +#include "Utils/Alignment.h" #include "Utils/Logging/Log.h" #include "Utils/QuatInt16.h" #include "Utils/StringUtils.h" @@ -39,6 +44,7 @@ #include #pragma warning(pop) +#include "XModel/CollisionTreeCreator.h" #include "XModel/PartClassificationState.h" #include "XModel/TangentData.h" #include "XModel/Tangentspace.h" @@ -54,6 +60,47 @@ using namespace GAME; namespace { + constexpr uint16_t XSURFACE_COLLISION_NODE_HAS_LEAFS = 0x8000; + constexpr uint16_t XSURFACE_COLLISION_LEAF_TWO_TRIANGLES = 0x8000; + + Eigen::Vector3f ToEigen(const vec3_t& vec) + { + return Eigen::Vector3f(vec.x, vec.y, vec.z); + } + + class RigidVertListAccessor : public xmodel::IRigidVertListAccessor + { + public: + RigidVertListAccessor(const XSurface& surface, const XRigidVertList& rigidVertList) + : m_surface(surface), + m_rigid_vert_list(rigidVertList) + { + } + + [[nodiscard]] size_t GetTriOffset() const override + { + return m_rigid_vert_list.triOffset; + } + + [[nodiscard]] size_t GetTriCount() const override + { + return m_rigid_vert_list.triCount; + } + + [[nodiscard]] std::array GetCoordinatesForTri(const size_t triIndex) const override + { + return std::array({ + ToEigen(m_surface.verts0[m_surface.triIndices[triIndex].i[0]].xyz), + ToEigen(m_surface.verts0[m_surface.triIndices[triIndex].i[1]].xyz), + ToEigen(m_surface.verts0[m_surface.triIndices[triIndex].i[2]].xyz), + }); + } + + private: + const XSurface& m_surface; + const XRigidVertList& m_rigid_vert_list; + }; + class XModelLoader final : public AssetCreator { public: @@ -500,6 +547,49 @@ namespace const auto shiftValue = 31u - (boneIndex % 32u); surface.partBits[partBitsIndex] |= 1 << shiftValue; } + + [[nodiscard]] XSurfaceCollisionTree* ConvertCollisionTree(const xmodel::CommonCollisionTree& commonTree) const + { + auto* tree = m_memory.Alloc(); + tree->trans.x = commonTree.trans.x(); + tree->trans.y = commonTree.trans.y(); + tree->trans.z = commonTree.trans.z(); + tree->scale.x = commonTree.scale.x(); + tree->scale.y = commonTree.scale.y(); + tree->scale.z = commonTree.scale.z(); + tree->nodeCount = static_cast(commonTree.nodes.size()); + tree->nodes = m_memory.Alloc(tree->nodeCount); + for (auto nodeIndex = 0u; nodeIndex < tree->nodeCount; nodeIndex++) + { + auto& node = tree->nodes[nodeIndex]; + const auto& commonNode = commonTree.nodes[nodeIndex]; + + node.aabb.mins[0] = commonNode.aabb.mins[0]; + node.aabb.mins[1] = commonNode.aabb.mins[1]; + node.aabb.mins[2] = commonNode.aabb.mins[2]; + node.aabb.maxs[0] = commonNode.aabb.maxs[0]; + node.aabb.maxs[1] = commonNode.aabb.maxs[1]; + node.aabb.maxs[2] = commonNode.aabb.maxs[2]; + node.childBeginIndex = commonNode.childBeginIndex; + node.childCount = commonNode.childCount; + if (commonNode.childrenAreLeafs) + node.childCount |= XSURFACE_COLLISION_NODE_HAS_LEAFS; + } + + tree->leafCount = static_castleafCount)>(commonTree.leafs.size()); + tree->leafs = m_memory.Alloc(tree->leafCount); + for (auto leafIndex = 0u; leafIndex < tree->leafCount; leafIndex++) + { + auto& leaf = tree->leafs[leafIndex]; + const auto& commonLeaf = commonTree.leafs[leafIndex]; + + leaf.triangleBeginIndex = commonLeaf.triangleBeginIndex; + if (commonLeaf.twoTriangles) + leaf.triangleBeginIndex |= XSURFACE_COLLISION_LEAF_TWO_TRIANGLES; + } + + return tree; + } void CreateVertListData(XSurface& surface, const std::vector& xmodelToCommonVertexIndexLookup, const XModelCommon& common) const { @@ -533,7 +623,10 @@ namespace if (boneVertList.triCount > 0 || boneVertList.vertCount > 0) { - boneVertList.collisionTree = nullptr; // TODO + RigidVertListAccessor vertListAccessor(surface, boneVertList); + const auto commonCollisionTree = xmodel::CreateCollisionTree(vertListAccessor); + boneVertList.collisionTree = ConvertCollisionTree(*commonCollisionTree); + vertLists.emplace_back(boneVertList); currentVertexTail = currentVertexHead; @@ -723,7 +816,14 @@ namespace } surface.triCount = static_cast(commonObject.m_faces.size()); + +#ifdef EVEN_TRI_COUNT + // Some games round up to an even tri count + const auto allocatedTriCount = utils::Align(surface.triCount, 2); + surface.triIndices = m_memory.Alloc(allocatedTriCount); +#else surface.triIndices = m_memory.Alloc(surface.triCount); +#endif xmodelToCommonVertexIndexLookup.reserve(static_cast(surface.triCount) * std::extent_v); for (auto faceIndex = 0u; faceIndex < surface.triCount; faceIndex++) @@ -752,9 +852,16 @@ namespace // Since bone weights are sorted by weight count, the last must have the highest weight count const auto maxWeightCount = common.m_vertex_bone_weights[xmodelToCommonVertexIndexLookup[xmodelToCommonVertexIndexLookup.size() - 1]].weightCount; - const auto modelIsRigid = maxWeightCount <= 1; + const auto surfaceIsRigid = maxWeightCount <= 1; - if (modelIsRigid) +#if defined(FEATURE_IW3) || defined(FEATURE_IW4) + surface.deformed = !surfaceIsRigid; +#else + if (!surfaceIsRigid) + surface.flags |= XSURFACE_FLAG_DEFORMED; +#endif + + if (surfaceIsRigid) { constexpr auto maxVerticesForRigid = static_cast(std::numeric_limits::max()); if (vertexOffset + xmodelToCommonVertexIndexLookup.size() > maxVerticesForRigid) @@ -790,7 +897,7 @@ namespace if (!common.m_bone_weight_data.weights.empty()) { - if (modelIsRigid) + if (surfaceIsRigid) { CreateVertListData(surface, xmodelToCommonVertexIndexLookup, common); } @@ -808,6 +915,18 @@ namespace return false; } } + +#ifdef EVEN_TRI_COUNT + // The game makes sure to allows have an even triCount + // If the triCount is uneven, an additional tri is added that uses the last vertex three times + if (allocatedTriCount > surface.triCount) + { + assert(allocatedTriCount == surface.triCount + 1); + surface.triIndices[allocatedTriCount - 1].i[0] = surface.triIndices[surface.triCount - 1].i[2]; + surface.triIndices[allocatedTriCount - 1].i[1] = surface.triIndices[surface.triCount - 1].i[2]; + surface.triIndices[allocatedTriCount - 1].i[2] = surface.triIndices[surface.triCount - 1].i[2]; + } +#endif return true; } diff --git a/src/ObjWriting/XModel/XModelDumper.cpp.template b/src/ObjWriting/XModel/XModelDumper.cpp.template index 434750af..06a7ff08 100644 --- a/src/ObjWriting/XModel/XModelDumper.cpp.template +++ b/src/ObjWriting/XModel/XModelDumper.cpp.template @@ -423,6 +423,12 @@ namespace if (surface.vertList) { +#if defined(FEATURE_IW3) || defined(FEATURE_IW4) + assert(!surface.deformed); +#else + assert((surface.flags & XSURFACE_FLAG_DEFORMED) == 0); +#endif + for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++) { const auto& vertList = surface.vertList[vertListIndex]; @@ -442,6 +448,12 @@ namespace auto vertsBlendOffset = 0u; if (surface.vertInfo.vertsBlend) { +#if defined(FEATURE_IW3) || defined(FEATURE_IW4) + assert(surface.deformed); +#else + assert((surface.flags & XSURFACE_FLAG_DEFORMED) > 0); +#endif + // 1 bone weight for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++) {