feat(editor): add --gen-mesh-pillar fluted column primitive

Procedural classical column. Cylindrical shaft with N concave
flutes (radius modulated by cos² of angle×flute-count) capped
by wider disc bases above and below — basic capital + base
geometry. Defaults: radius=0.4, height=4, flutes=12, capScale=1.25.

Useful for ruins, temples, dungeons, plaza decoration. Brings
the procedural mesh primitive set to 17.
This commit is contained in:
Kelsi 2026-05-08 01:49:32 -07:00
parent 6b0b6f6652
commit ddcd89bb4c

View file

@ -574,6 +574,8 @@ static void printUsage(const char* argv0) {
std::printf(" --gen-mesh-tree <wom-base> [trunkRadius] [trunkHeight] [foliageRadius]\n");
std::printf(" --gen-mesh-rock <wom-base> [radius] [roughness] [subdiv] [seed]\n");
std::printf(" Procedural boulder via subdivided octahedron + smooth noise displacement\n");
std::printf(" --gen-mesh-pillar <wom-base> [radius] [height] [flutes] [capScale]\n");
std::printf(" Fluted classical column with concave flutes + flared cap/base (default 12 flutes)\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");
@ -1075,7 +1077,7 @@ int main(int argc, char* argv[]) {
"--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-tree",
"--gen-mesh-rock",
"--gen-mesh-rock", "--gen-mesh-pillar",
"--gen-texture-gradient",
"--gen-mesh-from-heightmap", "--export-mesh-heightmap",
"--displace-mesh",
@ -20777,6 +20779,146 @@ 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-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<uint32_t>(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<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= radSegs; ++sg) {
float u = static_cast<float>(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<uint32_t>(wom.vertices.size());
for (int sg = 0; sg <= radSegs; ++sg) {
float u = static_cast<float>(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<uint32_t>(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], "--displace-mesh") == 0 && i + 2 < argc) {
// Displaces each vertex along its current normal by the
// heightmap brightness × scale. UVs determine where each