feat(editor): add --gen-mesh-coffin hexagonal-prism primitive

38th procedural mesh: classic 6-sided coffin with the
narrow-head / wide-shoulder / tapered-foot top-down profile
that's instantly recognizable from any angle. Built as 6 side
quads + top lid fan + bottom panel fan, all with face-shared
normals so it shades cleanly under any lighting.

Pairs with --gen-mesh-grave for graveyard set dressing.
Defaults to 2.0×0.8×0.6 (length × shoulder-width × height).
This commit is contained in:
Kelsi 2026-05-09 06:18:42 -07:00
parent f0bbc228ef
commit 2658e8297a
3 changed files with 143 additions and 1 deletions

View file

@ -4518,6 +4518,143 @@ int handleThrone(int& i, int argc, char** argv) {
return 0;
}
int handleCoffin(int& i, int argc, char** argv) {
// Coffin: classic 6-sided "hexagonal" prism with the
// characteristic narrow-head / wide-shoulder / tapered-foot
// top-down profile that reads as a coffin from any angle.
// Six side faces + top lid + bottom panel — face-shared
// normals via separate vertex sets per face. The 38th
// procedural mesh primitive — useful for graveyard set
// dressing alongside --gen-mesh-grave.
std::string womBase = argv[++i];
float length = 2.0f; // along Z
float width = 0.8f; // shoulder width along X
float height = 0.6f; // along Y
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 { height = std::stof(argv[++i]); } catch (...) {}
}
if (length <= 0 || width <= 0 || height <= 0) {
std::fprintf(stderr,
"gen-mesh-coffin: length/width/height must be > 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;
// Top-down hexagonal coffin profile (CCW from head looking
// down +Y). Head end is narrow, shoulder is widest, feet
// taper to a narrow toe — the canonical "casket" silhouette.
float hL = length * 0.5f;
float hW = width * 0.5f;
glm::vec2 ring[6] = {
{ 0.0f, hL }, // p0 head tip
{-hW, hL * 0.6f }, // p1 left shoulder (widest)
{-hW * 0.8f, -hL * 0.6f }, // p2 left hip
{ 0.0f, -hL }, // p3 foot tip
{ hW * 0.8f, -hL * 0.6f }, // p4 right hip
{ hW, hL * 0.6f }, // p5 right shoulder
};
auto addQuad = [&](glm::vec3 a, glm::vec3 b, glm::vec3 c, glm::vec3 d,
glm::vec3 n) {
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
auto push = [&](glm::vec3 p, float u, float v) {
wowee::pipeline::WoweeModel::Vertex vtx;
vtx.position = p; vtx.normal = n; vtx.texCoord = {u, v};
wom.vertices.push_back(vtx);
};
push(a, 0, 0);
push(b, 1, 0);
push(c, 1, 1);
push(d, 0, 1);
wom.indices.insert(wom.indices.end(),
{base, base + 1, base + 2, base, base + 2, base + 3});
};
// Six side faces — each a quad from bottom-edge to top-edge
// of one segment of the hexagon. Normal is the outward
// perpendicular to the side edge in the XZ plane.
for (int s = 0; s < 6; ++s) {
const glm::vec2& a = ring[s];
const glm::vec2& b = ring[(s + 1) % 6];
glm::vec3 bot0(a.x, 0.0f, a.y);
glm::vec3 bot1(b.x, 0.0f, b.y);
glm::vec3 top1(b.x, height, b.y);
glm::vec3 top0(a.x, height, a.y);
// Outward normal: 90° CW rotation of edge vector in XZ
// (since vertices wind CCW looking down, outward is +X
// when edge goes -Z, i.e. swap & negate one component).
glm::vec2 edge = b - a;
glm::vec3 n(edge.y, 0.0f, -edge.x);
n = glm::normalize(n);
addQuad(bot0, bot1, top1, top0, n);
}
// Top lid: fan of 4 triangles from p0, all sharing +Y normal.
{
glm::vec3 normal(0.0f, 1.0f, 0.0f);
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int v = 0; v < 6; ++v) {
wowee::pipeline::WoweeModel::Vertex vtx;
vtx.position = glm::vec3(ring[v].x, height, ring[v].y);
vtx.normal = normal;
// Cheap planar UV from top-down ring coords.
vtx.texCoord = { ring[v].x / width + 0.5f,
ring[v].y / length + 0.5f };
wom.vertices.push_back(vtx);
}
for (int t = 1; t < 5; ++t) {
wom.indices.insert(wom.indices.end(),
{base, base + static_cast<uint32_t>(t),
base + static_cast<uint32_t>(t + 1)});
}
}
// Bottom panel: same fan but reversed winding for -Y normal.
{
glm::vec3 normal(0.0f, -1.0f, 0.0f);
uint32_t base = static_cast<uint32_t>(wom.vertices.size());
for (int v = 0; v < 6; ++v) {
wowee::pipeline::WoweeModel::Vertex vtx;
vtx.position = glm::vec3(ring[v].x, 0.0f, ring[v].y);
vtx.normal = normal;
vtx.texCoord = { ring[v].x / width + 0.5f,
ring[v].y / length + 0.5f };
wom.vertices.push_back(vtx);
}
for (int t = 1; t < 5; ++t) {
wom.indices.insert(wom.indices.end(),
{base, base + static_cast<uint32_t>(t + 1),
base + static_cast<uint32_t>(t)});
}
}
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(-hW, 0.0f, -hL);
wom.boundMax = glm::vec3( hW, height, hL);
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-coffin: 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 (shoulder)\n", width);
std::printf(" height : %.3f\n", height);
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) {
@ -4624,6 +4761,9 @@ bool handleGenMesh(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-mesh-throne") == 0 && i + 1 < argc) {
outRc = handleThrone(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-mesh-coffin") == 0 && i + 1 < argc) {
outRc = handleCoffin(i, argc, argv); return true;
}
return false;
}

View file

@ -162,6 +162,8 @@ void printUsage(const char* argv0) {
std::printf(" Cage: top/bottom frames + 4 corner posts + N bars per side (default 1.5×2.0, 5 bars)\n");
std::printf(" --gen-mesh-throne <wom-base> [seatWidth] [seatHeight] [backHeight] [pedestalSize]\n");
std::printf(" Throne: pedestal + seat + backrest + 2 armrests (default 0.8/0.5/1.5/1.2)\n");
std::printf(" --gen-mesh-coffin <wom-base> [length] [width] [height]\n");
std::printf(" Hexagonal coffin: narrow head + wide shoulder + tapered foot prism (default 2.0/0.8/0.6)\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");

View file

@ -125,7 +125,7 @@ int main(int argc, char* argv[]) {
"--gen-mesh-anvil", "--gen-mesh-mushroom", "--gen-mesh-cart",
"--gen-mesh-banner", "--gen-mesh-grave", "--gen-mesh-bench",
"--gen-mesh-shrine", "--gen-mesh-totem", "--gen-mesh-cage",
"--gen-mesh-throne",
"--gen-mesh-throne", "--gen-mesh-coffin",
"--gen-texture-gradient",
"--gen-mesh-from-heightmap", "--export-mesh-heightmap",
"--displace-mesh",