feat(editor): add --gen-texture-bubbles overlapping-circle pattern

38th procedural texture: scattered bubbles done as randomly-
placed circles of varied radii, with bright rim outlines that
stay visible even where bubbles overlap (rim color wins on
any pixel that lies in any bubble's ring band).

Three colors: background, translucent-feeling fill for the
bubble interior, and a bright rim. Defaults to 50 bubbles
of radius 6-24 px with 2-px rims. Useful for water surfaces,
foam patches, soap suds, magical-effect overlays, slime
particle effects.
This commit is contained in:
Kelsi 2026-05-09 08:03:54 -07:00
parent 408d7a611a
commit 56cad647b0
3 changed files with 123 additions and 1 deletions

View file

@ -3590,6 +3590,123 @@ int handleParquet(int& i, int argc, char** argv) {
return 0;
}
int handleBubbles(int& i, int argc, char** argv) {
// Bubbles: scattered circles of varied radii, drawn as
// translucent fills with a brighter rim. Bubbles overlap;
// rim color wins at any pixel that lies in any bubble's
// ring band (so overlapping outlines stay readable).
std::string outPath = argv[++i];
std::string bgHex = argv[++i];
std::string fillHex = argv[++i];
std::string rimHex = argv[++i];
int bubbleCount = 50;
int minR = 6;
int maxR = 24;
int rimW = 2;
int W = 256, H = 256;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { bubbleCount = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { minR = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { maxR = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { rimW = 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 ||
bubbleCount < 1 || bubbleCount > 4096 ||
minR < 1 || maxR < minR || maxR > 1024 ||
rimW < 1 || rimW > minR) {
std::fprintf(stderr,
"gen-texture-bubbles: invalid dims (W/H 1..8192, bubbles 1..4096, "
"minR..maxR 1..1024, rimW 1..minR)\n");
return 1;
}
uint8_t br_, bg_, bb_, fr, fg, fb_, rr, rg, rb_;
if (!parseHex(bgHex, br_, bg_, bb_) ||
!parseHex(fillHex, fr, fg, fb_) ||
!parseHex(rimHex, rr, rg, rb_)) {
std::fprintf(stderr,
"gen-texture-bubbles: bg/fill/rim hex color is invalid\n");
return 1;
}
// Deterministic seed placement so re-runs reproduce.
struct Bubble { int x, y, r; int rimRsq; int rSq; };
std::vector<Bubble> bubbles;
bubbles.reserve(bubbleCount);
uint32_t rng = static_cast<uint32_t>(bubbleCount) * 0x9E3779B9u +
static_cast<uint32_t>(W) * 0x85EBCA6Bu +
static_cast<uint32_t>(maxR);
auto rngStep = [&]() {
rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
return rng;
};
int radSpan = maxR - minR + 1;
for (int s = 0; s < bubbleCount; ++s) {
Bubble b;
b.x = static_cast<int>((rngStep() & 0xFFFF) / 65535.0f * W);
b.y = static_cast<int>((rngStep() & 0xFFFF) / 65535.0f * H);
b.r = minR + static_cast<int>(rngStep() % radSpan);
b.rSq = b.r * b.r;
int innerR = std::max(1, b.r - rimW);
b.rimRsq = innerR * innerR;
bubbles.push_back(b);
}
std::vector<uint8_t> pixels(static_cast<size_t>(W) * H * 3, 0);
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
bool onRim = false;
bool hasFill = false;
for (const auto& b : bubbles) {
int dx = b.x - x;
int dy = b.y - y;
int distSq = dx * dx + dy * dy;
if (distSq > b.rSq) continue;
hasFill = true;
if (distSq >= b.rimRsq) {
onRim = true;
break; // rim wins; no need to check further
}
}
uint8_t r, g, b;
if (onRim) {
r = rr; g = rg; b = rb_;
} else if (hasFill) {
r = fr; g = fg; b = fb_;
} 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-bubbles: 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(" bg/fill/rim: %s / %s / %s\n",
bgHex.c_str(), fillHex.c_str(), rimHex.c_str());
std::printf(" bubbles : %d (radius %d-%d, rim %d px)\n",
bubbleCount, minR, maxR, rimW);
return 0;
}
} // namespace
bool handleGenTexture(int& i, int argc, char** argv, int& outRc) {
@ -3706,6 +3823,9 @@ bool handleGenTexture(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--gen-texture-parquet") == 0 && i + 4 < argc) {
outRc = handleParquet(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-texture-bubbles") == 0 && i + 4 < argc) {
outRc = handleBubbles(i, argc, argv); return true;
}
return false;
}

View file

@ -109,6 +109,8 @@ void printUsage(const char* argv0) {
std::printf(" Frost: scattered crystal nuclei with 6-spike rosettes that fade with distance\n");
std::printf(" --gen-texture-parquet <out.png> <woodAHex> <woodBHex> <gapHex> [cellSize] [gapW] [W H]\n");
std::printf(" Parquet: basket-weave wood floor pattern with checkered horizontal/vertical plank pairs\n");
std::printf(" --gen-texture-bubbles <out.png> <bgHex> <fillHex> <rimHex> [count] [minR] [maxR] [rimW] [W H]\n");
std::printf(" Bubbles: scattered overlapping circles with bright rims (foam, water, magic)\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

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