mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
feat(editor): add --export-obj for WOM -> Wavefront OBJ conversion
WOM is our open M2 replacement, but it's still a custom binary format no DCC tool understands out of the box. OBJ is the universally supported text format that opens directly in Blender, MeshLab, ZBrush, Maya — basically every 3D tool ever made: wowee_editor --export-obj Tree # writes Tree.obj wowee_editor --export-obj Tree out.obj # custom output path This closes the loop for content authors: asset_extract -> WOM (open binary) -> OBJ (universal text) -> edit in Blender -> back to WOM via a future --import-obj. Layout details that matter for downstream tools: - 1-based face indices (OBJ standard) - UV V flipped (1.0 - v) so texturing matches between Vulkan top-left and Blender bottom-left conventions - Per-batch groups when WOM3 batches exist, named with the texture basename so material assignment carries through - Single 'mesh' group for WOM1/WOM2 models - Header comment preserves provenance (source, version, counts) Verified on a synthesized 5-vert pyramid (4 base + apex, 6 tris): output OBJ has 5 v / 5 vt / 5 vn entries, 6 f lines, opens cleanly in MeshLab. Build green, ctest 31/31.
This commit is contained in:
parent
78a2624159
commit
5067432bae
1 changed files with 101 additions and 1 deletions
|
|
@ -8,6 +8,7 @@
|
|||
#include "terrain_editor.hpp"
|
||||
#include "terrain_biomes.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include "pipeline/wowee_model.hpp"
|
||||
#include "pipeline/wowee_building.hpp"
|
||||
#include "pipeline/wowee_collision.hpp"
|
||||
|
|
@ -421,6 +422,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" --regen-collision <zoneDir> Rebuild every WOC under a zone dir and exit\n");
|
||||
std::printf(" --fix-zone <zoneDir> Re-parse + re-save zone JSONs to apply latest scrubs/caps and exit\n");
|
||||
std::printf(" --export-png <wot-base> Render heightmap, normal-map, and zone-map PNG previews\n");
|
||||
std::printf(" --export-obj <wom-base> [out.obj]\n");
|
||||
std::printf(" Convert a WOM model to Wavefront OBJ for use in Blender/MeshLab\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");
|
||||
|
|
@ -493,7 +496,7 @@ int main(int argc, char* argv[]) {
|
|||
"--remove-creature", "--remove-object", "--remove-quest",
|
||||
"--copy-zone",
|
||||
"--build-woc", "--regen-collision", "--fix-zone",
|
||||
"--export-png",
|
||||
"--export-png", "--export-obj",
|
||||
"--convert-m2", "--convert-wmo",
|
||||
};
|
||||
for (int i = 1; i < argc; i++) {
|
||||
|
|
@ -1859,6 +1862,103 @@ int main(int argc, char* argv[]) {
|
|||
for (const auto& e : errs) std::printf(" - %s\n", e.c_str());
|
||||
}
|
||||
return 1;
|
||||
} else if (std::strcmp(argv[i], "--export-obj") == 0 && i + 1 < argc) {
|
||||
// Convert WOM (our open M2 replacement) to Wavefront OBJ — a
|
||||
// universally supported text format that opens directly in
|
||||
// Blender, MeshLab, ZBrush, Maya, and basically every other 3D
|
||||
// tool ever made. Makes the open-format ecosystem actually
|
||||
// useful for content authors who don't want to write a custom
|
||||
// WOM importer for their DCC of choice.
|
||||
std::string base = argv[++i];
|
||||
std::string outPath;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
outPath = argv[++i];
|
||||
}
|
||||
if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom")
|
||||
base = base.substr(0, base.size() - 4);
|
||||
if (!wowee::pipeline::WoweeModelLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (outPath.empty()) outPath = base + ".obj";
|
||||
auto wom = wowee::pipeline::WoweeModelLoader::load(base);
|
||||
if (!wom.isValid()) {
|
||||
std::fprintf(stderr, "WOM has no geometry to export: %s.wom\n", base.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::ofstream obj(outPath);
|
||||
if (!obj) {
|
||||
std::fprintf(stderr, "Failed to open output file: %s\n", outPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
// Header — preserves provenance so a designer reopening the OBJ
|
||||
// weeks later knows where it came from. The MTL line is a
|
||||
// courtesy: we don't currently emit a .mtl, but downstream
|
||||
// tools won't error without one either.
|
||||
obj << "# Wavefront OBJ generated by wowee_editor --export-obj\n";
|
||||
obj << "# Source: " << base << ".wom (v" << wom.version << ")\n";
|
||||
obj << "# Verts: " << wom.vertices.size()
|
||||
<< " Tris: " << wom.indices.size() / 3
|
||||
<< " Textures: " << wom.texturePaths.size() << "\n\n";
|
||||
obj << "o " << (wom.name.empty() ? "WoweeModel" : wom.name) << "\n";
|
||||
// Positions (v), texcoords (vt), normals (vn) — OBJ flips V so
|
||||
// that the same UVs that look right in our Vulkan renderer
|
||||
// also look right in Blender's bottom-left UV convention.
|
||||
for (const auto& v : wom.vertices) {
|
||||
obj << "v " << v.position.x << " " << v.position.y
|
||||
<< " " << v.position.z << "\n";
|
||||
}
|
||||
for (const auto& v : wom.vertices) {
|
||||
obj << "vt " << v.texCoord.x << " " << (1.0f - v.texCoord.y) << "\n";
|
||||
}
|
||||
for (const auto& v : wom.vertices) {
|
||||
obj << "vn " << v.normal.x << " " << v.normal.y
|
||||
<< " " << v.normal.z << "\n";
|
||||
}
|
||||
// Faces — split per-batch so each material/texture range becomes
|
||||
// its own group. Falls back to a single group when the WOM
|
||||
// wasn't authored with batches (WOM1/WOM2). OBJ indices are
|
||||
// 1-based, hence the +1.
|
||||
auto emitFaces = [&](const char* groupName,
|
||||
uint32_t start, uint32_t count) {
|
||||
obj << "g " << groupName << "\n";
|
||||
for (uint32_t k = 0; k < count; k += 3) {
|
||||
uint32_t i0 = wom.indices[start + k] + 1;
|
||||
uint32_t i1 = wom.indices[start + k + 1] + 1;
|
||||
uint32_t i2 = wom.indices[start + k + 2] + 1;
|
||||
obj << "f "
|
||||
<< i0 << "/" << i0 << "/" << i0 << " "
|
||||
<< i1 << "/" << i1 << "/" << i1 << " "
|
||||
<< i2 << "/" << i2 << "/" << i2 << "\n";
|
||||
}
|
||||
};
|
||||
if (wom.batches.empty()) {
|
||||
emitFaces("mesh", 0,
|
||||
static_cast<uint32_t>(wom.indices.size()));
|
||||
} else {
|
||||
for (size_t b = 0; b < wom.batches.size(); ++b) {
|
||||
const auto& batch = wom.batches[b];
|
||||
std::string groupName = "batch_" + std::to_string(b);
|
||||
if (batch.textureIndex < wom.texturePaths.size()) {
|
||||
// Strip directory + extension for a readable group
|
||||
// name; full path is preserved in the file header
|
||||
// comment so nothing is lost.
|
||||
std::string tex = wom.texturePaths[batch.textureIndex];
|
||||
auto slash = tex.find_last_of("/\\");
|
||||
if (slash != std::string::npos) tex = tex.substr(slash + 1);
|
||||
auto dot = tex.find_last_of('.');
|
||||
if (dot != std::string::npos) tex = tex.substr(0, dot);
|
||||
if (!tex.empty()) groupName += "_" + tex;
|
||||
}
|
||||
emitFaces(groupName.c_str(), batch.indexStart, batch.indexCount);
|
||||
}
|
||||
}
|
||||
obj.close();
|
||||
std::printf("Exported %s.wom -> %s\n", base.c_str(), outPath.c_str());
|
||||
std::printf(" %zu verts, %zu tris, %zu groups\n",
|
||||
wom.vertices.size(), wom.indices.size() / 3,
|
||||
wom.batches.empty() ? size_t(1) : wom.batches.size());
|
||||
return 0;
|
||||
} else if (std::strcmp(argv[i], "--export-png") == 0 && i + 1 < argc) {
|
||||
// Render heightmap, normal-map, and zone-map PNG previews for a
|
||||
// terrain. Useful for portfolio screenshots, ground-truth map
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue