Weather sim v2: river generation

This commit is contained in:
Nikita Lisitsa 2026-04-08 16:32:05 +03:00
parent 1044443e9b
commit c097420a50
2 changed files with 188 additions and 3 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 248 B

View file

@ -4,9 +4,11 @@
#include <psemek/gfx/gl.hpp> #include <psemek/gfx/gl.hpp>
#include <psemek/math/camera.hpp> #include <psemek/math/camera.hpp>
#include <psemek/math/gauss.hpp> #include <psemek/math/gauss.hpp>
#include <psemek/math/intersection.hpp>
#include <psemek/random/generator.hpp> #include <psemek/random/generator.hpp>
#include <psemek/random/device.hpp> #include <psemek/random/device.hpp>
#include <psemek/random/uniform_ball.hpp> #include <psemek/random/uniform_ball.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/pcg/perlin.hpp> #include <psemek/pcg/perlin.hpp>
#include <psemek/pcg/fractal.hpp> #include <psemek/pcg/fractal.hpp>
#include <psemek/util/ndarray.hpp> #include <psemek/util/ndarray.hpp>
@ -350,6 +352,22 @@ struct solver
} }
}; };
struct river_vertex
{
// Empty for river deltas
std::optional<math::point<int, 2>> parent;
std::vector<math::point<int, 2>> children = {};
float flow = 0.f;
};
struct river_net
{
util::ndarray<std::optional<river_vertex>, 2> vertices;
util::ndarray<bool, 2> water;
std::vector<math::point<int, 2>> queue;
};
struct weather_app struct weather_app
: app::application_base : app::application_base
{ {
@ -365,6 +383,7 @@ struct weather_app
bool show_temperature = false; bool show_temperature = false;
bool show_humidity = false; bool show_humidity = false;
bool show_biomes = true; bool show_biomes = true;
bool show_rivers = true;
gfx::pixmap_rgba biomes_map; gfx::pixmap_rgba biomes_map;
util::ndarray<float, 2> terrain; util::ndarray<float, 2> terrain;
@ -387,6 +406,8 @@ struct weather_app
std::optional<struct solver> solver; std::optional<struct solver> solver;
river_net rivers;
float display_season = 0.f; float display_season = 0.f;
util::clock<> frame_clock; util::clock<> frame_clock;
@ -396,7 +417,7 @@ struct weather_app
simulation_box = {{{0.f, N}, {0.f, N}}}; simulation_box = {{{0.f, N}, {0.f, N}}};
terrain.resize({N, N}, 0.f); terrain.resize({N, N}, 0.f);
auto heightmap = gfx::read_image<std::uint8_t>(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_1.png"}); auto heightmap = gfx::read_image<std::uint8_t>(io::file_istream{std::filesystem::path{PSEMEK_EXAMPLES_DIR} / "heightmap_seed_3.png"});
for (int y = 0; y < N; ++y) for (int y = 0; y < N; ++y)
for (int x = 0; x < N; ++x) for (int x = 0; x < N; ++x)
terrain(x, y) = ((heightmap(x, y) / 255.f) * 2048.f - 512.f) / 1024.f; 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) if (event.down && event.key == app::keycode::B)
show_biomes ^= true; show_biomes ^= true;
if (event.down && event.key == app::keycode::R)
show_rivers ^= true;
} }
void update() override void update() override
@ -443,6 +467,7 @@ struct weather_app
if (season == 4) if (season == 4)
{ {
compute_average(); compute_average();
init_river_deltas();
need_update_display_snapshot = true; need_update_display_snapshot = true;
} }
} }
@ -450,8 +475,30 @@ struct weather_app
if (!solver && season < 4) if (!solver && season < 4)
solver.emplace(terrain, rng, season, day_night, snapshots[season][day_night]); solver.emplace(terrain, rng, season, day_night, snapshots[season][day_night]);
if (!paused && solver) if (!paused)
{
if (solver)
{
for (int i = 0; i < 16; ++i)
{
solver->step(); 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)) if (state().key_down.contains(app::keycode::LEFT))
{ {
@ -490,6 +537,120 @@ struct weather_app
scale(average.humidity, 1.f / 8.f); 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<int>(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<int, 2> const neighbours[4]
{
{-1, 0},
{ 1, 0},
{ 0, -1},
{ 0, 1},
};
float height = sample_bilinear(terrain, math::cast<float>(p));
std::vector<math::point<int, 2>> 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<float>(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<int, 2> 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<float>(p)) - 0.05f);
}
else
{
for (auto c : v->children)
v->flow += compute_river_flow_at(c);
}
return v->flow;
}
void update_display_snapshot() void update_display_snapshot()
{ {
display_snapshot.wind_velocity.resize({N, N}, math::vector{0.f, 0.f}); 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<float>(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<float>(*v->parent), width * pixel_size, {0, 255, 255, 255}, false);
}
}
}
int row = 0; int row = 0;
auto push_text = [&](std::string const & text) mutable auto push_text = [&](std::string const & text) mutable
{ {