#include #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 = 256; float time = 0.f; util::ndarray bed; util::ndarray water; util::ndarray flowx; util::ndarray flowy; util::ndarray, 2> velocity; util::ndarray sediment; util::ndarray new_sediment; gfx::painter painter; bool paused = true; bool show_water = true; bool show_velocity = false; bool show_particles = false; bool erosion_on = false; bool rain_on = false; struct particle { math::point position; int lifetime; }; std::vector particles; }; 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::ndarray, 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; [[maybe_unused]] float tx = (x + 0.5f) / N; [[maybe_unused]] 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)))); // n = 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(x, y) = std::max(0.f, 1.f - bed(x, y)) * (0.5f + 0.5f * noise(tx, ty)); flowx(x, y) = (2.f * ty - 1.f) * water(x, y) * 10.f / N; } } // water(N / 2, N / 2) = 10000.f; } float const dt = 0.001f; 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(1.f, dt); float const viscosity = 0.f; float const sediment_capacity = 0.1f; float const erosion = 1.f; float const deposition = 10.f; float const coriolis = 0.01f; int const max_particles = 16 * 1024; int const particles_per_frame = 16; int const max_particle_lifetime = max_particles / particles_per_frame; // 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; flowy(x, 0) = 0.f; flowy(x, N) = 0.f; } for (int y = 0; y < N; ++y) { // flowx(0, y) = 5.f * std::sin(10.f * time) / N; // flowx(0, y) = 10.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)); flowx(0, y) = friction * flowx(0, y) + g * dt * (water(N - 1, y) + bed(N - 1, y) - water(0, y) - bed(0, 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) % N, 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) % N, y) > 0.f) flowx((x + 1) % N, 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) % N, 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) % N, y)) / 2.f / dx / w_avg; v[1] = (flowy(x, y) + flowy(x, y + 1)) / 2.f / dy / w_avg; } velocity(x, y) = v; } } // Add coriolis force for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { auto n = math::ort(velocity(x, y)) * coriolis * dt / 2.f * std::sin(float(math::pi) / 2.f * ((y + 0.5f) * 2.f / N - 1.f)); flowx(x, y) += n[0]; flowx((x + 1) % N, y) += n[0]; flowy(x, y) += n[1]; flowy(x, y + 1) += n[1]; } } 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); } // Init particles for (int i = 0; i < particles_per_frame; ++i) if (particles.size() < max_particles) { math::point pos{random::uniform(rng, 0.f, N), random::uniform(rng, 0.f, N)}; if (water(std::floor(pos[0]), std::floor(pos[1])) > 1e-3f) particles.push_back({pos, 0}); } // Update particles for (auto & p : particles) { p.position += velocity(std::min(N - 1, std::floor(p.position[0])), std::min(N - 1, std::floor(p.position[1]))) * (N * dt); p.position[0] = math::fmod(p.position[0], float(N)); p.position[1] = math::clamp(p.position[1], {0.f, N}); p.lifetime += 1; } // Destroy particles for (int i = 0; i < particles.size();) { if (particles[i].lifetime >= max_particle_lifetime) { std::swap(particles[i], particles.back()); particles.pop_back(); } else ++i; } 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) / 40.f; float max_length = 0.05f; auto l = math::length(v); // auto color = gfx::to_coloru8(gfx::color_4f{1.f, std::exp(- 10.f * l), 0.f, 1.f}); auto color = gfx::color_rgba{255, 255, 255, 255}; float s = std::min(1.f, l / max_length); // s = 1.f; auto d = math::normalized(v) * max_length * s / 2.f; painter.line(center - d, center + d, s * 0.001f, 0.f, color, color, false); } } } if (show_particles) { for (auto const & p : particles) { auto pos = math::lerp(simulation_area, math::vector{p.position[0] / N, p.position[1] / N}); painter.circle(pos, 0.005f, {255, 255, 255, 127}, 6); } } 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); { painter.text({20.f, 20.f}, std::format("{} particles", particles.size()), {.scale = {2.f, 2.f}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); painter.render(math::window_camera{state().size[0], state().size[1]}.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::P) show_particles ^= 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}); } }