feat(editor): add --gen-texture-shingles roof-tile pattern

35th procedural texture: roof shingles done as half-row-staggered
rows of rectangular tiles. Three colors: shingle base, a shadow
band at the top of each row (where the row above overlaps), and
thin vertical seams between adjacent shingles in the same row.

The half-row stagger comes from the standard roofing convention
of offsetting alternate courses by half a tile width — gives the
classic interlocked look that pure brick patterns lack.

Useful for roofs, scale armor close-ups, fish bellies, anywhere
needing tightly-packed offset rectangles. Defaults to 32x24
shingles with 4-px shadow + 1-px seams.
This commit is contained in:
Kelsi 2026-05-09 07:30:50 -07:00
parent a25961e2dd
commit 4ec0c1aea2
3 changed files with 97 additions and 0 deletions

View file

@ -3275,6 +3275,97 @@ int handleStainedGlass(int& i, int argc, char** argv) {
return 0;
}
int handleShingles(int& i, int argc, char** argv) {
// Roof shingles: offset rows of rectangular tiles, with a
// dark shadow band at the top of each row (where the row
// above overlaps) and thin vertical seams between adjacent
// shingles in a row. Three colors give the shingle body
// its base tone, a shadow tone for the overlap band, and
// a darker seam color.
std::string outPath = argv[++i];
std::string baseHex = argv[++i];
std::string shadowHex = argv[++i];
std::string seamHex = argv[++i];
int shingleW = 32;
int shingleH = 24;
int shadowH = 4; // shadow band thickness at top of each row
int seamW = 1; // vertical seam width between shingles
int W = 256, H = 256;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { shingleW = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { shingleH = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { shadowH = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { W = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { H = std::stoi(argv[++i]); } catch (...) {}
}
if (W < 1 || H < 1 || W > 8192 || H > 8192 ||
shingleW < 4 || shingleW > 512 ||
shingleH < 4 || shingleH > 512 ||
shadowH < 0 || shadowH >= shingleH) {
std::fprintf(stderr,
"gen-texture-shingles: invalid dims (W/H 1..8192, shingleW/H 4..512, shadowH 0..shingleH-1)\n");
return 1;
}
uint8_t br_, bg_, bb_, sr_, sg_, sb_, er_, eg_, eb_;
if (!parseHex(baseHex, br_, bg_, bb_) ||
!parseHex(shadowHex, sr_, sg_, sb_) ||
!parseHex(seamHex, er_, eg_, eb_)) {
std::fprintf(stderr,
"gen-texture-shingles: base/shadow/seam hex color is invalid\n");
return 1;
}
std::vector<uint8_t> pixels(static_cast<size_t>(W) * H * 3, 0);
for (int y = 0; y < H; ++y) {
int rowIdx = y / shingleH;
int withinRow = y - rowIdx * shingleH;
int shift = (rowIdx & 1) ? shingleW / 2 : 0;
for (int x = 0; x < W; ++x) {
// Position within the current shingle along x.
int xRel = x - shift;
int xMod;
if (xRel >= 0) xMod = xRel % shingleW;
else xMod = ((xRel % shingleW) + shingleW) % shingleW;
uint8_t r, g, b;
if (withinRow < shadowH) {
// Top of row: shadow band where the row above
// overlaps this row's shingles.
r = sr_; g = sg_; b = sb_;
} else if (xMod < seamW || xMod >= shingleW - seamW) {
// Vertical seam between adjacent shingles.
r = er_; g = eg_; b = eb_;
} else {
r = br_; g = bg_; b = bb_;
}
size_t idx = (static_cast<size_t>(y) * W + x) * 3;
pixels[idx + 0] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
if (!stbi_write_png(outPath.c_str(), W, H, 3,
pixels.data(), W * 3)) {
std::fprintf(stderr,
"gen-texture-shingles: stbi_write_png failed for %s\n",
outPath.c_str());
return 1;
}
std::printf("Wrote %s\n", outPath.c_str());
std::printf(" size : %dx%d\n", W, H);
std::printf(" base/shadow/seam: %s / %s / %s\n",
baseHex.c_str(), shadowHex.c_str(), seamHex.c_str());
std::printf(" shingle : %dx%d (shadow %d px, seam %d px)\n",
shingleW, shingleH, shadowH, seamW);
return 0;
}
} // namespace
bool handleGenTexture(int& i, int argc, char** argv, int& outRc) {
@ -3382,6 +3473,9 @@ bool handleGenTexture(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-texture-stained-glass") == 0 && i + 5 < argc) {
outRc = handleStainedGlass(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-texture-shingles") == 0 && i + 4 < argc) {
outRc = handleShingles(i, argc, argv); return true;
}
return false;
}

View file

@ -103,6 +103,8 @@ void printUsage(const char* argv0) {
std::printf(" Scales: half-row-staggered overlapping circles forming fish/dragon/chain-mail look\n");
std::printf(" --gen-texture-stained-glass <out.png> <leadHex> <colorAHex> <colorBHex> <colorCHex> [cells] [W H]\n");
std::printf(" Stained glass: Voronoi cells in 3-color rotation, separated by dark lead lines\n");
std::printf(" --gen-texture-shingles <out.png> <baseHex> <shadowHex> <seamHex> [shingleW] [shingleH] [shadowH] [W H]\n");
std::printf(" Roof shingles: half-offset rows of rectangular tiles with shadow band + vertical seams\n");
std::printf(" --add-texture-to-zone <zoneDir> <png-path> [renameTo]\n");
std::printf(" Copy an existing PNG into <zoneDir> (optionally renaming it on the way in)\n");
std::printf(" --gen-mesh <wom-base> <cube|plane|sphere|cylinder|torus|cone|ramp> [size]\n");

View file

@ -155,6 +155,7 @@ int main(int argc, char* argv[]) {
"--gen-texture-coral", "--gen-texture-flame", "--gen-texture-tartan",
"--gen-texture-argyle", "--gen-texture-herringbone",
"--gen-texture-scales", "--gen-texture-stained-glass",
"--gen-texture-shingles",
"--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes",
"--validate-jsondbc", "--check-glb-bounds", "--validate-stl",
"--validate-png", "--validate-blp",