feat(editor): add --gen-mesh-weathervane rooftop primitive

53rd procedural mesh: 6-box rooftop wind indicator — base
plate, tall vertical post, perpendicular N-S and E-W cross
arms (cardinal direction markers), a long horizontal arrow
on top of the cross, and a small tail box at the back end of
the arrow that visually balances the head and gives the arrow
its directional read.

Useful for farm rooftops, chapel spires, town halls,
lighthouse caps, manor turrets — anywhere a fantasy world
wants visible wind direction. Defaults to a 1.50m post with
0.40m cross arms and a 0.55m-half-length arrow (~1.67m total).
This commit is contained in:
Kelsi 2026-05-09 09:20:08 -07:00
parent 6821549856
commit 4932947631
3 changed files with 136 additions and 1 deletions

View file

@ -4655,6 +4655,136 @@ int handleCoffin(int& i, int argc, char** argv) {
return 0;
}
int handleWeathervane(int& i, int argc, char** argv) {
// Weathervane: 6-box rooftop wind indicator — base plate,
// tall vertical post, perpendicular N-S and E-W cross arms
// (cardinal direction markers), a long horizontal arrow on
// top of the cross, and a small tail box at the back end of
// the arrow that visually balances the head. Useful for
// farm rooftops, chapel spires, town halls, lighthouse caps.
// The 53rd procedural mesh primitive.
std::string womBase = argv[++i];
float postHeight = 1.50f;
float postT = 0.05f;
float baseSize = 0.30f;
float armLen = 0.40f; // half-length of each cross arm
float arrowLen = 0.55f; // half-length of the arrow body
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { postHeight = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { postT = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { baseSize = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { armLen = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { arrowLen = std::stof(argv[++i]); } catch (...) {}
}
if (postHeight <= 0 || postT <= 0 || baseSize <= 0 ||
armLen <= 0 || arrowLen <= 0 || postT >= baseSize) {
std::fprintf(stderr,
"gen-mesh-weathervane: dims > 0; post must fit in base\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;
auto addBox = [&](float cx, float cy, float cz,
float hx, float hy, float hz) {
struct Face { glm::vec3 n, du, dv; };
Face faces[6] = {
{{0, 1, 0}, {1, 0, 0}, {0, 0, 1}},
{{0,-1, 0}, {1, 0, 0}, {0, 0,-1}},
{{1, 0, 0}, {0, 0, 1}, {0, 1, 0}},
{{-1,0, 0}, {0, 0,-1}, {0, 1, 0}},
{{0, 0, 1}, {-1,0, 0}, {0, 1, 0}},
{{0, 0,-1}, {1, 0, 0}, {0, 1, 0}},
};
glm::vec3 c(cx, cy, cz);
for (const Face& f : faces) {
glm::vec3 center = c + glm::vec3(f.n.x*hx, f.n.y*hy, f.n.z*hz);
glm::vec3 du = glm::vec3(f.du.x*hx, f.du.y*hy, f.du.z*hz);
glm::vec3 dv = glm::vec3(f.dv.x*hx, f.dv.y*hy, f.dv.z*hz);
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 = f.n; vtx.texCoord = {u, v};
wom.vertices.push_back(vtx);
};
push(center - du - dv, 0, 0);
push(center + du - dv, 1, 0);
push(center + du + dv, 1, 1);
push(center - du + dv, 0, 1);
wom.indices.insert(wom.indices.end(),
{base, base + 1, base + 2, base, base + 2, base + 3});
}
};
// Base plate at floor.
float baseHeight = baseSize * 0.30f;
float halfBase = baseSize * 0.5f;
addBox(0, baseHeight * 0.5f, 0,
halfBase, baseHeight * 0.5f, halfBase);
// Vertical post.
float halfPost = postT * 0.5f;
float poleBottomY = baseHeight;
float poleTopY = baseHeight + postHeight;
float poleCY = (poleBottomY + poleTopY) * 0.5f;
addBox(0, poleCY, 0, halfPost, postHeight * 0.5f, halfPost);
// Cross arms at the top of the post — 2 perpendicular thin
// bars forming the cardinal-direction "+" marker.
float armT = postT * 0.7f;
float halfArmT = armT * 0.5f;
float crossY = poleTopY - armT * 1.0f;
addBox(0, crossY, 0, armLen, halfArmT, halfArmT); // E-W (along X)
addBox(0, crossY, 0, halfArmT, halfArmT, armLen); // N-S (along Z)
// Arrow body on top of the cross — long thin bar that
// would rotate to the wind direction. Aligned along +X by
// default (designers can rotate at placement time).
float arrowY = poleTopY + armT * 0.7f;
float arrowT2 = armT * 1.1f;
float halfAT = arrowT2 * 0.5f;
addBox(0, arrowY, 0, arrowLen, halfAT, halfAT);
// Arrow tail: small box at -X end so the arrow looks
// directional rather than symmetric.
float tailLen = arrowLen * 0.3f;
float tailX = -arrowLen + tailLen;
addBox(tailX, arrowY, 0, halfAT, arrowT2 * 1.4f, halfAT);
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);
float totalH = arrowY + arrowT2 * 1.4f;
float maxX = std::max({halfBase, arrowLen, tailX + halfAT});
wom.boundMin = glm::vec3(-maxX, 0.0f, -armLen);
wom.boundMax = glm::vec3( maxX, totalH, armLen);
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-weathervane: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom\n", womBase.c_str());
std::printf(" total H : %.3f\n", totalH);
std::printf(" base : %.3f square × %.3f tall\n",
baseSize, baseHeight);
std::printf(" post : %.3f square × %.3f tall\n",
postT, postHeight);
std::printf(" cross arms : 2 × %.3f half-length (N-S + E-W)\n", armLen);
std::printf(" arrow : %.3f half-length (with tail at -X)\n",
arrowLen);
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" triangles : %zu\n", wom.indices.size() / 3);
return 0;
}
int handleBeehive(int& i, int argc, char** argv) {
// Beehive: 5-box woven straw skep — stacked tiers of
// decreasing width approximating a dome, with a small
@ -6482,6 +6612,9 @@ bool handleGenMesh(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-mesh-beehive") == 0 && i + 1 < argc) {
outRc = handleBeehive(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-mesh-weathervane") == 0 && i + 1 < argc) {
outRc = handleWeathervane(i, argc, argv); return true;
}
return false;
}

View file

@ -218,6 +218,8 @@ void printUsage(const char* argv0) {
std::printf(" Gate: 2 vertical posts + 3 horizontal rails (default 1.80/1.30/0.10/0.06)\n");
std::printf(" --gen-mesh-beehive <wom-base> [baseWidth] [height] [plateH]\n");
std::printf(" Beehive (skep): 4 tapered tiers + entrance notch on +Z face (default 0.70/0.85/0.05)\n");
std::printf(" --gen-mesh-weathervane <wom-base> [postH] [postT] [baseSize] [armLen] [arrowLen]\n");
std::printf(" Weathervane: base + post + N-S/E-W cross arms + arrow with tail (default 1.50/0.05/0.30/0.40/0.55)\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

@ -159,7 +159,7 @@ int main(int argc, char* argv[]) {
"--gen-mesh-ladder", "--gen-mesh-well", "--gen-mesh-signpost",
"--gen-mesh-mailbox", "--gen-mesh-tombstone", "--gen-mesh-crate",
"--gen-mesh-stool", "--gen-mesh-cauldron", "--gen-mesh-gate",
"--gen-mesh-beehive",
"--gen-mesh-beehive", "--gen-mesh-weathervane",
"--gen-texture-gradient",
"--gen-mesh-from-heightmap", "--export-mesh-heightmap",
"--displace-mesh",