diff --git a/include/pipeline/wowee_collision.hpp b/include/pipeline/wowee_collision.hpp index 9d047b27..b9a8eed6 100644 --- a/include/pipeline/wowee_collision.hpp +++ b/include/pipeline/wowee_collision.hpp @@ -40,6 +40,16 @@ public: static WoweeCollision fromTerrain(const ADTTerrain& terrain, float steepAngle = 50.0f); + // Append a transformed mesh to an existing collision (for WMO/M2 instances). + // `vertices` are local-space; `transform` puts them into world space. + // Triangles are classified by slope using `steepAngle`. + static void addMesh(WoweeCollision& collision, + const std::vector& vertices, + const std::vector& indices, + const glm::mat4& transform, + uint8_t extraFlags = 0, + float steepAngle = 50.0f); + // Save collision mesh to binary file static bool save(const WoweeCollision& collision, const std::string& path); diff --git a/src/pipeline/wowee_collision.cpp b/src/pipeline/wowee_collision.cpp index 6060139a..3e9dbdd1 100644 --- a/src/pipeline/wowee_collision.cpp +++ b/src/pipeline/wowee_collision.cpp @@ -113,6 +113,41 @@ WoweeCollision WoweeCollisionBuilder::fromTerrain(const ADTTerrain& terrain, 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()); diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index ead6ccef..cc143dbc 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -12,6 +12,7 @@ #include "sql_exporter.hpp" #include "server_module_gen.hpp" #include "core/coordinates.hpp" +#include #include #include "rendering/vk_context.hpp" #include "pipeline/adt_loader.hpp" @@ -1196,8 +1197,44 @@ void EditorApp::exportZone(const std::string& outputDir) { WoweeTerrain::exportOpen(terrain_, openBase, loadedTileX_, loadedTileY_); WoweeTerrain::exportNormalMap(terrain_, openBase + "_normals.png"); - // Export collision mesh (.woc) + // Export collision mesh (.woc) — terrain plus placed WMO/M2 meshes so that + // movement queries on the exported zone respect buildings and large props. auto collision = pipeline::WoweeCollisionBuilder::fromTerrain(terrain_); + { + std::unordered_set visitedWMOcol; + std::unordered_set visitedM2col; + for (const auto& obj : objectPlacer_.getObjects()) { + if (obj.type == PlaceableType::WMO && visitedWMOcol.insert(obj.path).second) { + auto data = assetManager_->readFile(obj.path); + if (data.empty()) continue; + auto wmo = pipeline::WMOLoader::load(data); + std::string wmoBase = obj.path; + if (wmoBase.size() > 4) wmoBase = wmoBase.substr(0, wmoBase.size() - 4); + for (uint32_t gi = 0; gi < wmo.nGroups; gi++) { + char suffix[16]; + std::snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi); + auto gd = assetManager_->readFile(wmoBase + suffix); + if (!gd.empty()) pipeline::WMOLoader::loadGroup(gd, wmo, gi); + } + glm::mat4 t = glm::translate(glm::mat4(1.0f), obj.position); + glm::vec3 r = glm::radians(obj.rotation); + t = glm::rotate(t, r.x, glm::vec3(1, 0, 0)); + t = glm::rotate(t, r.y, glm::vec3(0, 1, 0)); + t = glm::rotate(t, r.z, glm::vec3(0, 0, 1)); + t = glm::scale(t, glm::vec3(obj.scale)); + for (const auto& g : wmo.groups) { + std::vector verts; + verts.reserve(g.vertices.size()); + for (const auto& v : g.vertices) verts.push_back(v.position); + std::vector idx; + idx.reserve(g.indices.size()); + for (uint16_t i : g.indices) idx.push_back(i); + uint8_t flags = (g.flags & 0x08) ? 0 : 0x08; // indoor flag + pipeline::WoweeCollisionBuilder::addMesh(collision, verts, idx, t, flags); + } + } + } + } if (collision.isValid()) pipeline::WoweeCollisionBuilder::save(collision, openBase + ".woc"); WoweeTerrain::exportAlphaMaps(terrain_, base + "/alphamaps");