From 525b48ade7004521b9cd8d6bfbf90f1d22b14927 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 20:45:11 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-pyramid N-sided polygonal pyramid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N-sided polygonal pyramid with apex at +Y. 3 sides → tetrahedron- like shape; 4 → classic square pyramid; 8+ → faceted approximation of a cone. Different from --gen-mesh cone: cone has smooth curved sides with per-vertex radial-ish normals; pyramid has flat per-face normals on each triangular side, giving a clearly faceted look. Args: [sides] [baseRadius] [height] Defaults: 4 / 1.0 / 1.0. Sides hard-capped at 256. Useful for monuments, witch hats, gem props, treasure piles, dunce caps. Brings the procedural primitive set to 13. Verified: 4-sided pyramid → 17 verts / 8 tris (4 side + 4 base); 3-sided → 13 verts / 6 tris. --- tools/editor/main.cpp | 113 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 60e14319..6ad9db20 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -559,6 +559,8 @@ static void printUsage(const char* argv0) { std::printf(" Capsule along Y axis: cylinder body with hemispherical caps (default 0.5/1.0/16/8)\n"); std::printf(" --gen-mesh-arch [openingWidth] [openingHeight] [thickness] [depth] [segments]\n"); std::printf(" Doorway arch: two columns + semicircular top (default 1.0/1.5/0.2/0.3, 12 segs)\n"); + std::printf(" --gen-mesh-pyramid [sides] [baseRadius] [height]\n"); + std::printf(" N-sided polygonal pyramid with apex at +Y (default 4 sides, 1.0/1.0)\n"); std::printf(" --displace-mesh [scale]\n"); std::printf(" Offset each vertex along its normal by heightmap brightness × scale (default 1.0)\n"); std::printf(" --gen-mesh-from-heightmap [scaleXZ] [scaleY]\n"); @@ -1043,6 +1045,7 @@ int main(int argc, char* argv[]) { "--add-texture-to-mesh", "--add-texture-to-zone", "--gen-mesh-stairs", "--gen-mesh-grid", "--gen-mesh-disc", "--gen-mesh-tube", "--gen-mesh-capsule", "--gen-mesh-arch", + "--gen-mesh-pyramid", "--gen-texture-gradient", "--gen-mesh-from-heightmap", "--export-mesh-heightmap", "--displace-mesh", @@ -19152,6 +19155,116 @@ int main(int argc, char* argv[]) { wom.boundMin.x, wom.boundMin.y, wom.boundMin.z, wom.boundMax.x, wom.boundMax.y, wom.boundMax.z); return 0; + } else if (std::strcmp(argv[i], "--gen-mesh-pyramid") == 0 && i + 1 < argc) { + // N-sided polygonal pyramid with apex at +Y. 4 sides + // gives a square pyramid; 3 gives a tetrahedron-like + // shape; 8+ approaches a cone. + // + // Different from --gen-mesh cone: cone has smooth + // round sides with per-vertex radial-ish normals; + // pyramid has flat per-face normals on N triangular + // sides + a flat polygonal base. + std::string womBase = argv[++i]; + int sides = 4; + float baseR = 1.0f; + float height = 1.0f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { sides = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { baseR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { height = std::stof(argv[++i]); } catch (...) {} + } + if (sides < 3 || sides > 256 || baseR <= 0 || height <= 0) { + std::fprintf(stderr, + "gen-mesh-pyramid: sides 3..256, baseR > 0, height > 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; + 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); + }; + // Build base ring vertices (one per side). + std::vector basePts; + for (int k = 0; k < sides; ++k) { + float a = static_cast(k) / sides * 2.0f * pi; + basePts.push_back(glm::vec3(baseR * std::cos(a), 0, + baseR * std::sin(a))); + } + glm::vec3 apex(0, height, 0); + // Side faces: per-face flat normals (cross of two edges). + for (int k = 0; k < sides; ++k) { + glm::vec3 a = basePts[k]; + glm::vec3 b = basePts[(k + 1) % sides]; + glm::vec3 e1 = b - a; + glm::vec3 e2 = apex - a; + glm::vec3 n = glm::normalize(glm::cross(e1, e2)); + float u0 = static_cast(k) / sides; + float u1 = static_cast(k + 1) / sides; + uint32_t i0 = addV(a, n, glm::vec2(u0, 1)); + uint32_t i1 = addV(b, n, glm::vec2(u1, 1)); + uint32_t i2 = addV(apex, n, glm::vec2(0.5f * (u0 + u1), 0)); + wom.indices.push_back(i0); + wom.indices.push_back(i1); + wom.indices.push_back(i2); + } + // Base: fan from a center vertex (normal -Y). + uint32_t baseCenter = addV(glm::vec3(0, 0, 0), + glm::vec3(0, -1, 0), + glm::vec2(0.5f, 0.5f)); + uint32_t baseRingStart = static_cast(wom.vertices.size()); + for (int k = 0; k < sides; ++k) { + float a = static_cast(k) / sides * 2.0f * pi; + addV(basePts[k], glm::vec3(0, -1, 0), + glm::vec2(0.5f + 0.5f * std::cos(a), + 0.5f - 0.5f * std::sin(a))); + } + for (int k = 0; k < sides; ++k) { + wom.indices.push_back(baseCenter); + wom.indices.push_back(baseRingStart + (k + 1) % sides); + wom.indices.push_back(baseRingStart + k); + } + wom.boundMin = glm::vec3(-baseR, 0, -baseR); + wom.boundMax = glm::vec3( baseR, height, baseR); + wom.boundRadius = glm::length(wom.boundMax - wom.boundMin) * 0.5f; + wowee::pipeline::WoweeModel::Batch b; + b.indexStart = 0; + b.indexCount = static_cast(wom.indices.size()); + b.textureIndex = 0; + b.blendMode = 0; + b.flags = 0; + wom.batches.push_back(b); + wom.texturePaths.push_back(""); + std::filesystem::path womPath(womBase); + std::filesystem::create_directories(womPath.parent_path()); + if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) { + std::fprintf(stderr, + "gen-mesh-pyramid: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" sides : %d\n", sides); + std::printf(" base R : %.3f\n", baseR); + std::printf(" height : %.3f\n", height); + std::printf(" vertices : %zu (%d side tris × 3 + 1 base center + %d base ring)\n", + wom.vertices.size(), sides, sides); + std::printf(" triangles : %zu (%d sides + %d base)\n", + wom.indices.size() / 3, sides, sides); + 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