From c097420a5002454fc7f0593a5211b60a878ee3bd Mon Sep 17 00:00:00 2001 From: lisyarus Date: Wed, 8 Apr 2026 16:32:05 +0300 Subject: [PATCH] Weather sim v2: river generation --- examples/biomes.png | Bin 4595 -> 248 bytes examples/weather_v2.cpp | 191 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 3 deletions(-) diff --git a/examples/biomes.png b/examples/biomes.png index 36b3c9bb5a810424dcca536a930e8be1e05cf8a0..d6a16c38af154c7aeb457c523ff3aa70f4de4d47 100644 GIT binary patch delta 195 zcmeyY{DX0VvO5D~age(c!lvI6-E$sR$z3=CCj3=9n| z3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$YMvZtV2!7COHUS26~Z4Q2*IzLYr#}JO| z$sGlTnHs161Hs|-|IhFLe}2-${GBudA83t+~batM*J2UZOV0Xb=SjT})?1V(kJl7L@ch)mw z@7hX89YRVTsze;1QD~Y(N{XP$gCx*|v}~FXhgL0WP+k(@5mk*M5MU`!oXR~jyI$KR zQe?IK^Q`XPJLjJB-E+Qs?y>Ja(9*oNsN%{H}-nUkTQ@z0NYiM&o?&v$M&NI#J_riEVaSTf! zON%;=Jq5N^(7y(ahH(w_??BUkSy=Z3jGbq?`w5$>XsZgE)$Mk4Z7kk`e7g)mHo^R8 z?+8)aN0B~;14NSc3luF-6iPD!Nelh};zjO%qym`_HkY7zo*eTRUQ5sAD{VjgFA4Ow z=;3fnEF4BNrlIMbDuU#8-@QH5^ybQIj{JP%Gf&U0*s_gN_B8k0c=h0|Td~B_S61(< zuHUtJ)xiCWj~ux%gL7ZM*w&rtdjGvQZkl~n@BxbE03p`-)vj=(TTk^ebIS$&t7;(leUxE^|POxx&BY*uReYD_D7T7{C3#- zd|&TF9?t{Z+p~_>pG2Sd=;f-v55I*iKYHB|KXhNLcJbzazV^w`f={3RtM^#V-`4j% z(YAQ!gFpJcO)h_|ZiwxQyl4NVcK?s}?LNqL?5OMbc;GO(`-9eG9gr5rhr6yF*>?7Z?duk{&VTrtnR`}mId|!ek<;0OH;Z?^yRLGu|9tJ> zH}lF?A6og+dG6HUuZ}l(?md>@aq`R71^IJJKKtUp4|#9-f&Pci9(eYgXV>-livtTE z-@kmCUb)qKar2r}kCv4i{U`m;AAEmB^R>U2)xG0&Pjz+w5T-sq{Q9Bay>#%NV=rGC z_N+Z+?zkY_mW0&Xq$k?!w)lEMHc~!OF(lQOOJ&?`Y-r47M7cw?QAthe=@53|&|wVK zl@QkIkCX9CSZ&vv`b>3WUvon4>yUW`Yg|#;kP`qPrP?BzOLe9#As512Tmi<8n7~k1 z#qJ1UZIEnX!&Fh$$NES-n$vq|Y(*v7U@DrhA+n|j0dFC!-L^9VL1eR8UzYJ1W|E+I zo+n6}plKX5aH}_Mi#a@PEpi|V7!lQyO+91lMjCZ6Mak&4Ll_40=%|0GOgvtKPg_M6 zfDa-kW(dkh5~&n1-omn@JpfX4=sPW}L~lkVHmH`-ZOUr2M@`#{#zQD_$v)F=DNwawRcgYL&ao_6I20uHRK~RevL|TTx;94E1mBz)H=OZ~fO!dbg7#?at}WV*DJ=2Lmdl(2^7&RToN;g*Aqm5@9>_l$cbBOgiaKID`wU zT4EuL_K{`YoNW;x7$8=3@)Q7EIm9BkRn#yOhS3?qN~O6p9f#wjh_)Dkq7I;?Vx1T**2_t# z+6DI=xVWL{TJQI1JBJ5t0DaiI41=-iX=sn=A?;E|BoU5S56c|Y#^MnIJ&6~#AVxFP#9N7ftrF0 z>NAvkqEzpToH>>C3O>bjyMikK^wc7g;&+O!DY_=bz@(I?vTKU2Nii@f<*DrYztL4W zc4Ji2@Q+>=?t>2o{@4$<$#QAk+K79rTwv9k12CGAX}ZNi5Z@xFd2-J*_JFX$j>V%D zpUs+CRM2W#v-c{(?5{E9T{mXD|>V64`$8I zn;H2`S$Y3`RTb!6Z;#a0)vx$NKA(T#Ox{yoUS40G$p1N?KYjG*JNbOe#i25IkUjI} V{A0 #include #include +#include #include #include #include +#include #include #include #include @@ -350,6 +352,22 @@ struct solver } }; +struct river_vertex +{ + // Empty for river deltas + std::optional> parent; + std::vector> children = {}; + float flow = 0.f; +}; + +struct river_net +{ + util::ndarray, 2> vertices; + util::ndarray water; + + std::vector> queue; +}; + struct weather_app : app::application_base { @@ -365,6 +383,7 @@ struct weather_app bool show_temperature = false; bool show_humidity = false; bool show_biomes = true; + bool show_rivers = true; gfx::pixmap_rgba biomes_map; util::ndarray terrain; @@ -387,6 +406,8 @@ struct weather_app std::optional solver; + river_net rivers; + float display_season = 0.f; util::clock<> frame_clock; @@ -396,7 +417,7 @@ struct weather_app simulation_box = {{{0.f, N}, {0.f, N}}}; terrain.resize({N, N}, 0.f); - auto heightmap = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_1.png"}); + auto heightmap = gfx::read_image(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_3.png"}); for (int y = 0; y < N; ++y) for (int x = 0; x < N; ++x) terrain(x, y) = ((heightmap(x, y) / 255.f) * 2048.f - 512.f) / 1024.f; @@ -424,6 +445,9 @@ struct weather_app if (event.down && event.key == app::keycode::B) show_biomes ^= true; + + if (event.down && event.key == app::keycode::R) + show_rivers ^= true; } void update() override @@ -443,6 +467,7 @@ struct weather_app if (season == 4) { compute_average(); + init_river_deltas(); need_update_display_snapshot = true; } } @@ -450,8 +475,30 @@ struct weather_app if (!solver && season < 4) solver.emplace(terrain, rng, season, day_night, snapshots[season][day_night]); - if (!paused && solver) - solver->step(); + if (!paused) + { + if (solver) + { + for (int i = 0; i < 16; ++i) + { + solver->step(); + if (solver->finished) + break; + } + } + else if (season == 4 && !rivers.queue.empty()) + { + for (int i = 0; i < 16; ++i) + { + propagate_rivers(); + if (rivers.queue.empty()) + { + compute_river_flow(); + break; + } + } + } + } if (state().key_down.contains(app::keycode::LEFT)) { @@ -490,6 +537,120 @@ struct weather_app scale(average.humidity, 1.f / 8.f); } + void init_river_deltas() + { + rivers.vertices.resize({N, N}); + rivers.water.resize({N, N}, false); + + for (int y = 1; y + 1 < N; ++y) + { + for (int x = 1; x + 1 < N; ++x) + { + int land = 0; + land += (terrain(x - 1, y - 1) >= 0.f ? 1 : 0); + land += (terrain(x + 0, y - 1) >= 0.f ? 1 : 0); + land += (terrain(x - 1, y + 0) >= 0.f ? 1 : 0); + land += (terrain(x + 0, y + 0) >= 0.f ? 1 : 0); + + if (land > 0 && land < 4) + { + rivers.queue.push_back({x, y}); + rivers.vertices(x, y).emplace(); + } + + if (land < 4) + rivers.water(x, y) = true; + } + } + } + + void propagate_rivers() + { + auto i = random::uniform(rng, 0, rivers.queue.size() - 1); + if (i + 1 < rivers.queue.size()) + std::swap(rivers.queue[i], rivers.queue.back()); + + auto p = rivers.queue.back(); + + math::vector const neighbours[4] + { + {-1, 0}, + { 1, 0}, + { 0, -1}, + { 0, 1}, + }; + + float height = sample_bilinear(terrain, math::cast(p)); + + std::vector> candidates; + for (auto n : neighbours) + { + auto q = p + n; + if (q[0] == 0 || q[0] == N || q[1] == 0 || q[1] == N) + continue; + + if (rivers.water(q[0], q[1])) + continue; + + if (rivers.vertices(q[0], q[1])) + continue; + + float nheight = sample_bilinear(terrain, math::cast(q)); + + if (nheight + 0.02f < height) + continue; + + candidates.push_back(q); + } + + if (candidates.size() <= 1) + rivers.queue.pop_back(); + + if (candidates.empty()) + { + // TODO: remove if no parent and not going anywhere? + return; + } + + auto next = random::uniform_from(rng, candidates); + + rivers.vertices(p[0], p[1])->children.push_back(next); + rivers.vertices(next[0], next[1]) = river_vertex{.parent = p}; + rivers.queue.push_back(next); + } + + void compute_river_flow() + { + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + if (auto & v = rivers.vertices(x, y); v && !v->parent) + { + compute_river_flow_at({x, y}); + } + } + } + } + + float compute_river_flow_at(math::point const & p) + { + auto & v = rivers.vertices(p[0], p[1]); + if (!v) return 0.f; + + if (v->children.empty()) + { + v->flow = std::max(0.f, sample_bilinear(average.humidity, math::cast(p)) - 0.05f); + } + else + { + for (auto c : v->children) + v->flow += compute_river_flow_at(c); + } + + return v->flow; + } + void update_display_snapshot() { display_snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); @@ -646,6 +807,30 @@ struct weather_app } } + if (show_rivers) + { + for (auto const & p : rivers.queue) + { + painter.circle(math::cast(p), 3.f * pixel_size, {0, 255, 255, 255}, 12); + } + + if (!rivers.vertices.empty()) + for (int y = 0; y < N; ++y) + { + for (int x = 0; x < N; ++x) + { + auto & v = rivers.vertices(x, y); + if (!v || !v->parent) continue; + + float width = 2.f; + if (rivers.queue.empty()) + width = 2.f * std::sqrt(v->flow); + + painter.line({x, y}, math::cast(*v->parent), width * pixel_size, {0, 255, 255, 255}, false); + } + } + } + int row = 0; auto push_text = [&](std::string const & text) mutable {