From 56cad647b01471a07fdb5eceb661974e50f4862c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 08:03:54 -0700 Subject: [PATCH] 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. --- tools/editor/cli_gen_texture.cpp | 120 +++++++++++++++++++++++++++++++ tools/editor/cli_help.cpp | 2 + tools/editor/main.cpp | 2 +- 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/tools/editor/cli_gen_texture.cpp b/tools/editor/cli_gen_texture.cpp index 0a0bf13e..b8cba8d7 100644 --- a/tools/editor/cli_gen_texture.cpp +++ b/tools/editor/cli_gen_texture.cpp @@ -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 bubbles; + bubbles.reserve(bubbleCount); + uint32_t rng = static_cast(bubbleCount) * 0x9E3779B9u + + static_cast(W) * 0x85EBCA6Bu + + static_cast(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((rngStep() & 0xFFFF) / 65535.0f * W); + b.y = static_cast((rngStep() & 0xFFFF) / 65535.0f * H); + b.r = minR + static_cast(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 pixels(static_cast(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(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; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index c246b3bc..61220daa 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -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 [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 [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 [renameTo]\n"); std::printf(" Copy an existing PNG into (optionally renaming it on the way in)\n"); std::printf(" --gen-mesh [size]\n"); diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 1d4203e1..0a80aaad 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -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",