From a3333b7b4de4209610e50f1f2deb3b0df3e6aa1d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 14:07:22 -0700 Subject: [PATCH] feat(editor): add --bake-zone-obj completing the bake-zone trio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OBJ companion to --bake-zone-glb / --bake-zone-stl. Same multi-tile WHM aggregation, this time as Wavefront OBJ — opens directly in Blender / MeshLab / 3DS Max for hand-editing the terrain mesh: wowee_editor --bake-zone-obj custom_zones/MyZone # -> custom_zones/MyZone/MyZone.obj Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.obj 2 tile(s), 41472 verts, 65536 tris Each tile becomes its own 'g tile_TX_TY' block so designers can hide tiles independently in Blender. Single global vertex pool with per-tile vertex base indices for face emission (OBJ requires verts before faces, so we collect per-tile face indices in memory then emit them after all verts are streamed to disk). Hole bits respected (cave-entrance quads dropped). Coords match WoweeCollisionBuilder's outer-grid layout exactly so .obj/.glb/.stl of the same source align spatially when overlaid. Why three formats for full-zone export: glTF for on-screen 3D viewers, STL for fabrication, OBJ for DCC editing. Three different ecosystems, three different format sweet spots. Verified: 2-tile zone (Z + added tile) baked correctly. 41472 verts (2 × 20736), 65536 tris (2 × 32768), 2 'g' blocks (tile_30_30 + tile_31_30) — matches what --bake-zone-glb reports for the same input. --- tools/editor/main.cpp | 144 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 10eeb6fb..b4d9dc8e 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -489,6 +489,8 @@ static void printUsage(const char* argv0) { std::printf(" Bake every WHM tile in a zone into one glTF (one node per tile)\n"); std::printf(" --bake-zone-stl [out.stl]\n"); 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(" --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"); @@ -643,7 +645,8 @@ int main(int argc, char* argv[]) { "--export-wob-obj", "--import-wob-obj", "--export-woc-obj", "--export-whm-obj", "--export-glb", "--export-wob-glb", "--export-whm-glb", - "--export-stl", "--import-stl", "--bake-zone-glb", "--bake-zone-stl", + "--export-stl", "--import-stl", + "--bake-zone-glb", "--bake-zone-stl", "--bake-zone-obj", "--convert-m2", "--convert-wmo", "--convert-dbc-json", "--convert-json-dbc", "--convert-blp-png", "--migrate-wom", "--migrate-zone", @@ -5423,6 +5426,145 @@ int main(int argc, char* argv[]) { loadedTiles, static_cast(triCount), holesSkipped); return 0; + } else if (std::strcmp(argv[i], "--bake-zone-obj") == 0 && i + 1 < argc) { + // OBJ companion to --bake-zone-glb / --bake-zone-stl. Same + // multi-tile WHM aggregation, but as Wavefront OBJ — opens + // directly in Blender / MeshLab / 3DS Max for hand-editing. + // Each tile becomes its own 'g' block so designers can hide + // tiles independently. + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "bake-zone-obj: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, "bake-zone-obj: parse failed\n"); + return 1; + } + if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + ".obj"; + if (zm.tiles.empty()) { + std::fprintf(stderr, "bake-zone-obj: zone has no tiles\n"); + return 1; + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, "bake-zone-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-zone-obj\n"; + out << "# Zone: " << zm.mapName << " (" << zm.tiles.size() + << " tiles)\n"; + out << "o " << zm.mapName << "\n"; + // OBJ uses a single global vertex pool with per-tile g-blocks + // and per-tile face index offsetting. We accumulate per-tile + // vertex blocks first (so face indices know their offsets), + // then per-tile face blocks at the end. + // Layout: emit ALL verts first (organized by tile, in order), + // then emit ALL face blocks. OBJ requires verts before faces + // that reference them. + int loadedTiles = 0; + int totalVerts = 0; + // Per-tile bookkeeping: vertex base index (1-based for OBJ) + // and which faces reference it. + struct TileMeta { + int tx, ty; + uint32_t vertBase; // 1-based OBJ index of first vert + uint32_t vertCount; + std::vector faceI0, faceI1, faceI2; // local indices + }; + std::vector tiles; + 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)) { + std::fprintf(stderr, + "bake-zone-obj: tile (%d, %d) WHM/WOT missing — skipping\n", + tx, ty); + continue; + } + wowee::pipeline::ADTTerrain terrain; + wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain); + TileMeta tm{tx, ty, static_cast(totalVerts + 1), 0, {}, {}, {}}; + // Walk chunks; emit verts to file as we go (so we don't + // hold a giant vector in memory). Track local indices for + // face emission afterwards. + uint32_t tileLocalIdx = 0; + 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 = tileLocalIdx; + 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"; + tileLocalIdx++; + } + } + 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; + }; + tm.faceI0.push_back(idx(row, col)); + tm.faceI1.push_back(idx(row, col + 1)); + tm.faceI2.push_back(idx(row + 1, col + 1)); + tm.faceI0.push_back(idx(row, col)); + tm.faceI1.push_back(idx(row + 1, col + 1)); + tm.faceI2.push_back(idx(row + 1, col)); + } + } + } + } + tm.vertCount = tileLocalIdx; + totalVerts += tm.vertCount; + if (tm.vertCount > 0) { + tiles.push_back(std::move(tm)); + loadedTiles++; + } + } + // Now emit per-tile face groups (after all verts are written). + uint64_t totalFaces = 0; + for (const auto& tm : tiles) { + out << "g tile_" << tm.tx << "_" << tm.ty << "\n"; + for (size_t k = 0; k < tm.faceI0.size(); ++k) { + uint32_t a = tm.faceI0[k] + tm.vertBase; + uint32_t b = tm.faceI1[k] + tm.vertBase; + uint32_t c = tm.faceI2[k] + tm.vertBase; + out << "f " << a << " " << b << " " << c << "\n"; + totalFaces++; + } + } + out.close(); + if (loadedTiles == 0) { + std::fprintf(stderr, "bake-zone-obj: no tiles loaded\n"); + std::filesystem::remove(outPath); + return 1; + } + std::printf("Baked %s -> %s\n", zoneDir.c_str(), outPath.c_str()); + std::printf(" %d tile(s), %d verts, %llu tris\n", + loadedTiles, 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