mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
refactor(editor): move --import-obj into cli_wom_io.cpp
Adds --import-obj as the 5th handler in cli_wom_io.{hpp,cpp},
joining its export-side sibling. The handler is the WOM-side
counterpart to --import-wob-obj (which lives in cli_world_io
because it produces WOB) — together they close the OBJ
round-trip story for both single meshes and multi-group
buildings.
main.cpp shrinks by 183 lines (6,786 to 6,603).
This commit is contained in:
parent
549bdaf191
commit
191a821ec8
3 changed files with 189 additions and 183 deletions
|
|
@ -501,6 +501,191 @@ int handleImportStl(int& i, int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int handleImportObj(int& i, int argc, char** argv) {
|
||||
// Convert a Wavefront OBJ back into WOM. Round-trips with
|
||||
// --export-obj for the geometry/UV/normal data; bones,
|
||||
// animations, and material flags are not in OBJ and stay
|
||||
// empty (the resulting WOM is WOM1, static-only). The intent
|
||||
// is "edit a static prop in Blender, ship it".
|
||||
std::string objPath = argv[++i];
|
||||
std::string womBase;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
womBase = argv[++i];
|
||||
}
|
||||
if (!std::filesystem::exists(objPath)) {
|
||||
std::fprintf(stderr, "OBJ not found: %s\n", objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (womBase.empty()) {
|
||||
womBase = objPath;
|
||||
if (womBase.size() >= 4 &&
|
||||
womBase.substr(womBase.size() - 4) == ".obj") {
|
||||
womBase = womBase.substr(0, womBase.size() - 4);
|
||||
}
|
||||
}
|
||||
std::ifstream in(objPath);
|
||||
if (!in) {
|
||||
std::fprintf(stderr, "Failed to open OBJ: %s\n", objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
// Pools — OBJ stores positions/UVs/normals in independent
|
||||
// arrays and references them by index in face lines, so we
|
||||
// collect each pool first then expand into WOM vertices on
|
||||
// the fly (one WOM vertex per (vIdx, vtIdx, vnIdx) triple
|
||||
// since WOM has interleaved vertex data, not pooled).
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<glm::vec2> texcoords;
|
||||
std::vector<glm::vec3> normals;
|
||||
wowee::pipeline::WoweeModel wom;
|
||||
wom.version = 1;
|
||||
std::unordered_map<std::string, uint32_t> dedupe;
|
||||
int badFaces = 0;
|
||||
int triangulatedNgons = 0;
|
||||
std::string objectName;
|
||||
std::string line;
|
||||
// Convert a single OBJ vertex token like "3/4/5" or "3//5" or
|
||||
// "3/4" or "3" into a WOM vertex index, deduping identical
|
||||
// (pos, uv, normal) triples to keep the buffer compact.
|
||||
auto resolveCorner = [&](const std::string& token) -> int {
|
||||
int v = 0, t = 0, n = 0;
|
||||
{
|
||||
const char* p = token.c_str();
|
||||
char* endp = nullptr;
|
||||
v = std::strtol(p, &endp, 10);
|
||||
if (*endp == '/') {
|
||||
++endp;
|
||||
if (*endp != '/') {
|
||||
t = std::strtol(endp, &endp, 10);
|
||||
}
|
||||
if (*endp == '/') {
|
||||
++endp;
|
||||
n = std::strtol(endp, &endp, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Translate negative (relative) indices to absolute.
|
||||
auto absIdx = [](int idx, size_t poolSize) -> int {
|
||||
if (idx < 0) return static_cast<int>(poolSize) + idx;
|
||||
return idx - 1; // OBJ is 1-based
|
||||
};
|
||||
int vi = absIdx(v, positions.size());
|
||||
int ti = (t == 0) ? -1 : absIdx(t, texcoords.size());
|
||||
int ni = (n == 0) ? -1 : absIdx(n, normals.size());
|
||||
if (vi < 0 || vi >= static_cast<int>(positions.size())) return -1;
|
||||
std::string key = std::to_string(vi) + "/" +
|
||||
std::to_string(ti) + "/" +
|
||||
std::to_string(ni);
|
||||
auto it = dedupe.find(key);
|
||||
if (it != dedupe.end()) return static_cast<int>(it->second);
|
||||
wowee::pipeline::WoweeModel::Vertex vert;
|
||||
vert.position = positions[vi];
|
||||
if (ti >= 0 && ti < static_cast<int>(texcoords.size())) {
|
||||
vert.texCoord = texcoords[ti];
|
||||
// Reverse the V-flip from --export-obj so a round-trip
|
||||
// returns the original UVs unchanged.
|
||||
vert.texCoord.y = 1.0f - vert.texCoord.y;
|
||||
} else {
|
||||
vert.texCoord = {0, 0};
|
||||
}
|
||||
if (ni >= 0 && ni < static_cast<int>(normals.size())) {
|
||||
vert.normal = normals[ni];
|
||||
} else {
|
||||
vert.normal = {0, 0, 1};
|
||||
}
|
||||
uint32_t newIdx = static_cast<uint32_t>(wom.vertices.size());
|
||||
wom.vertices.push_back(vert);
|
||||
dedupe[key] = newIdx;
|
||||
return static_cast<int>(newIdx);
|
||||
};
|
||||
while (std::getline(in, line)) {
|
||||
// Strip CR for CRLF files.
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
std::istringstream ss(line);
|
||||
std::string tag;
|
||||
ss >> tag;
|
||||
if (tag == "v") {
|
||||
glm::vec3 p; ss >> p.x >> p.y >> p.z;
|
||||
positions.push_back(p);
|
||||
} else if (tag == "vt") {
|
||||
glm::vec2 t; ss >> t.x >> t.y;
|
||||
texcoords.push_back(t);
|
||||
} else if (tag == "vn") {
|
||||
glm::vec3 n; ss >> n.x >> n.y >> n.z;
|
||||
normals.push_back(n);
|
||||
} else if (tag == "o") {
|
||||
if (objectName.empty()) ss >> objectName;
|
||||
} else if (tag == "f") {
|
||||
std::vector<std::string> corners;
|
||||
std::string c;
|
||||
while (ss >> c) corners.push_back(c);
|
||||
if (corners.size() < 3) { badFaces++; continue; }
|
||||
std::vector<int> resolved;
|
||||
resolved.reserve(corners.size());
|
||||
bool ok = true;
|
||||
for (const auto& cc : corners) {
|
||||
int idx = resolveCorner(cc);
|
||||
if (idx < 0) { ok = false; break; }
|
||||
resolved.push_back(idx);
|
||||
}
|
||||
if (!ok) { badFaces++; continue; }
|
||||
// Fan-triangulate (works for triangles, quads, and
|
||||
// n-gons; assumes the polygon is convex which is the
|
||||
// common case from DCC exporters).
|
||||
if (resolved.size() > 3) triangulatedNgons++;
|
||||
for (size_t k = 1; k + 1 < resolved.size(); ++k) {
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[0]));
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[k]));
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[k + 1]));
|
||||
}
|
||||
}
|
||||
// mtllib/usemtl/g/s lines are silently skipped — material
|
||||
// info doesn't survive the round-trip but groups would
|
||||
// (left as future work; current import keeps it simple).
|
||||
}
|
||||
if (wom.vertices.empty() || wom.indices.empty()) {
|
||||
std::fprintf(stderr, "import-obj: no geometry found in %s\n",
|
||||
objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
wom.name = objectName.empty()
|
||||
? std::filesystem::path(objPath).stem().string()
|
||||
: objectName;
|
||||
// Compute bounds from positions — the renderer culls by these
|
||||
// so wrong values cause the model to disappear at distance.
|
||||
wom.boundMin = wom.vertices[0].position;
|
||||
wom.boundMax = wom.boundMin;
|
||||
for (const auto& v : wom.vertices) {
|
||||
wom.boundMin = glm::min(wom.boundMin, v.position);
|
||||
wom.boundMax = glm::max(wom.boundMax, v.position);
|
||||
}
|
||||
glm::vec3 center = (wom.boundMin + wom.boundMax) * 0.5f;
|
||||
float r2 = 0;
|
||||
for (const auto& v : wom.vertices) {
|
||||
glm::vec3 d = v.position - center;
|
||||
r2 = std::max(r2, glm::dot(d, d));
|
||||
}
|
||||
wom.boundRadius = std::sqrt(r2);
|
||||
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
|
||||
std::fprintf(stderr, "import-obj: failed to write %s.wom\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("Imported %s -> %s.wom\n", objPath.c_str(), womBase.c_str());
|
||||
std::printf(" %zu verts, %zu tris, bounds [%.2f, %.2f, %.2f] - [%.2f, %.2f, %.2f]\n",
|
||||
wom.vertices.size(), wom.indices.size() / 3,
|
||||
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
|
||||
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
|
||||
if (triangulatedNgons > 0) {
|
||||
std::printf(" fan-triangulated %d n-gon(s)\n", triangulatedNgons);
|
||||
}
|
||||
if (badFaces > 0) {
|
||||
std::printf(" warning: skipped %d malformed face(s)\n", badFaces);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleWomIo(int& i, int argc, char** argv, int& outRc) {
|
||||
|
|
@ -516,6 +701,9 @@ bool handleWomIo(int& i, int argc, char** argv, int& outRc) {
|
|||
if (std::strcmp(argv[i], "--import-stl") == 0 && i + 1 < argc) {
|
||||
outRc = handleImportStl(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--import-obj") == 0 && i + 1 < argc) {
|
||||
outRc = handleImportObj(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace cli {
|
|||
// --export-glb WOM -> glTF 2.0 binary (browsers, Three.js)
|
||||
// --export-stl WOM -> ASCII STL (slicers / 3D printers)
|
||||
// --import-stl ASCII STL -> WOM (round-trip from CAD tools)
|
||||
// --import-obj Wavefront OBJ -> WOM (round-trip from Blender etc.)
|
||||
//
|
||||
// Returns true if matched; outRc holds the exit code.
|
||||
bool handleWomIo(int& i, int argc, char** argv, int& outRc);
|
||||
|
|
|
|||
|
|
@ -1276,189 +1276,6 @@ int main(int argc, char* argv[]) {
|
|||
t.loadMs, t.tiles, t.chunks, mspt);
|
||||
}
|
||||
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,
|
||||
// animations, and material flags are not in OBJ and stay
|
||||
// empty (the resulting WOM is WOM1, static-only). The intent
|
||||
// is "edit a static prop in Blender, ship it".
|
||||
std::string objPath = argv[++i];
|
||||
std::string womBase;
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
womBase = argv[++i];
|
||||
}
|
||||
if (!std::filesystem::exists(objPath)) {
|
||||
std::fprintf(stderr, "OBJ not found: %s\n", objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (womBase.empty()) {
|
||||
womBase = objPath;
|
||||
if (womBase.size() >= 4 &&
|
||||
womBase.substr(womBase.size() - 4) == ".obj") {
|
||||
womBase = womBase.substr(0, womBase.size() - 4);
|
||||
}
|
||||
}
|
||||
std::ifstream in(objPath);
|
||||
if (!in) {
|
||||
std::fprintf(stderr, "Failed to open OBJ: %s\n", objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
// Pools — OBJ stores positions/UVs/normals in independent
|
||||
// arrays and references them by index in face lines, so we
|
||||
// collect each pool first then expand into WOM vertices on
|
||||
// the fly (one WOM vertex per (vIdx, vtIdx, vnIdx) triple
|
||||
// since WOM has interleaved vertex data, not pooled).
|
||||
std::vector<glm::vec3> positions;
|
||||
std::vector<glm::vec2> texcoords;
|
||||
std::vector<glm::vec3> normals;
|
||||
wowee::pipeline::WoweeModel wom;
|
||||
wom.version = 1;
|
||||
std::unordered_map<std::string, uint32_t> dedupe;
|
||||
int badFaces = 0;
|
||||
int triangulatedNgons = 0;
|
||||
std::string objectName;
|
||||
std::string line;
|
||||
// Convert a single OBJ vertex token like "3/4/5" or "3//5" or
|
||||
// "3/4" or "3" into a WOM vertex index, deduping identical
|
||||
// (pos, uv, normal) triples to keep the buffer compact.
|
||||
auto resolveCorner = [&](const std::string& token) -> int {
|
||||
int v = 0, t = 0, n = 0;
|
||||
{
|
||||
const char* p = token.c_str();
|
||||
char* endp = nullptr;
|
||||
v = std::strtol(p, &endp, 10);
|
||||
if (*endp == '/') {
|
||||
++endp;
|
||||
if (*endp != '/') {
|
||||
t = std::strtol(endp, &endp, 10);
|
||||
}
|
||||
if (*endp == '/') {
|
||||
++endp;
|
||||
n = std::strtol(endp, &endp, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Translate negative (relative) indices to absolute.
|
||||
auto absIdx = [](int idx, size_t poolSize) -> int {
|
||||
if (idx < 0) return static_cast<int>(poolSize) + idx;
|
||||
return idx - 1; // OBJ is 1-based
|
||||
};
|
||||
int vi = absIdx(v, positions.size());
|
||||
int ti = (t == 0) ? -1 : absIdx(t, texcoords.size());
|
||||
int ni = (n == 0) ? -1 : absIdx(n, normals.size());
|
||||
if (vi < 0 || vi >= static_cast<int>(positions.size())) return -1;
|
||||
std::string key = std::to_string(vi) + "/" +
|
||||
std::to_string(ti) + "/" +
|
||||
std::to_string(ni);
|
||||
auto it = dedupe.find(key);
|
||||
if (it != dedupe.end()) return static_cast<int>(it->second);
|
||||
wowee::pipeline::WoweeModel::Vertex vert;
|
||||
vert.position = positions[vi];
|
||||
if (ti >= 0 && ti < static_cast<int>(texcoords.size())) {
|
||||
vert.texCoord = texcoords[ti];
|
||||
// Reverse the V-flip from --export-obj so a round-trip
|
||||
// returns the original UVs unchanged.
|
||||
vert.texCoord.y = 1.0f - vert.texCoord.y;
|
||||
} else {
|
||||
vert.texCoord = {0, 0};
|
||||
}
|
||||
if (ni >= 0 && ni < static_cast<int>(normals.size())) {
|
||||
vert.normal = normals[ni];
|
||||
} else {
|
||||
vert.normal = {0, 0, 1};
|
||||
}
|
||||
uint32_t newIdx = static_cast<uint32_t>(wom.vertices.size());
|
||||
wom.vertices.push_back(vert);
|
||||
dedupe[key] = newIdx;
|
||||
return static_cast<int>(newIdx);
|
||||
};
|
||||
while (std::getline(in, line)) {
|
||||
// Strip CR for CRLF files.
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
std::istringstream ss(line);
|
||||
std::string tag;
|
||||
ss >> tag;
|
||||
if (tag == "v") {
|
||||
glm::vec3 p; ss >> p.x >> p.y >> p.z;
|
||||
positions.push_back(p);
|
||||
} else if (tag == "vt") {
|
||||
glm::vec2 t; ss >> t.x >> t.y;
|
||||
texcoords.push_back(t);
|
||||
} else if (tag == "vn") {
|
||||
glm::vec3 n; ss >> n.x >> n.y >> n.z;
|
||||
normals.push_back(n);
|
||||
} else if (tag == "o") {
|
||||
if (objectName.empty()) ss >> objectName;
|
||||
} else if (tag == "f") {
|
||||
std::vector<std::string> corners;
|
||||
std::string c;
|
||||
while (ss >> c) corners.push_back(c);
|
||||
if (corners.size() < 3) { badFaces++; continue; }
|
||||
std::vector<int> resolved;
|
||||
resolved.reserve(corners.size());
|
||||
bool ok = true;
|
||||
for (const auto& cc : corners) {
|
||||
int idx = resolveCorner(cc);
|
||||
if (idx < 0) { ok = false; break; }
|
||||
resolved.push_back(idx);
|
||||
}
|
||||
if (!ok) { badFaces++; continue; }
|
||||
// Fan-triangulate (works for triangles, quads, and
|
||||
// n-gons; assumes the polygon is convex which is the
|
||||
// common case from DCC exporters).
|
||||
if (resolved.size() > 3) triangulatedNgons++;
|
||||
for (size_t k = 1; k + 1 < resolved.size(); ++k) {
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[0]));
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[k]));
|
||||
wom.indices.push_back(static_cast<uint32_t>(resolved[k + 1]));
|
||||
}
|
||||
}
|
||||
// mtllib/usemtl/g/s lines are silently skipped — material
|
||||
// info doesn't survive the round-trip but groups would
|
||||
// (left as future work; current import keeps it simple).
|
||||
}
|
||||
if (wom.vertices.empty() || wom.indices.empty()) {
|
||||
std::fprintf(stderr, "import-obj: no geometry found in %s\n",
|
||||
objPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
wom.name = objectName.empty()
|
||||
? std::filesystem::path(objPath).stem().string()
|
||||
: objectName;
|
||||
// Compute bounds from positions — the renderer culls by these
|
||||
// so wrong values cause the model to disappear at distance.
|
||||
wom.boundMin = wom.vertices[0].position;
|
||||
wom.boundMax = wom.boundMin;
|
||||
for (const auto& v : wom.vertices) {
|
||||
wom.boundMin = glm::min(wom.boundMin, v.position);
|
||||
wom.boundMax = glm::max(wom.boundMax, v.position);
|
||||
}
|
||||
glm::vec3 center = (wom.boundMin + wom.boundMax) * 0.5f;
|
||||
float r2 = 0;
|
||||
for (const auto& v : wom.vertices) {
|
||||
glm::vec3 d = v.position - center;
|
||||
r2 = std::max(r2, glm::dot(d, d));
|
||||
}
|
||||
wom.boundRadius = std::sqrt(r2);
|
||||
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
|
||||
std::fprintf(stderr, "import-obj: failed to write %s.wom\n",
|
||||
womBase.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("Imported %s -> %s.wom\n", objPath.c_str(), womBase.c_str());
|
||||
std::printf(" %zu verts, %zu tris, bounds [%.2f, %.2f, %.2f] - [%.2f, %.2f, %.2f]\n",
|
||||
wom.vertices.size(), wom.indices.size() / 3,
|
||||
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
|
||||
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
|
||||
if (triangulatedNgons > 0) {
|
||||
std::printf(" fan-triangulated %d n-gon(s)\n", triangulatedNgons);
|
||||
}
|
||||
if (badFaces > 0) {
|
||||
std::printf(" warning: skipped %d malformed face(s)\n", badFaces);
|
||||
}
|
||||
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