refactor(editor): extract 10 mesh edit ops into cli_mesh_edit.cpp

Moves the WOM mesh editing/transform handlers out of main.cpp:

  --add-texture-to-mesh   --scale-mesh
  --translate-mesh        --strip-mesh
  --rotate-mesh           --center-mesh
  --flip-mesh-normals     --mirror-mesh
  --smooth-mesh-normals   --merge-meshes

These are the post-generation manipulators (load → mutate → save)
distinct from the geometry generators in cli_gen_mesh.cpp and
the heightmap I/O in cli_mesh_io.cpp.

main.cpp drops 20,741 → 20,005 lines (-736). Behavior verified
by piping a fresh cube through scale → translate → center.
This commit is contained in:
Kelsi 2026-05-09 00:36:51 -07:00
parent 08f740a1ff
commit 3da5e5517f
4 changed files with 848 additions and 741 deletions

View file

@ -0,0 +1,824 @@
#include "cli_mesh_edit.hpp"
#include "pipeline/wowee_model.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
int handleAddTextureToMesh(int& i, int argc, char** argv) {
// Manual companion to --gen-mesh-textured. Binds an
// existing PNG to a WOM by appending it to texturePaths
// (or reusing the slot if already present) and pointing
// the chosen batch at it.
//
// The PNG path stored in the WOM is just the leaf — the
// runtime resolves textures relative to the model's own
// directory, so the user is responsible for placing the
// PNG next to the WOM.
std::string womBase = argv[++i];
std::string pngPath = argv[++i];
int batchIdx = 0;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { batchIdx = std::stoi(argv[++i]); }
catch (...) {
std::fprintf(stderr,
"add-texture-to-mesh: batchIdx must be an integer\n");
return 1;
}
}
// Strip .wom if user passed a full filename.
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
namespace fs = std::filesystem;
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"add-texture-to-mesh: %s.wom does not exist\n",
womBase.c_str());
return 1;
}
if (!fs::exists(pngPath)) {
std::fprintf(stderr,
"add-texture-to-mesh: png '%s' does not exist\n",
pngPath.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"add-texture-to-mesh: failed to load %s.wom\n",
womBase.c_str());
return 1;
}
if (wom.batches.empty()) {
std::fprintf(stderr,
"add-texture-to-mesh: %s.wom has no batches "
"(run --migrate-wom to upgrade WOM1/WOM2 first)\n",
womBase.c_str());
return 1;
}
if (batchIdx < 0 ||
static_cast<size_t>(batchIdx) >= wom.batches.size()) {
std::fprintf(stderr,
"add-texture-to-mesh: batchIdx %d out of range "
"(have %zu batches)\n",
batchIdx, wom.batches.size());
return 1;
}
std::string pngLeaf = fs::path(pngPath).filename().string();
// Reuse texture slot if the leaf is already in the table;
// otherwise append a new slot at the end.
uint32_t texIdx = static_cast<uint32_t>(wom.texturePaths.size());
for (size_t k = 0; k < wom.texturePaths.size(); ++k) {
if (wom.texturePaths[k] == pngLeaf) {
texIdx = static_cast<uint32_t>(k);
break;
}
}
if (texIdx == wom.texturePaths.size()) {
wom.texturePaths.push_back(pngLeaf);
}
wom.batches[batchIdx].textureIndex = texIdx;
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"add-texture-to-mesh: failed to re-save %s.wom\n",
womBase.c_str());
return 1;
}
std::printf("Bound %s -> %s.wom batch %d (texture slot %u)\n",
pngLeaf.c_str(), womBase.c_str(),
batchIdx, texIdx);
std::printf(" total texture slots : %zu\n", wom.texturePaths.size());
// Warn if the PNG isn't sitting next to the WOM — the
// runtime resolves leaf paths relative to the WOM dir.
std::string womDir = fs::path(womBase).parent_path().string();
if (womDir.empty()) womDir = ".";
std::string expected = womDir + "/" + pngLeaf;
if (!fs::exists(expected)) {
std::printf(" NOTE: %s does not exist next to the WOM\n",
expected.c_str());
std::printf(" copy or move %s -> %s before shipping\n",
pngPath.c_str(), expected.c_str());
}
return 0;
}
int handleScaleMesh(int& i, int argc, char** argv) {
// Uniformly scale a WOM in place. Multiplies every
// vertex position, every bone pivot, and the bounds by
// <factor>. Normals are unchanged (uniform scale
// preserves direction). Useful for "I imported this OBJ
// but it's the wrong size" cleanup.
std::string womBase = argv[++i];
float factor = 1.0f;
try { factor = std::stof(argv[++i]); }
catch (...) {
std::fprintf(stderr,
"scale-mesh: <factor> must be a number\n");
return 1;
}
if (factor <= 0.0f || !std::isfinite(factor)) {
std::fprintf(stderr,
"scale-mesh: factor must be positive and finite (got %g)\n",
factor);
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"scale-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"scale-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
for (auto& v : wom.vertices) v.position *= factor;
for (auto& b : wom.bones) b.pivot *= factor;
// Animation translations also scale; rotation/scale
// tracks are dimensionless.
for (auto& a : wom.animations) {
for (auto& bone : a.boneKeyframes) {
for (auto& kf : bone) kf.translation *= factor;
}
}
wom.boundMin *= factor;
wom.boundMax *= factor;
wom.boundRadius *= factor;
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"scale-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Scaled %s.wom by %g\n", womBase.c_str(), factor);
std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
std::printf(" new radius : %.3f\n", wom.boundRadius);
return 0;
}
int handleTranslateMesh(int& i, int argc, char** argv) {
// Offset every vertex (and bones / anim translations /
// bounds) by (dx, dy, dz). Useful for re-centering a
// mesh whose origin was wrong on import, or for shifting
// a procedural primitive that isn't centered the way
// you want.
std::string womBase = argv[++i];
float dx = 0, dy = 0, dz = 0;
try {
dx = std::stof(argv[++i]);
dy = std::stof(argv[++i]);
dz = std::stof(argv[++i]);
} catch (...) {
std::fprintf(stderr,
"translate-mesh: dx/dy/dz must be numbers\n");
return 1;
}
if (!std::isfinite(dx) || !std::isfinite(dy) || !std::isfinite(dz)) {
std::fprintf(stderr,
"translate-mesh: offsets must be finite\n");
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"translate-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"translate-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
glm::vec3 d(dx, dy, dz);
for (auto& v : wom.vertices) v.position += d;
for (auto& b : wom.bones) b.pivot += d;
// Bone-relative animation translations don't shift with
// the model — only the bone pivots do, since translations
// are in bone-local space. Leave anim keyframes alone.
wom.boundMin += d;
wom.boundMax += d;
// Radius is unchanged (translation is rigid, doesn't
// change extent).
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"translate-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Translated %s.wom by (%g, %g, %g)\n",
womBase.c_str(), dx, dy, dz);
std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
return 0;
}
int handleStripMesh(int& i, int argc, char** argv) {
// Drop bones and/or animations from a WOM in place. Use
// case: a model imported with full skeleton + anims that
// will only ever be placed as static decoration — there's
// no point shipping the bone data, and stripping it can
// shrink the file substantially.
//
// Default (no flags) is a no-op so the user explicitly
// opts in to destruction. --bones drops bones (and
// therefore animations, since they reference bones).
// --anims drops only animations. --all is shorthand for
// both.
std::string womBase = argv[++i];
bool dropBones = false, dropAnims = false;
while (i + 1 < argc && argv[i + 1][0] == '-') {
std::string flag = argv[++i];
if (flag == "--bones") { dropBones = true; }
else if (flag == "--anims") { dropAnims = true; }
else if (flag == "--all") { dropBones = true; dropAnims = true; }
else {
std::fprintf(stderr,
"strip-mesh: unknown flag '%s'\n", flag.c_str());
return 1;
}
}
if (!dropBones && !dropAnims) {
std::fprintf(stderr,
"strip-mesh: no --bones / --anims / --all specified — nothing to do\n");
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
namespace fs = std::filesystem;
std::string fullPath = womBase + ".wom";
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"strip-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
uint64_t bytesBefore = fs::file_size(fullPath);
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"strip-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
size_t bonesBefore = wom.bones.size();
size_t animsBefore = wom.animations.size();
if (dropBones) {
wom.bones.clear();
// Bones implies anims (anims reference bones).
wom.animations.clear();
// Reset per-vertex skinning to identity-on-bone-0 so
// a renderer that expects the field doesn't read
// stale indices.
for (auto& v : wom.vertices) {
v.boneWeights[0] = 255;
v.boneWeights[1] = 0;
v.boneWeights[2] = 0;
v.boneWeights[3] = 0;
v.boneIndices[0] = 0;
v.boneIndices[1] = 0;
v.boneIndices[2] = 0;
v.boneIndices[3] = 0;
}
} else if (dropAnims) {
wom.animations.clear();
}
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"strip-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
uint64_t bytesAfter = fs::file_size(fullPath);
std::printf("Stripped %s.wom\n", womBase.c_str());
std::printf(" bones : %zu -> %zu\n", bonesBefore, wom.bones.size());
std::printf(" animations : %zu -> %zu\n", animsBefore, wom.animations.size());
std::printf(" bytes : %llu -> %llu (%+lld)\n",
static_cast<unsigned long long>(bytesBefore),
static_cast<unsigned long long>(bytesAfter),
static_cast<long long>(bytesAfter) -
static_cast<long long>(bytesBefore));
return 0;
}
int handleRotateMesh(int& i, int argc, char** argv) {
// Rotate every vertex position and normal around the
// chosen axis (x, y, or z) by <degrees>. Bone pivots
// also rotate so the skeleton stays in sync. Bounds are
// recomputed from rotated positions (axis-aligned bbox
// grows during rotation).
std::string womBase = argv[++i];
std::string axisStr = argv[++i];
float degrees = 0.0f;
try { degrees = std::stof(argv[++i]); }
catch (...) {
std::fprintf(stderr,
"rotate-mesh: <degrees> must be a number\n");
return 1;
}
if (!std::isfinite(degrees)) {
std::fprintf(stderr,
"rotate-mesh: degrees must be finite\n");
return 1;
}
std::transform(axisStr.begin(), axisStr.end(), axisStr.begin(),
[](unsigned char c) { return std::tolower(c); });
int axis = -1;
if (axisStr == "x") axis = 0;
else if (axisStr == "y") axis = 1;
else if (axisStr == "z") axis = 2;
else {
std::fprintf(stderr,
"rotate-mesh: axis must be x, y, or z (got '%s')\n",
axisStr.c_str());
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"rotate-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"rotate-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
float rad = degrees * 3.14159265358979f / 180.0f;
float cs = std::cos(rad), sn = std::sin(rad);
// Rotation around each axis: standard right-hand rule.
auto rot = [axis, cs, sn](glm::vec3 v) -> glm::vec3 {
if (axis == 0) {
return glm::vec3(v.x,
cs * v.y - sn * v.z,
sn * v.y + cs * v.z);
}
if (axis == 1) {
return glm::vec3( cs * v.x + sn * v.z,
v.y,
-sn * v.x + cs * v.z);
}
return glm::vec3(cs * v.x - sn * v.y,
sn * v.x + cs * v.y,
v.z);
};
for (auto& v : wom.vertices) {
v.position = rot(v.position);
v.normal = rot(v.normal);
}
for (auto& b : wom.bones) {
b.pivot = rot(b.pivot);
}
// Recompute bounds from rotated vertices (axis-aligned
// bbox can only grow under rotation, so reuse the loop).
wom.boundMin = glm::vec3(1e30f);
wom.boundMax = glm::vec3(-1e30f);
for (const auto& v : wom.vertices) {
wom.boundMin = glm::min(wom.boundMin, v.position);
wom.boundMax = glm::max(wom.boundMax, v.position);
}
wom.boundRadius = glm::length(wom.boundMax - wom.boundMin) * 0.5f;
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"rotate-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Rotated %s.wom by %g° around %s\n",
womBase.c_str(), degrees, axisStr.c_str());
std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
return 0;
}
int handleCenterMesh(int& i, int argc, char** argv) {
// Translate the mesh so the bounds center lands at the
// origin. Convenience for "this mesh's pivot is in some
// weird corner — make it center-pivoted." Doesn't change
// shape, just shifts.
std::string womBase = argv[++i];
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"center-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"center-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
glm::vec3 center = (wom.boundMin + wom.boundMax) * 0.5f;
for (auto& v : wom.vertices) v.position -= center;
for (auto& b : wom.bones) b.pivot -= center;
wom.boundMin -= center;
wom.boundMax -= center;
// Radius is preserved (pure translation).
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"center-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Centered %s.wom (shifted by %g, %g, %g)\n",
womBase.c_str(), -center.x, -center.y, -center.z);
std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
return 0;
}
int handleFlipMeshNormals(int& i, int argc, char** argv) {
// Invert every vertex normal. Use case: an OBJ imported
// with flipped winding renders inside-out — flipping the
// normals makes shading correct without re-winding the
// index buffer (which would also need batch-aware care).
// Also useful for skybox-like meshes where the "outside"
// texture should appear when looking from inside.
std::string womBase = argv[++i];
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"flip-mesh-normals: %s.wom does not exist\n",
womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"flip-mesh-normals: failed to load %s.wom\n",
womBase.c_str());
return 1;
}
for (auto& v : wom.vertices) v.normal = -v.normal;
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"flip-mesh-normals: failed to save %s.wom\n",
womBase.c_str());
return 1;
}
std::printf("Flipped normals on %s.wom (%zu vertices)\n",
womBase.c_str(), wom.vertices.size());
return 0;
}
int handleMirrorMesh(int& i, int argc, char** argv) {
// Mirror every vertex + normal across the chosen axis.
// Negating just one position component reverses face
// winding (the triangle's signed area flips), so we
// also swap the second and third index of every triangle
// to keep front-faces facing forward and lighting
// correct. Bone pivots mirror too.
//
// Useful for "I have a left arm, mirror it for the right
// arm" content reuse. The output is byte-stable
// independent of execution order.
std::string womBase = argv[++i];
std::string axisStr = argv[++i];
std::transform(axisStr.begin(), axisStr.end(), axisStr.begin(),
[](unsigned char c) { return std::tolower(c); });
int axis = -1;
if (axisStr == "x") axis = 0;
else if (axisStr == "y") axis = 1;
else if (axisStr == "z") axis = 2;
else {
std::fprintf(stderr,
"mirror-mesh: axis must be x, y, or z (got '%s')\n",
axisStr.c_str());
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"mirror-mesh: %s.wom does not exist\n", womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"mirror-mesh: failed to load %s.wom\n", womBase.c_str());
return 1;
}
for (auto& v : wom.vertices) {
v.position[axis] = -v.position[axis];
v.normal[axis] = -v.normal[axis];
}
for (auto& b : wom.bones) {
b.pivot[axis] = -b.pivot[axis];
}
// Flip winding: swap idx[1] and idx[2] of every triangle.
// Indices are stored as a flat list of triangle triples.
for (size_t k = 0; k + 2 < wom.indices.size(); k += 3) {
std::swap(wom.indices[k + 1], wom.indices[k + 2]);
}
// Bounds: the mirrored extent on this axis is just the
// negation of the previous extent — recompute from
// vertices to be safe.
wom.boundMin = glm::vec3(1e30f);
wom.boundMax = glm::vec3(-1e30f);
for (const auto& v : wom.vertices) {
wom.boundMin = glm::min(wom.boundMin, v.position);
wom.boundMax = glm::max(wom.boundMax, v.position);
}
wom.boundRadius = glm::length(wom.boundMax - wom.boundMin) * 0.5f;
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"mirror-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Mirrored %s.wom across %s axis\n",
womBase.c_str(), axisStr.c_str());
std::printf(" vertices touched : %zu\n", wom.vertices.size());
std::printf(" triangles flipped: %zu\n", wom.indices.size() / 3);
std::printf(" new bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
wom.boundMin.x, wom.boundMin.y, wom.boundMin.z,
wom.boundMax.x, wom.boundMax.y, wom.boundMax.z);
return 0;
}
int handleSmoothMeshNormals(int& i, int argc, char** argv) {
// Recompute per-vertex normals as the area-weighted
// average of incident face normals. Useful when:
// - Imported geometry has no normals (--import-obj
// leaves them zero or face-flat).
// - Custom transforms have desynced normals from the
// positions (e.g., user post-processed the WOM
// externally).
// - Faceted-by-construction meshes (cube, stairs) need
// a smooth re-shade for stylistic reasons.
//
// The cross-product magnitude is twice the triangle area,
// which weights large faces more — bigger triangles
// contribute more to the local surface direction.
std::string womBase = argv[++i];
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
if (!wowee::pipeline::WoweeModelLoader::exists(womBase)) {
std::fprintf(stderr,
"smooth-mesh-normals: %s.wom does not exist\n",
womBase.c_str());
return 1;
}
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"smooth-mesh-normals: failed to load %s.wom\n",
womBase.c_str());
return 1;
}
// Reset vertex normals to zero so the accumulator sums
// cleanly.
for (auto& v : wom.vertices) v.normal = glm::vec3(0);
for (size_t k = 0; k + 2 < wom.indices.size(); k += 3) {
uint32_t i0 = wom.indices[k];
uint32_t i1 = wom.indices[k + 1];
uint32_t i2 = wom.indices[k + 2];
if (i0 >= wom.vertices.size() ||
i1 >= wom.vertices.size() ||
i2 >= wom.vertices.size()) continue;
glm::vec3 p0 = wom.vertices[i0].position;
glm::vec3 p1 = wom.vertices[i1].position;
glm::vec3 p2 = wom.vertices[i2].position;
// Cross product magnitude == 2 * triangle area, used
// as the weight.
glm::vec3 faceN = glm::cross(p1 - p0, p2 - p0);
wom.vertices[i0].normal += faceN;
wom.vertices[i1].normal += faceN;
wom.vertices[i2].normal += faceN;
}
int normalized = 0, degenerate = 0;
for (auto& v : wom.vertices) {
float len = glm::length(v.normal);
if (len > 1e-6f) {
v.normal /= len;
normalized++;
} else {
// Vertex unreferenced or sum cancelled — fall
// back to "up" rather than leaving zero so the
// shader doesn't get a dark NaN spot.
v.normal = glm::vec3(0, 1, 0);
degenerate++;
}
}
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"smooth-mesh-normals: failed to save %s.wom\n",
womBase.c_str());
return 1;
}
std::printf("Smoothed normals on %s.wom\n", womBase.c_str());
std::printf(" vertices touched : %zu\n", wom.vertices.size());
std::printf(" triangles read : %zu\n", wom.indices.size() / 3);
std::printf(" normalized : %d\n", normalized);
if (degenerate > 0) {
std::printf(" degenerate : %d (set to (0,1,0))\n",
degenerate);
}
return 0;
}
int handleMergeMeshes(int& i, int argc, char** argv) {
// Combine two WOMs into one. The second mesh's indices
// are offset by the first mesh's vertex count, and its
// batches are appended with their indexStart shifted by
// the first mesh's index count and their textureIndex
// shifted by the first mesh's texture-slot count.
//
// Bones/animations are NOT merged — that requires
// skeleton retargeting which is beyond a simple
// concatenation. If either input has bones, the merged
// output is treated as static (bones cleared, weights
// reset to identity-on-bone-0) so renderers don't read
// mismatched indices.
std::string aBase = argv[++i];
std::string bBase = argv[++i];
std::string outBase = argv[++i];
auto stripExt = [](std::string p) {
if (p.size() >= 4 && p.substr(p.size() - 4) == ".wom") {
return p.substr(0, p.size() - 4);
}
return p;
};
aBase = stripExt(aBase);
bBase = stripExt(bBase);
outBase = stripExt(outBase);
if (!wowee::pipeline::WoweeModelLoader::exists(aBase)) {
std::fprintf(stderr,
"merge-meshes: %s.wom does not exist\n", aBase.c_str());
return 1;
}
if (!wowee::pipeline::WoweeModelLoader::exists(bBase)) {
std::fprintf(stderr,
"merge-meshes: %s.wom does not exist\n", bBase.c_str());
return 1;
}
auto a = wowee::pipeline::WoweeModelLoader::load(aBase);
auto b = wowee::pipeline::WoweeModelLoader::load(bBase);
if (!a.isValid() || !b.isValid()) {
std::fprintf(stderr,
"merge-meshes: failed to load one of the inputs\n");
return 1;
}
wowee::pipeline::WoweeModel out;
out.name = std::filesystem::path(outBase).stem().string();
out.version = 3;
out.vertices = a.vertices;
out.vertices.insert(out.vertices.end(),
b.vertices.begin(), b.vertices.end());
out.indices = a.indices;
uint32_t indexOffset = static_cast<uint32_t>(a.vertices.size());
for (uint32_t idx : b.indices) {
out.indices.push_back(idx + indexOffset);
}
out.texturePaths = a.texturePaths;
uint32_t textureOffset = static_cast<uint32_t>(a.texturePaths.size());
for (const auto& t : b.texturePaths) {
out.texturePaths.push_back(t);
}
// Promote single-batch / no-batch inputs into proper
// batches so the merged output is well-formed v3.
auto ensureBatch = [](const wowee::pipeline::WoweeModel& m) {
std::vector<wowee::pipeline::WoweeModel::Batch> bs = m.batches;
if (bs.empty()) {
wowee::pipeline::WoweeModel::Batch only;
only.indexStart = 0;
only.indexCount = static_cast<uint32_t>(m.indices.size());
only.textureIndex = 0;
only.blendMode = 0;
only.flags = 0;
bs.push_back(only);
}
return bs;
};
auto aBatches = ensureBatch(a);
auto bBatches = ensureBatch(b);
for (const auto& bt : aBatches) out.batches.push_back(bt);
uint32_t indexStartOffset = static_cast<uint32_t>(a.indices.size());
for (auto bt : bBatches) {
bt.indexStart += indexStartOffset;
bt.textureIndex += textureOffset;
out.batches.push_back(bt);
}
// Static-only output (see header comment).
for (auto& v : out.vertices) {
v.boneWeights[0] = 255;
v.boneWeights[1] = 0;
v.boneWeights[2] = 0;
v.boneWeights[3] = 0;
v.boneIndices[0] = 0;
v.boneIndices[1] = 0;
v.boneIndices[2] = 0;
v.boneIndices[3] = 0;
}
// Bounds: union of inputs.
out.boundMin = glm::min(a.boundMin, b.boundMin);
out.boundMax = glm::max(a.boundMax, b.boundMax);
out.boundRadius = glm::length(out.boundMax - out.boundMin) * 0.5f;
std::filesystem::path outPath(outBase);
std::filesystem::create_directories(outPath.parent_path());
if (!wowee::pipeline::WoweeModelLoader::save(out, outBase)) {
std::fprintf(stderr,
"merge-meshes: failed to save %s.wom\n", outBase.c_str());
return 1;
}
std::printf("Merged %s.wom + %s.wom -> %s.wom\n",
aBase.c_str(), bBase.c_str(), outBase.c_str());
std::printf(" vertices : %zu = %zu + %zu\n",
out.vertices.size(),
a.vertices.size(), b.vertices.size());
std::printf(" indices : %zu = %zu + %zu\n",
out.indices.size(),
a.indices.size(), b.indices.size());
std::printf(" batches : %zu = %zu + %zu\n",
out.batches.size(),
aBatches.size(), bBatches.size());
std::printf(" textures : %zu = %zu + %zu\n",
out.texturePaths.size(),
a.texturePaths.size(), b.texturePaths.size());
std::printf(" bounds : (%.3f, %.3f, %.3f) - (%.3f, %.3f, %.3f)\n",
out.boundMin.x, out.boundMin.y, out.boundMin.z,
out.boundMax.x, out.boundMax.y, out.boundMax.z);
return 0;
}
} // namespace
bool handleMeshEdit(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--add-texture-to-mesh") == 0 && i + 2 < argc) {
outRc = handleAddTextureToMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--scale-mesh") == 0 && i + 2 < argc) {
outRc = handleScaleMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--translate-mesh") == 0 && i + 4 < argc) {
outRc = handleTranslateMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--strip-mesh") == 0 && i + 1 < argc) {
outRc = handleStripMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--rotate-mesh") == 0 && i + 2 < argc) {
outRc = handleRotateMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--center-mesh") == 0 && i + 1 < argc) {
outRc = handleCenterMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--flip-mesh-normals") == 0 && i + 1 < argc) {
outRc = handleFlipMeshNormals(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--mirror-mesh") == 0 && i + 2 < argc) {
outRc = handleMirrorMesh(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--smooth-mesh-normals") == 0 && i + 1 < argc) {
outRc = handleSmoothMeshNormals(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--merge-meshes") == 0 && i + 3 < argc) {
outRc = handleMergeMeshes(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee