refactor(editor): extract --gen-mesh dispatcher into cli_gen_mesh.cpp

Moves the bare --gen-mesh dispatcher (cube/plane/sphere/cylinder/
torus/cone/ramp internal switch — 391 lines) and the related
--gen-mesh-textured handler (~72 lines) into the existing
cli_gen_mesh.cpp module.

The bare --gen-mesh handler renamed to handleMeshDispatch since
'handleMesh' would shadow the dispatcher class. --gen-mesh-textured
matched first in the dispatch chain to keep the longer-name
convention consistent with --gen-texture-noise vs -noise-color.

main.cpp drops 21,526 → 21,061 lines (-465). Behavior verified
by re-running --gen-mesh cube/sphere/torus.
This commit is contained in:
Kelsi 2026-05-08 23:32:04 -07:00
parent 16ae6489c9
commit 251a830966
2 changed files with 479 additions and 465 deletions

View file

@ -2966,9 +2966,488 @@ int handleTree(int& i, int argc, char** argv) {
return 0;
}
int handleMeshDispatch(int& i, int argc, char** argv) {
// Synthesize a procedural primitive WOM. Generates proper
// per-face normals, planar UVs, a bounding box, and a
// single batch covering all indices so the model renders
// immediately in the editor without further processing.
//
// Shapes:
// cube — 24 verts / 12 tris, axis-aligned, ±size/2
// plane — 4 verts / 2 tris, on XY plane (Z=0), ±size/2
// sphere — UV sphere, 16 segments × 12 stacks, radius=size/2
std::string womBase = argv[++i];
std::string shape = argv[++i];
float size = 1.0f;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { size = std::stof(argv[++i]); } catch (...) {}
}
if (size <= 0.0f) {
std::fprintf(stderr,
"gen-mesh: size must be positive (got %g)\n", size);
return 1;
}
// Strip .wom if user passed a full filename — saver expects base.
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
wowee::pipeline::WoweeModel wom;
wom.name = std::filesystem::path(womBase).stem().string();
wom.version = 3;
// Helper to push a vertex with explicit normal + uv.
auto addVertex = [&](float x, float y, float z,
float nx, float ny, float nz,
float u, float v) -> uint32_t {
wowee::pipeline::WoweeModel::Vertex vtx;
vtx.position = glm::vec3(x, y, z);
vtx.normal = glm::vec3(nx, ny, nz);
vtx.texCoord = glm::vec2(u, v);
wom.vertices.push_back(vtx);
return static_cast<uint32_t>(wom.vertices.size() - 1);
};
std::string s = shape;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
float h = size * 0.5f;
if (s == "cube") {
// 6 faces, 4 verts each (so per-face normals are flat).
struct Face { float nx, ny, nz; float verts[4][3]; };
Face faces[6] = {
{ 0, 0, 1, {{-h,-h, h},{ h,-h, h},{ h, h, h},{-h, h, h}}}, // +Z
{ 0, 0, -1, {{ h,-h,-h},{-h,-h,-h},{-h, h,-h},{ h, h,-h}}}, // -Z
{ 1, 0, 0, {{ h,-h, h},{ h,-h,-h},{ h, h,-h},{ h, h, h}}}, // +X
{-1, 0, 0, {{-h,-h,-h},{-h,-h, h},{-h, h, h},{-h, h,-h}}}, // -X
{ 0, 1, 0, {{-h, h, h},{ h, h, h},{ h, h,-h},{-h, h,-h}}}, // +Y
{ 0, -1, 0, {{-h,-h,-h},{ h,-h,-h},{ h,-h, h},{-h,-h, h}}}, // -Y
};
float uvs[4][2] = {{0,0},{1,0},{1,1},{0,1}};
for (auto& f : faces) {
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int k = 0; k < 4; ++k) {
addVertex(f.verts[k][0], f.verts[k][1], f.verts[k][2],
f.nx, f.ny, f.nz, uvs[k][0], uvs[k][1]);
}
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 1);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 3);
}
} else if (s == "plane") {
addVertex(-h, -h, 0, 0, 0, 1, 0, 0);
addVertex( h, -h, 0, 0, 0, 1, 1, 0);
addVertex( h, h, 0, 0, 0, 1, 1, 1);
addVertex(-h, h, 0, 0, 0, 1, 0, 1);
wom.indices = {0, 1, 2, 0, 2, 3};
} else if (s == "sphere") {
const int segments = 16;
const int stacks = 12;
float r = h;
for (int st = 0; st <= stacks; ++st) {
float v = static_cast<float>(st) / stacks;
float phi = v * 3.14159265358979f;
float sphi = std::sin(phi), cphi = std::cos(phi);
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float theta = u * 2.0f * 3.14159265358979f;
float stheta = std::sin(theta), ctheta = std::cos(theta);
float nx = sphi * ctheta;
float ny = sphi * stheta;
float nz = cphi;
addVertex(r * nx, r * ny, r * nz, nx, ny, nz, u, v);
}
}
int stride = segments + 1;
for (int st = 0; st < stacks; ++st) {
for (int sg = 0; sg < segments; ++sg) {
uint32_t a = st * stride + sg;
uint32_t b = a + 1;
uint32_t c = a + stride;
uint32_t d = c + 1;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
}
} else if (s == "cylinder") {
// Capped cylinder along the Y axis. radius=size/2,
// height=size. 24 side segments — smooth enough for
// pillars and torches without exploding the vertex
// count. UVs: side wraps the texture once around;
// caps map [0..1] from a square sampled at the disc.
const int segments = 24;
float r = h;
// Side ring: 2 vertex rows (top, bottom), each with
// (segments+1) verts so UV-seam doesn't share verts.
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
// Bottom ring (Y = -h).
addVertex(r * ca, -h, r * sa, ca, 0, sa, u, 0);
// Top ring (Y = +h).
addVertex(r * ca, h, r * sa, ca, 0, sa, u, 1);
}
// Side quad indices.
for (int sg = 0; sg < segments; ++sg) {
uint32_t a = sg * 2;
uint32_t b = a + 1;
uint32_t c = a + 2;
uint32_t d = a + 3;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
// Top cap fan.
uint32_t topCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0, h, 0, 0, 1, 0, 0.5f, 0.5f);
uint32_t topRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, h, r * sa, 0, 1, 0,
0.5f + 0.5f * ca, 0.5f + 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(topCenter);
wom.indices.push_back(topRingStart + sg);
wom.indices.push_back(topRingStart + sg + 1);
}
// Bottom cap fan (winding flipped so normal points -Y).
uint32_t botCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0, -h, 0, 0, -1, 0, 0.5f, 0.5f);
uint32_t botRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, -h, r * sa, 0, -1, 0,
0.5f + 0.5f * ca, 0.5f - 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(botCenter);
wom.indices.push_back(botRingStart + sg + 1);
wom.indices.push_back(botRingStart + sg);
}
} else if (s == "torus") {
// Torus around the Y axis. Major radius (ring center
// distance from origin) = size/2, minor radius (tube
// thickness) = size/8 — the 4:1 ratio reads as a
// ring rather than a fat donut. 32 ring segments × 16
// tube segments = ~544 verts / ~1024 tris.
const int ringSeg = 32;
const int tubeSeg = 16;
float R = h; // major radius
float r = h * 0.25f; // minor radius (h/4)
for (int i2 = 0; i2 <= ringSeg; ++i2) {
float u = static_cast<float>(i2) / ringSeg;
float theta = u * 2.0f * 3.14159265358979f;
float ct = std::cos(theta), st = std::sin(theta);
for (int j2 = 0; j2 <= tubeSeg; ++j2) {
float v = static_cast<float>(j2) / tubeSeg;
float phi = v * 2.0f * 3.14159265358979f;
float cp = std::cos(phi), sp = std::sin(phi);
// Position on the surface.
float x = (R + r * cp) * ct;
float y = r * sp;
float z = (R + r * cp) * st;
// Normal: from the tube center outward.
float nx = cp * ct;
float ny = sp;
float nz = cp * st;
addVertex(x, y, z, nx, ny, nz, u, v);
}
}
int stride = tubeSeg + 1;
for (int i2 = 0; i2 < ringSeg; ++i2) {
for (int j2 = 0; j2 < tubeSeg; ++j2) {
uint32_t a = i2 * stride + j2;
uint32_t b = a + 1;
uint32_t c = a + stride;
uint32_t d = c + 1;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
}
} else if (s == "cone") {
// Cone with apex at +Y. radius=size/2, height=size.
// 24 side segments. Side has smooth radial-ish normals
// (slanted up by half the slope angle) for a curved
// shaded surface; bottom cap has flat -Y normal.
const int segments = 24;
float r = h;
float H = size;
// Slant length used for the side normal Y component.
// Side normal direction: (cos(a), nyComponent, sin(a))
// where the slope is r/H per unit of horizontal travel.
// Normalize so the normal has unit length.
float sideXZScale = H / std::sqrt(H * H + r * r);
float sideY = r / std::sqrt(H * H + r * r);
// Side ring (apex repeated per segment so each tri has
// its own apex vertex with the correct normal).
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
// Base vertex (Y = 0).
addVertex(r * ca, 0.0f, r * sa,
sideXZScale * ca, sideY, sideXZScale * sa,
u, 1.0f);
// Apex vertex (Y = H), one per ring step so the
// top vertex carries the segment-specific normal.
addVertex(0.0f, H, 0.0f,
sideXZScale * ca, sideY, sideXZScale * sa,
u, 0.0f);
}
// Side triangle indices.
for (int sg = 0; sg < segments; ++sg) {
uint32_t base = sg * 2;
// Two tris per quad band. The apex collapses to a
// point, so really one triangle per segment, but
// emitting both keeps the indexing uniform across
// the cylinder/cone code paths.
uint32_t a = base + 0; // base k
uint32_t b = base + 1; // apex k
uint32_t c = base + 2; // base k+1
uint32_t d = base + 3; // apex k+1
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
// Second triangle would be (b,c,d) but b == d at
// the apex visually — we still emit it so the
// per-vertex normals on b and d shade the joining
// seam smoothly.
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
// Bottom cap fan (flat -Y normal).
uint32_t botCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f);
uint32_t botRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, 0.0f, r * sa, 0.0f, -1.0f, 0.0f,
0.5f + 0.5f * ca, 0.5f - 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(botCenter);
wom.indices.push_back(botRingStart + sg + 1);
wom.indices.push_back(botRingStart + sg);
}
} else if (s == "ramp") {
// Right-triangular prism: a wedge that climbs along
// +X. Footprint is size×size on XY (centered on origin
// in X, Y from 0 to size); rises from Z=0 at -X to
// Z=size at +X. Useful for ramps onto platforms,
// simple roof slopes, cliff faces.
//
// 6 verts × 5 faces = 18 verts so per-face normals
// stay flat: top slope, bottom, back-tall, +Y side,
// -Y side. Front-short (X = -size/2) is open since
// the ramp meets ground there at zero height.
// Actually we still emit 5 faces — the "front" edge
// is just where slope and ground meet, no separate
// face needed.
float xMin = -h, xMax = h;
float yMin = 0, yMax = size;
float zMin = 0, zMax = size;
// Faces: top slope (normal = normalize(-1,0,1) since
// the slope rises with +X going up, normal points
// up-and-back).
float slopeLen = std::sqrt(size * size + size * size);
float nSlopeX = -size / slopeLen;
float nSlopeZ = size / slopeLen;
struct Face { float nx, ny, nz; float verts[4][3]; };
Face faces[5] = {
// Top sloped quad: from (xMin, yMin, zMin) up to
// (xMax, yMin/yMax, zMax)
{ nSlopeX, 0, nSlopeZ,
{{xMin, yMin, zMin},{xMin, yMax, zMin},
{xMax, yMax, zMax},{xMax, yMin, zMax}}},
// Bottom (-Z normal)
{ 0, 0, -1,
{{xMin, yMin, zMin},{xMax, yMin, zMin},
{xMax, yMax, zMin},{xMin, yMax, zMin}}},
// Back-tall vertical wall (+X)
{ 1, 0, 0,
{{xMax, yMin, zMin},{xMax, yMin, zMax},
{xMax, yMax, zMax},{xMax, yMax, zMin}}},
// -Y side triangle (degenerate quad — last 2 verts
// collapse to a point — but indexing uniformly is
// simpler than a special tri path)
{ 0, -1, 0,
{{xMin, yMin, zMin},{xMax, yMin, zMin},
{xMax, yMin, zMax},{xMax, yMin, zMax}}},
// +Y side triangle (same shape mirrored)
{ 0, 1, 0,
{{xMin, yMax, zMin},{xMax, yMax, zMax},
{xMax, yMax, zMin},{xMax, yMax, zMin}}},
};
float uvs[4][2] = {{0,0},{1,0},{1,1},{0,1}};
for (auto& f : faces) {
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int k = 0; k < 4; ++k) {
addVertex(f.verts[k][0], f.verts[k][1], f.verts[k][2],
f.nx, f.ny, f.nz, uvs[k][0], uvs[k][1]);
}
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 1);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 3);
}
} else {
std::fprintf(stderr,
"gen-mesh: shape must be cube, plane, sphere, cylinder, torus, cone, or ramp (got '%s')\n",
shape.c_str());
return 1;
}
// Compute bounds from the vertex positions we just emitted.
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;
// Single material batch covering everything — keeps the
// model immediately renderable.
wowee::pipeline::WoweeModel::Batch b;
b.indexStart = 0;
b.indexCount = static_cast<uint32_t>(wom.indices.size());
b.textureIndex = 0;
b.blendMode = 0;
b.flags = 0;
wom.batches.push_back(b);
// Empty texture path slot so batch.textureIndex=0 is a
// valid index into texturePaths. The user can later set a
// real path or run --gen-texture next to it.
wom.texturePaths.push_back("");
std::filesystem::path womPath(womBase);
std::filesystem::create_directories(womPath.parent_path());
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom\n", womBase.c_str());
std::printf(" shape : %s\n", s.c_str());
std::printf(" size : %.3f\n", size);
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" indices : %zu (%zu tri%s)\n",
wom.indices.size(), wom.indices.size() / 3,
wom.indices.size() / 3 == 1 ? "" : "s");
std::printf(" 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 handleTextured(int& i, int argc, char** argv) {
// One-shot composer: --gen-mesh + --gen-texture wired
// together so the resulting WOM's texturePaths[0] points
// at the freshly-written PNG sidecar. Output is a model
// that renders with the synthesized texture out of the
// box — useful for prototyping textured props without
// chaining three commands by hand.
//
// The texture is written next to the mesh as
// <wom-base>.png
// and the WOM's texturePaths[0] is set to that filename
// (just the leaf — runtime resolves it relative to the
// model's own directory).
std::string womBase = argv[++i];
std::string shape = argv[++i];
std::string colorSpec = argv[++i];
std::string sizeArg;
if (i + 1 < argc && argv[i + 1][0] != '-') sizeArg = argv[++i];
// Strip .wom if user passed full filename.
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
std::string self = argv[0];
// 1) Mesh.
std::string meshCmd = "\"" + self + "\" --gen-mesh \"" + womBase +
"\" " + shape;
if (!sizeArg.empty()) meshCmd += " " + sizeArg;
meshCmd += " >/dev/null 2>&1";
int rc = std::system(meshCmd.c_str());
if (rc != 0) {
std::fprintf(stderr,
"gen-mesh-textured: gen-mesh step failed (rc=%d)\n", rc);
return 1;
}
// 2) Texture as a PNG sidecar at the mesh's base path.
std::string pngPath = womBase + ".png";
std::string texCmd = "\"" + self + "\" --gen-texture \"" + pngPath +
"\" \"" + colorSpec + "\" 256 256";
texCmd += " >/dev/null 2>&1";
rc = std::system(texCmd.c_str());
if (rc != 0) {
std::fprintf(stderr,
"gen-mesh-textured: gen-texture step failed (rc=%d)\n", rc);
return 1;
}
// 3) Load the WOM, set texturePaths[0] to the PNG leaf,
// and re-save so the binding is permanent.
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"gen-mesh-textured: cannot load %s.wom after gen-mesh\n",
womBase.c_str());
return 1;
}
std::string pngLeaf = std::filesystem::path(pngPath).filename().string();
if (wom.texturePaths.empty()) {
wom.texturePaths.push_back(pngLeaf);
} else {
wom.texturePaths[0] = pngLeaf;
}
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-textured: failed to re-save %s.wom\n",
womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom + %s\n", womBase.c_str(), pngPath.c_str());
std::printf(" shape : %s\n", shape.c_str());
std::printf(" color : %s\n", colorSpec.c_str());
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" texture : %s (wired into batch 0)\n", pngLeaf.c_str());
return 0;
}
} // namespace
bool handleGenMesh(int& i, int argc, char** argv, int& outRc) {
// Match --gen-mesh-textured BEFORE the bare --gen-mesh dispatcher.
// strcmp is exact-match so the order doesn't actually matter, but
// keeping the longer name first matches the convention used for
// --gen-texture-noise vs --gen-texture-noise-color.
if (std::strcmp(argv[i], "--gen-mesh-textured") == 0 && i + 3 < argc) {
outRc = handleTextured(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-mesh") == 0 && i + 2 < argc) {
outRc = handleMeshDispatch(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-mesh-stairs") == 0 && i + 2 < argc) {
outRc = handleStairs(i, argc, argv); return true;
}

View file

@ -16619,471 +16619,6 @@ int main(int argc, char* argv[]) {
std::printf(" size : %dx%d\n", W, H);
std::printf(" spec : %s\n", spec.c_str());
return 0;
} else if (std::strcmp(argv[i], "--gen-mesh") == 0 && i + 2 < argc) {
// Synthesize a procedural primitive WOM. Generates proper
// per-face normals, planar UVs, a bounding box, and a
// single batch covering all indices so the model renders
// immediately in the editor without further processing.
//
// Shapes:
// cube — 24 verts / 12 tris, axis-aligned, ±size/2
// plane — 4 verts / 2 tris, on XY plane (Z=0), ±size/2
// sphere — UV sphere, 16 segments × 12 stacks, radius=size/2
std::string womBase = argv[++i];
std::string shape = argv[++i];
float size = 1.0f;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { size = std::stof(argv[++i]); } catch (...) {}
}
if (size <= 0.0f) {
std::fprintf(stderr,
"gen-mesh: size must be positive (got %g)\n", size);
return 1;
}
// Strip .wom if user passed a full filename — saver expects base.
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
wowee::pipeline::WoweeModel wom;
wom.name = std::filesystem::path(womBase).stem().string();
wom.version = 3;
// Helper to push a vertex with explicit normal + uv.
auto addVertex = [&](float x, float y, float z,
float nx, float ny, float nz,
float u, float v) -> uint32_t {
wowee::pipeline::WoweeModel::Vertex vtx;
vtx.position = glm::vec3(x, y, z);
vtx.normal = glm::vec3(nx, ny, nz);
vtx.texCoord = glm::vec2(u, v);
wom.vertices.push_back(vtx);
return static_cast<uint32_t>(wom.vertices.size() - 1);
};
std::string s = shape;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
float h = size * 0.5f;
if (s == "cube") {
// 6 faces, 4 verts each (so per-face normals are flat).
struct Face { float nx, ny, nz; float verts[4][3]; };
Face faces[6] = {
{ 0, 0, 1, {{-h,-h, h},{ h,-h, h},{ h, h, h},{-h, h, h}}}, // +Z
{ 0, 0, -1, {{ h,-h,-h},{-h,-h,-h},{-h, h,-h},{ h, h,-h}}}, // -Z
{ 1, 0, 0, {{ h,-h, h},{ h,-h,-h},{ h, h,-h},{ h, h, h}}}, // +X
{-1, 0, 0, {{-h,-h,-h},{-h,-h, h},{-h, h, h},{-h, h,-h}}}, // -X
{ 0, 1, 0, {{-h, h, h},{ h, h, h},{ h, h,-h},{-h, h,-h}}}, // +Y
{ 0, -1, 0, {{-h,-h,-h},{ h,-h,-h},{ h,-h, h},{-h,-h, h}}}, // -Y
};
float uvs[4][2] = {{0,0},{1,0},{1,1},{0,1}};
for (auto& f : faces) {
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int k = 0; k < 4; ++k) {
addVertex(f.verts[k][0], f.verts[k][1], f.verts[k][2],
f.nx, f.ny, f.nz, uvs[k][0], uvs[k][1]);
}
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 1);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 3);
}
} else if (s == "plane") {
addVertex(-h, -h, 0, 0, 0, 1, 0, 0);
addVertex( h, -h, 0, 0, 0, 1, 1, 0);
addVertex( h, h, 0, 0, 0, 1, 1, 1);
addVertex(-h, h, 0, 0, 0, 1, 0, 1);
wom.indices = {0, 1, 2, 0, 2, 3};
} else if (s == "sphere") {
const int segments = 16;
const int stacks = 12;
float r = h;
for (int st = 0; st <= stacks; ++st) {
float v = static_cast<float>(st) / stacks;
float phi = v * 3.14159265358979f;
float sphi = std::sin(phi), cphi = std::cos(phi);
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float theta = u * 2.0f * 3.14159265358979f;
float stheta = std::sin(theta), ctheta = std::cos(theta);
float nx = sphi * ctheta;
float ny = sphi * stheta;
float nz = cphi;
addVertex(r * nx, r * ny, r * nz, nx, ny, nz, u, v);
}
}
int stride = segments + 1;
for (int st = 0; st < stacks; ++st) {
for (int sg = 0; sg < segments; ++sg) {
uint32_t a = st * stride + sg;
uint32_t b = a + 1;
uint32_t c = a + stride;
uint32_t d = c + 1;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
}
} else if (s == "cylinder") {
// Capped cylinder along the Y axis. radius=size/2,
// height=size. 24 side segments — smooth enough for
// pillars and torches without exploding the vertex
// count. UVs: side wraps the texture once around;
// caps map [0..1] from a square sampled at the disc.
const int segments = 24;
float r = h;
// Side ring: 2 vertex rows (top, bottom), each with
// (segments+1) verts so UV-seam doesn't share verts.
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
// Bottom ring (Y = -h).
addVertex(r * ca, -h, r * sa, ca, 0, sa, u, 0);
// Top ring (Y = +h).
addVertex(r * ca, h, r * sa, ca, 0, sa, u, 1);
}
// Side quad indices.
for (int sg = 0; sg < segments; ++sg) {
uint32_t a = sg * 2;
uint32_t b = a + 1;
uint32_t c = a + 2;
uint32_t d = a + 3;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
// Top cap fan.
uint32_t topCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0, h, 0, 0, 1, 0, 0.5f, 0.5f);
uint32_t topRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, h, r * sa, 0, 1, 0,
0.5f + 0.5f * ca, 0.5f + 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(topCenter);
wom.indices.push_back(topRingStart + sg);
wom.indices.push_back(topRingStart + sg + 1);
}
// Bottom cap fan (winding flipped so normal points -Y).
uint32_t botCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0, -h, 0, 0, -1, 0, 0.5f, 0.5f);
uint32_t botRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, -h, r * sa, 0, -1, 0,
0.5f + 0.5f * ca, 0.5f - 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(botCenter);
wom.indices.push_back(botRingStart + sg + 1);
wom.indices.push_back(botRingStart + sg);
}
} else if (s == "torus") {
// Torus around the Y axis. Major radius (ring center
// distance from origin) = size/2, minor radius (tube
// thickness) = size/8 — the 4:1 ratio reads as a
// ring rather than a fat donut. 32 ring segments × 16
// tube segments = ~544 verts / ~1024 tris.
const int ringSeg = 32;
const int tubeSeg = 16;
float R = h; // major radius
float r = h * 0.25f; // minor radius (h/4)
for (int i2 = 0; i2 <= ringSeg; ++i2) {
float u = static_cast<float>(i2) / ringSeg;
float theta = u * 2.0f * 3.14159265358979f;
float ct = std::cos(theta), st = std::sin(theta);
for (int j2 = 0; j2 <= tubeSeg; ++j2) {
float v = static_cast<float>(j2) / tubeSeg;
float phi = v * 2.0f * 3.14159265358979f;
float cp = std::cos(phi), sp = std::sin(phi);
// Position on the surface.
float x = (R + r * cp) * ct;
float y = r * sp;
float z = (R + r * cp) * st;
// Normal: from the tube center outward.
float nx = cp * ct;
float ny = sp;
float nz = cp * st;
addVertex(x, y, z, nx, ny, nz, u, v);
}
}
int stride = tubeSeg + 1;
for (int i2 = 0; i2 < ringSeg; ++i2) {
for (int j2 = 0; j2 < tubeSeg; ++j2) {
uint32_t a = i2 * stride + j2;
uint32_t b = a + 1;
uint32_t c = a + stride;
uint32_t d = c + 1;
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
}
} else if (s == "cone") {
// Cone with apex at +Y. radius=size/2, height=size.
// 24 side segments. Side has smooth radial-ish normals
// (slanted up by half the slope angle) for a curved
// shaded surface; bottom cap has flat -Y normal.
const int segments = 24;
float r = h;
float H = size;
// Slant length used for the side normal Y component.
// Side normal direction: (cos(a), nyComponent, sin(a))
// where the slope is r/H per unit of horizontal travel.
// Normalize so the normal has unit length.
float sideXZScale = H / std::sqrt(H * H + r * r);
float sideY = r / std::sqrt(H * H + r * r);
// Side ring (apex repeated per segment so each tri has
// its own apex vertex with the correct normal).
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
// Base vertex (Y = 0).
addVertex(r * ca, 0.0f, r * sa,
sideXZScale * ca, sideY, sideXZScale * sa,
u, 1.0f);
// Apex vertex (Y = H), one per ring step so the
// top vertex carries the segment-specific normal.
addVertex(0.0f, H, 0.0f,
sideXZScale * ca, sideY, sideXZScale * sa,
u, 0.0f);
}
// Side triangle indices.
for (int sg = 0; sg < segments; ++sg) {
uint32_t base = sg * 2;
// Two tris per quad band. The apex collapses to a
// point, so really one triangle per segment, but
// emitting both keeps the indexing uniform across
// the cylinder/cone code paths.
uint32_t a = base + 0; // base k
uint32_t b = base + 1; // apex k
uint32_t c = base + 2; // base k+1
uint32_t d = base + 3; // apex k+1
wom.indices.push_back(a);
wom.indices.push_back(c);
wom.indices.push_back(b);
// Second triangle would be (b,c,d) but b == d at
// the apex visually — we still emit it so the
// per-vertex normals on b and d shade the joining
// seam smoothly.
wom.indices.push_back(b);
wom.indices.push_back(c);
wom.indices.push_back(d);
}
// Bottom cap fan (flat -Y normal).
uint32_t botCenter = static_cast<uint32_t>(wom.vertices.size());
addVertex(0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f);
uint32_t botRingStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= segments; ++sg) {
float u = static_cast<float>(sg) / segments;
float ang = u * 2.0f * 3.14159265358979f;
float ca = std::cos(ang), sa = std::sin(ang);
addVertex(r * ca, 0.0f, r * sa, 0.0f, -1.0f, 0.0f,
0.5f + 0.5f * ca, 0.5f - 0.5f * sa);
}
for (int sg = 0; sg < segments; ++sg) {
wom.indices.push_back(botCenter);
wom.indices.push_back(botRingStart + sg + 1);
wom.indices.push_back(botRingStart + sg);
}
} else if (s == "ramp") {
// Right-triangular prism: a wedge that climbs along
// +X. Footprint is size×size on XY (centered on origin
// in X, Y from 0 to size); rises from Z=0 at -X to
// Z=size at +X. Useful for ramps onto platforms,
// simple roof slopes, cliff faces.
//
// 6 verts × 5 faces = 18 verts so per-face normals
// stay flat: top slope, bottom, back-tall, +Y side,
// -Y side. Front-short (X = -size/2) is open since
// the ramp meets ground there at zero height.
// Actually we still emit 5 faces — the "front" edge
// is just where slope and ground meet, no separate
// face needed.
float xMin = -h, xMax = h;
float yMin = 0, yMax = size;
float zMin = 0, zMax = size;
// Faces: top slope (normal = normalize(-1,0,1) since
// the slope rises with +X going up, normal points
// up-and-back).
float slopeLen = std::sqrt(size * size + size * size);
float nSlopeX = -size / slopeLen;
float nSlopeZ = size / slopeLen;
struct Face { float nx, ny, nz; float verts[4][3]; };
Face faces[5] = {
// Top sloped quad: from (xMin, yMin, zMin) up to
// (xMax, yMin/yMax, zMax)
{ nSlopeX, 0, nSlopeZ,
{{xMin, yMin, zMin},{xMin, yMax, zMin},
{xMax, yMax, zMax},{xMax, yMin, zMax}}},
// Bottom (-Z normal)
{ 0, 0, -1,
{{xMin, yMin, zMin},{xMax, yMin, zMin},
{xMax, yMax, zMin},{xMin, yMax, zMin}}},
// Back-tall vertical wall (+X)
{ 1, 0, 0,
{{xMax, yMin, zMin},{xMax, yMin, zMax},
{xMax, yMax, zMax},{xMax, yMax, zMin}}},
// -Y side triangle (degenerate quad — last 2 verts
// collapse to a point — but indexing uniformly is
// simpler than a special tri path)
{ 0, -1, 0,
{{xMin, yMin, zMin},{xMax, yMin, zMin},
{xMax, yMin, zMax},{xMax, yMin, zMax}}},
// +Y side triangle (same shape mirrored)
{ 0, 1, 0,
{{xMin, yMax, zMin},{xMax, yMax, zMax},
{xMax, yMax, zMin},{xMax, yMax, zMin}}},
};
float uvs[4][2] = {{0,0},{1,0},{1,1},{0,1}};
for (auto& f : faces) {
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int k = 0; k < 4; ++k) {
addVertex(f.verts[k][0], f.verts[k][1], f.verts[k][2],
f.nx, f.ny, f.nz, uvs[k][0], uvs[k][1]);
}
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 1);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 0);
wom.indices.push_back(base + 2);
wom.indices.push_back(base + 3);
}
} else {
std::fprintf(stderr,
"gen-mesh: shape must be cube, plane, sphere, cylinder, torus, cone, or ramp (got '%s')\n",
shape.c_str());
return 1;
}
// Compute bounds from the vertex positions we just emitted.
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;
// Single material batch covering everything — keeps the
// model immediately renderable.
wowee::pipeline::WoweeModel::Batch b;
b.indexStart = 0;
b.indexCount = static_cast<uint32_t>(wom.indices.size());
b.textureIndex = 0;
b.blendMode = 0;
b.flags = 0;
wom.batches.push_back(b);
// Empty texture path slot so batch.textureIndex=0 is a
// valid index into texturePaths. The user can later set a
// real path or run --gen-texture next to it.
wom.texturePaths.push_back("");
std::filesystem::path womPath(womBase);
std::filesystem::create_directories(womPath.parent_path());
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom\n", womBase.c_str());
std::printf(" shape : %s\n", s.c_str());
std::printf(" size : %.3f\n", size);
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" indices : %zu (%zu tri%s)\n",
wom.indices.size(), wom.indices.size() / 3,
wom.indices.size() / 3 == 1 ? "" : "s");
std::printf(" 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;
} else if (std::strcmp(argv[i], "--gen-mesh-textured") == 0 && i + 3 < argc) {
// One-shot composer: --gen-mesh + --gen-texture wired
// together so the resulting WOM's texturePaths[0] points
// at the freshly-written PNG sidecar. Output is a model
// that renders with the synthesized texture out of the
// box — useful for prototyping textured props without
// chaining three commands by hand.
//
// The texture is written next to the mesh as
// <wom-base>.png
// and the WOM's texturePaths[0] is set to that filename
// (just the leaf — runtime resolves it relative to the
// model's own directory).
std::string womBase = argv[++i];
std::string shape = argv[++i];
std::string colorSpec = argv[++i];
std::string sizeArg;
if (i + 1 < argc && argv[i + 1][0] != '-') sizeArg = argv[++i];
// Strip .wom if user passed full filename.
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
std::string self = argv[0];
// 1) Mesh.
std::string meshCmd = "\"" + self + "\" --gen-mesh \"" + womBase +
"\" " + shape;
if (!sizeArg.empty()) meshCmd += " " + sizeArg;
meshCmd += " >/dev/null 2>&1";
int rc = std::system(meshCmd.c_str());
if (rc != 0) {
std::fprintf(stderr,
"gen-mesh-textured: gen-mesh step failed (rc=%d)\n", rc);
return 1;
}
// 2) Texture as a PNG sidecar at the mesh's base path.
std::string pngPath = womBase + ".png";
std::string texCmd = "\"" + self + "\" --gen-texture \"" + pngPath +
"\" \"" + colorSpec + "\" 256 256";
texCmd += " >/dev/null 2>&1";
rc = std::system(texCmd.c_str());
if (rc != 0) {
std::fprintf(stderr,
"gen-mesh-textured: gen-texture step failed (rc=%d)\n", rc);
return 1;
}
// 3) Load the WOM, set texturePaths[0] to the PNG leaf,
// and re-save so the binding is permanent.
auto wom = wowee::pipeline::WoweeModelLoader::load(womBase);
if (!wom.isValid()) {
std::fprintf(stderr,
"gen-mesh-textured: cannot load %s.wom after gen-mesh\n",
womBase.c_str());
return 1;
}
std::string pngLeaf = std::filesystem::path(pngPath).filename().string();
if (wom.texturePaths.empty()) {
wom.texturePaths.push_back(pngLeaf);
} else {
wom.texturePaths[0] = pngLeaf;
}
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-textured: failed to re-save %s.wom\n",
womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom + %s\n", womBase.c_str(), pngPath.c_str());
std::printf(" shape : %s\n", shape.c_str());
std::printf(" color : %s\n", colorSpec.c_str());
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" texture : %s (wired into batch 0)\n", pngLeaf.c_str());
return 0;
} else if (std::strcmp(argv[i], "--displace-mesh") == 0 && i + 2 < argc) {
// Displaces each vertex along its current normal by the
// heightmap brightness × scale. UVs determine where each