mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
feat(editor): add --bake-zone-obj completing the bake-zone trio
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.
This commit is contained in:
parent
6113582a7d
commit
a3333b7b4d
1 changed files with 143 additions and 1 deletions
|
|
@ -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 <zoneDir> [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 <zoneDir> [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 <obj-path> [wom-base]\n");
|
||||
std::printf(" Convert a Wavefront OBJ back into WOM (round-trips with --export-obj)\n");
|
||||
std::printf(" --export-wob-obj <wob-base> [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<unsigned long long>(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<uint32_t> faceI0, faceI1, faceI2; // local indices
|
||||
};
|
||||
std::vector<TileMeta> 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<uint32_t>(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<unsigned long long>(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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue