#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; } int handleAnvil(int& i, int argc, char** argv) { // Blacksmith anvil: stepped pedestal base + flat work // surface (the "face") + tapered horn extending forward. // Built from 3 boxes + a 4-vertex tapered prism for the // horn. The 28th procedural mesh primitive. std::string womBase = argv[++i]; float length = 1.0f; // along X (face length) float width = 0.4f; // along Z float hornLen = 0.5f; // horn extending past face float bodyH = 0.5f; // total height 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 { hornLen = std::stof(argv[++i]); } catch (...) {} } if (i + 1 < argc && argv[i + 1][0] != '-') { try { bodyH = std::stof(argv[++i]); } catch (...) {} } if (length <= 0 || width <= 0 || hornLen < 0 || bodyH <= 0) { std::fprintf(stderr, "gen-mesh-anvil: length/width/bodyH > 0, hornLen >= 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; 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}); } }; // Pedestal: bottom 60% of total height, narrower base // (4-step taper would be classic but for simplicity we use // a wide base + narrow waist + wide cap structure as 3 boxes). float baseH = bodyH * 0.25f; float waistH = bodyH * 0.30f; float capH = bodyH * 0.20f; float faceH = bodyH * 0.25f; float baseHx = length * 0.45f; float baseHz = width * 0.55f; float waistHx = length * 0.30f; float waistHz = width * 0.40f; float capHx = length * 0.50f; float capHz = width * 0.55f; float faceHx = length * 0.50f; float faceHz = width * 0.50f; float y0 = 0.0f; addBox(0, y0 + baseH * 0.5f, 0, baseHx, baseH * 0.5f, baseHz); y0 += baseH; addBox(0, y0 + waistH * 0.5f, 0, waistHx, waistH * 0.5f, waistHz); y0 += waistH; addBox(0, y0 + capH * 0.5f, 0, capHx, capH * 0.5f, capHz); y0 += capH; addBox(0, y0 + faceH * 0.5f, 0, faceHx, faceH * 0.5f, faceHz); // Horn: tapered prism extending in +X past the face. 6 verts // (rectangle at face edge tapering to a point at the tip). if (hornLen > 0.0f) { float hornBaseX = faceHx; float hornTipX = faceHx + hornLen; float hornY0 = y0 + faceH * 0.25f; float hornY1 = y0 + faceH * 0.75f; float hornHz = faceHz * 0.6f; // 4 base verts + 2 tip verts (tip is a vertical edge) // Build 4 face triangles + 2 base/tip caps glm::vec3 b00(hornBaseX, hornY0, hornHz); glm::vec3 b01(hornBaseX, hornY0, -hornHz); glm::vec3 b10(hornBaseX, hornY1, hornHz); glm::vec3 b11(hornBaseX, hornY1, -hornHz); glm::vec3 t0 (hornTipX, (hornY0 + hornY1) * 0.5f, 0); // Top face triangles (b10, b11, t0) auto addTri = [&](glm::vec3 a, glm::vec3 b, glm::vec3 c) { glm::vec3 n = glm::normalize(glm::cross(b - a, c - a)); uint32_t base = static_cast(wom.vertices.size()); wom.vertices.push_back({a, n, {0, 0}}); wom.vertices.push_back({b, n, {1, 0}}); wom.vertices.push_back({c, n, {0.5f, 1}}); wom.indices.insert(wom.indices.end(), {base, base + 1, base + 2}); }; // 4 side faces converging to t0 addTri(b00, b01, t0); // bottom addTri(b11, b10, t0); // top addTri(b10, b00, t0); // +Z side addTri(b01, b11, t0); // -Z side // Base of horn (closes the rectangle on the face side). // The base is hidden against the anvil face but include it // so the mesh is watertight. glm::vec3 baseN(-1, 0, 0); uint32_t base = static_cast(wom.vertices.size()); wom.vertices.push_back({b00, baseN, {0, 0}}); wom.vertices.push_back({b10, baseN, {0, 1}}); wom.vertices.push_back({b11, baseN, {1, 1}}); wom.vertices.push_back({b01, baseN, {1, 0}}); wom.indices.insert(wom.indices.end(), {base, base + 1, base + 2, base, base + 2, base + 3}); } wowee::pipeline::WoweeModel::Batch batch; batch.indexStart = 0; batch.indexCount = static_cast(wom.indices.size()); batch.textureIndex = 0; wom.batches.push_back(batch); float maxX = std::max(faceHx, faceHx + hornLen); float maxZ = std::max({baseHz, waistHz, capHz, faceHz}); wom.boundMin = glm::vec3(-faceHx, 0, -maxZ); wom.boundMax = glm::vec3( maxX, bodyH, maxZ); if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { std::fprintf(stderr, "gen-mesh-anvil: failed to save %s.wom\n", womBase.c_str()); return 1; } std::printf("Wrote %s.wom\n", womBase.c_str()); std::printf(" length × width : %.3f × %.3f\n", length, width); std::printf(" body H : %.3f\n", bodyH); std::printf(" horn length : %.3f\n", hornLen); std::printf(" components : 4 step pedestal + tapered horn\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; } if (std::strcmp(argv[i], "--gen-mesh-anvil") == 0 && i + 1 < argc) { outRc = handleAnvil(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee