#include "pipeline/wowee_collision.hpp" #include "pipeline/adt_loader.hpp" #include "core/logger.hpp" #include #include #include #include namespace wowee { namespace pipeline { static constexpr uint32_t WOC_MAGIC = 0x31434F57; // "WOC1" size_t WoweeCollision::walkableCount() const { size_t n = 0; for (const auto& t : triangles) if (t.flags & 0x01) n++; return n; } size_t WoweeCollision::steepCount() const { size_t n = 0; for (const auto& t : triangles) if (t.flags & 0x04) n++; return n; } WoweeCollision WoweeCollisionBuilder::fromTerrain(const ADTTerrain& terrain, float steepAngle) { WoweeCollision col; col.tileX = terrain.coord.x; col.tileY = terrain.coord.y; float steepCos = std::cos(steepAngle * 3.14159265f / 180.0f); float tileSize = 533.33333f; float chunkSize = tileSize / 16.0f; float vertSpacing = chunkSize / 8.0f; for (int ci = 0; ci < 256; ci++) { const auto& chunk = terrain.chunks[ci]; if (!chunk.hasHeightMap()) continue; int cx = ci % 16; int cy = ci / 16; float chunkBaseX = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize; float chunkBaseY = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize; bool isHoleChunk = (chunk.holes != 0); // Build outer vertex grid (9x9) for (int row = 0; row < 8; row++) { for (int col2 = 0; col2 < 8; col2++) { // Check hole mask (4x4 grid, each bit covers 2x2 sub-quads) if (isHoleChunk) { int hx = col2 / 2, hy = row / 2; if (chunk.holes & (1 << (hy * 4 + hx))) continue; } int i00 = row * 17 + col2; int i10 = row * 17 + col2 + 1; int i01 = (row + 1) * 17 + col2; int i11 = (row + 1) * 17 + col2 + 1; auto vtx = [&](int idx) -> glm::vec3 { int r = idx / 17, c = idx % 17; float x = chunkBaseX - r * vertSpacing; float y = chunkBaseY - c * vertSpacing; float z = chunk.position[2] + chunk.heightMap.heights[idx]; return glm::vec3(x, y, z); }; glm::vec3 v00 = vtx(i00), v10 = vtx(i10); glm::vec3 v01 = vtx(i01), v11 = vtx(i11); auto classifyTri = [&](const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) { WoweeCollision::Triangle tri; tri.v0 = a; tri.v1 = b; tri.v2 = c; glm::vec3 normal = glm::normalize(glm::cross(b - a, c - a)); float nz = std::abs(normal.z); tri.flags = 0; if (nz >= steepCos) tri.flags |= 0x01; // walkable else tri.flags |= 0x04; // steep col.bounds.expand(a); col.bounds.expand(b); col.bounds.expand(c); col.triangles.push_back(tri); }; classifyTri(v00, v10, v01); classifyTri(v10, v11, v01); } } // Mark water triangles if (terrain.waterData[ci].hasWater()) { float waterH = terrain.waterData[ci].layers[0].maxHeight; for (size_t ti = col.triangles.size() - 128; ti < col.triangles.size(); ti++) { auto& tri = col.triangles[ti]; float avgZ = (tri.v0.z + tri.v1.z + tri.v2.z) / 3.0f; if (avgZ < waterH) tri.flags |= 0x02; } } } LOG_INFO("Collision mesh: ", col.triangles.size(), " triangles (", col.walkableCount(), " walkable, ", col.steepCount(), " steep)"); return col; } void WoweeCollisionBuilder::addMesh(WoweeCollision& collision, const std::vector& vertices, const std::vector& indices, const glm::mat4& transform, uint8_t extraFlags, float steepAngle) { if (vertices.empty() || indices.size() < 3) return; const float steepCos = std::cos(glm::radians(steepAngle)); collision.triangles.reserve(collision.triangles.size() + indices.size() / 3); for (size_t i = 0; i + 2 < indices.size(); i += 3) { uint32_t i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2]; if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) continue; WoweeCollision::Triangle tri; tri.v0 = glm::vec3(transform * glm::vec4(vertices[i0], 1.0f)); tri.v1 = glm::vec3(transform * glm::vec4(vertices[i1], 1.0f)); tri.v2 = glm::vec3(transform * glm::vec4(vertices[i2], 1.0f)); glm::vec3 n = glm::cross(tri.v1 - tri.v0, tri.v2 - tri.v0); float len = glm::length(n); tri.flags = extraFlags; if (len > 1e-6f) { float nz = n.z / len; if (nz >= steepCos) tri.flags |= 0x01; // walkable else if (nz < 0.0f) tri.flags |= 0x04; // steep / facing-down } collision.bounds.expand(tri.v0); collision.bounds.expand(tri.v1); collision.bounds.expand(tri.v2); collision.triangles.push_back(tri); } } bool WoweeCollisionBuilder::save(const WoweeCollision& collision, const std::string& path) { namespace fs = std::filesystem; fs::create_directories(fs::path(path).parent_path()); std::ofstream f(path, std::ios::binary); if (!f) return false; f.write(reinterpret_cast(&WOC_MAGIC), 4); uint32_t triCount = static_cast(collision.triangles.size()); f.write(reinterpret_cast(&triCount), 4); f.write(reinterpret_cast(&collision.tileX), 4); f.write(reinterpret_cast(&collision.tileY), 4); f.write(reinterpret_cast(&collision.bounds.min), 12); f.write(reinterpret_cast(&collision.bounds.max), 12); for (const auto& tri : collision.triangles) { f.write(reinterpret_cast(&tri.v0), 12); f.write(reinterpret_cast(&tri.v1), 12); f.write(reinterpret_cast(&tri.v2), 12); f.write(reinterpret_cast(&tri.flags), 1); } LOG_INFO("WOC saved: ", path, " (", triCount, " triangles)"); return true; } WoweeCollision WoweeCollisionBuilder::load(const std::string& path) { WoweeCollision col; std::ifstream f(path, std::ios::binary); if (!f) return col; uint32_t magic; f.read(reinterpret_cast(&magic), 4); if (magic != WOC_MAGIC) return col; uint32_t triCount; f.read(reinterpret_cast(&triCount), 4); f.read(reinterpret_cast(&col.tileX), 4); f.read(reinterpret_cast(&col.tileY), 4); f.read(reinterpret_cast(&col.bounds.min), 12); f.read(reinterpret_cast(&col.bounds.max), 12); col.triangles.resize(triCount); for (uint32_t i = 0; i < triCount; i++) { auto& tri = col.triangles[i]; f.read(reinterpret_cast(&tri.v0), 12); f.read(reinterpret_cast(&tri.v1), 12); f.read(reinterpret_cast(&tri.v2), 12); f.read(reinterpret_cast(&tri.flags), 1); } LOG_INFO("WOC loaded: ", path, " (", triCount, " triangles)"); return col; } bool WoweeCollisionBuilder::exists(const std::string& basePath) { return std::filesystem::exists(basePath + ".woc"); } } // namespace pipeline } // namespace wowee