From a07df2375595a8f4c2cc27a9fd80a20cfbcbcd1b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 07:41:17 -0700 Subject: [PATCH] feat(editor): add --gen-texture-frost crystal-rosette pattern 36th procedural texture: scattered ice nuclei with 6-spike rosettes radiating at 60 deg intervals. Each spike's pixel intensity falls linearly from full at the seed to zero at the end of the ray, so spikes fade naturally into the background. Per-seed angular jitter prevents all rosettes from aligning to the same orientation. Useful for winter zones, ice biomes, frosted-window decals, magical cold-effect overlays. Defaults to 80 seeds with 18-px rays in 256x256. --- 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 79c0790a..491eac2c 100644 --- a/tools/editor/cli_gen_texture.cpp +++ b/tools/editor/cli_gen_texture.cpp @@ -3366,6 +3366,123 @@ int handleShingles(int& i, int argc, char** argv) { return 0; } +int handleFrost(int& i, int argc, char** argv) { + // Frost: scattered crystal nuclei with radial spikes. + // Each seed gets six thin lines radiating at 60° intervals + // (with a per-seed random angular offset so they don't all + // align). Line lengths are jittered per spike, and pixel + // intensity falls off linearly toward the end of each line + // so spikes fade naturally into the background. + std::string outPath = argv[++i]; + std::string bgHex = argv[++i]; + std::string iceHex = argv[++i]; + int seedCount = 80; + int rayLen = 18; + int W = 256, H = 256; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { seedCount = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { rayLen = 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 || + seedCount < 1 || seedCount > 8192 || + rayLen < 2 || rayLen > 256) { + std::fprintf(stderr, + "gen-texture-frost: invalid dims (W/H 1..8192, seeds 1..8192, ray 2..256)\n"); + return 1; + } + uint8_t br_, bg_, bb_, ir, ig, ib; + if (!parseHex(bgHex, br_, bg_, bb_) || + !parseHex(iceHex, ir, ig, ib)) { + std::fprintf(stderr, + "gen-texture-frost: bg or ice hex color is invalid\n"); + return 1; + } + std::vector pixels(static_cast(W) * H * 3, 0); + // Fill background. + for (size_t p = 0; p < pixels.size(); p += 3) { + pixels[p + 0] = br_; + pixels[p + 1] = bg_; + pixels[p + 2] = bb_; + } + // Deterministic RNG so re-runs reproduce the same frost. + uint32_t rng = static_cast(seedCount) * 0x9E3779B9u + + static_cast(W) * 0x85EBCA6Bu + + static_cast(rayLen); + auto rngStep = [&]() { + rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; + return rng; + }; + auto blendPixel = [&](int x, int y, float alpha) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + if (alpha <= 0) return; + if (alpha > 1.0f) alpha = 1.0f; + size_t idx = (static_cast(y) * W + x) * 3; + // Linear blend from bg toward ice color by alpha. + pixels[idx + 0] = static_cast( + pixels[idx + 0] + (ir - pixels[idx + 0]) * alpha); + pixels[idx + 1] = static_cast( + pixels[idx + 1] + (ig - pixels[idx + 1]) * alpha); + pixels[idx + 2] = static_cast( + pixels[idx + 2] + (ib - pixels[idx + 2]) * alpha); + }; + constexpr float kPi = 3.14159265358979323846f; + for (int s = 0; s < seedCount; ++s) { + // Seed position uniformly random across the image. + float sx = (rngStep() & 0xFFFF) / 65535.0f * W; + float sy = (rngStep() & 0xFFFF) / 65535.0f * H; + // Angular jitter so spikes don't all align to the same + // 6-fold rosette. + float baseAngle = (rngStep() & 0xFFFF) / 65535.0f * kPi / 3.0f; + // 6 rays per nucleus at 60° spacing. + for (int r = 0; r < 6; ++r) { + float angle = baseAngle + r * (kPi / 3.0f); + float dx = std::cos(angle); + float dy = std::sin(angle); + // Per-spike length jitter (60-100% of nominal). + float lenScale = 0.6f + (rngStep() & 0xFFFF) / 65535.0f * 0.4f; + int spikeLen = static_cast(rayLen * lenScale); + // Walk pixels along the ray. Alpha falls linearly + // from 1.0 at the seed to 0.0 at the end of the spike. + for (int t = 0; t < spikeLen; ++t) { + int px = static_cast(sx + dx * t); + int py = static_cast(sy + dy * t); + float alpha = 1.0f - static_cast(t) / spikeLen; + blendPixel(px, py, alpha); + } + } + // Bright nucleus dot — a 2x2 block to make the seed + // visible even when its spikes are short. + for (int dyN = 0; dyN < 2; ++dyN) { + for (int dxN = 0; dxN < 2; ++dxN) { + int px = static_cast(sx) + dxN; + int py = static_cast(sy) + dyN; + blendPixel(px, py, 1.0f); + } + } + } + if (!stbi_write_png(outPath.c_str(), W, H, 3, + pixels.data(), W * 3)) { + std::fprintf(stderr, + "gen-texture-frost: 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 / ice : %s / %s\n", bgHex.c_str(), iceHex.c_str()); + std::printf(" seeds : %d (6-spike rosettes, ray %d px)\n", + seedCount, rayLen); + return 0; +} + } // namespace bool handleGenTexture(int& i, int argc, char** argv, int& outRc) { @@ -3476,6 +3593,9 @@ bool handleGenTexture(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-texture-shingles") == 0 && i + 4 < argc) { outRc = handleShingles(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--gen-texture-frost") == 0 && i + 3 < argc) { + outRc = handleFrost(i, argc, argv); return true; + } return false; } diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index bbf3bda4..ae494995 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -105,6 +105,8 @@ void printUsage(const char* argv0) { std::printf(" Stained glass: Voronoi cells in 3-color rotation, separated by dark lead lines\n"); std::printf(" --gen-texture-shingles [shingleW] [shingleH] [shadowH] [W H]\n"); std::printf(" Roof shingles: half-offset rows of rectangular tiles with shadow band + vertical seams\n"); + std::printf(" --gen-texture-frost [seeds] [rayLen] [W H]\n"); + std::printf(" Frost: scattered crystal nuclei with 6-spike rosettes that fade with distance\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 e81e301b..d215fcc6 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -158,7 +158,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", + "--gen-texture-shingles", "--gen-texture-frost", "--validate-glb", "--info-glb", "--info-glb-tree", "--info-glb-bytes", "--validate-jsondbc", "--check-glb-bounds", "--validate-stl", "--validate-png", "--validate-blp",