feat: client-side WOC collision loading + walkability queries

- TerrainManager loads WOC collision meshes alongside WOT/WHM terrain
  from both custom_zones/ and output/ directories
- CollisionData stored per-tile with triangle array + bounds
- isPositionWalkable(x, y): returns whether a world position is on
  walkable terrain (barycentric point-in-triangle test)
- getCollisionFlags(x, y): returns per-triangle flags (walkable,
  water, steep, indoor) for movement system integration
- Defaults to walkable when no collision data is loaded (backward compat)
- Custom zone players now have proper terrain physics boundaries
This commit is contained in:
Kelsi 2026-05-05 15:32:48 -07:00
parent a25c4cfe1f
commit cff8d359e4
2 changed files with 72 additions and 0 deletions

View file

@ -260,6 +260,10 @@ public:
*/
std::optional<float> getHeightAt(float glX, float glY) const;
// Collision queries using WOC data (custom zones)
bool isPositionWalkable(float glX, float glY) const;
uint8_t getCollisionFlags(float glX, float glY) const;
/**
* Get dominant terrain texture name at a GL position.
* Returns empty if terrain is not loaded at that position.
@ -356,6 +360,16 @@ private:
bool taxiStreamingMode_ = false;
bool isCustomZone_ = false;
// Collision data for custom zones (loaded from WOC files)
struct CollisionData {
struct Triangle { glm::vec3 v0, v1, v2; uint8_t flags; };
std::vector<Triangle> triangles;
glm::vec3 boundsMin{1e30f}, boundsMax{-1e30f};
bool loaded = false;
};
std::unordered_map<uint64_t, CollisionData> collisionTiles_;
uint64_t tileKey(int x, int y) const { return (static_cast<uint64_t>(x) << 32) | static_cast<uint32_t>(y); }
// Tile size constants (WoW ADT specifications)
// A tile (ADT) = 16x16 chunks = 533.33 units across
// A chunk = 8x8 vertex quads = 33.33 units across

View file

@ -11,6 +11,7 @@
#include "pipeline/wowee_terrain_loader.hpp"
#include "pipeline/wowee_model.hpp"
#include "pipeline/wowee_building.hpp"
#include "pipeline/wowee_collision.hpp"
#include "core/memory_monitor.hpp"
#include "core/profiler.hpp"
#include "pipeline/asset_manager.hpp"
@ -323,6 +324,21 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
if (pipeline::WoweeTerrainLoader::load(wotBase, *terrainPtr)) {
loadedFromWot = true;
LOG_INFO("Loaded custom zone terrain: ", wotBase);
// Load collision mesh if available
if (pipeline::WoweeCollisionBuilder::exists(wotBase)) {
auto woc = pipeline::WoweeCollisionBuilder::load(wotBase + ".woc");
if (woc.isValid()) {
CollisionData cd;
cd.triangles.reserve(woc.triangles.size());
for (const auto& t : woc.triangles)
cd.triangles.push_back({t.v0, t.v1, t.v2, t.flags});
cd.boundsMin = woc.bounds.min;
cd.boundsMax = woc.bounds.max;
cd.loaded = true;
collisionTiles_[tileKey(coord.x, coord.y)] = std::move(cd);
LOG_INFO("Loaded WOC collision: ", woc.triangles.size(), " triangles");
}
}
}
}
@ -334,6 +350,20 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
if (pipeline::WoweeTerrainLoader::load(outputBase, *terrainPtr)) {
loadedFromWot = true;
LOG_INFO("Loaded editor output terrain: ", outputBase);
if (pipeline::WoweeCollisionBuilder::exists(outputBase)) {
auto woc = pipeline::WoweeCollisionBuilder::load(outputBase + ".woc");
if (woc.isValid()) {
CollisionData cd;
cd.triangles.reserve(woc.triangles.size());
for (const auto& t : woc.triangles)
cd.triangles.push_back({t.v0, t.v1, t.v2, t.flags});
cd.boundsMin = woc.bounds.min;
cd.boundsMax = woc.bounds.max;
cd.loaded = true;
collisionTiles_[tileKey(coord.x, coord.y)] = std::move(cd);
LOG_INFO("Loaded WOC collision: ", woc.triangles.size(), " triangles");
}
}
}
}
}
@ -2532,5 +2562,33 @@ void TerrainManager::precacheTiles(const std::vector<std::pair<int, int>>& tiles
queueCV.notify_all();
}
uint8_t TerrainManager::getCollisionFlags(float glX, float glY) const {
for (const auto& [key, cd] : collisionTiles_) {
if (!cd.loaded) continue;
if (glX < cd.boundsMin.x || glX > cd.boundsMax.x ||
glY < cd.boundsMin.y || glY > cd.boundsMax.y) continue;
for (const auto& tri : cd.triangles) {
// Barycentric point-in-triangle test (XY plane)
glm::vec2 p(glX, glY);
glm::vec2 a(tri.v0.x, tri.v0.y), b(tri.v1.x, tri.v1.y), c(tri.v2.x, tri.v2.y);
glm::vec2 v0 = c - a, v1 = b - a, v2 = p - a;
float d00 = glm::dot(v0, v0), d01 = glm::dot(v0, v1), d02 = glm::dot(v0, v2);
float d11 = glm::dot(v1, v1), d12 = glm::dot(v1, v2);
float inv = d00 * d11 - d01 * d01;
if (std::abs(inv) < 1e-10f) continue;
float u = (d11 * d02 - d01 * d12) / inv;
float v = (d00 * d12 - d01 * d02) / inv;
if (u >= 0 && v >= 0 && u + v <= 1)
return tri.flags;
}
}
return 0x01; // default walkable if no collision data
}
bool TerrainManager::isPositionWalkable(float glX, float glY) const {
return (getCollisionFlags(glX, glY) & 0x01) != 0;
}
} // namespace rendering
} // namespace wowee