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