mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 09:33:51 +00:00
feat(editor): add --export-whm-obj for terrain heightmap visualization
Completes the open-format -> universal-text bridge for the last binary geometry format. WHM was the missing one; designers now have OBJ exports for all four (WOM models, WOB buildings, WOC collision, WHM terrain). wowee_editor --export-whm-obj custom_zones/MyZone/MyZone_30_30 Mesh layout: - 9x9 outer vertex grid per chunk (skips the 8x8 inner verts the engine uses for 4-tri fans). That's 81 verts and 128 tris per chunk; full ADT = 20736 verts + 32768 tris. - One OBJ 'g chunk_X_Y' per MapChunk so designers can hide chunks individually in Blender (e.g. to inspect a single problem area). - Hole bits respected — cave-entrance quads correctly disappear. - Coords match WoweeCollisionBuilder's outer-grid layout exactly, so an --export-whm-obj and --export-woc-obj of the same source align spatially when overlaid in Blender. (Verified: first vertex of both is (1066.67, 1066.67, ~98.5) for a tile (30, 30) export.) - UVs are simply row/8, col/8 in [0,1] per chunk so a checker texture renders at the canonical scale for size reference. Verified: scaffolded zone -> WHM/WOT auto-built -> --export-whm-obj produces 256 chunks loaded, 20736 verts, 32768 faces, 256 'g' blocks. Counts exactly match the chunk × outer-grid math.
This commit is contained in:
parent
0f05759027
commit
92ea41f1ae
1 changed files with 112 additions and 1 deletions
|
|
@ -441,6 +441,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Convert a Wavefront OBJ back into WOB (round-trips with --export-wob-obj)\n");
|
||||
std::printf(" --export-woc-obj <woc-path> [out.obj]\n");
|
||||
std::printf(" Convert a WOC collision mesh to OBJ for visualization (per-flag color groups)\n");
|
||||
std::printf(" --export-whm-obj <wot-base> [out.obj]\n");
|
||||
std::printf(" Convert a WHM heightmap to OBJ terrain mesh (9x9 outer grid per chunk)\n");
|
||||
std::printf(" --validate <zoneDir> [--json]\n");
|
||||
std::printf(" Score zone open-format completeness and exit\n");
|
||||
std::printf(" --validate-wom <wom-base> [--json]\n");
|
||||
|
|
@ -523,7 +525,7 @@ int main(int argc, char* argv[]) {
|
|||
"--build-woc", "--regen-collision", "--fix-zone",
|
||||
"--export-png", "--export-obj", "--import-obj",
|
||||
"--export-wob-obj", "--import-wob-obj",
|
||||
"--export-woc-obj",
|
||||
"--export-woc-obj", "--export-whm-obj",
|
||||
"--convert-m2", "--convert-wmo",
|
||||
};
|
||||
for (int i = 1; i < argc; i++) {
|
||||
|
|
@ -2693,6 +2695,115 @@ int main(int argc, char* argv[]) {
|
|||
std::printf(" %zu triangles in %zu flag class(es), tile (%u, %u)\n",
|
||||
woc.triangles.size(), byFlag.size(), woc.tileX, woc.tileY);
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--export-whm-obj") == 0 && i + 1 < argc) {
|
||||
// Convert a WHM/WOT terrain pair to OBJ for visualization in
|
||||
// Blender / MeshLab. Emits the 9x9 outer vertex grid per
|
||||
// chunk (skipping the 8x8 inner verts the engine uses for
|
||||
// 4-tri fans) — that's the canonical 'heightmap as mesh'
|
||||
// view, 256 chunks × 81 verts = 20736 verts, 32768 tris.
|
||||
// Geometry mirrors WoweeCollisionBuilder's outer-grid layout
|
||||
// exactly so the OBJ aligns with the corresponding WOC.
|
||||
std::string base = argv[++i];
|
||||
std::string outPath;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
outPath = argv[++i];
|
||||
}
|
||||
for (const char* ext : {".wot", ".whm"}) {
|
||||
if (base.size() >= 4 && base.substr(base.size() - 4) == ext) {
|
||||
base = base.substr(0, base.size() - 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WHM/WOT not found: %s.{whm,wot}\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (outPath.empty()) outPath = base + ".obj";
|
||||
wowee::pipeline::ADTTerrain terrain;
|
||||
wowee::pipeline::WoweeTerrainLoader::load(base, terrain);
|
||||
std::ofstream obj(outPath);
|
||||
if (!obj) {
|
||||
std::fprintf(stderr, "Failed to open output file: %s\n", outPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
// Tile + chunk constants — must match WoweeCollisionBuilder so
|
||||
// exports of the same source align in space when overlaid.
|
||||
constexpr float kTileSize = 533.33333f;
|
||||
constexpr float kChunkSize = kTileSize / 16.0f;
|
||||
constexpr float kVertSpacing = kChunkSize / 8.0f;
|
||||
obj << "# Wavefront OBJ generated by wowee_editor --export-whm-obj\n";
|
||||
obj << "# Source: " << base << ".whm\n";
|
||||
obj << "# Tile coord: (" << terrain.coord.x << ", " << terrain.coord.y << ")\n";
|
||||
obj << "# Layout: 9x9 outer vertex grid per chunk, 8x8 quads -> 2 tris each\n\n";
|
||||
obj << "o WoweeTerrain_" << terrain.coord.x << "_" << terrain.coord.y << "\n";
|
||||
int loadedChunks = 0;
|
||||
uint32_t vertOffset = 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;
|
||||
loadedChunks++;
|
||||
// Same XY origin formula as collision builder so
|
||||
// overlaid OBJ exports line up exactly.
|
||||
float chunkBaseX = (32.0f - terrain.coord.y) * kTileSize - cy * kChunkSize;
|
||||
float chunkBaseY = (32.0f - terrain.coord.x) * kTileSize - cx * kChunkSize;
|
||||
// Emit 9x9 outer verts. Layout: heights[row*17 + col]
|
||||
// for col in [0,8] (the inner 8 verts at col 9..16
|
||||
// are skipped — they're the quad-center verts).
|
||||
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];
|
||||
obj << "v " << x << " " << y << " " << z << "\n";
|
||||
}
|
||||
}
|
||||
// Per-vertex UV: just the row/col in 0..1 — Blender
|
||||
// can use this to slap a checker texture for scale.
|
||||
for (int row = 0; row < 9; ++row) {
|
||||
for (int col = 0; col < 9; ++col) {
|
||||
obj << "vt " << (col / 8.0f) << " "
|
||||
<< (row / 8.0f) << "\n";
|
||||
}
|
||||
}
|
||||
// 8x8 quads — two tris each, respecting hole bits so
|
||||
// cave-entrance quads correctly disappear from the mesh.
|
||||
bool isHoleChunk = (chunk.holes != 0);
|
||||
obj << "g chunk_" << cx << "_" << cy << "\n";
|
||||
auto idx = [&](int r, int c) {
|
||||
return vertOffset + r * 9 + c + 1; // 1-based
|
||||
};
|
||||
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;
|
||||
}
|
||||
uint32_t i00 = idx(row, col);
|
||||
uint32_t i10 = idx(row, col + 1);
|
||||
uint32_t i01 = idx(row + 1, col);
|
||||
uint32_t i11 = idx(row + 1, col + 1);
|
||||
obj << "f " << i00 << "/" << i00 << " "
|
||||
<< i10 << "/" << i10 << " "
|
||||
<< i11 << "/" << i11 << "\n";
|
||||
obj << "f " << i00 << "/" << i00 << " "
|
||||
<< i11 << "/" << i11 << " "
|
||||
<< i01 << "/" << i01 << "\n";
|
||||
}
|
||||
}
|
||||
vertOffset += 81; // 9x9 verts per chunk
|
||||
}
|
||||
}
|
||||
obj.close();
|
||||
// Estimated tri count: chunks × 128 (8x8 quads × 2 tris).
|
||||
// Holes reduce this but counting exactly would mean walking
|
||||
// the bitmask again — the rough estimate is the user-visible
|
||||
// useful number anyway.
|
||||
std::printf("Exported %s.whm -> %s\n", base.c_str(), outPath.c_str());
|
||||
std::printf(" %d chunks loaded, ~%d verts, ~%d tris\n",
|
||||
loadedChunks, loadedChunks * 81, loadedChunks * 128);
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue