#include #include #include #include #include #include #include #include #include #include #include #include using namespace psemek; struct water_2d_app : app::application_base { water_2d_app(options const &, context const &); void update() override; void present() override; void on_event(app::resize_event const & event) override; void on_event(app::key_event const & event) override; math::box simulation_area; math::box inner_area; float aspect_ratio; random::generator rng{random::device{}}; int const N = 128; float time = 0.f; util::array bed; util::array water; util::array flowx; util::array flowy; util::array, 2> velocity; util::array sediment; util::array new_sediment; gfx::painter painter; bool paused = true; bool show_water = true; bool show_velocity = false; bool erosion_on = false; bool rain_on = false; }; water_2d_app::water_2d_app(options const &, context const & ctx) { (void)ctx; ctx.vsync(true); simulation_area[0] = {-1.f, 1.f}; simulation_area[1] = {-1.f, 1.f}; inner_area = math::shrink(simulation_area, simulation_area.dimensions() / (2.f * N)); bed.assign({N, N}, 0.f); water.assign({N, N}, 0.f); flowx.assign({N + 1, N}, 0.f); flowy.assign({N, N + 1}, 0.f); velocity.assign({N, N}, math::vector::zero()); sediment.assign({N, N}, 0.f); new_sediment.assign({N, N}, 0.f); // water(N / 2, N / 2) = 100.f; random::uniform_sphere_vector_distribution d; util::array, 2> grads({9, 9}); for (auto & v : grads) v = d(rng); pcg::perlin noise(std::move(grads), pcg::seamless); for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { // if (x == N / 2 && (y != N / 4 && y != 3 * N / 4)) // bed(x, y) = 100.f; float tx = (x + 0.5f) / N; float ty = (y + 0.5f) / N; float n = 0.f; // Archipelago n = 0.5f * noise(tx, ty) + 0.5f * noise(math::frac(2.f * tx), math::frac(2.f * ty)); n -= 0.5f * math::distance(math::point{tx, ty}, math::point{0.5f, 0.5f}); n = 5.f * math::smoothstep(math::clamp(math::unlerp({0.45f, 0.55f}, n), {0.f, 1.f})); // Island // n = 4.f * std::max(0.f, 1.f - 3.f * math::distance(math::point{tx, ty}, math::point{0.5f, 0.5f}) + (2.f * noise(tx, ty) - 1.f) * 0.25f); // Delta // n = 4.f * std::abs(2.f * noise(tx, ty) - 1.f) * (1.f - tx); // Double delta // n = 4.f * std::abs(2.f * noise(tx, ty) - 1.f) * std::pow(4.f * tx * (1.f - tx), 2.f); // River n = std::min(4.f, math::lerp(10.f, 20.f, noise(tx, ty)) * std::abs(ty - math::lerp({0.4f, 0.6f}, noise(tx, 0.f)))); // Canyon // n = 10.f * math::clamp(10.f * math::lerp(-1.f, 1.f, noise(tx, ty)), {0.f, 1.f}); // Canyon v2 // n = math::clamp(20.f * std::abs(2.f * noise(tx, ty) - 1.f) - 2.f, {0.f, 5.f}); // Shore // n = math::clamp(20.f * (ty - math::lerp(0.4f, 0.6f, noise(0.f, tx))), {-4.f, 4.f}) + 4.f; // Circles // n = 20.f * std::max(0.f, 1.f - 25.f * math::length(math::pointwise_mult({1.f, 0.1f}, math::point{tx, std::fmod(10.f * ty, 1.f)} - math::point{0.5f, 0.5f}))); bed(x, y) = n; // water(x, y) = std::max(0.f, 4.0f - bed(x, y)); // water(x, y) = 10.f - bed(x, y); // if (x == 0) // water(x, y) = 100.f - bed(x, y); } } // water(N / 2, N / 2) = 10000.f; } float const dt = 0.002f; void water_2d_app::update() { if (paused) return; float const dx = simulation_area[0].length() / N; float const dy = simulation_area[1].length() / N; float const g = 10.f; float const friction = std::pow(0.5f, dt); float const viscosity = 0.f; float const sediment_capacity = 0.1f; float const erosion = 1.f; float const deposition = 10.f; // Init boundary flows for (int x = 0; x < N; ++x) { // flowy(x, 0) = -1.f / N; // flowy(x, N) = 1.f / N; // if (x >= 3 * N / 4) flowy(x, 0) = 3.f / N; // if (x < N / 4) flowy(x, N) = -3.f / N; // flowy(x, 0) = 5.f * std::sin(10.f * time) / N; } for (int y = 0; y < N; ++y) { // flowx(0, y) = 5.f * std::sin(10.f * time) / N; // flowx(0, y) = 1.f / N; // flowx(N, y) = 1.f / N; // flowx(0, y) = 10.f * ((y + 0.5f) / N) / N; // flowx(N, y) = - (10.f / N - flowx(0, y)); // if (y < N / 4) flowx(0, y) = 3.f / N; // if (y >= 3 * N / 4) flowx(N, y) = -3.f / N; if (bed(0, y) < 0.75f) flowx(0, y) = 4.f / N; // flowx(0, y) = 5.f * math::sqr(std::max(0.f, std::sin(15.f * time))) / N; if (bed(N - 1, y) < 0.75f) flowx(N, y) = 5.f / N; } // Rain :) if (rain_on) for (int y = 0; y < N; ++y) for (int x = 0; x < N; ++x) // if (random::uniform(rng) < 0.1f) water(x, y) += random::uniform(rng) * dt; // water(x, y) += dt; // water(N/2, N/2) += 1000.f * dt; // Update X flows for (int y = 0; y < N; ++y) for (int x = 1; x < N; ++x) flowx(x, y) = friction * flowx(x, y) + g * dt * (water(x - 1, y) + bed(x - 1, y) - water(x, y) - bed(x, y)); // Update Y flows for (int y = 1; y < N; ++y) for (int x = 0; x < N; ++x) flowy(x, y) = friction * flowy(x, y) + g * dt * (water(x, y - 1) + bed(x, y - 1) - water(x, y) - bed(x, y)); // Apply viscosity to X flows for (int y = 0; y < N; ++y) { for (int x = 1; x < N; ++x) { float h = (flowx(x, y) > 0.f) ? water(x - 1, y) : water(x, y); h *= h; if (h > 0.f) flowx(x, y) *= h / (h + 3.f * dt * viscosity); } } // Apply viscosity to Y flows for (int y = 1; y < N; ++y) { for (int x = 0; x < N; ++x) { float h = (flowy(x, y) > 0.f) ? water(x, y - 1) : water(x, y); h *= h; if (h > 0.f) flowy(x, y) *= h / (h + 3.f * dt * viscosity); } } // Scale flows for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { float outflow = 0.f; outflow += std::max(0.f, - flowx(x , y )); outflow += std::max(0.f, flowx(x + 1, y )); outflow += std::max(0.f, - flowy(x , y )); outflow += std::max(0.f, flowy(x , y + 1)); float max_outflow = water(x, y) * dx * dy / dt; if (outflow > 0.f) { float scale = std::min(1.f, max_outflow / outflow); if (flowx(x, y) < 0.f) flowx(x, y) *= scale; if (flowx(x + 1, y) > 0.f) flowx(x + 1, y) *= scale; if (flowy(x, y) < 0.f) flowy(x, y) *= scale; if (flowy(x, y + 1) > 0.f) flowy(x, y + 1) *= scale; } } } // Update water and compute velocity for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { float w_old = water(x, y); water(x, y) += dt / dx / dy * (flowx(x, y) + flowy(x, y) - flowx(x + 1, y) - flowy(x, y + 1)); float w_avg = (w_old + water(x, y)) / 2.f; math::vector v{0.f, 0.f}; if (w_avg > 0.f) { v[0] = (flowx(x, y) + flowx(x + 1, y)) / 2.f / dx / w_avg; v[1] = (flowy(x, y) + flowy(x, y + 1)) / 2.f / dy / w_avg; } velocity(x, y) = v; } } if (erosion_on) { // Erosion-deposition for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { float c = sediment_capacity * water(x, y) * math::length(velocity(x, y)); if (c >= sediment(x, y)) { float delta = std::min(bed(x, y), dt * erosion * (c - sediment(x, y))); bed(x, y) -= delta; sediment(x, y) += delta; } else { float delta = std::min(sediment(x, y), dt * deposition * (sediment(x, y) - c)); bed(x, y) += delta; sediment(x, y) -= delta; } } } // Sediment transport for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { math::point p = math::lerp(simulation_area, {(x + 0.5f) / N, (y + 0.5f) / N}); p -= velocity(x, y) * dt; p = math::clamp(p, inner_area); auto q = math::unlerp(inner_area, p) * (N - 1.f); int ix = std::min(N - 2, std::floor(q[0])); int iy = std::min(N - 2, std::floor(q[1])); float tx = q[0] - ix; float ty = q[1] - iy; new_sediment(x, y) = 0.f + sediment(ix, iy) * (1.f - tx) * (1.f - ty) + sediment(ix + 1, iy) * tx * (1.f - ty) + sediment(ix, iy + 1) * (1.f - tx) * ty + sediment(ix + 1, iy + 1) * tx * ty ; } } std::swap(sediment, new_sediment); } time += dt; } void water_2d_app::present() { gl::ClearColor(0.8f, 0.8f, 0.8f, 0.8f); gl::Clear(gl::COLOR_BUFFER_BIT); math::box view_area; { view_area[0] = simulation_area[0]; view_area[1] = simulation_area[1]; view_area[2] = {-1.f, 1.f}; float extra_y = view_area[1].length() * 0.f; view_area[1].min -= extra_y; view_area[1].max += extra_y; float extra_x = view_area[1].length() * aspect_ratio - view_area[0].length(); view_area[0].min -= extra_x / 2.f; view_area[0].max += extra_x / 2.f; } math::matrix const transform = math::orthographic{view_area}.homogeneous_matrix(); painter.rect(simulation_area, {0, 0, 0, 255}); float invN = 1.f / N; for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { auto min = simulation_area.corner(x * invN, y * invN); auto max = simulation_area.corner((x + 1) * invN, (y + 1) * invN); float b = 1.f - std::exp(- 1.f * bed(x, y)); float w = 1.f - std::exp(- 1.f * water(x, y)); float s = 1.f - std::exp(- 1.f * sediment(x, y)); if (water(x, y) > 1e-3f) w = 0.5f + 0.5f * w; auto bed_color = gfx::to_coloru8(gfx::color_4f{0.96f, 0.72f, 0.53f, b}); // auto bed_color = gfx::to_coloru8(gfx::color_4f{0.3f, 0.5f, 0.1f, b}); auto color = gfx::to_coloru8(gfx::color_4f{0.f, 0.25f, 1.f, w}); auto sediment_color = gfx::to_coloru8(gfx::color_4f{1.f, 0.25f, 0.f, s * w}); auto box = math::box::singleton(min) | math::box::singleton(max); painter.rect(box, bed_color); if (show_water) { painter.rect(box, color); painter.rect(box, sediment_color); } } } if (show_velocity) { for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { auto center = simulation_area.corner((x + 0.5f) * invN, (y + 0.5f) * invN); auto v = water(x, y) * velocity(x, y) / 100.f; float max_length = 0.02f; auto l = math::length(v); auto color = gfx::to_coloru8(gfx::color_4f{1.f, std::exp(- 10.f * l), 0.f, 1.f}); float s = std::min(1.f, l / max_length); painter.line(center, center + math::normalized(v) * max_length * s, s * 0.004f, s * 0.002f, color, color, false); } } } auto mouse = math::cast(state().mouse); mouse[0] = math::lerp(view_area[0], mouse[0] / state().size[0]); mouse[1] = math::lerp(view_area[1], 1.f - mouse[1] / state().size[1]); int mx = std::floor(N * math::unlerp(simulation_area[0], mouse[0])); int my = std::floor(N * math::unlerp(simulation_area[1], mouse[1])); if (mx >= 0 && mx < N && my >= 0 && my < N) { gfx::painter::text_options opts { .scale = {0.004f, -0.004f}, .c = {255, 0, 255, 255}, }; painter.text({view_area[0].center(), math::lerp(view_area[1], 0.03f)}, std::format("b {:.3f} + w {:.3f} = h {:.3f}", bed(mx, my), water(mx, my), bed(mx, my) + water(mx, my)), opts); painter.text({view_area[0].center(), math::lerp(view_area[1], 0.05f)}, std::format("s {:.3f}", sediment(mx, my)), opts); if (state().mouse_button_down.contains(app::mouse_button::left)) { for (int dy = -2; dy <= 2; ++dy) { for (int dx = -2; dx <= 2; ++dx) { int x = mx + dx; int y = my + dy; if (x >= 0 && x < N && y >= 0 && y < N) water(x, y) += 1000.f * dt; } } } } painter.render(transform); } void water_2d_app::on_event(app::resize_event const & event) { app::application_base::on_event(event); aspect_ratio = (event.size[0] * 1.f) / event.size[1]; } void water_2d_app::on_event(app::key_event const & event) { if (event.down && event.key == app::keycode::SPACE) paused ^= true; if (event.down && event.key == app::keycode::W) show_water ^= true; if (event.down && event.key == app::keycode::V) show_velocity ^= true; if (event.down && event.key == app::keycode::E) erosion_on ^= true; if (event.down && event.key == app::keycode::R) rain_on ^= true; } namespace psemek::app { std::unique_ptr make_application_factory() { return default_application_factory({.name = "Water 2D example", .multisampling = 4}); } }