From 54c309a779f48d6a49def3a13a584f26a8e54cd2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 15:59:51 -0700 Subject: [PATCH] feat(editor): add --bake-project-obj for whole-project terrain export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project-level OBJ bake — combines every zone's terrain into one giant OBJ with one 'g zone_NAME' block per zone. Useful for previewing an entire multi-zone project's terrain in MeshLab/ Blender at once, or for printing the full map: wowee_editor --bake-project-obj custom_zones Baked custom_zones -> custom_zones/project.obj 2 zone(s), 3 tiles, 62208 verts, 98304 tris Layout: single global vertex pool (so OBJ indexing stays valid), per-zone face groups so designers can hide individual zones in their viewer for area-by-area inspection. Hole bits respected. Coords match WoweeCollisionBuilder's outer-grid layout exactly so zones spatially line up at WoW grid boundaries — adjacent tiles across zones connect seamlessly. Pairs with the existing --bake-zone-* family (single zone) and --export-project-html (web index of per-zone viewers). Three levels of granularity now available: --export-glb / --export-obj / --export-stl single model/file --bake-zone-glb / -obj / -stl single zone --bake-project-obj entire project <- new Verified: 2-zone project (Forest 2 tiles + Desert 1 tile) baked to project.obj with 62208 verts (3 × 20736), 98304 tris (3 × 32768), 2 'g' blocks correctly named (zone_Desert, zone_Forest). --- tools/editor/main.cpp | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 54a1c06f..fbd39b3f 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -592,6 +592,8 @@ static void printUsage(const char* argv0) { std::printf(" Bake every WHM tile in a zone into one STL for 3D-printing the terrain\n"); std::printf(" --bake-zone-obj [out.obj]\n"); std::printf(" Bake every WHM tile in a zone into one Wavefront OBJ (one g-block per tile)\n"); + std::printf(" --bake-project-obj [out.obj]\n"); + std::printf(" Bake every zone in a project into one Wavefront OBJ (one g-block per zone)\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"); @@ -797,6 +799,7 @@ int main(int argc, char* argv[]) { "--export-glb", "--export-wob-glb", "--export-whm-glb", "--export-stl", "--import-stl", "--bake-zone-glb", "--bake-zone-stl", "--bake-zone-obj", + "--bake-project-obj", "--convert-m2", "--convert-wmo", "--convert-dbc-json", "--convert-json-dbc", "--convert-blp-png", "--migrate-wom", "--migrate-zone", "--migrate-jsondbc", @@ -7636,6 +7639,133 @@ int main(int argc, char* argv[]) { loadedTiles, totalVerts, static_cast(totalFaces)); return 0; + } else if (std::strcmp(argv[i], "--bake-project-obj") == 0 && i + 1 < argc) { + // Project-level OBJ bake: every zone in gets + // emitted into one giant OBJ with one 'g zone_NAME' block + // per zone. Useful for previewing an entire project's terrain + // in MeshLab/Blender at once, or for printing the whole map. + std::string projectDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "bake-project-obj: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + if (outPath.empty()) outPath = projectDir + "/project.obj"; + std::vector zoneDirs; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + zoneDirs.push_back(entry.path().string()); + } + std::sort(zoneDirs.begin(), zoneDirs.end()); + if (zoneDirs.empty()) { + std::fprintf(stderr, + "bake-project-obj: no zones found in %s\n", + projectDir.c_str()); + return 1; + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "bake-project-obj: cannot write %s\n", outPath.c_str()); + return 1; + } + constexpr float kTileSize = 533.33333f; + constexpr float kChunkSize = kTileSize / 16.0f; + constexpr float kVertSpacing = kChunkSize / 8.0f; + out << "# Wavefront OBJ generated by wowee_editor --bake-project-obj\n"; + out << "# Project: " << projectDir << " (" << zoneDirs.size() << " zones)\n"; + // Single global vertex pool. Per-zone we accumulate verts then + // emit faces; same shape as --bake-zone-obj. + int totalZones = 0, totalTiles = 0; + int totalVerts = 0; + uint64_t totalFaces = 0; + struct Pending { + std::string zoneName; + uint32_t vertBase; // 1-based OBJ index + std::vector faceI0, faceI1, faceI2; + }; + std::vector queues; + for (const auto& zoneDir : zoneDirs) { + wowee::editor::ZoneManifest zm; + if (!zm.load(zoneDir + "/zone.json")) continue; + Pending pq; + pq.zoneName = zm.mapName; + pq.vertBase = static_cast(totalVerts + 1); + int zoneTiles = 0; + uint32_t zoneLocalIdx = 0; + for (const auto& [tx, ty] : zm.tiles) { + std::string tileBase = zoneDir + "/" + zm.mapName + "_" + + std::to_string(tx) + "_" + + std::to_string(ty); + if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) continue; + wowee::pipeline::ADTTerrain terrain; + wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain); + zoneTiles++; + for (int cx = 0; cx < 16; ++cx) { + for (int cy = 0; cy < 16; ++cy) { + const auto& chunk = terrain.getChunk(cx, cy); + if (!chunk.heightMap.isLoaded()) continue; + float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize; + float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize; + uint32_t chunkBaseLocal = zoneLocalIdx; + for (int row = 0; row < 9; ++row) { + for (int col = 0; col < 9; ++col) { + float x = chunkBaseX - row * kVertSpacing; + float y = chunkBaseY - col * kVertSpacing; + float z = chunk.position[2] + + chunk.heightMap.heights[row * 17 + col]; + out << "v " << x << " " << y << " " << z << "\n"; + zoneLocalIdx++; + } + } + bool isHoleChunk = (chunk.holes != 0); + for (int row = 0; row < 8; ++row) { + for (int col = 0; col < 8; ++col) { + if (isHoleChunk) { + int hx = col / 2, hy = row / 2; + if (chunk.holes & (1 << (hy * 4 + hx))) continue; + } + auto idx = [&](int r, int c) { + return chunkBaseLocal + r * 9 + c; + }; + pq.faceI0.push_back(idx(row, col)); + pq.faceI1.push_back(idx(row, col + 1)); + pq.faceI2.push_back(idx(row + 1, col + 1)); + pq.faceI0.push_back(idx(row, col)); + pq.faceI1.push_back(idx(row + 1, col + 1)); + pq.faceI2.push_back(idx(row + 1, col)); + } + } + } + } + } + if (zoneLocalIdx == 0) continue; + totalVerts += zoneLocalIdx; + totalTiles += zoneTiles; + totalZones++; + queues.push_back(std::move(pq)); + } + // After all verts written, emit faces grouped by zone. + for (const auto& pq : queues) { + out << "g zone_" << pq.zoneName << "\n"; + for (size_t k = 0; k < pq.faceI0.size(); ++k) { + out << "f " << (pq.faceI0[k] + pq.vertBase) << " " + << (pq.faceI1[k] + pq.vertBase) << " " + << (pq.faceI2[k] + pq.vertBase) << "\n"; + totalFaces++; + } + } + out.close(); + std::printf("Baked %s -> %s\n", projectDir.c_str(), outPath.c_str()); + std::printf(" %d zone(s), %d tiles, %d verts, %llu tris\n", + totalZones, totalTiles, totalVerts, + static_cast(totalFaces)); + 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