From 23a2233852ce73b2e8071db9a8067f1dec8f9a17 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:14:04 -0700 Subject: [PATCH] feat(editor): add --export-wob-obj for buildings -> Wavefront OBJ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WOM (the open M2 replacement) already round-trips through OBJ; this extends the universal-format bridge to WOB (the open WMO replacement) so buildings can also be edited in Blender / MeshLab / Maya / etc. wowee_editor --export-wob-obj House # writes House.obj wowee_editor --export-wob-obj House out.obj # custom path Mapping decisions: - Each WOB group becomes one OBJ 'g' block (named after the group; outdoor groups get an '_outdoor' suffix). Preserves the room/floor structure for downstream selection and per-area editing. - Single global vertex pool with per-group offsets (OBJ requires v indices to be globally 1-based; we track a running vertOffset). - UV V flipped (1.0 - v) so texturing matches Blender bottom-left convention, same as --export-obj for WOM. - Doodad placements written as # comment lines at the end. OBJ has no native concept for instanced models, but emitting them as structured comments keeps the placement data recoverable for tools that want to re-instance them. - Portals and material flags drop on the floor — OBJ has no semantics for either. The native WOB always remains canonical. Verified on a synthesized 2-group house (4-vert floor + 3-vert wall, 1 doodad): output OBJ has 7 verts / 7 vt / 7 vn entries, 2 'g' blocks with proper index offsetting, doodad comment line preserved. --- tools/editor/main.cpp | 99 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index b5b73f9f..41931dbd 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -429,6 +429,8 @@ static void printUsage(const char* argv0) { std::printf(" Convert a WOM model to Wavefront OBJ for use in Blender/MeshLab\n"); std::printf(" --import-obj [wom-base]\n"); std::printf(" Convert a Wavefront OBJ back into WOM (round-trips with --export-obj)\n"); + std::printf(" --export-wob-obj [out.obj]\n"); + std::printf(" Convert a WOB building to Wavefront OBJ (one group per WOB group)\n"); std::printf(" --validate [--json]\n"); std::printf(" Score zone open-format completeness and exit\n"); std::printf(" --validate-wom [--json]\n"); @@ -504,7 +506,7 @@ int main(int argc, char* argv[]) { "--remove-creature", "--remove-object", "--remove-quest", "--copy-zone", "--build-woc", "--regen-collision", "--fix-zone", - "--export-png", "--export-obj", "--import-obj", + "--export-png", "--export-obj", "--import-obj", "--export-wob-obj", "--convert-m2", "--convert-wmo", }; for (int i = 1; i < argc; i++) { @@ -2116,6 +2118,101 @@ int main(int argc, char* argv[]) { wom.vertices.size(), wom.indices.size() / 3, wom.batches.empty() ? size_t(1) : wom.batches.size()); return 0; + } else if (std::strcmp(argv[i], "--export-wob-obj") == 0 && i + 1 < argc) { + // WOB is the WMO replacement; like --export-obj for WOM, this + // bridges WOB into the universal-3D-tool ecosystem. Each WOB + // group becomes one OBJ 'g' block, preserving the room/floor + // structure for downstream selection in Blender/MeshLab. + std::string base = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wob") + base = base.substr(0, base.size() - 4); + if (!wowee::pipeline::WoweeBuildingLoader::exists(base)) { + std::fprintf(stderr, "WOB not found: %s.wob\n", base.c_str()); + return 1; + } + if (outPath.empty()) outPath = base + ".obj"; + auto bld = wowee::pipeline::WoweeBuildingLoader::load(base); + if (!bld.isValid()) { + std::fprintf(stderr, "WOB has no groups to export: %s.wob\n", base.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; + } + // Total verts/tris across all groups for the header. + size_t totalV = 0, totalI = 0; + for (const auto& g : bld.groups) { + totalV += g.vertices.size(); + totalI += g.indices.size(); + } + obj << "# Wavefront OBJ generated by wowee_editor --export-wob-obj\n"; + obj << "# Source: " << base << ".wob\n"; + obj << "# Groups: " << bld.groups.size() + << " Verts: " << totalV + << " Tris: " << totalI / 3 + << " Portals: " << bld.portals.size() + << " Doodads: " << bld.doodads.size() << "\n\n"; + obj << "o " << (bld.name.empty() ? "WoweeBuilding" : bld.name) << "\n"; + // OBJ uses a single global vertex pool, so we offset each group's + // local indices by the running total of verts written so far. + uint32_t vertOffset = 0; + for (size_t g = 0; g < bld.groups.size(); ++g) { + const auto& grp = bld.groups[g]; + if (grp.vertices.empty()) continue; + for (const auto& v : grp.vertices) { + obj << "v " << v.position.x << " " + << v.position.y << " " + << v.position.z << "\n"; + } + for (const auto& v : grp.vertices) { + obj << "vt " << v.texCoord.x << " " + << (1.0f - v.texCoord.y) << "\n"; + } + for (const auto& v : grp.vertices) { + obj << "vn " << v.normal.x << " " + << v.normal.y << " " + << v.normal.z << "\n"; + } + std::string groupName = grp.name.empty() + ? "group_" + std::to_string(g) + : grp.name; + if (grp.isOutdoor) groupName += "_outdoor"; + obj << "g " << groupName << "\n"; + for (size_t k = 0; k + 2 < grp.indices.size(); k += 3) { + uint32_t i0 = grp.indices[k] + 1 + vertOffset; + uint32_t i1 = grp.indices[k + 1] + 1 + vertOffset; + uint32_t i2 = grp.indices[k + 2] + 1 + vertOffset; + obj << "f " + << i0 << "/" << i0 << "/" << i0 << " " + << i1 << "/" << i1 << "/" << i1 << " " + << i2 << "/" << i2 << "/" << i2 << "\n"; + } + vertOffset += static_cast(grp.vertices.size()); + } + // Doodad placements as a separate informational block — emit + // each as a comment line so OBJ stays valid but the data is + // recoverable for tools that want to re-create the placements. + if (!bld.doodads.empty()) { + obj << "\n# Doodad placements (model, position, rotation, scale):\n"; + for (const auto& d : bld.doodads) { + obj << "# doodad " << d.modelPath + << " pos " << d.position.x << "," << d.position.y << "," << d.position.z + << " rot " << d.rotation.x << "," << d.rotation.y << "," << d.rotation.z + << " scale " << d.scale << "\n"; + } + } + obj.close(); + std::printf("Exported %s.wob -> %s\n", base.c_str(), outPath.c_str()); + std::printf(" %zu groups, %zu verts, %zu tris, %zu doodad placements\n", + bld.groups.size(), totalV, totalI / 3, + bld.doodads.size()); + 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,