feat(editor): add --gen-mesh-tree composite tree primitive

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: <wom-base> [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.
This commit is contained in:
Kelsi 2026-05-07 22:19:51 -07:00
parent 010cf3b6c5
commit 6bd7f18328

View file

@ -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 <wom-base> [posts] [postSpacing] [postHeight] [railThick]\n");
std::printf(" Repeating fence: N posts along +X with two horizontal rails between\n");
std::printf(" --gen-mesh-tree <wom-base> [trunkRadius] [trunkHeight] [foliageRadius]\n");
std::printf(" Procedural tree: cylindrical trunk + spherical foliage (default 0.1/2.0/0.7)\n");
std::printf(" --displace-mesh <wom-base> <heightmap.png> [scale]\n");
std::printf(" Offset each vertex along its normal by heightmap brightness × scale (default 1.0)\n");
std::printf(" --gen-mesh-from-heightmap <wom-base> <heightmap.png> [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<uint32_t>(wom.vertices.size() - 1);
};
// Trunk cylinder: 12 segments, side ring + top + bottom.
const int trunkSegs = 12;
uint32_t trunkSideStart = static_cast<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= trunkSegs; ++sg) {
float u = static_cast<float>(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<uint32_t>(wom.vertices.size());
for (int st = 0; st <= fStacks; ++st) {
float v = static_cast<float>(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<float>(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<uint32_t>(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