From 6bd7f1832898538abe0e44150dc6f3ade8b6d6ee Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 22:19:51 -0700 Subject: [PATCH] feat(editor): add --gen-mesh-tree composite tree primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Procedural tree: cylindrical trunk (12 segments) + UV-sphere foliage (12 segs × 8 stacks). Foliage centered above the trunk with a slight overlap so the trunk pokes into the bottom of the canopy. Useful for ambient zone decoration, distant tree placeholders, magic-grove props. The 15th procedural primitive — composite of cylinder + sphere with everything in a single batch so it ships as one mesh. Args: [trunkRadius] [trunkHeight] [foliageRadius] Defaults: 0.1 / 2.0 / 0.7 (3.2y total height — small/medium tree). Pairs naturally with --add-texture-to-mesh for the bark+leaf textures, or just one composite texture since this is a single- batch mesh. Verified: defaults produce 143 verts / 216 tris. --- tools/editor/main.cpp | 137 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 94bba475..20bba30e 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -565,6 +565,8 @@ static void printUsage(const char* argv0) { std::printf(" N-sided polygonal pyramid with apex at +Y (default 4 sides, 1.0/1.0)\n"); std::printf(" --gen-mesh-fence [posts] [postSpacing] [postHeight] [railThick]\n"); std::printf(" Repeating fence: N posts along +X with two horizontal rails between\n"); + std::printf(" --gen-mesh-tree [trunkRadius] [trunkHeight] [foliageRadius]\n"); + std::printf(" Procedural tree: cylindrical trunk + spherical foliage (default 0.1/2.0/0.7)\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"); @@ -1053,7 +1055,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-mesh-fence", + "--gen-mesh-pyramid", "--gen-mesh-fence", "--gen-mesh-tree", "--gen-texture-gradient", "--gen-mesh-from-heightmap", "--export-mesh-heightmap", "--displace-mesh", @@ -19671,6 +19673,139 @@ 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-tree") == 0 && i + 1 < argc) { + // Procedural tree: cylinder trunk + UV-sphere foliage. + // Trunk goes from Y=0 up to Y=trunkHeight; foliage sphere + // centered at trunk-top + foliageRadius/2 so the trunk + // pokes up into the bottom of the canopy. + // + // Useful for ambient zone decoration, distant tree + // placeholders, magic-grove props. The 15th procedural + // primitive — pairs naturally with --add-texture-to-mesh + // for trunk-bark and leaf textures (or just one texture + // since this is a single-batch mesh). + std::string womBase = argv[++i]; + float trunkR = 0.1f; + float trunkH = 2.0f; + float foliR = 0.7f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { trunkR = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { trunkH = std::stof(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { foliR = std::stof(argv[++i]); } catch (...) {} + } + if (trunkR <= 0 || trunkH <= 0 || foliR <= 0) { + std::fprintf(stderr, + "gen-mesh-tree: trunkR / trunkH / foliR 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); + }; + // Trunk cylinder: 12 segments, side ring + top + bottom. + const int trunkSegs = 12; + uint32_t trunkSideStart = static_cast(wom.vertices.size()); + for (int sg = 0; sg <= trunkSegs; ++sg) { + float u = static_cast(sg) / trunkSegs; + float ang = u * 2.0f * pi; + float ca = std::cos(ang), sa = std::sin(ang); + addV(glm::vec3(trunkR * ca, 0, trunkR * sa), + glm::vec3(ca, 0, sa), + glm::vec2(u, 0)); + addV(glm::vec3(trunkR * ca, trunkH, trunkR * sa), + glm::vec3(ca, 0, sa), + glm::vec2(u, 1)); + } + for (int sg = 0; sg < trunkSegs; ++sg) { + uint32_t a = trunkSideStart + sg * 2; + uint32_t b = a + 1, c = a + 2, d = a + 3; + wom.indices.push_back(a); + wom.indices.push_back(c); + wom.indices.push_back(b); + wom.indices.push_back(b); + wom.indices.push_back(c); + wom.indices.push_back(d); + } + // Foliage UV sphere: 12 segments × 8 stacks. Center at + // (0, trunkH + foliR * 0.7, 0) so the trunk pokes into + // the bottom of the canopy. + const int fSegs = 12; + const int fStacks = 8; + float foliCY = trunkH + foliR * 0.7f; + uint32_t foliStart = static_cast(wom.vertices.size()); + for (int st = 0; st <= fStacks; ++st) { + float v = static_cast(st) / fStacks; + float phi = v * pi; + float sphi = std::sin(phi), cphi = std::cos(phi); + for (int sg = 0; sg <= fSegs; ++sg) { + float u = static_cast(sg) / fSegs; + float theta = u * 2.0f * pi; + float ctheta = std::cos(theta), stheta = std::sin(theta); + float nx = sphi * ctheta; + float ny = cphi; + float nz = sphi * stheta; + addV(glm::vec3(foliR * nx, foliCY + foliR * ny, foliR * nz), + glm::vec3(nx, ny, nz), + glm::vec2(u, v)); + } + } + int fStride = fSegs + 1; + for (int st = 0; st < fStacks; ++st) { + for (int sg = 0; sg < fSegs; ++sg) { + uint32_t a = foliStart + st * fStride + sg; + uint32_t b = a + 1; + uint32_t c = a + fStride; + uint32_t d = c + 1; + wom.indices.push_back(a); + wom.indices.push_back(c); + wom.indices.push_back(b); + wom.indices.push_back(b); + wom.indices.push_back(c); + wom.indices.push_back(d); + } + } + wom.boundMin = glm::vec3(-foliR, 0, -foliR); + wom.boundMax = glm::vec3( foliR, foliCY + foliR, foliR); + 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-tree: failed to save %s.wom\n", womBase.c_str()); + return 1; + } + std::printf("Wrote %s.wom\n", womBase.c_str()); + std::printf(" trunk R : %.3f\n", trunkR); + std::printf(" trunk H : %.3f\n", trunkH); + std::printf(" foliage R : %.3f\n", foliR); + std::printf(" total H : %.3f\n", foliCY + foliR); + 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