From 00754941093e66066259ab4e107276bad47f508f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 8 May 2026 22:19:41 -0700 Subject: [PATCH] refactor(editor): extract 12 composite mesh primitives into cli_gen_mesh.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the recently-added composite-prop mesh handlers (rock, pillar, bridge, tower, house, fountain, statue, altar, portal, archway, barrel, chest) into their own translation unit. These 12 handlers were the most contiguous block of similar-shaped mesh code in main.cpp. Other mesh handlers (--gen-mesh dispatcher, fence, tree, grid, stairs, disc, tube, capsule, arch, pyramid, from-heightmap, textured) still live in main.cpp and may be migrated in subsequent batches. main.cpp drops 24,270 → 22,681 lines (-1,589). Behavior verified by re-running rock/chest/archway/fountain. --- CMakeLists.txt | 1 + tools/editor/cli_gen_mesh.cpp | 1686 +++++++++++++++++++++++++++++++++ tools/editor/cli_gen_mesh.hpp | 26 + tools/editor/main.cpp | 1597 +------------------------------ 4 files changed, 1717 insertions(+), 1593 deletions(-) create mode 100644 tools/editor/cli_gen_mesh.cpp create mode 100644 tools/editor/cli_gen_mesh.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d4ff2ede..6115a718 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1306,6 +1306,7 @@ add_executable(wowee_editor tools/editor/cli_project_inventory.cpp tools/editor/cli_help.cpp tools/editor/cli_gen_texture.cpp + tools/editor/cli_gen_mesh.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_gen_mesh.cpp b/tools/editor/cli_gen_mesh.cpp new file mode 100644 index 00000000..cc867276 --- /dev/null +++ b/tools/editor/cli_gen_mesh.cpp @@ -0,0 +1,1686 @@ +#include "cli_gen_mesh.hpp" + +#include "pipeline/wowee_model.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleRock(int& i, int argc, char** argv) { + // Procedural boulder. Starts as an octahedron, subdivides + // each face N times to get a rounded base, then displaces + // each vertex along its outward direction by a smooth + // sin/cos noise term controlled by `seed` and `roughness`. + // Result is a unique-shaped rock per seed — perfect for + // scattering across a zone via random-populate-zone. + // + // The 16th procedural primitive in the WOM library. + std::string womBase = argv[++i]; + float radius = 1.0f; + float roughness = 0.25f; // 0..1, fraction of radius + int subdiv = 2; // 0=8 tris, 1=32, 2=128, 3=512 + uint32_t seed = 1; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { radius = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { roughness = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { subdiv = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} + } + if (radius <= 0 || roughness < 0 || roughness > 1 || + subdiv < 0 || subdiv > 4) { + std::fprintf(stderr, + "gen-mesh-rock: radius>0, roughness 0..1, subdiv 0..4\n"); + return 1; + } + if (womBase.size() >= 4 && + womBase.substr(womBase.size() - 4) == ".wom") { + womBase = womBase.substr(0, womBase.size() - 4); + } + // Build sphere via octahedron subdivision. Vertices are + // accumulated in unit-length form first, then displaced. + std::vector sv; // sphere verts (unit) + std::vector st; // sphere tris (vertex indices) + sv = { + { 1, 0, 0}, {-1, 0, 0}, + { 0, 1, 0}, { 0,-1, 0}, + { 0, 0, 1}, { 0, 0,-1}, + }; + st = { + {0, 2, 4}, {2, 1, 4}, {1, 3, 4}, {3, 0, 4}, + {2, 0, 5}, {1, 2, 5}, {3, 1, 5}, {0, 3, 5}, + }; + // Edge-midpoint cache so shared edges don't duplicate verts. + for (int s = 0; s < subdiv; ++s) { + std::map, uint32_t> midCache; + auto midpoint = [&](uint32_t a, uint32_t b) -> uint32_t { + auto key = std::make_pair(std::min(a,b), std::max(a,b)); + auto it = midCache.find(key); + if (it != midCache.end()) return it->second; + glm::vec3 m = glm::normalize((sv[a] + sv[b]) * 0.5f); + uint32_t idx = static_cast(sv.size()); + sv.push_back(m); + midCache[key] = idx; + return idx; + }; + std::vector next; + next.reserve(st.size() * 4); + for (auto& tri : st) { + uint32_t a = tri.x, b = tri.y, c = tri.z; + uint32_t ab = midpoint(a, b); + uint32_t bc = midpoint(b, c); + uint32_t ca = midpoint(c, a); + next.push_back({a, ab, ca}); + next.push_back({b, bc, ab}); + next.push_back({c, ca, bc}); + next.push_back({ab, bc, ca}); + } + st.swap(next); + } + // Smooth pseudo-noise displacement. Three orthogonal sin + // products give a coherent bumpy surface; phase shift uses + // the seed so each value yields a distinct silhouette. + float sf = static_cast(seed); + auto displace = [&](glm::vec3 p) -> float { + float n = std::sin(p.x * 3.1f + sf * 0.91f) * + std::sin(p.y * 4.7f + sf * 1.37f) * + std::sin(p.z * 5.3f + sf * 0.43f); + float n2 = std::sin(p.x * 7.1f + sf * 0.11f) * + std::sin(p.y * 8.3f + sf * 2.13f) * + std::sin(p.z * 9.7f + sf * 1.91f); + return 1.0f + roughness * (0.7f * n + 0.3f * n2); + }; + wowee::pipeline::WoweeModel wom; + wom.name = std::filesystem::path(womBase).stem().string(); + wom.version = 3; + std::vector finalPos(sv.size()); + for (size_t v = 0; v < sv.size(); ++v) { + finalPos[v] = sv[v] * (radius * displace(sv[v])); + } + // Per-vertex normals from triangle face normals (averaged). + std::vector normals(sv.size(), glm::vec3(0)); + for (auto& tri : st) { + glm::vec3 a = finalPos[tri.x]; + glm::vec3 b = finalPos[tri.y]; + glm::vec3 c = finalPos[tri.z]; + glm::vec3 fn = glm::normalize(glm::cross(b - a, c - a)); + normals[tri.x] += fn; + normals[tri.y] += fn; + normals[tri.z] += fn; + } + for (auto& n : normals) n = glm::length(n) > 1e-6f + ? glm::normalize(n) : glm::vec3(0, 1, 0); + for (size_t v = 0; v < sv.size(); ++v) { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = finalPos[v]; + vtx.normal = normals[v]; + // Spherical UV unwrap. Visible seam at u=0/1 is + // acceptable for rocks — usually hidden by terrain. + glm::vec3 d = glm::normalize(sv[v]); + vtx.texCoord = { + 0.5f + std::atan2(d.z, d.x) / (2.0f * 3.14159265f), + 0.5f - std::asin(d.y) / 3.14159265f, + }; + wom.vertices.push_back(vtx); + } + for (auto& tri : st) { + wom.indices.push_back(tri.x); + wom.indices.push_back(tri.y); + wom.indices.push_back(tri.z); + } + float bound = radius * (1.0f + roughness); + wom.boundMin = glm::vec3(-bound); + wom.boundMax = glm::vec3( bound); + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-rock: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" radius : %.3f\n", radius); + std::printf(" roughness : %.3f\n", roughness); + std::printf(" subdiv : %d\n", subdiv); + std::printf(" seed : %u\n", seed); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handlePillar(int& i, int argc, char** argv) { + // Procedural classical column. Central shaft is a + // cylinder with N concave flutes (radius modulated by + // cos²(theta*flutes/2)), capped above and below by + // wider disc caps that act as a simple capital and + // base. The 17th procedural mesh primitive — useful + // for ruins, temples, dungeons, plaza decoration. + std::string womBase = argv[++i]; + float radius = 0.4f; + float height = 4.0f; + int flutes = 12; + float capScale = 1.25f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { radius = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { flutes = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { capScale = std::stof(argv[++i]); } catch (...) {} + } + if (radius <= 0 || height <= 0 || + flutes < 4 || flutes > 64 || + capScale < 1.0f || capScale > 4.0f) { + std::fprintf(stderr, + "gen-mesh-pillar: radius>0, height>0, flutes 4..64, capScale 1..4\n"); + return 1; + } + if (womBase.size() >= 4 && + womBase.substr(womBase.size() - 4) == ".wom") { + womBase = womBase.substr(0, womBase.size() - 4); + } + const float pi = 3.14159265358979f; + // We use 8 segments per flute so the cosine-modulated + // groove resolves smoothly. Vertical: 2 rings (top/bot + // of shaft) + cap/base discs. + const int radSegs = flutes * 8; + const float fluteDepth = radius * 0.12f; + float capR = radius * capScale; + float capThick = radius * 0.25f; + wowee::pipeline::WoweeModel wom; + wom.name = std::filesystem::path(womBase).stem().string(); + wom.version = 3; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Shaft side ring at given y. radius modulated by flute count. + auto buildShaftRing = [&](float y) -> uint32_t { + uint32_t start = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= radSegs; ++sg) { + float u = static_cast(sg) / radSegs; + float ang = u * 2.0f * pi; + float c = std::cos(ang * flutes * 0.5f); + float r = radius - fluteDepth * (c * c); + glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, glm::normalize(n), glm::vec2(u, y / height)); + } + return start; + }; + // Cap/base disc ring (constant radius capR) at given y. + auto buildCapRing = [&](float y, float r) -> uint32_t { + uint32_t start = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= radSegs; ++sg) { + float u = static_cast(sg) / radSegs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, glm::normalize(n), glm::vec2(u, y / height)); + } + return start; + }; + // Layout (Y goes up): + // capThick: base disc bottom + // capThick: base disc top + // ...shaft from capThick to height-capThick... + // height-capThick: cap disc bottom + // height: cap disc top + float shaftY0 = capThick; + float shaftY1 = height - capThick; + uint32_t baseBot = buildCapRing(0.0f, capR); + uint32_t baseTop = buildCapRing(shaftY0, capR); + uint32_t shaftBot = buildShaftRing(shaftY0); + uint32_t shaftTop = buildShaftRing(shaftY1); + uint32_t capBot = buildCapRing(shaftY1, capR); + uint32_t capTop = buildCapRing(height, capR); + // Quad connector helper. + auto connect = [&](uint32_t a0, uint32_t a1) { + for (int sg = 0; sg < radSegs; ++sg) { + uint32_t i00 = a0 + sg; + uint32_t i01 = a0 + sg + 1; + uint32_t i10 = a1 + sg; + uint32_t i11 = a1 + sg + 1; + wom.indices.insert(wom.indices.end(), + { i00, i10, i01, i01, i10, i11 }); + } + }; + connect(baseBot, baseTop); // base side + connect(shaftBot, shaftTop); // shaft + connect(capBot, capTop); // cap side + // Bottom cap (downward fan), top cap (upward fan). + uint32_t bottomCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); + uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < radSegs; ++sg) { + wom.indices.insert(wom.indices.end(), + { bottomCenter, baseBot + sg + 1, baseBot + sg }); + wom.indices.insert(wom.indices.end(), + { topCenter, capTop + sg, capTop + sg + 1 }); + } + // Annular surfaces where caps meet shaft (top of base disc + // out to shaft, etc.). Just connect the two rings — they + // sit at the same Y so this looks like a flat ring. + connect(baseTop, shaftBot); + connect(shaftTop, capBot); + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + wom.boundMin = glm::vec3(-capR, 0, -capR); + wom.boundMax = glm::vec3( capR, height, capR); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-pillar: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" radius : %.3f\n", radius); + std::printf(" height : %.3f\n", height); + std::printf(" flutes : %d\n", flutes); + std::printf(" cap scale : %.2fx (capR=%.3f)\n", capScale, capR); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleBridge(int& i, int argc, char** argv) { + // Procedural plank bridge. Deck is N axis-aligned planks + // running across the bridge's width with small gaps + // between, plus two side rails (top + bottom rails on + // posts). Bridge length runs along +X, width is on Z. + // The 18th procedural mesh primitive — useful for + // river crossings, dungeon catwalks, scenic overlooks. + std::string womBase = argv[++i]; + float length = 6.0f; // along X + float width = 2.0f; // along Z + int planks = 6; // plank count across the length + float railHeight = 1.0f; // rail height above deck (0 = no rails) + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { length = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { width = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { planks = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { railHeight = std::stof(argv[++i]); } catch (...) {} + } + if (length <= 0 || width <= 0 || + planks < 1 || planks > 64 || + railHeight < 0 || railHeight > 4.0f) { + std::fprintf(stderr, + "gen-mesh-bridge: length>0, width>0, planks 1..64, rail 0..4\n"); + return 1; + } + 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; + // Box helper — builds 24-vert / 12-tri box centered on + // (cx, cy, cz) with half-extents (hx, hy, hz). Each face + // gets unique vertices so flat-shading works. Indices are + // pushed into wom.indices directly. + auto addBox = [&](float cx, float cy, float cz, + float hx, float hy, float hz) { + glm::vec3 c(cx, cy, cz); + struct Face { + glm::vec3 n; + glm::vec3 du, dv; // unit-length axes spanning the face + }; + Face faces[6] = { + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // top (+Y) + {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, // bottom (-Y) + {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, // right (+X) + {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, // left (-X) + {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, // front (+Z) + {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, // back (-Z) + }; + glm::vec3 ext(hx, hy, hz); + for (const Face& f : faces) { + glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); + glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); + glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); + uint32_t base = static_cast(wom.vertices.size()); + auto push = [&](glm::vec3 p, float u, float v) { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; + vtx.normal = f.n; + vtx.texCoord = {u, v}; + wom.vertices.push_back(vtx); + }; + push(center - du - dv, 0, 0); + push(center + du - dv, 1, 0); + push(center + du + dv, 1, 1); + push(center - du + dv, 0, 1); + wom.indices.insert(wom.indices.end(), + { base, base + 1, base + 2, base, base + 2, base + 3 }); + } + }; + // Deck: planks along X, gap = 5% of plank pitch. + float plankThickness = 0.08f; + float plankPitch = length / planks; + float plankWidth = plankPitch * 0.95f; + for (int p = 0; p < planks; ++p) { + float cx = -length * 0.5f + plankPitch * (p + 0.5f); + addBox(cx, plankThickness * 0.5f, 0, + plankWidth * 0.5f, plankThickness * 0.5f, width * 0.5f); + } + // Rails: 2 sides × (top rail + 3 posts) when railHeight > 0 + if (railHeight > 0.0f) { + float postR = 0.06f; + float topRailR = 0.08f; + int postCount = 3; + float rzOffset = width * 0.5f - postR; + for (int side = 0; side < 2; ++side) { + float zSign = (side == 0) ? 1.0f : -1.0f; + float z = zSign * rzOffset; + // Top rail: long thin box spanning length + addBox(0, plankThickness + railHeight, z, + length * 0.5f, topRailR, topRailR); + // Posts evenly spaced + for (int p = 0; p < postCount; ++p) { + float t = (postCount > 1) + ? static_cast(p) / (postCount - 1) + : 0.5f; + float cx = -length * 0.5f + length * t; + if (p == 0) cx += postR; + if (p == postCount - 1) cx -= postR; + addBox(cx, plankThickness + railHeight * 0.5f, z, + postR, railHeight * 0.5f, postR); + } + } + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = plankThickness + railHeight; + wom.boundMin = glm::vec3(-length * 0.5f, 0, -width * 0.5f); + wom.boundMax = glm::vec3( length * 0.5f, maxY, width * 0.5f); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-bridge: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" length : %.3f\n", length); + std::printf(" width : %.3f\n", width); + std::printf(" planks : %d\n", planks); + std::printf(" rail H : %.3f%s\n", railHeight, + railHeight > 0 ? "" : " (no rails)"); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleTower(int& i, int argc, char** argv) { + // Procedural castle tower. Solid cylindrical shaft with + // crenellated battlements ringing the top: alternating + // raised "merlons" and gaps. Each merlon is a thin + // angular wedge sitting on the top rim. Useful for + // keeps, watchtowers, perimeter walls. + // + // The 19th procedural mesh primitive. + std::string womBase = argv[++i]; + float radius = 1.5f; + float height = 8.0f; + int battlements = 8; // merlons around the rim + float battlementH = 0.5f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { radius = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { battlements = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { battlementH = std::stof(argv[++i]); } catch (...) {} + } + if (radius <= 0 || height <= 0 || + battlements < 4 || battlements > 64 || + battlementH < 0 || battlementH > 4.0f) { + std::fprintf(stderr, + "gen-mesh-tower: radius>0, height>0, battlements 4..64, bH 0..4\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + const int radSegs = std::max(24, battlements * 4); + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Cylinder shaft: side ring at y=0 and y=height. + uint32_t botRing = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= radSegs; ++sg) { + float u = static_cast(sg) / radSegs; + float ang = u * 2.0f * pi; + glm::vec3 p(radius * std::cos(ang), 0, radius * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, glm::vec2(u, 0)); + } + uint32_t topRing = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= radSegs; ++sg) { + float u = static_cast(sg) / radSegs; + float ang = u * 2.0f * pi; + glm::vec3 p(radius * std::cos(ang), height, radius * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, glm::vec2(u, 1)); + } + for (int sg = 0; sg < radSegs; ++sg) { + wom.indices.insert(wom.indices.end(), { + botRing + sg, topRing + sg, botRing + sg + 1, + botRing + sg + 1, topRing + sg, topRing + sg + 1 + }); + } + // Top cap (fan toward upward-facing center). + uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < radSegs; ++sg) { + wom.indices.insert(wom.indices.end(), + { topCenter, topRing + sg, topRing + sg + 1 }); + } + // Bottom cap (fan toward downward-facing center). + uint32_t botCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < radSegs; ++sg) { + wom.indices.insert(wom.indices.end(), + { botCenter, botRing + sg + 1, botRing + sg }); + } + // Battlements: thin curved blocks around the top rim, + // half the slots filled (alternating merlon/gap). + // Each merlon is approximated by an extruded arc segment + // at the wall radius extending outward slightly. + if (battlementH > 0.0f) { + int merlonSpan = radSegs / battlements; + int merlonHalf = std::max(1, merlonSpan / 2); + float outerR = radius * 1.05f; + float innerR = radius * 0.95f; + for (int b = 0; b < battlements; ++b) { + int startSeg = b * merlonSpan; + // Build 8-vert box-like segment between angles + // covering merlonHalf slots (so half the rim is + // filled, forming the merlon/gap pattern). + float ang0 = 2.0f * pi * static_cast(startSeg) / radSegs; + float ang1 = 2.0f * pi * static_cast(startSeg + merlonHalf) / radSegs; + glm::vec3 outer0(outerR * std::cos(ang0), 0, outerR * std::sin(ang0)); + glm::vec3 outer1(outerR * std::cos(ang1), 0, outerR * std::sin(ang1)); + glm::vec3 inner0(innerR * std::cos(ang0), 0, innerR * std::sin(ang0)); + glm::vec3 inner1(innerR * std::cos(ang1), 0, innerR * std::sin(ang1)); + glm::vec3 yLow(0, height, 0); + glm::vec3 yHigh(0, height + battlementH, 0); + glm::vec3 norm = glm::normalize( + outer0 + outer1 - inner0 - inner1); + auto V = [&](glm::vec3 p, glm::vec3 n) { + return addV(p, n, {0, 0}); + }; + // 8 verts: 4 corners × 2 heights + uint32_t bbl = V(outer0 + yLow, norm); // bot outer left + uint32_t bbr = V(outer1 + yLow, norm); + uint32_t btl = V(outer0 + yHigh, norm); // top outer left + uint32_t btr = V(outer1 + yHigh, norm); + uint32_t ibl = V(inner0 + yLow, -norm); // bot inner left + uint32_t ibr = V(inner1 + yLow, -norm); + uint32_t itl = V(inner0 + yHigh, -norm); // top inner left + uint32_t itr = V(inner1 + yHigh, -norm); + // outer face + wom.indices.insert(wom.indices.end(), {bbl, btl, bbr, bbr, btl, btr}); + // inner face + wom.indices.insert(wom.indices.end(), {ibr, itr, ibl, ibl, itr, itl}); + // top face + wom.indices.insert(wom.indices.end(), {btl, itl, btr, btr, itl, itr}); + // left and right end caps + wom.indices.insert(wom.indices.end(), {bbl, ibl, btl, btl, ibl, itl}); + wom.indices.insert(wom.indices.end(), {bbr, btr, ibr, ibr, btr, itr}); + } + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = height + battlementH; + float maxR = radius * 1.05f; + wom.boundMin = glm::vec3(-maxR, 0, -maxR); + wom.boundMax = glm::vec3( maxR, maxY, maxR); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-tower: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" radius : %.3f\n", radius); + std::printf(" height : %.3f\n", height); + std::printf(" battlements : %d (%.3fm tall)\n", + battlements, battlementH); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleHouse(int& i, int argc, char** argv) { + // Simple procedural house: cube body + pyramid roof + // meeting at a central apex above the body's roofline. + // The pyramid sits flush on the body so the eaves + // line up with the wall edges. No door cutout — that + // can be added later via mesh boolean ops or texture. + // + // The 20th procedural mesh primitive. + std::string womBase = argv[++i]; + float width = 4.0f; // along X + float depth = 4.0f; // along Z + float height = 3.0f; // wall height (Y) + float roofH = 2.0f; // pyramid above walls + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { width = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { depth = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { roofH = std::stof(argv[++i]); } catch (...) {} + } + if (width <= 0 || depth <= 0 || height <= 0 || + roofH < 0 || roofH > 20.0f) { + std::fprintf(stderr, + "gen-mesh-house: width/depth/height>0, roof 0..20\n"); + return 1; + } + 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; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + float hx = width * 0.5f; + float hz = depth * 0.5f; + // 4 walls — each a quad with an outward-facing normal so + // the house reads as solid even with backface culling on. + struct Wall { + glm::vec3 a, b, c, d; // CCW from outside + glm::vec3 n; + }; + Wall walls[4] = { + {{ hx, 0, hz}, {-hx, 0, hz}, {-hx, height, hz}, { hx, height, hz}, { 0, 0, 1}}, // +Z + {{-hx, 0, -hz}, { hx, 0, -hz}, { hx, height, -hz}, {-hx, height, -hz}, { 0, 0, -1}}, // -Z + {{ hx, 0, -hz}, { hx, 0, hz}, { hx, height, hz}, { hx, height, -hz}, { 1, 0, 0}}, // +X + {{-hx, 0, hz}, {-hx, 0, -hz}, {-hx, height, -hz}, {-hx, height, hz}, {-1, 0, 0}}, // -X + }; + for (const Wall& w : walls) { + uint32_t a = addV(w.a, w.n, {0, 0}); + uint32_t b = addV(w.b, w.n, {1, 0}); + uint32_t c = addV(w.c, w.n, {1, 1}); + uint32_t d = addV(w.d, w.n, {0, 1}); + wom.indices.insert(wom.indices.end(), {a, b, c, a, c, d}); + } + // Floor (single quad, normal-down so it shows from below; + // texturable as a foundation slab). + { + uint32_t a = addV({-hx, 0, -hz}, {0, -1, 0}, {0, 0}); + uint32_t b = addV({ hx, 0, -hz}, {0, -1, 0}, {1, 0}); + uint32_t c = addV({ hx, 0, hz}, {0, -1, 0}, {1, 1}); + uint32_t d = addV({-hx, 0, hz}, {0, -1, 0}, {0, 1}); + wom.indices.insert(wom.indices.end(), {a, c, b, a, d, c}); + } + // Roof: 4 triangles meeting at central apex. + float apexY = height + roofH; + glm::vec3 apex(0, apexY, 0); + // Eave corners (Y = wall height) — each triangle shares + // two adjacent corners + the apex. Per-face normal is + // computed once so flat shading works. + glm::vec3 eaves[4] = { + {-hx, height, hz}, + { hx, height, hz}, + { hx, height, -hz}, + {-hx, height, -hz}, + }; + for (int s = 0; s < 4; ++s) { + glm::vec3 e0 = eaves[s]; + glm::vec3 e1 = eaves[(s + 1) % 4]; + glm::vec3 fn = glm::normalize(glm::cross(e1 - e0, apex - e0)); + uint32_t a = addV(e0, fn, {0, 0}); + uint32_t b = addV(e1, fn, {1, 0}); + uint32_t c = addV(apex, fn, {0.5f, 1}); + wom.indices.insert(wom.indices.end(), {a, b, c}); + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + wom.boundMin = glm::vec3(-hx, 0, -hz); + wom.boundMax = glm::vec3( hx, apexY, hz); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-house: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" width : %.3f\n", width); + std::printf(" depth : %.3f\n", depth); + std::printf(" wall H : %.3f\n", height); + std::printf(" roof H : %.3f (apex %.3f)\n", roofH, apexY); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleFountain(int& i, int argc, char** argv) { + // Procedural fountain: low cylindrical basin with a + // narrower spout column rising from its center. Solid + // basin (not hollow) for simplicity — readable as a + // fountain because of the spout silhouette. Useful for + // town squares, plazas, garden centerpieces. + // + // The 21st procedural mesh primitive. + std::string womBase = argv[++i]; + float basinR = 1.5f; + float basinH = 0.5f; + float spoutR = 0.2f; + float spoutH = 1.5f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { basinR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { basinH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { spoutR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { spoutH = std::stof(argv[++i]); } catch (...) {} + } + if (basinR <= 0 || basinH <= 0 || spoutR <= 0 || spoutH <= 0 || + spoutR >= basinR) { + std::fprintf(stderr, + "gen-mesh-fountain: all dims > 0; spoutR must be < basinR\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + const int segs = 24; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Cylinder helper: build side ring + caps from y0 to y1 + // at given radius. Returns when done; indices appended + // directly. Side ring is 2× (segs+1) verts at y0 then y1. + auto cylinder = [&](float r, float y0, float y1) { + uint32_t bot = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y0, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, glm::vec2(u, 0)); + } + uint32_t top = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y1, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, glm::vec2(u, 1)); + } + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), { + bot + sg, top + sg, bot + sg + 1, + bot + sg + 1, top + sg, top + sg + 1 + }); + } + // Top cap (faces +Y) + uint32_t topC = addV({0, y1, 0}, {0, 1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), + {topC, top + sg, top + sg + 1}); + } + // Bottom cap (faces -Y) + uint32_t botC = addV({0, y0, 0}, {0, -1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), + {botC, bot + sg + 1, bot + sg}); + } + }; + // Basin: cylinder from y=0 to y=basinH at basinR. + cylinder(basinR, 0.0f, basinH); + // Spout: cylinder from y=basinH to y=basinH+spoutH at spoutR. + cylinder(spoutR, basinH, basinH + spoutH); + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = basinH + spoutH; + wom.boundMin = glm::vec3(-basinR, 0, -basinR); + wom.boundMax = glm::vec3( basinR, maxY, basinR); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-fountain: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" basin : R=%.3f H=%.3f\n", basinR, basinH); + std::printf(" spout : R=%.3f H=%.3f\n", spoutR, spoutH); + std::printf(" total H : %.3f\n", maxY); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles: %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleStatue(int& i, int argc, char** argv) { + // Humanoid placeholder: square pedestal block + tall + // narrow body cylinder + head sphere. The silhouette + // reads as a statue without needing limbs. Useful for + // monuments, hero statues, plaza centerpieces, religious + // shrines. + // + // The 22nd procedural mesh primitive. + std::string womBase = argv[++i]; + float pedSize = 1.0f; // pedestal width and depth + float bodyH = 2.5f; // body cylinder height + float headR = 0.4f; // head sphere radius + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { pedSize = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { bodyH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { headR = std::stof(argv[++i]); } catch (...) {} + } + if (pedSize <= 0 || bodyH <= 0 || headR <= 0) { + std::fprintf(stderr, + "gen-mesh-statue: all dims must be positive\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Pedestal: low square block (24 unique verts). + float pedH = pedSize * 0.4f; + float hp = pedSize * 0.5f; + { + struct Face { glm::vec3 n, du, dv; }; + Face faces[6] = { + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, + {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, + {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, + {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, + {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, + {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, + }; + glm::vec3 c(0, pedH * 0.5f, 0); + glm::vec3 ext(hp, pedH * 0.5f, hp); + for (const Face& f : faces) { + glm::vec3 center = c + glm::vec3(f.n.x*ext.x, f.n.y*ext.y, f.n.z*ext.z); + glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); + glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); + uint32_t base = static_cast(wom.vertices.size()); + addV(center - du - dv, f.n, {0, 0}); + addV(center + du - dv, f.n, {1, 0}); + addV(center + du + dv, f.n, {1, 1}); + addV(center - du + dv, f.n, {0, 1}); + wom.indices.insert(wom.indices.end(), + {base, base + 1, base + 2, base, base + 2, base + 3}); + } + } + // Body cylinder from y=pedH to y=pedH+bodyH at radius pedSize*0.2 + float bodyR = pedSize * 0.2f; + float bodyY0 = pedH; + float bodyY1 = pedH + bodyH; + const int segs = 16; + uint32_t bodyBot = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(bodyR * std::cos(ang), bodyY0, bodyR * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 0}); + } + uint32_t bodyTop = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(bodyR * std::cos(ang), bodyY1, bodyR * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 1}); + } + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), { + bodyBot + sg, bodyTop + sg, bodyBot + sg + 1, + bodyBot + sg + 1, bodyTop + sg, bodyTop + sg + 1 + }); + } + // Head sphere centered above body. UV-sphere with 16 + // longitude × 12 latitude segments. + float headY = bodyY1 + headR; + const int headLon = 16; + const int headLat = 12; + uint32_t headStart = static_cast(wom.vertices.size()); + for (int la = 0; la <= headLat; ++la) { + float v = static_cast(la) / headLat; + float phi = v * pi; // 0..pi + float sphi = std::sin(phi), cphi = std::cos(phi); + for (int lo = 0; lo <= headLon; ++lo) { + float u = static_cast(lo) / headLon; + float theta = u * 2.0f * pi; + glm::vec3 dir(sphi * std::cos(theta), + cphi, + sphi * std::sin(theta)); + glm::vec3 p = glm::vec3(0, headY, 0) + dir * headR; + addV(p, dir, {u, v}); + } + } + int rowSize = headLon + 1; + for (int la = 0; la < headLat; ++la) { + for (int lo = 0; lo < headLon; ++lo) { + uint32_t i00 = headStart + la * rowSize + lo; + uint32_t i01 = headStart + la * rowSize + lo + 1; + uint32_t i10 = headStart + (la + 1) * rowSize + lo; + uint32_t i11 = headStart + (la + 1) * rowSize + lo + 1; + wom.indices.insert(wom.indices.end(), + {i00, i10, i01, i01, i10, i11}); + } + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = headY + headR; + wom.boundMin = glm::vec3(-hp, 0, -hp); + wom.boundMax = glm::vec3( hp, maxY, hp); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-statue: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" pedestal : %.3f × %.3f × %.3f\n", pedSize, pedH, pedSize); + std::printf(" body : R=%.3f H=%.3f\n", bodyR, bodyH); + std::printf(" head : R=%.3f\n", headR); + std::printf(" total H : %.3f\n", maxY); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleAltar(int& i, int argc, char** argv) { + // Round altar: stack of N stepped cylindrical discs, + // each one wider and shorter than the next so the + // silhouette descends like a wedding cake. Top disc is + // the altar surface (where offerings would go); base + // discs widen out to anchor the structure visually. + // + // The 23rd procedural mesh primitive — pairs naturally + // with --gen-texture-marble for a temple aesthetic. + std::string womBase = argv[++i]; + float topR = 0.7f; // top altar disc radius + float topH = 0.3f; // top altar disc height + int steps = 3; // base steps below the top + float stepStride = 0.3f; // each step grows R by this much, shrinks H + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { topR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { topH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { steps = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { stepStride = std::stof(argv[++i]); } catch (...) {} + } + if (topR <= 0 || topH <= 0 || steps < 0 || steps > 16 || + stepStride <= 0 || stepStride > 5.0f) { + std::fprintf(stderr, + "gen-mesh-altar: topR/topH > 0, steps 0..16, stride 0..5\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + const int segs = 24; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Build a cylindrical disc from y0 to y1 at radius r. + // Side ring + top cap (faces +Y). Bottom of each disc + // is hidden by the next disc below, so we skip a bottom + // cap on all discs except the last (saves ~24 tris/disc). + auto disc = [&](float r, float y0, float y1, bool capBottom) { + uint32_t bot = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y0, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 0}); + } + uint32_t top = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y1, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 1}); + } + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), { + bot + sg, top + sg, bot + sg + 1, + bot + sg + 1, top + sg, top + sg + 1 + }); + } + // Top cap fan (faces +Y). + uint32_t tc = addV({0, y1, 0}, {0, 1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), + {tc, top + sg, top + sg + 1}); + } + if (capBottom) { + uint32_t bc = addV({0, y0, 0}, {0, -1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), + {bc, bot + sg + 1, bot + sg}); + } + } + }; + // Build bottom-up so y0 starts at floor and tops stack. + // Step k (k=0 is bottom-most) has radius = topR + (steps-k)*stride + // and height = topH * (1 - 0.2 * k). Y position accumulates. + float curY = 0.0f; + for (int k = steps - 1; k >= 0; --k) { // bottom step first + float r = topR + (k + 1) * stepStride; + float h = topH * (1.0f - 0.2f * k); + if (h < topH * 0.4f) h = topH * 0.4f; + bool isBottom = (k == steps - 1); + disc(r, curY, curY + h, isBottom); + curY += h; + } + // Top disc (the actual altar surface) + disc(topR, curY, curY + topH, steps == 0); + float maxY = curY + topH; + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxR = topR + steps * stepStride; + wom.boundMin = glm::vec3(-maxR, 0, -maxR); + wom.boundMax = glm::vec3( maxR, maxY, maxR); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-altar: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" top : R=%.3f H=%.3f\n", topR, topH); + std::printf(" steps : %d (stride %.3f)\n", steps, stepStride); + std::printf(" base R : %.3f\n", maxR); + std::printf(" total H : %.3f\n", maxY); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles: %zu\n", wom.indices.size() / 3); + return 0; +} + +int handlePortal(int& i, int argc, char** argv) { + // Doorway portal: two vertical post boxes plus a + // horizontal lintel box across the top. Posts run along + // the Z axis (so width spans Z), opening faces +X. The + // gap between the posts is the actual doorway. Useful + // for entrances, gates, magical portals, ruins. + // + // The 24th procedural mesh primitive. + std::string womBase = argv[++i]; + float width = 2.5f; // outer-to-outer along Z + float height = 4.0f; // total Y + float postThick = 0.4f; // post width in X and Z + float lintelH = 0.5f; // top lintel height (Y) + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { width = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { postThick = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { lintelH = std::stof(argv[++i]); } catch (...) {} + } + if (width <= 0 || height <= 0 || postThick <= 0 || + lintelH < 0 || postThick * 2 >= width || + lintelH > height) { + std::fprintf(stderr, + "gen-mesh-portal: posts must fit inside width; lintel <= height\n"); + return 1; + } + 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; + // Box helper — same pattern as other multi-box meshes. + auto addBox = [&](float cx, float cy, float cz, + float hx, float hy, float hz) { + struct Face { glm::vec3 n, du, dv; }; + Face faces[6] = { + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, + {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, + {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, + {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, + {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, + {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, + }; + glm::vec3 c(cx, cy, cz); + glm::vec3 ext(hx, hy, hz); + for (const Face& f : faces) { + glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); + glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); + glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); + uint32_t base = static_cast(wom.vertices.size()); + auto push = [&](glm::vec3 p, float u, float v) { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; + vtx.normal = f.n; + vtx.texCoord = {u, v}; + wom.vertices.push_back(vtx); + }; + push(center - du - dv, 0, 0); + push(center + du - dv, 1, 0); + push(center + du + dv, 1, 1); + push(center - du + dv, 0, 1); + wom.indices.insert(wom.indices.end(), + {base, base + 1, base + 2, base, base + 2, base + 3}); + } + }; + // Two posts at z = ±(width/2 - postThick/2). Each + // post extends from y=0 to y=height-lintelH so it + // tucks under the lintel. + float postY = (height - lintelH) * 0.5f; + float postHy = (height - lintelH) * 0.5f; + float postZ = (width - postThick) * 0.5f; + float postHt = postThick * 0.5f; + addBox(0, postY, postZ, postHt, postHy, postHt); + addBox(0, postY, -postZ, postHt, postHy, postHt); + // Lintel: spans full width across the top, same X + // thickness as posts. + if (lintelH > 0.0f) { + float lintelY = height - lintelH * 0.5f; + addBox(0, lintelY, 0, + postHt, lintelH * 0.5f, width * 0.5f); + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + wom.boundMin = glm::vec3(-postHt, 0, -width * 0.5f); + wom.boundMax = glm::vec3( postHt, height, width * 0.5f); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-portal: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" width : %.3f\n", width); + std::printf(" height : %.3f\n", height); + std::printf(" post thick : %.3f\n", postThick); + std::printf(" lintel H : %.3f%s\n", lintelH, + lintelH > 0 ? "" : " (no lintel)"); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleArchway(int& i, int argc, char** argv) { + // Semicircular arched doorway. Two cylindrical pillars + // hold up a curved keystone vault: the vault is a series + // of N angular wedge segments tracing a half-circle from + // pillar-top to pillar-top. The opening is the empty + // semicircular space below. + // + // The 25th procedural mesh primitive — the "fancier" + // sibling of --gen-mesh-portal which uses a flat lintel. + std::string womBase = argv[++i]; + float width = 3.0f; // outer-to-outer pillar centers along Z + float pillarH = 3.0f; // pillar height (Y) + float thickness = 0.4f; // pillar radius and arch radial thickness + int archSegs = 12; // segments around the half-circle + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { width = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { pillarH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { thickness = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { archSegs = std::stoi(argv[++i]); } catch (...) {} + } + if (width <= 0 || pillarH <= 0 || thickness <= 0 || + archSegs < 4 || archSegs > 64 || + thickness * 4 >= width) { + std::fprintf(stderr, + "gen-mesh-archway: thickness×4 < width, archSegs 4..64\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + const int pillarSegs = 16; + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Cylindrical pillar at given (cx, cz), from y=0 to y=pillarH. + auto pillar = [&](float cx, float cz) { + float r = thickness; + uint32_t bot = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= pillarSegs; ++sg) { + float u = static_cast(sg) / pillarSegs; + float ang = u * 2.0f * pi; + glm::vec3 p(cx + r * std::cos(ang), 0, + cz + r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 0}); + } + uint32_t top = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= pillarSegs; ++sg) { + float u = static_cast(sg) / pillarSegs; + float ang = u * 2.0f * pi; + glm::vec3 p(cx + r * std::cos(ang), pillarH, + cz + r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, 1}); + } + for (int sg = 0; sg < pillarSegs; ++sg) { + wom.indices.insert(wom.indices.end(), { + bot + sg, top + sg, bot + sg + 1, + bot + sg + 1, top + sg, top + sg + 1 + }); + } + // Caps + uint32_t bc = addV({cx, 0, cz}, {0, -1, 0}, {0.5f, 0.5f}); + uint32_t tc = addV({cx, pillarH, cz}, {0, 1, 0}, {0.5f, 0.5f}); + for (int sg = 0; sg < pillarSegs; ++sg) { + wom.indices.insert(wom.indices.end(), + {bc, bot + sg + 1, bot + sg}); + wom.indices.insert(wom.indices.end(), + {tc, top + sg, top + sg + 1}); + } + }; + float pillarZ = (width - 2 * thickness) * 0.5f; + pillar(0, pillarZ); + pillar(0, -pillarZ); + // Arch vault: trace half-circle from (z = +pillarZ, y = pillarH) + // up over to (z = -pillarZ, y = pillarH). Center of arch: + // (z = 0, y = pillarH). Arch radius = pillarZ. + // Inner arch (radius pillarZ - thickness*0.5) and outer + // (radius pillarZ + thickness*0.5) — the vault sits between. + float archCY = pillarH; + float arcInner = pillarZ - thickness * 0.5f; + float arcOuter = pillarZ + thickness * 0.5f; + // Each segment: 4 verts (inner-near, outer-near, inner-far, + // outer-far) extruded along X by thickness so the vault + // has front and back faces. + float archX = thickness * 0.5f; // half-depth in X + // Build vertex rings for inner and outer surfaces at + // each segment boundary, then connect. + // Top half-circle goes from theta=0 to theta=pi. + std::vector innerRing; + std::vector outerRing; + for (int s = 0; s <= archSegs; ++s) { + float t = static_cast(s) / archSegs; + float theta = t * pi; // 0..pi + float zi = arcInner * std::cos(theta); + float yi = arcInner * std::sin(theta); + float zo = arcOuter * std::cos(theta); + float yo = arcOuter * std::sin(theta); + innerRing.push_back({0, archCY + yi, zi}); + outerRing.push_back({0, archCY + yo, zo}); + } + // For each segment, add 8 vertices (4 corners × front/back face) + // and stitch them into 6 quads = 12 tris each. + for (int s = 0; s < archSegs; ++s) { + glm::vec3 i0 = innerRing[s]; + glm::vec3 i1 = innerRing[s + 1]; + glm::vec3 o0 = outerRing[s]; + glm::vec3 o1 = outerRing[s + 1]; + // Estimate outward (radial) normal as midpoint of o0+o1 + // direction from center. + glm::vec3 outDir = glm::normalize(glm::vec3(0, + (i0.y + i1.y + o0.y + o1.y) * 0.25f - archCY, + (i0.z + i1.z + o0.z + o1.z) * 0.25f)); + glm::vec3 frontN(1, 0, 0); + glm::vec3 backN(-1, 0, 0); + auto V = [&](glm::vec3 p, glm::vec3 n) { + return addV(p, n, {0, 0}); + }; + // Outer surface (top of arch): faces outward radially + uint32_t a = V({-archX, o0.y, o0.z}, outDir); + uint32_t b = V({ archX, o0.y, o0.z}, outDir); + uint32_t c = V({ archX, o1.y, o1.z}, outDir); + uint32_t d = V({-archX, o1.y, o1.z}, outDir); + wom.indices.insert(wom.indices.end(), {a, b, c, a, c, d}); + // Inner surface (underside of arch): faces inward + uint32_t e = V({-archX, i0.y, i0.z}, -outDir); + uint32_t f = V({ archX, i0.y, i0.z}, -outDir); + uint32_t g = V({ archX, i1.y, i1.z}, -outDir); + uint32_t h = V({-archX, i1.y, i1.z}, -outDir); + wom.indices.insert(wom.indices.end(), {e, g, f, e, h, g}); + // Front face (+X) of this wedge + uint32_t fi0 = V({ archX, i0.y, i0.z}, frontN); + uint32_t fo0 = V({ archX, o0.y, o0.z}, frontN); + uint32_t fo1 = V({ archX, o1.y, o1.z}, frontN); + uint32_t fi1 = V({ archX, i1.y, i1.z}, frontN); + wom.indices.insert(wom.indices.end(), + {fi0, fo0, fo1, fi0, fo1, fi1}); + // Back face (-X) + uint32_t bi0 = V({-archX, i0.y, i0.z}, backN); + uint32_t bo0 = V({-archX, o0.y, o0.z}, backN); + uint32_t bo1 = V({-archX, o1.y, o1.z}, backN); + uint32_t bi1 = V({-archX, i1.y, i1.z}, backN); + wom.indices.insert(wom.indices.end(), + {bi0, bo1, bo0, bi0, bi1, bo1}); + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = pillarH + arcOuter; + wom.boundMin = glm::vec3(-thickness, 0, -width * 0.5f); + wom.boundMax = glm::vec3( thickness, maxY, width * 0.5f); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-archway: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" width : %.3f\n", width); + std::printf(" pillar H : %.3f\n", pillarH); + std::printf(" thickness : %.3f\n", thickness); + std::printf(" arch segs : %d (radius %.3f)\n", archSegs, arcOuter); + std::printf(" apex Y : %.3f\n", maxY); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleBarrel(int& i, int argc, char** argv) { + // Tapered barrel: cylindrical body whose radius bulges + // smoothly from `topRadius` at the rims to `midRadius` + // at the middle (the classic stave-cooper barrel + // silhouette), plus 2 raised hoop bands at 25% and 75% + // of the height. The 26th procedural mesh primitive. + std::string womBase = argv[++i]; + float topR = 0.4f; // radius at top and bottom rim + float midR = 0.5f; // radius at the middle bulge + float height = 1.0f; + float hoopThick = 0.06f; // hoop band radial protrusion + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { topR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { midR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { hoopThick = std::stof(argv[++i]); } catch (...) {} + } + if (topR <= 0 || midR <= 0 || height <= 0 || + hoopThick < 0 || hoopThick > 0.5f) { + std::fprintf(stderr, + "gen-mesh-barrel: radii/height > 0, hoopThick 0..0.5\n"); + return 1; + } + 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; + const float pi = 3.14159265358979f; + const int segs = 16; // angular subdivisions + const int rings = 12; // vertical slices + auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = n; vtx.texCoord = uv; + wom.vertices.push_back(vtx); + return static_cast(wom.vertices.size() - 1); + }; + // Radius profile: smooth cosine bulge from rim to mid. + // r(t) = topR + (midR - topR) * sin(pi*t) where t in 0..1 + // gives 0 at t=0/1 and 1 at t=0.5 — exact rim fit. + auto radiusAt = [&](float t) -> float { + return topR + (midR - topR) * std::sin(pi * t); + }; + uint32_t firstRing = static_cast(wom.vertices.size()); + for (int ri = 0; ri <= rings; ++ri) { + float t = static_cast(ri) / rings; + float y = t * height; + float r = radiusAt(t); + // Hoops: bump radius outward in two narrow bands. + float hoop1 = std::abs(t - 0.25f); + float hoop2 = std::abs(t - 0.75f); + if (hoop1 < 0.04f) r += hoopThick * (1.0f - hoop1 / 0.04f); + if (hoop2 < 0.04f) r += hoopThick * (1.0f - hoop2 / 0.04f); + for (int sg = 0; sg <= segs; ++sg) { + float u = static_cast(sg) / segs; + float ang = u * 2.0f * pi; + glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); + glm::vec3 n(std::cos(ang), 0, std::sin(ang)); + addV(p, n, {u, t}); + } + } + int rowSize = segs + 1; + for (int ri = 0; ri < rings; ++ri) { + for (int sg = 0; sg < segs; ++sg) { + uint32_t i00 = firstRing + ri * rowSize + sg; + uint32_t i01 = firstRing + ri * rowSize + sg + 1; + uint32_t i10 = firstRing + (ri + 1) * rowSize + sg; + uint32_t i11 = firstRing + (ri + 1) * rowSize + sg + 1; + wom.indices.insert(wom.indices.end(), + {i00, i10, i01, i01, i10, i11}); + } + } + // End caps (top + bottom). topR is also the bottom-most + // and top-most ring radius since sin(0) = sin(pi) = 0. + uint32_t botCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); + uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); + uint32_t botRing = firstRing; + uint32_t topRing = firstRing + rings * rowSize; + for (int sg = 0; sg < segs; ++sg) { + wom.indices.insert(wom.indices.end(), + {botCenter, botRing + sg + 1, botRing + sg}); + wom.indices.insert(wom.indices.end(), + {topCenter, topRing + sg, topRing + sg + 1}); + } + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxR = midR + hoopThick; + wom.boundMin = glm::vec3(-maxR, 0, -maxR); + wom.boundMax = glm::vec3( maxR, height, maxR); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-barrel: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" rim R : %.3f\n", topR); + std::printf(" bulge R : %.3f\n", midR); + std::printf(" height : %.3f\n", height); + std::printf(" hoops : 2 (thickness %.3f)\n", hoopThick); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + +int handleChest(int& i, int argc, char** argv) { + // Treasure chest: rectangular body box + smaller lid + // box on top + 3 thin iron bands wrapping around the + // body + a small lock plate on the front center face. + // The 27th procedural mesh primitive — useful for + // dungeon loot, room decoration, quest objectives. + std::string womBase = argv[++i]; + float width = 1.4f; // along X + float depth = 0.9f; // along Z + float bodyH = 0.9f; // body box height + float lidH = 0.25f; // lid height above body + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { width = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { depth = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { bodyH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { lidH = std::stof(argv[++i]); } catch (...) {} + } + if (width <= 0 || depth <= 0 || bodyH <= 0 || lidH < 0) { + std::fprintf(stderr, + "gen-mesh-chest: width/depth/bodyH > 0, lidH >= 0\n"); + return 1; + } + 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; + // Box helper — adds 24 unique verts / 12 tris centered + // on (cx, cy, cz) with half-extents (hx, hy, hz). Each + // face gets unique normals for flat shading. + auto addBox = [&](float cx, float cy, float cz, + float hx, float hy, float hz) { + struct Face { glm::vec3 n, du, dv; }; + Face faces[6] = { + {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, + {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, + {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, + {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, + {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, + {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, + }; + glm::vec3 c(cx, cy, cz); + glm::vec3 ext(hx, hy, hz); + for (const Face& f : faces) { + glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); + glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); + glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); + uint32_t base = static_cast(wom.vertices.size()); + auto push = [&](glm::vec3 p, float u, float v) { + wowee::pipeline::WoweeModel::Vertex vtx; + vtx.position = p; vtx.normal = f.n; vtx.texCoord = {u, v}; + wom.vertices.push_back(vtx); + }; + push(center - du - dv, 0, 0); + push(center + du - dv, 1, 0); + push(center + du + dv, 1, 1); + push(center - du + dv, 0, 1); + wom.indices.insert(wom.indices.end(), + {base, base + 1, base + 2, base, base + 2, base + 3}); + } + }; + float hx = width * 0.5f; + float hz = depth * 0.5f; + // Body: y=0 to y=bodyH + addBox(0, bodyH * 0.5f, 0, hx, bodyH * 0.5f, hz); + // Lid: smaller box on top, slightly inset on each side + float lidInset = std::min(width, depth) * 0.04f; + float lidHx = hx - lidInset; + float lidHz = hz - lidInset; + if (lidH > 0.0f && lidHx > 0 && lidHz > 0) { + addBox(0, bodyH + lidH * 0.5f, 0, + lidHx, lidH * 0.5f, lidHz); + } + // 3 iron bands wrapping the body — thin slabs + // protruding ~3% radially on the sides + top. + // Band positions: 15%, 50%, 85% of body width. + float bandThickX = width * 0.04f; // band depth along X + float bandHy = bodyH * 0.5f + 0.005f; + float bandHz = hz + 0.012f; + float bandPositions[3] = {-hx * 0.7f, 0.0f, hx * 0.7f}; + for (float bx : bandPositions) { + addBox(bx, bandHy, 0, + bandThickX * 0.5f, bandHy, bandHz); + } + // Lock plate: small thin box on the front face, centered. + // Front face is +Z. Plate sits at z = hz + tiny epsilon. + float lockW = width * 0.10f; + float lockH = bodyH * 0.18f; + float lockY = bodyH * 0.65f; + float lockEps = 0.008f; + addBox(0, lockY, hz + lockEps, + lockW * 0.5f, lockH * 0.5f, lockEps); + wowee::pipeline::WoweeModel::Batch batch; + batch.indexStart = 0; + batch.indexCount = static_cast(wom.indices.size()); + batch.textureIndex = 0; + wom.batches.push_back(batch); + float maxY = bodyH + lidH; + wom.boundMin = glm::vec3(-hx, 0, -hz - 0.012f); + wom.boundMax = glm::vec3( hx, maxY, hz + 0.012f); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-chest: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" width × depth : %.3f × %.3f\n", width, depth); + std::printf(" body H : %.3f\n", bodyH); + std::printf(" lid H : %.3f\n", lidH); + std::printf(" components : body + lid + 3 bands + lock\n"); + std::printf(" vertices : %zu\n", wom.vertices.size()); + std::printf(" triangles : %zu\n", wom.indices.size() / 3); + return 0; +} + + +} // namespace + +bool handleGenMesh(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-mesh-rock") == 0 && i + 1 < argc) { + outRc = handleRock(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-pillar") == 0 && i + 1 < argc) { + outRc = handlePillar(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-bridge") == 0 && i + 1 < argc) { + outRc = handleBridge(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-tower") == 0 && i + 1 < argc) { + outRc = handleTower(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-house") == 0 && i + 1 < argc) { + outRc = handleHouse(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-fountain") == 0 && i + 1 < argc) { + outRc = handleFountain(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-statue") == 0 && i + 1 < argc) { + outRc = handleStatue(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-altar") == 0 && i + 1 < argc) { + outRc = handleAltar(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-portal") == 0 && i + 1 < argc) { + outRc = handlePortal(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-archway") == 0 && i + 1 < argc) { + outRc = handleArchway(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-barrel") == 0 && i + 1 < argc) { + outRc = handleBarrel(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mesh-chest") == 0 && i + 1 < argc) { + outRc = handleChest(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_gen_mesh.hpp b/tools/editor/cli_gen_mesh.hpp new file mode 100644 index 00000000..99c5b0f7 --- /dev/null +++ b/tools/editor/cli_gen_mesh.hpp @@ -0,0 +1,26 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the procedural composite-prop mesh generators that +// have been moved out of main.cpp: +// --gen-mesh-rock --gen-mesh-pillar +// --gen-mesh-bridge --gen-mesh-tower +// --gen-mesh-house --gen-mesh-fountain +// --gen-mesh-statue --gen-mesh-altar +// --gen-mesh-portal --gen-mesh-archway +// --gen-mesh-barrel --gen-mesh-chest +// +// Other mesh handlers (the `--gen-mesh ` dispatcher, +// gen-mesh-fence/-tree/-grid/-stairs/-disc/-tube/-capsule/-arch/ +// -pyramid/-from-heightmap/-textured) still live in main.cpp +// and may be migrated in subsequent batches. +// +// Returns true if matched; outRc holds the exit code. +bool handleGenMesh(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 4eb2c6a2..f1f8b4a5 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -7,6 +7,7 @@ #include "cli_project_inventory.hpp" #include "cli_help.hpp" #include "cli_gen_texture.hpp" +#include "cli_gen_mesh.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -800,6 +801,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleGenTexture(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleGenMesh(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -18235,1599 +18239,6 @@ int main(int argc, char* argv[]) { std::printf(" vertices : %zu\n", wom.vertices.size()); std::printf(" triangles : %zu\n", wom.indices.size() / 3); return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-rock") == 0 && i + 1 < argc) { - // Procedural boulder. Starts as an octahedron, subdivides - // each face N times to get a rounded base, then displaces - // each vertex along its outward direction by a smooth - // sin/cos noise term controlled by `seed` and `roughness`. - // Result is a unique-shaped rock per seed — perfect for - // scattering across a zone via random-populate-zone. - // - // The 16th procedural primitive in the WOM library. - std::string womBase = argv[++i]; - float radius = 1.0f; - float roughness = 0.25f; // 0..1, fraction of radius - int subdiv = 2; // 0=8 tris, 1=32, 2=128, 3=512 - uint32_t seed = 1; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { radius = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { roughness = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { subdiv = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} - } - if (radius <= 0 || roughness < 0 || roughness > 1 || - subdiv < 0 || subdiv > 4) { - std::fprintf(stderr, - "gen-mesh-rock: radius>0, roughness 0..1, subdiv 0..4\n"); - return 1; - } - if (womBase.size() >= 4 && - womBase.substr(womBase.size() - 4) == ".wom") { - womBase = womBase.substr(0, womBase.size() - 4); - } - // Build sphere via octahedron subdivision. Vertices are - // accumulated in unit-length form first, then displaced. - std::vector sv; // sphere verts (unit) - std::vector st; // sphere tris (vertex indices) - sv = { - { 1, 0, 0}, {-1, 0, 0}, - { 0, 1, 0}, { 0,-1, 0}, - { 0, 0, 1}, { 0, 0,-1}, - }; - st = { - {0, 2, 4}, {2, 1, 4}, {1, 3, 4}, {3, 0, 4}, - {2, 0, 5}, {1, 2, 5}, {3, 1, 5}, {0, 3, 5}, - }; - // Edge-midpoint cache so shared edges don't duplicate verts. - for (int s = 0; s < subdiv; ++s) { - std::map, uint32_t> midCache; - auto midpoint = [&](uint32_t a, uint32_t b) -> uint32_t { - auto key = std::make_pair(std::min(a,b), std::max(a,b)); - auto it = midCache.find(key); - if (it != midCache.end()) return it->second; - glm::vec3 m = glm::normalize((sv[a] + sv[b]) * 0.5f); - uint32_t idx = static_cast(sv.size()); - sv.push_back(m); - midCache[key] = idx; - return idx; - }; - std::vector next; - next.reserve(st.size() * 4); - for (auto& tri : st) { - uint32_t a = tri.x, b = tri.y, c = tri.z; - uint32_t ab = midpoint(a, b); - uint32_t bc = midpoint(b, c); - uint32_t ca = midpoint(c, a); - next.push_back({a, ab, ca}); - next.push_back({b, bc, ab}); - next.push_back({c, ca, bc}); - next.push_back({ab, bc, ca}); - } - st.swap(next); - } - // Smooth pseudo-noise displacement. Three orthogonal sin - // products give a coherent bumpy surface; phase shift uses - // the seed so each value yields a distinct silhouette. - float sf = static_cast(seed); - auto displace = [&](glm::vec3 p) -> float { - float n = std::sin(p.x * 3.1f + sf * 0.91f) * - std::sin(p.y * 4.7f + sf * 1.37f) * - std::sin(p.z * 5.3f + sf * 0.43f); - float n2 = std::sin(p.x * 7.1f + sf * 0.11f) * - std::sin(p.y * 8.3f + sf * 2.13f) * - std::sin(p.z * 9.7f + sf * 1.91f); - return 1.0f + roughness * (0.7f * n + 0.3f * n2); - }; - wowee::pipeline::WoweeModel wom; - wom.name = std::filesystem::path(womBase).stem().string(); - wom.version = 3; - std::vector finalPos(sv.size()); - for (size_t v = 0; v < sv.size(); ++v) { - finalPos[v] = sv[v] * (radius * displace(sv[v])); - } - // Per-vertex normals from triangle face normals (averaged). - std::vector normals(sv.size(), glm::vec3(0)); - for (auto& tri : st) { - glm::vec3 a = finalPos[tri.x]; - glm::vec3 b = finalPos[tri.y]; - glm::vec3 c = finalPos[tri.z]; - glm::vec3 fn = glm::normalize(glm::cross(b - a, c - a)); - normals[tri.x] += fn; - normals[tri.y] += fn; - normals[tri.z] += fn; - } - for (auto& n : normals) n = glm::length(n) > 1e-6f - ? glm::normalize(n) : glm::vec3(0, 1, 0); - for (size_t v = 0; v < sv.size(); ++v) { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = finalPos[v]; - vtx.normal = normals[v]; - // Spherical UV unwrap. Visible seam at u=0/1 is - // acceptable for rocks — usually hidden by terrain. - glm::vec3 d = glm::normalize(sv[v]); - vtx.texCoord = { - 0.5f + std::atan2(d.z, d.x) / (2.0f * 3.14159265f), - 0.5f - std::asin(d.y) / 3.14159265f, - }; - wom.vertices.push_back(vtx); - } - for (auto& tri : st) { - wom.indices.push_back(tri.x); - wom.indices.push_back(tri.y); - wom.indices.push_back(tri.z); - } - float bound = radius * (1.0f + roughness); - wom.boundMin = glm::vec3(-bound); - wom.boundMax = glm::vec3( bound); - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-rock: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" radius : %.3f\n", radius); - std::printf(" roughness : %.3f\n", roughness); - std::printf(" subdiv : %d\n", subdiv); - std::printf(" seed : %u\n", seed); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-pillar") == 0 && i + 1 < argc) { - // Procedural classical column. Central shaft is a - // cylinder with N concave flutes (radius modulated by - // cos²(theta*flutes/2)), capped above and below by - // wider disc caps that act as a simple capital and - // base. The 17th procedural mesh primitive — useful - // for ruins, temples, dungeons, plaza decoration. - std::string womBase = argv[++i]; - float radius = 0.4f; - float height = 4.0f; - int flutes = 12; - float capScale = 1.25f; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { radius = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { height = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { flutes = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { capScale = std::stof(argv[++i]); } catch (...) {} - } - if (radius <= 0 || height <= 0 || - flutes < 4 || flutes > 64 || - capScale < 1.0f || capScale > 4.0f) { - std::fprintf(stderr, - "gen-mesh-pillar: radius>0, height>0, flutes 4..64, capScale 1..4\n"); - return 1; - } - if (womBase.size() >= 4 && - womBase.substr(womBase.size() - 4) == ".wom") { - womBase = womBase.substr(0, womBase.size() - 4); - } - const float pi = 3.14159265358979f; - // We use 8 segments per flute so the cosine-modulated - // groove resolves smoothly. Vertical: 2 rings (top/bot - // of shaft) + cap/base discs. - const int radSegs = flutes * 8; - const float fluteDepth = radius * 0.12f; - float capR = radius * capScale; - float capThick = radius * 0.25f; - wowee::pipeline::WoweeModel wom; - wom.name = std::filesystem::path(womBase).stem().string(); - wom.version = 3; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Shaft side ring at given y. radius modulated by flute count. - auto buildShaftRing = [&](float y) -> uint32_t { - uint32_t start = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= radSegs; ++sg) { - float u = static_cast(sg) / radSegs; - float ang = u * 2.0f * pi; - float c = std::cos(ang * flutes * 0.5f); - float r = radius - fluteDepth * (c * c); - glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, glm::normalize(n), glm::vec2(u, y / height)); - } - return start; - }; - // Cap/base disc ring (constant radius capR) at given y. - auto buildCapRing = [&](float y, float r) -> uint32_t { - uint32_t start = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= radSegs; ++sg) { - float u = static_cast(sg) / radSegs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, glm::normalize(n), glm::vec2(u, y / height)); - } - return start; - }; - // Layout (Y goes up): - // capThick: base disc bottom - // capThick: base disc top - // ...shaft from capThick to height-capThick... - // height-capThick: cap disc bottom - // height: cap disc top - float shaftY0 = capThick; - float shaftY1 = height - capThick; - uint32_t baseBot = buildCapRing(0.0f, capR); - uint32_t baseTop = buildCapRing(shaftY0, capR); - uint32_t shaftBot = buildShaftRing(shaftY0); - uint32_t shaftTop = buildShaftRing(shaftY1); - uint32_t capBot = buildCapRing(shaftY1, capR); - uint32_t capTop = buildCapRing(height, capR); - // Quad connector helper. - auto connect = [&](uint32_t a0, uint32_t a1) { - for (int sg = 0; sg < radSegs; ++sg) { - uint32_t i00 = a0 + sg; - uint32_t i01 = a0 + sg + 1; - uint32_t i10 = a1 + sg; - uint32_t i11 = a1 + sg + 1; - wom.indices.insert(wom.indices.end(), - { i00, i10, i01, i01, i10, i11 }); - } - }; - connect(baseBot, baseTop); // base side - connect(shaftBot, shaftTop); // shaft - connect(capBot, capTop); // cap side - // Bottom cap (downward fan), top cap (upward fan). - uint32_t bottomCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); - uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < radSegs; ++sg) { - wom.indices.insert(wom.indices.end(), - { bottomCenter, baseBot + sg + 1, baseBot + sg }); - wom.indices.insert(wom.indices.end(), - { topCenter, capTop + sg, capTop + sg + 1 }); - } - // Annular surfaces where caps meet shaft (top of base disc - // out to shaft, etc.). Just connect the two rings — they - // sit at the same Y so this looks like a flat ring. - connect(baseTop, shaftBot); - connect(shaftTop, capBot); - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - wom.boundMin = glm::vec3(-capR, 0, -capR); - wom.boundMax = glm::vec3( capR, height, capR); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-pillar: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" radius : %.3f\n", radius); - std::printf(" height : %.3f\n", height); - std::printf(" flutes : %d\n", flutes); - std::printf(" cap scale : %.2fx (capR=%.3f)\n", capScale, capR); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-bridge") == 0 && i + 1 < argc) { - // Procedural plank bridge. Deck is N axis-aligned planks - // running across the bridge's width with small gaps - // between, plus two side rails (top + bottom rails on - // posts). Bridge length runs along +X, width is on Z. - // The 18th procedural mesh primitive — useful for - // river crossings, dungeon catwalks, scenic overlooks. - std::string womBase = argv[++i]; - float length = 6.0f; // along X - float width = 2.0f; // along Z - int planks = 6; // plank count across the length - float railHeight = 1.0f; // rail height above deck (0 = no rails) - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { length = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { width = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { planks = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { railHeight = std::stof(argv[++i]); } catch (...) {} - } - if (length <= 0 || width <= 0 || - planks < 1 || planks > 64 || - railHeight < 0 || railHeight > 4.0f) { - std::fprintf(stderr, - "gen-mesh-bridge: length>0, width>0, planks 1..64, rail 0..4\n"); - return 1; - } - 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; - // Box helper — builds 24-vert / 12-tri box centered on - // (cx, cy, cz) with half-extents (hx, hy, hz). Each face - // gets unique vertices so flat-shading works. Indices are - // pushed into wom.indices directly. - auto addBox = [&](float cx, float cy, float cz, - float hx, float hy, float hz) { - glm::vec3 c(cx, cy, cz); - struct Face { - glm::vec3 n; - glm::vec3 du, dv; // unit-length axes spanning the face - }; - Face faces[6] = { - {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, // top (+Y) - {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, // bottom (-Y) - {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, // right (+X) - {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, // left (-X) - {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, // front (+Z) - {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, // back (-Z) - }; - glm::vec3 ext(hx, hy, hz); - for (const Face& f : faces) { - glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); - glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); - glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); - uint32_t base = static_cast(wom.vertices.size()); - auto push = [&](glm::vec3 p, float u, float v) { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; - vtx.normal = f.n; - vtx.texCoord = {u, v}; - wom.vertices.push_back(vtx); - }; - push(center - du - dv, 0, 0); - push(center + du - dv, 1, 0); - push(center + du + dv, 1, 1); - push(center - du + dv, 0, 1); - wom.indices.insert(wom.indices.end(), - { base, base + 1, base + 2, base, base + 2, base + 3 }); - } - }; - // Deck: planks along X, gap = 5% of plank pitch. - float plankThickness = 0.08f; - float plankPitch = length / planks; - float plankWidth = plankPitch * 0.95f; - for (int p = 0; p < planks; ++p) { - float cx = -length * 0.5f + plankPitch * (p + 0.5f); - addBox(cx, plankThickness * 0.5f, 0, - plankWidth * 0.5f, plankThickness * 0.5f, width * 0.5f); - } - // Rails: 2 sides × (top rail + 3 posts) when railHeight > 0 - if (railHeight > 0.0f) { - float postR = 0.06f; - float topRailR = 0.08f; - int postCount = 3; - float rzOffset = width * 0.5f - postR; - for (int side = 0; side < 2; ++side) { - float zSign = (side == 0) ? 1.0f : -1.0f; - float z = zSign * rzOffset; - // Top rail: long thin box spanning length - addBox(0, plankThickness + railHeight, z, - length * 0.5f, topRailR, topRailR); - // Posts evenly spaced - for (int p = 0; p < postCount; ++p) { - float t = (postCount > 1) - ? static_cast(p) / (postCount - 1) - : 0.5f; - float cx = -length * 0.5f + length * t; - if (p == 0) cx += postR; - if (p == postCount - 1) cx -= postR; - addBox(cx, plankThickness + railHeight * 0.5f, z, - postR, railHeight * 0.5f, postR); - } - } - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = plankThickness + railHeight; - wom.boundMin = glm::vec3(-length * 0.5f, 0, -width * 0.5f); - wom.boundMax = glm::vec3( length * 0.5f, maxY, width * 0.5f); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-bridge: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" length : %.3f\n", length); - std::printf(" width : %.3f\n", width); - std::printf(" planks : %d\n", planks); - std::printf(" rail H : %.3f%s\n", railHeight, - railHeight > 0 ? "" : " (no rails)"); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-tower") == 0 && i + 1 < argc) { - // Procedural castle tower. Solid cylindrical shaft with - // crenellated battlements ringing the top: alternating - // raised "merlons" and gaps. Each merlon is a thin - // angular wedge sitting on the top rim. Useful for - // keeps, watchtowers, perimeter walls. - // - // The 19th procedural mesh primitive. - std::string womBase = argv[++i]; - float radius = 1.5f; - float height = 8.0f; - int battlements = 8; // merlons around the rim - float battlementH = 0.5f; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { radius = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { height = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { battlements = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { battlementH = std::stof(argv[++i]); } catch (...) {} - } - if (radius <= 0 || height <= 0 || - battlements < 4 || battlements > 64 || - battlementH < 0 || battlementH > 4.0f) { - std::fprintf(stderr, - "gen-mesh-tower: radius>0, height>0, battlements 4..64, bH 0..4\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - const int radSegs = std::max(24, battlements * 4); - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Cylinder shaft: side ring at y=0 and y=height. - uint32_t botRing = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= radSegs; ++sg) { - float u = static_cast(sg) / radSegs; - float ang = u * 2.0f * pi; - glm::vec3 p(radius * std::cos(ang), 0, radius * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, glm::vec2(u, 0)); - } - uint32_t topRing = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= radSegs; ++sg) { - float u = static_cast(sg) / radSegs; - float ang = u * 2.0f * pi; - glm::vec3 p(radius * std::cos(ang), height, radius * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, glm::vec2(u, 1)); - } - for (int sg = 0; sg < radSegs; ++sg) { - wom.indices.insert(wom.indices.end(), { - botRing + sg, topRing + sg, botRing + sg + 1, - botRing + sg + 1, topRing + sg, topRing + sg + 1 - }); - } - // Top cap (fan toward upward-facing center). - uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < radSegs; ++sg) { - wom.indices.insert(wom.indices.end(), - { topCenter, topRing + sg, topRing + sg + 1 }); - } - // Bottom cap (fan toward downward-facing center). - uint32_t botCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < radSegs; ++sg) { - wom.indices.insert(wom.indices.end(), - { botCenter, botRing + sg + 1, botRing + sg }); - } - // Battlements: thin curved blocks around the top rim, - // half the slots filled (alternating merlon/gap). - // Each merlon is approximated by an extruded arc segment - // at the wall radius extending outward slightly. - if (battlementH > 0.0f) { - int merlonSpan = radSegs / battlements; - int merlonHalf = std::max(1, merlonSpan / 2); - float outerR = radius * 1.05f; - float innerR = radius * 0.95f; - for (int b = 0; b < battlements; ++b) { - int startSeg = b * merlonSpan; - // Build 8-vert box-like segment between angles - // covering merlonHalf slots (so half the rim is - // filled, forming the merlon/gap pattern). - float ang0 = 2.0f * pi * static_cast(startSeg) / radSegs; - float ang1 = 2.0f * pi * static_cast(startSeg + merlonHalf) / radSegs; - glm::vec3 outer0(outerR * std::cos(ang0), 0, outerR * std::sin(ang0)); - glm::vec3 outer1(outerR * std::cos(ang1), 0, outerR * std::sin(ang1)); - glm::vec3 inner0(innerR * std::cos(ang0), 0, innerR * std::sin(ang0)); - glm::vec3 inner1(innerR * std::cos(ang1), 0, innerR * std::sin(ang1)); - glm::vec3 yLow(0, height, 0); - glm::vec3 yHigh(0, height + battlementH, 0); - glm::vec3 norm = glm::normalize( - outer0 + outer1 - inner0 - inner1); - auto V = [&](glm::vec3 p, glm::vec3 n) { - return addV(p, n, {0, 0}); - }; - // 8 verts: 4 corners × 2 heights - uint32_t bbl = V(outer0 + yLow, norm); // bot outer left - uint32_t bbr = V(outer1 + yLow, norm); - uint32_t btl = V(outer0 + yHigh, norm); // top outer left - uint32_t btr = V(outer1 + yHigh, norm); - uint32_t ibl = V(inner0 + yLow, -norm); // bot inner left - uint32_t ibr = V(inner1 + yLow, -norm); - uint32_t itl = V(inner0 + yHigh, -norm); // top inner left - uint32_t itr = V(inner1 + yHigh, -norm); - // outer face - wom.indices.insert(wom.indices.end(), {bbl, btl, bbr, bbr, btl, btr}); - // inner face - wom.indices.insert(wom.indices.end(), {ibr, itr, ibl, ibl, itr, itl}); - // top face - wom.indices.insert(wom.indices.end(), {btl, itl, btr, btr, itl, itr}); - // left and right end caps - wom.indices.insert(wom.indices.end(), {bbl, ibl, btl, btl, ibl, itl}); - wom.indices.insert(wom.indices.end(), {bbr, btr, ibr, ibr, btr, itr}); - } - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = height + battlementH; - float maxR = radius * 1.05f; - wom.boundMin = glm::vec3(-maxR, 0, -maxR); - wom.boundMax = glm::vec3( maxR, maxY, maxR); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-tower: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" radius : %.3f\n", radius); - std::printf(" height : %.3f\n", height); - std::printf(" battlements : %d (%.3fm tall)\n", - battlements, battlementH); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-house") == 0 && i + 1 < argc) { - // Simple procedural house: cube body + pyramid roof - // meeting at a central apex above the body's roofline. - // The pyramid sits flush on the body so the eaves - // line up with the wall edges. No door cutout — that - // can be added later via mesh boolean ops or texture. - // - // The 20th procedural mesh primitive. - std::string womBase = argv[++i]; - float width = 4.0f; // along X - float depth = 4.0f; // along Z - float height = 3.0f; // wall height (Y) - float roofH = 2.0f; // pyramid above walls - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { width = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { depth = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { height = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { roofH = std::stof(argv[++i]); } catch (...) {} - } - if (width <= 0 || depth <= 0 || height <= 0 || - roofH < 0 || roofH > 20.0f) { - std::fprintf(stderr, - "gen-mesh-house: width/depth/height>0, roof 0..20\n"); - return 1; - } - 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; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - float hx = width * 0.5f; - float hz = depth * 0.5f; - // 4 walls — each a quad with an outward-facing normal so - // the house reads as solid even with backface culling on. - struct Wall { - glm::vec3 a, b, c, d; // CCW from outside - glm::vec3 n; - }; - Wall walls[4] = { - {{ hx, 0, hz}, {-hx, 0, hz}, {-hx, height, hz}, { hx, height, hz}, { 0, 0, 1}}, // +Z - {{-hx, 0, -hz}, { hx, 0, -hz}, { hx, height, -hz}, {-hx, height, -hz}, { 0, 0, -1}}, // -Z - {{ hx, 0, -hz}, { hx, 0, hz}, { hx, height, hz}, { hx, height, -hz}, { 1, 0, 0}}, // +X - {{-hx, 0, hz}, {-hx, 0, -hz}, {-hx, height, -hz}, {-hx, height, hz}, {-1, 0, 0}}, // -X - }; - for (const Wall& w : walls) { - uint32_t a = addV(w.a, w.n, {0, 0}); - uint32_t b = addV(w.b, w.n, {1, 0}); - uint32_t c = addV(w.c, w.n, {1, 1}); - uint32_t d = addV(w.d, w.n, {0, 1}); - wom.indices.insert(wom.indices.end(), {a, b, c, a, c, d}); - } - // Floor (single quad, normal-down so it shows from below; - // texturable as a foundation slab). - { - uint32_t a = addV({-hx, 0, -hz}, {0, -1, 0}, {0, 0}); - uint32_t b = addV({ hx, 0, -hz}, {0, -1, 0}, {1, 0}); - uint32_t c = addV({ hx, 0, hz}, {0, -1, 0}, {1, 1}); - uint32_t d = addV({-hx, 0, hz}, {0, -1, 0}, {0, 1}); - wom.indices.insert(wom.indices.end(), {a, c, b, a, d, c}); - } - // Roof: 4 triangles meeting at central apex. - float apexY = height + roofH; - glm::vec3 apex(0, apexY, 0); - // Eave corners (Y = wall height) — each triangle shares - // two adjacent corners + the apex. Per-face normal is - // computed once so flat shading works. - glm::vec3 eaves[4] = { - {-hx, height, hz}, - { hx, height, hz}, - { hx, height, -hz}, - {-hx, height, -hz}, - }; - for (int s = 0; s < 4; ++s) { - glm::vec3 e0 = eaves[s]; - glm::vec3 e1 = eaves[(s + 1) % 4]; - glm::vec3 fn = glm::normalize(glm::cross(e1 - e0, apex - e0)); - uint32_t a = addV(e0, fn, {0, 0}); - uint32_t b = addV(e1, fn, {1, 0}); - uint32_t c = addV(apex, fn, {0.5f, 1}); - wom.indices.insert(wom.indices.end(), {a, b, c}); - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - wom.boundMin = glm::vec3(-hx, 0, -hz); - wom.boundMax = glm::vec3( hx, apexY, hz); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-house: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" width : %.3f\n", width); - std::printf(" depth : %.3f\n", depth); - std::printf(" wall H : %.3f\n", height); - std::printf(" roof H : %.3f (apex %.3f)\n", roofH, apexY); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-fountain") == 0 && i + 1 < argc) { - // Procedural fountain: low cylindrical basin with a - // narrower spout column rising from its center. Solid - // basin (not hollow) for simplicity — readable as a - // fountain because of the spout silhouette. Useful for - // town squares, plazas, garden centerpieces. - // - // The 21st procedural mesh primitive. - std::string womBase = argv[++i]; - float basinR = 1.5f; - float basinH = 0.5f; - float spoutR = 0.2f; - float spoutH = 1.5f; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { basinR = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { basinH = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { spoutR = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { spoutH = std::stof(argv[++i]); } catch (...) {} - } - if (basinR <= 0 || basinH <= 0 || spoutR <= 0 || spoutH <= 0 || - spoutR >= basinR) { - std::fprintf(stderr, - "gen-mesh-fountain: all dims > 0; spoutR must be < basinR\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - const int segs = 24; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Cylinder helper: build side ring + caps from y0 to y1 - // at given radius. Returns when done; indices appended - // directly. Side ring is 2× (segs+1) verts at y0 then y1. - auto cylinder = [&](float r, float y0, float y1) { - uint32_t bot = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y0, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, glm::vec2(u, 0)); - } - uint32_t top = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y1, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, glm::vec2(u, 1)); - } - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), { - bot + sg, top + sg, bot + sg + 1, - bot + sg + 1, top + sg, top + sg + 1 - }); - } - // Top cap (faces +Y) - uint32_t topC = addV({0, y1, 0}, {0, 1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), - {topC, top + sg, top + sg + 1}); - } - // Bottom cap (faces -Y) - uint32_t botC = addV({0, y0, 0}, {0, -1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), - {botC, bot + sg + 1, bot + sg}); - } - }; - // Basin: cylinder from y=0 to y=basinH at basinR. - cylinder(basinR, 0.0f, basinH); - // Spout: cylinder from y=basinH to y=basinH+spoutH at spoutR. - cylinder(spoutR, basinH, basinH + spoutH); - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = basinH + spoutH; - wom.boundMin = glm::vec3(-basinR, 0, -basinR); - wom.boundMax = glm::vec3( basinR, maxY, basinR); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-fountain: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" basin : R=%.3f H=%.3f\n", basinR, basinH); - std::printf(" spout : R=%.3f H=%.3f\n", spoutR, spoutH); - std::printf(" total H : %.3f\n", maxY); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles: %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-statue") == 0 && i + 1 < argc) { - // Humanoid placeholder: square pedestal block + tall - // narrow body cylinder + head sphere. The silhouette - // reads as a statue without needing limbs. Useful for - // monuments, hero statues, plaza centerpieces, religious - // shrines. - // - // The 22nd procedural mesh primitive. - std::string womBase = argv[++i]; - float pedSize = 1.0f; // pedestal width and depth - float bodyH = 2.5f; // body cylinder height - float headR = 0.4f; // head sphere radius - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { pedSize = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { bodyH = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { headR = std::stof(argv[++i]); } catch (...) {} - } - if (pedSize <= 0 || bodyH <= 0 || headR <= 0) { - std::fprintf(stderr, - "gen-mesh-statue: all dims must be positive\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Pedestal: low square block (24 unique verts). - float pedH = pedSize * 0.4f; - float hp = pedSize * 0.5f; - { - struct Face { glm::vec3 n, du, dv; }; - Face faces[6] = { - {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, - {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, - {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, - {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, - {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, - {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, - }; - glm::vec3 c(0, pedH * 0.5f, 0); - glm::vec3 ext(hp, pedH * 0.5f, hp); - for (const Face& f : faces) { - glm::vec3 center = c + glm::vec3(f.n.x*ext.x, f.n.y*ext.y, f.n.z*ext.z); - glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); - glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); - uint32_t base = static_cast(wom.vertices.size()); - addV(center - du - dv, f.n, {0, 0}); - addV(center + du - dv, f.n, {1, 0}); - addV(center + du + dv, f.n, {1, 1}); - addV(center - du + dv, f.n, {0, 1}); - wom.indices.insert(wom.indices.end(), - {base, base + 1, base + 2, base, base + 2, base + 3}); - } - } - // Body cylinder from y=pedH to y=pedH+bodyH at radius pedSize*0.2 - float bodyR = pedSize * 0.2f; - float bodyY0 = pedH; - float bodyY1 = pedH + bodyH; - const int segs = 16; - uint32_t bodyBot = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(bodyR * std::cos(ang), bodyY0, bodyR * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 0}); - } - uint32_t bodyTop = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(bodyR * std::cos(ang), bodyY1, bodyR * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 1}); - } - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), { - bodyBot + sg, bodyTop + sg, bodyBot + sg + 1, - bodyBot + sg + 1, bodyTop + sg, bodyTop + sg + 1 - }); - } - // Head sphere centered above body. UV-sphere with 16 - // longitude × 12 latitude segments. - float headY = bodyY1 + headR; - const int headLon = 16; - const int headLat = 12; - uint32_t headStart = static_cast(wom.vertices.size()); - for (int la = 0; la <= headLat; ++la) { - float v = static_cast(la) / headLat; - float phi = v * pi; // 0..pi - float sphi = std::sin(phi), cphi = std::cos(phi); - for (int lo = 0; lo <= headLon; ++lo) { - float u = static_cast(lo) / headLon; - float theta = u * 2.0f * pi; - glm::vec3 dir(sphi * std::cos(theta), - cphi, - sphi * std::sin(theta)); - glm::vec3 p = glm::vec3(0, headY, 0) + dir * headR; - addV(p, dir, {u, v}); - } - } - int rowSize = headLon + 1; - for (int la = 0; la < headLat; ++la) { - for (int lo = 0; lo < headLon; ++lo) { - uint32_t i00 = headStart + la * rowSize + lo; - uint32_t i01 = headStart + la * rowSize + lo + 1; - uint32_t i10 = headStart + (la + 1) * rowSize + lo; - uint32_t i11 = headStart + (la + 1) * rowSize + lo + 1; - wom.indices.insert(wom.indices.end(), - {i00, i10, i01, i01, i10, i11}); - } - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = headY + headR; - wom.boundMin = glm::vec3(-hp, 0, -hp); - wom.boundMax = glm::vec3( hp, maxY, hp); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-statue: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" pedestal : %.3f × %.3f × %.3f\n", pedSize, pedH, pedSize); - std::printf(" body : R=%.3f H=%.3f\n", bodyR, bodyH); - std::printf(" head : R=%.3f\n", headR); - std::printf(" total H : %.3f\n", maxY); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-altar") == 0 && i + 1 < argc) { - // Round altar: stack of N stepped cylindrical discs, - // each one wider and shorter than the next so the - // silhouette descends like a wedding cake. Top disc is - // the altar surface (where offerings would go); base - // discs widen out to anchor the structure visually. - // - // The 23rd procedural mesh primitive — pairs naturally - // with --gen-texture-marble for a temple aesthetic. - std::string womBase = argv[++i]; - float topR = 0.7f; // top altar disc radius - float topH = 0.3f; // top altar disc height - int steps = 3; // base steps below the top - float stepStride = 0.3f; // each step grows R by this much, shrinks H - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { topR = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { topH = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { steps = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { stepStride = std::stof(argv[++i]); } catch (...) {} - } - if (topR <= 0 || topH <= 0 || steps < 0 || steps > 16 || - stepStride <= 0 || stepStride > 5.0f) { - std::fprintf(stderr, - "gen-mesh-altar: topR/topH > 0, steps 0..16, stride 0..5\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - const int segs = 24; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Build a cylindrical disc from y0 to y1 at radius r. - // Side ring + top cap (faces +Y). Bottom of each disc - // is hidden by the next disc below, so we skip a bottom - // cap on all discs except the last (saves ~24 tris/disc). - auto disc = [&](float r, float y0, float y1, bool capBottom) { - uint32_t bot = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y0, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 0}); - } - uint32_t top = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y1, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 1}); - } - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), { - bot + sg, top + sg, bot + sg + 1, - bot + sg + 1, top + sg, top + sg + 1 - }); - } - // Top cap fan (faces +Y). - uint32_t tc = addV({0, y1, 0}, {0, 1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), - {tc, top + sg, top + sg + 1}); - } - if (capBottom) { - uint32_t bc = addV({0, y0, 0}, {0, -1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), - {bc, bot + sg + 1, bot + sg}); - } - } - }; - // Build bottom-up so y0 starts at floor and tops stack. - // Step k (k=0 is bottom-most) has radius = topR + (steps-k)*stride - // and height = topH * (1 - 0.2 * k). Y position accumulates. - float curY = 0.0f; - for (int k = steps - 1; k >= 0; --k) { // bottom step first - float r = topR + (k + 1) * stepStride; - float h = topH * (1.0f - 0.2f * k); - if (h < topH * 0.4f) h = topH * 0.4f; - bool isBottom = (k == steps - 1); - disc(r, curY, curY + h, isBottom); - curY += h; - } - // Top disc (the actual altar surface) - disc(topR, curY, curY + topH, steps == 0); - float maxY = curY + topH; - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxR = topR + steps * stepStride; - wom.boundMin = glm::vec3(-maxR, 0, -maxR); - wom.boundMax = glm::vec3( maxR, maxY, maxR); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-altar: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" top : R=%.3f H=%.3f\n", topR, topH); - std::printf(" steps : %d (stride %.3f)\n", steps, stepStride); - std::printf(" base R : %.3f\n", maxR); - std::printf(" total H : %.3f\n", maxY); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles: %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-portal") == 0 && i + 1 < argc) { - // Doorway portal: two vertical post boxes plus a - // horizontal lintel box across the top. Posts run along - // the Z axis (so width spans Z), opening faces +X. The - // gap between the posts is the actual doorway. Useful - // for entrances, gates, magical portals, ruins. - // - // The 24th procedural mesh primitive. - std::string womBase = argv[++i]; - float width = 2.5f; // outer-to-outer along Z - float height = 4.0f; // total Y - float postThick = 0.4f; // post width in X and Z - float lintelH = 0.5f; // top lintel height (Y) - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { width = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { height = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { postThick = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { lintelH = std::stof(argv[++i]); } catch (...) {} - } - if (width <= 0 || height <= 0 || postThick <= 0 || - lintelH < 0 || postThick * 2 >= width || - lintelH > height) { - std::fprintf(stderr, - "gen-mesh-portal: posts must fit inside width; lintel <= height\n"); - return 1; - } - 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; - // Box helper — same pattern as other multi-box meshes. - auto addBox = [&](float cx, float cy, float cz, - float hx, float hy, float hz) { - struct Face { glm::vec3 n, du, dv; }; - Face faces[6] = { - {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, - {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, - {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, - {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, - {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, - {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, - }; - glm::vec3 c(cx, cy, cz); - glm::vec3 ext(hx, hy, hz); - for (const Face& f : faces) { - glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); - glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); - glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); - uint32_t base = static_cast(wom.vertices.size()); - auto push = [&](glm::vec3 p, float u, float v) { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; - vtx.normal = f.n; - vtx.texCoord = {u, v}; - wom.vertices.push_back(vtx); - }; - push(center - du - dv, 0, 0); - push(center + du - dv, 1, 0); - push(center + du + dv, 1, 1); - push(center - du + dv, 0, 1); - wom.indices.insert(wom.indices.end(), - {base, base + 1, base + 2, base, base + 2, base + 3}); - } - }; - // Two posts at z = ±(width/2 - postThick/2). Each - // post extends from y=0 to y=height-lintelH so it - // tucks under the lintel. - float postY = (height - lintelH) * 0.5f; - float postHy = (height - lintelH) * 0.5f; - float postZ = (width - postThick) * 0.5f; - float postHt = postThick * 0.5f; - addBox(0, postY, postZ, postHt, postHy, postHt); - addBox(0, postY, -postZ, postHt, postHy, postHt); - // Lintel: spans full width across the top, same X - // thickness as posts. - if (lintelH > 0.0f) { - float lintelY = height - lintelH * 0.5f; - addBox(0, lintelY, 0, - postHt, lintelH * 0.5f, width * 0.5f); - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - wom.boundMin = glm::vec3(-postHt, 0, -width * 0.5f); - wom.boundMax = glm::vec3( postHt, height, width * 0.5f); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-portal: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" width : %.3f\n", width); - std::printf(" height : %.3f\n", height); - std::printf(" post thick : %.3f\n", postThick); - std::printf(" lintel H : %.3f%s\n", lintelH, - lintelH > 0 ? "" : " (no lintel)"); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-archway") == 0 && i + 1 < argc) { - // Semicircular arched doorway. Two cylindrical pillars - // hold up a curved keystone vault: the vault is a series - // of N angular wedge segments tracing a half-circle from - // pillar-top to pillar-top. The opening is the empty - // semicircular space below. - // - // The 25th procedural mesh primitive — the "fancier" - // sibling of --gen-mesh-portal which uses a flat lintel. - std::string womBase = argv[++i]; - float width = 3.0f; // outer-to-outer pillar centers along Z - float pillarH = 3.0f; // pillar height (Y) - float thickness = 0.4f; // pillar radius and arch radial thickness - int archSegs = 12; // segments around the half-circle - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { width = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { pillarH = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { thickness = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { archSegs = std::stoi(argv[++i]); } catch (...) {} - } - if (width <= 0 || pillarH <= 0 || thickness <= 0 || - archSegs < 4 || archSegs > 64 || - thickness * 4 >= width) { - std::fprintf(stderr, - "gen-mesh-archway: thickness×4 < width, archSegs 4..64\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - const int pillarSegs = 16; - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Cylindrical pillar at given (cx, cz), from y=0 to y=pillarH. - auto pillar = [&](float cx, float cz) { - float r = thickness; - uint32_t bot = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= pillarSegs; ++sg) { - float u = static_cast(sg) / pillarSegs; - float ang = u * 2.0f * pi; - glm::vec3 p(cx + r * std::cos(ang), 0, - cz + r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 0}); - } - uint32_t top = static_cast(wom.vertices.size()); - for (int sg = 0; sg <= pillarSegs; ++sg) { - float u = static_cast(sg) / pillarSegs; - float ang = u * 2.0f * pi; - glm::vec3 p(cx + r * std::cos(ang), pillarH, - cz + r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, 1}); - } - for (int sg = 0; sg < pillarSegs; ++sg) { - wom.indices.insert(wom.indices.end(), { - bot + sg, top + sg, bot + sg + 1, - bot + sg + 1, top + sg, top + sg + 1 - }); - } - // Caps - uint32_t bc = addV({cx, 0, cz}, {0, -1, 0}, {0.5f, 0.5f}); - uint32_t tc = addV({cx, pillarH, cz}, {0, 1, 0}, {0.5f, 0.5f}); - for (int sg = 0; sg < pillarSegs; ++sg) { - wom.indices.insert(wom.indices.end(), - {bc, bot + sg + 1, bot + sg}); - wom.indices.insert(wom.indices.end(), - {tc, top + sg, top + sg + 1}); - } - }; - float pillarZ = (width - 2 * thickness) * 0.5f; - pillar(0, pillarZ); - pillar(0, -pillarZ); - // Arch vault: trace half-circle from (z = +pillarZ, y = pillarH) - // up over to (z = -pillarZ, y = pillarH). Center of arch: - // (z = 0, y = pillarH). Arch radius = pillarZ. - // Inner arch (radius pillarZ - thickness*0.5) and outer - // (radius pillarZ + thickness*0.5) — the vault sits between. - float archCY = pillarH; - float arcInner = pillarZ - thickness * 0.5f; - float arcOuter = pillarZ + thickness * 0.5f; - // Each segment: 4 verts (inner-near, outer-near, inner-far, - // outer-far) extruded along X by thickness so the vault - // has front and back faces. - float archX = thickness * 0.5f; // half-depth in X - // Build vertex rings for inner and outer surfaces at - // each segment boundary, then connect. - // Top half-circle goes from theta=0 to theta=pi. - std::vector innerRing; - std::vector outerRing; - for (int s = 0; s <= archSegs; ++s) { - float t = static_cast(s) / archSegs; - float theta = t * pi; // 0..pi - float zi = arcInner * std::cos(theta); - float yi = arcInner * std::sin(theta); - float zo = arcOuter * std::cos(theta); - float yo = arcOuter * std::sin(theta); - innerRing.push_back({0, archCY + yi, zi}); - outerRing.push_back({0, archCY + yo, zo}); - } - // For each segment, add 8 vertices (4 corners × front/back face) - // and stitch them into 6 quads = 12 tris each. - for (int s = 0; s < archSegs; ++s) { - glm::vec3 i0 = innerRing[s]; - glm::vec3 i1 = innerRing[s + 1]; - glm::vec3 o0 = outerRing[s]; - glm::vec3 o1 = outerRing[s + 1]; - // Estimate outward (radial) normal as midpoint of o0+o1 - // direction from center. - glm::vec3 outDir = glm::normalize(glm::vec3(0, - (i0.y + i1.y + o0.y + o1.y) * 0.25f - archCY, - (i0.z + i1.z + o0.z + o1.z) * 0.25f)); - glm::vec3 frontN(1, 0, 0); - glm::vec3 backN(-1, 0, 0); - auto V = [&](glm::vec3 p, glm::vec3 n) { - return addV(p, n, {0, 0}); - }; - // Outer surface (top of arch): faces outward radially - uint32_t a = V({-archX, o0.y, o0.z}, outDir); - uint32_t b = V({ archX, o0.y, o0.z}, outDir); - uint32_t c = V({ archX, o1.y, o1.z}, outDir); - uint32_t d = V({-archX, o1.y, o1.z}, outDir); - wom.indices.insert(wom.indices.end(), {a, b, c, a, c, d}); - // Inner surface (underside of arch): faces inward - uint32_t e = V({-archX, i0.y, i0.z}, -outDir); - uint32_t f = V({ archX, i0.y, i0.z}, -outDir); - uint32_t g = V({ archX, i1.y, i1.z}, -outDir); - uint32_t h = V({-archX, i1.y, i1.z}, -outDir); - wom.indices.insert(wom.indices.end(), {e, g, f, e, h, g}); - // Front face (+X) of this wedge - uint32_t fi0 = V({ archX, i0.y, i0.z}, frontN); - uint32_t fo0 = V({ archX, o0.y, o0.z}, frontN); - uint32_t fo1 = V({ archX, o1.y, o1.z}, frontN); - uint32_t fi1 = V({ archX, i1.y, i1.z}, frontN); - wom.indices.insert(wom.indices.end(), - {fi0, fo0, fo1, fi0, fo1, fi1}); - // Back face (-X) - uint32_t bi0 = V({-archX, i0.y, i0.z}, backN); - uint32_t bo0 = V({-archX, o0.y, o0.z}, backN); - uint32_t bo1 = V({-archX, o1.y, o1.z}, backN); - uint32_t bi1 = V({-archX, i1.y, i1.z}, backN); - wom.indices.insert(wom.indices.end(), - {bi0, bo1, bo0, bi0, bi1, bo1}); - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = pillarH + arcOuter; - wom.boundMin = glm::vec3(-thickness, 0, -width * 0.5f); - wom.boundMax = glm::vec3( thickness, maxY, width * 0.5f); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-archway: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" width : %.3f\n", width); - std::printf(" pillar H : %.3f\n", pillarH); - std::printf(" thickness : %.3f\n", thickness); - std::printf(" arch segs : %d (radius %.3f)\n", archSegs, arcOuter); - std::printf(" apex Y : %.3f\n", maxY); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-barrel") == 0 && i + 1 < argc) { - // Tapered barrel: cylindrical body whose radius bulges - // smoothly from `topRadius` at the rims to `midRadius` - // at the middle (the classic stave-cooper barrel - // silhouette), plus 2 raised hoop bands at 25% and 75% - // of the height. The 26th procedural mesh primitive. - std::string womBase = argv[++i]; - float topR = 0.4f; // radius at top and bottom rim - float midR = 0.5f; // radius at the middle bulge - float height = 1.0f; - float hoopThick = 0.06f; // hoop band radial protrusion - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { topR = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { midR = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { height = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { hoopThick = std::stof(argv[++i]); } catch (...) {} - } - if (topR <= 0 || midR <= 0 || height <= 0 || - hoopThick < 0 || hoopThick > 0.5f) { - std::fprintf(stderr, - "gen-mesh-barrel: radii/height > 0, hoopThick 0..0.5\n"); - return 1; - } - 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; - const float pi = 3.14159265358979f; - const int segs = 16; // angular subdivisions - const int rings = 12; // vertical slices - auto addV = [&](glm::vec3 p, glm::vec3 n, glm::vec2 uv) -> uint32_t { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = n; vtx.texCoord = uv; - wom.vertices.push_back(vtx); - return static_cast(wom.vertices.size() - 1); - }; - // Radius profile: smooth cosine bulge from rim to mid. - // r(t) = topR + (midR - topR) * sin(pi*t) where t in 0..1 - // gives 0 at t=0/1 and 1 at t=0.5 — exact rim fit. - auto radiusAt = [&](float t) -> float { - return topR + (midR - topR) * std::sin(pi * t); - }; - uint32_t firstRing = static_cast(wom.vertices.size()); - for (int ri = 0; ri <= rings; ++ri) { - float t = static_cast(ri) / rings; - float y = t * height; - float r = radiusAt(t); - // Hoops: bump radius outward in two narrow bands. - float hoop1 = std::abs(t - 0.25f); - float hoop2 = std::abs(t - 0.75f); - if (hoop1 < 0.04f) r += hoopThick * (1.0f - hoop1 / 0.04f); - if (hoop2 < 0.04f) r += hoopThick * (1.0f - hoop2 / 0.04f); - for (int sg = 0; sg <= segs; ++sg) { - float u = static_cast(sg) / segs; - float ang = u * 2.0f * pi; - glm::vec3 p(r * std::cos(ang), y, r * std::sin(ang)); - glm::vec3 n(std::cos(ang), 0, std::sin(ang)); - addV(p, n, {u, t}); - } - } - int rowSize = segs + 1; - for (int ri = 0; ri < rings; ++ri) { - for (int sg = 0; sg < segs; ++sg) { - uint32_t i00 = firstRing + ri * rowSize + sg; - uint32_t i01 = firstRing + ri * rowSize + sg + 1; - uint32_t i10 = firstRing + (ri + 1) * rowSize + sg; - uint32_t i11 = firstRing + (ri + 1) * rowSize + sg + 1; - wom.indices.insert(wom.indices.end(), - {i00, i10, i01, i01, i10, i11}); - } - } - // End caps (top + bottom). topR is also the bottom-most - // and top-most ring radius since sin(0) = sin(pi) = 0. - uint32_t botCenter = addV({0, 0, 0}, {0, -1, 0}, {0.5f, 0.5f}); - uint32_t topCenter = addV({0, height, 0}, {0, 1, 0}, {0.5f, 0.5f}); - uint32_t botRing = firstRing; - uint32_t topRing = firstRing + rings * rowSize; - for (int sg = 0; sg < segs; ++sg) { - wom.indices.insert(wom.indices.end(), - {botCenter, botRing + sg + 1, botRing + sg}); - wom.indices.insert(wom.indices.end(), - {topCenter, topRing + sg, topRing + sg + 1}); - } - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxR = midR + hoopThick; - wom.boundMin = glm::vec3(-maxR, 0, -maxR); - wom.boundMax = glm::vec3( maxR, height, maxR); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-barrel: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" rim R : %.3f\n", topR); - std::printf(" bulge R : %.3f\n", midR); - std::printf(" height : %.3f\n", height); - std::printf(" hoops : 2 (thickness %.3f)\n", hoopThick); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - return 0; - } else if (std::strcmp(argv[i], "--gen-mesh-chest") == 0 && i + 1 < argc) { - // Treasure chest: rectangular body box + smaller lid - // box on top + 3 thin iron bands wrapping around the - // body + a small lock plate on the front center face. - // The 27th procedural mesh primitive — useful for - // dungeon loot, room decoration, quest objectives. - std::string womBase = argv[++i]; - float width = 1.4f; // along X - float depth = 0.9f; // along Z - float bodyH = 0.9f; // body box height - float lidH = 0.25f; // lid height above body - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { width = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { depth = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { bodyH = std::stof(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { lidH = std::stof(argv[++i]); } catch (...) {} - } - if (width <= 0 || depth <= 0 || bodyH <= 0 || lidH < 0) { - std::fprintf(stderr, - "gen-mesh-chest: width/depth/bodyH > 0, lidH >= 0\n"); - return 1; - } - 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; - // Box helper — adds 24 unique verts / 12 tris centered - // on (cx, cy, cz) with half-extents (hx, hy, hz). Each - // face gets unique normals for flat shading. - auto addBox = [&](float cx, float cy, float cz, - float hx, float hy, float hz) { - struct Face { glm::vec3 n, du, dv; }; - Face faces[6] = { - {{0, 1, 0}, {1, 0, 0}, {0, 0, 1}}, - {{0,-1, 0}, {1, 0, 0}, {0, 0,-1}}, - {{1, 0, 0}, {0, 0, 1}, {0, 1, 0}}, - {{-1,0, 0}, {0, 0,-1}, {0, 1, 0}}, - {{0, 0, 1}, {-1,0, 0}, {0, 1, 0}}, - {{0, 0,-1}, {1, 0, 0}, {0, 1, 0}}, - }; - glm::vec3 c(cx, cy, cz); - glm::vec3 ext(hx, hy, hz); - for (const Face& f : faces) { - glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz); - glm::vec3 du = glm::vec3(f.du.x*ext.x, f.du.y*ext.y, f.du.z*ext.z); - glm::vec3 dv = glm::vec3(f.dv.x*ext.x, f.dv.y*ext.y, f.dv.z*ext.z); - uint32_t base = static_cast(wom.vertices.size()); - auto push = [&](glm::vec3 p, float u, float v) { - wowee::pipeline::WoweeModel::Vertex vtx; - vtx.position = p; vtx.normal = f.n; vtx.texCoord = {u, v}; - wom.vertices.push_back(vtx); - }; - push(center - du - dv, 0, 0); - push(center + du - dv, 1, 0); - push(center + du + dv, 1, 1); - push(center - du + dv, 0, 1); - wom.indices.insert(wom.indices.end(), - {base, base + 1, base + 2, base, base + 2, base + 3}); - } - }; - float hx = width * 0.5f; - float hz = depth * 0.5f; - // Body: y=0 to y=bodyH - addBox(0, bodyH * 0.5f, 0, hx, bodyH * 0.5f, hz); - // Lid: smaller box on top, slightly inset on each side - float lidInset = std::min(width, depth) * 0.04f; - float lidHx = hx - lidInset; - float lidHz = hz - lidInset; - if (lidH > 0.0f && lidHx > 0 && lidHz > 0) { - addBox(0, bodyH + lidH * 0.5f, 0, - lidHx, lidH * 0.5f, lidHz); - } - // 3 iron bands wrapping the body — thin slabs - // protruding ~3% radially on the sides + top. - // Band positions: 15%, 50%, 85% of body width. - float bandThickX = width * 0.04f; // band depth along X - float bandHy = bodyH * 0.5f + 0.005f; - float bandHz = hz + 0.012f; - float bandPositions[3] = {-hx * 0.7f, 0.0f, hx * 0.7f}; - for (float bx : bandPositions) { - addBox(bx, bandHy, 0, - bandThickX * 0.5f, bandHy, bandHz); - } - // Lock plate: small thin box on the front face, centered. - // Front face is +Z. Plate sits at z = hz + tiny epsilon. - float lockW = width * 0.10f; - float lockH = bodyH * 0.18f; - float lockY = bodyH * 0.65f; - float lockEps = 0.008f; - addBox(0, lockY, hz + lockEps, - lockW * 0.5f, lockH * 0.5f, lockEps); - wowee::pipeline::WoweeModel::Batch batch; - batch.indexStart = 0; - batch.indexCount = static_cast(wom.indices.size()); - batch.textureIndex = 0; - wom.batches.push_back(batch); - float maxY = bodyH + lidH; - wom.boundMin = glm::vec3(-hx, 0, -hz - 0.012f); - wom.boundMax = glm::vec3( hx, maxY, hz + 0.012f); - if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { - std::fprintf(stderr, - "gen-mesh-chest: failed to save %s.wom\n", womBase.c_str()); - return 1; - } - std::printf("Wrote %s.wom\n", womBase.c_str()); - std::printf(" width × depth : %.3f × %.3f\n", width, depth); - std::printf(" body H : %.3f\n", bodyH); - std::printf(" lid H : %.3f\n", lidH); - std::printf(" components : body + lid + 3 bands + lock\n"); - std::printf(" vertices : %zu\n", wom.vertices.size()); - std::printf(" triangles : %zu\n", wom.indices.size() / 3); - 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