diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 02e48836..0fc7a9c7 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -437,6 +437,8 @@ static void printUsage(const char* argv0) { std::printf(" Convert a WOB building to Wavefront OBJ (one group per WOB group)\n"); std::printf(" --import-wob-obj [wob-base]\n"); std::printf(" Convert a Wavefront OBJ back into WOB (round-trips with --export-wob-obj)\n"); + std::printf(" --export-woc-obj [out.obj]\n"); + std::printf(" Convert a WOC collision mesh to OBJ for visualization (per-flag color groups)\n"); std::printf(" --validate [--json]\n"); std::printf(" Score zone open-format completeness and exit\n"); std::printf(" --validate-wom [--json]\n"); @@ -519,6 +521,7 @@ int main(int argc, char* argv[]) { "--build-woc", "--regen-collision", "--fix-zone", "--export-png", "--export-obj", "--import-obj", "--export-wob-obj", "--import-wob-obj", + "--export-woc-obj", "--convert-m2", "--convert-wmo", }; for (int i = 1; i < argc; i++) { @@ -2601,6 +2604,88 @@ int main(int argc, char* argv[]) { std::printf(" warning: skipped %d malformed face(s)\n", badFaces); } return 0; + } else if (std::strcmp(argv[i], "--export-woc-obj") == 0 && i + 1 < argc) { + // Visualize a WOC collision mesh in any 3D tool. Each + // walkability class becomes its own OBJ group (walkable / + // steep / water / indoor) so designers can hide categories + // independently in Blender to debug 'why can the player + // walk here?' or 'why can't they walk there?'. + std::string path = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "WOC not found: %s\n", path.c_str()); + return 1; + } + if (outPath.empty()) { + outPath = path; + if (outPath.size() >= 4 && + outPath.substr(outPath.size() - 4) == ".woc") { + outPath = outPath.substr(0, outPath.size() - 4); + } + outPath += ".obj"; + } + auto woc = wowee::pipeline::WoweeCollisionBuilder::load(path); + if (!woc.isValid()) { + std::fprintf(stderr, "WOC has no triangles: %s\n", path.c_str()); + return 1; + } + std::ofstream obj(outPath); + if (!obj) { + std::fprintf(stderr, "Failed to open output file: %s\n", outPath.c_str()); + return 1; + } + // Bucket triangles by flag combination so the OBJ can split + // them into named groups. Flag bits: walkable=0x01, water=0x02, + // steep=0x04, indoor=0x08 (per WoweeCollision::Triangle). + // Triangles can have multiple flags set so a per-flag group + // would over-count; instead we bucket by exact flag value. + std::unordered_map> byFlag; + for (size_t t = 0; t < woc.triangles.size(); ++t) { + byFlag[woc.triangles[t].flags].push_back(t); + } + obj << "# Wavefront OBJ generated by wowee_editor --export-woc-obj\n"; + obj << "# Source: " << path << "\n"; + obj << "# Triangles: " << woc.triangles.size() + << " (walkable=" << woc.walkableCount() + << " steep=" << woc.steepCount() << ")\n"; + obj << "# Tile: (" << woc.tileX << ", " << woc.tileY << ")\n\n"; + obj << "o WoweeCollision\n"; + // Emit ALL vertices first (3 per triangle, no dedupe — the + // collision mesh has triangle-soup topology where shared + // verts often have different flags, so deduping would + // actually merge categories). + for (const auto& tri : woc.triangles) { + obj << "v " << tri.v0.x << " " << tri.v0.y << " " << tri.v0.z << "\n"; + obj << "v " << tri.v1.x << " " << tri.v1.y << " " << tri.v1.z << "\n"; + obj << "v " << tri.v2.x << " " << tri.v2.y << " " << tri.v2.z << "\n"; + } + // Emit faces grouped by flag class. OBJ index of triangle t + // vertex k is (t * 3 + k + 1) — 1-based, three verts per tri. + auto flagName = [](uint8_t f) { + if (f == 0) return std::string("nonwalkable"); + std::string s; + if (f & 0x01) s += "walkable"; + if (f & 0x02) { if (!s.empty()) s += "_"; s += "water"; } + if (f & 0x04) { if (!s.empty()) s += "_"; s += "steep"; } + if (f & 0x08) { if (!s.empty()) s += "_"; s += "indoor"; } + if (s.empty()) s = "flag" + std::to_string(int(f)); + return s; + }; + for (const auto& [flag, tris] : byFlag) { + obj << "g " << flagName(flag) << "\n"; + for (size_t t : tris) { + uint32_t base = static_cast(t * 3 + 1); + obj << "f " << base << " " << (base + 1) << " " << (base + 2) << "\n"; + } + } + obj.close(); + std::printf("Exported %s -> %s\n", path.c_str(), outPath.c_str()); + std::printf(" %zu triangles in %zu flag class(es), tile (%u, %u)\n", + woc.triangles.size(), byFlag.size(), woc.tileX, woc.tileY); + return 0; } else if (std::strcmp(argv[i], "--import-obj") == 0 && i + 1 < argc) { // Convert a Wavefront OBJ back into WOM. Round-trips with // --export-obj for the geometry/UV/normal data; bones,