From 251a83096673f75a7aa37aae00cd2a0339e76189 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 8 May 2026 23:32:04 -0700 Subject: [PATCH] refactor(editor): extract --gen-mesh dispatcher into cli_gen_mesh.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/editor/cli_gen_mesh.cpp | 479 ++++++++++++++++++++++++++++++++++ tools/editor/main.cpp | 465 --------------------------------- 2 files changed, 479 insertions(+), 465 deletions(-) diff --git a/tools/editor/cli_gen_mesh.cpp b/tools/editor/cli_gen_mesh.cpp index 6dcbeea4..1a5124a9 100644 --- a/tools/editor/cli_gen_mesh.cpp +++ b/tools/editor/cli_gen_mesh.cpp @@ -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(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(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(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(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(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(wom.vertices.size()); + addVertex(0, h, 0, 0, 1, 0, 0.5f, 0.5f); + uint32_t topRingStart = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segments; ++sg) { + float u = static_cast(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(wom.vertices.size()); + addVertex(0, -h, 0, 0, -1, 0, 0.5f, 0.5f); + uint32_t botRingStart = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segments; ++sg) { + float u = static_cast(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(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(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(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(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(wom.vertices.size()); + for (int sg = 0; sg <= segments; ++sg) { + float u = static_cast(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(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(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 + // .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; } diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 8629c17f..1adfb734 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -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(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(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(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(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(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(wom.vertices.size()); - addVertex(0, h, 0, 0, 1, 0, 0.5f, 0.5f); - uint32_t topRingStart = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segments; ++sg) { - float u = static_cast(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(wom.vertices.size()); - addVertex(0, -h, 0, 0, -1, 0, 0.5f, 0.5f); - uint32_t botRingStart = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segments; ++sg) { - float u = static_cast(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(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(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(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(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(wom.vertices.size()); - for (int sg = 0; sg <= segments; ++sg) { - float u = static_cast(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(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(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 - // .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