diff --git a/examples/water_2d.cpp b/examples/water_2d.cpp new file mode 100644 index 00000000..ce22777b --- /dev/null +++ b/examples/water_2d.cpp @@ -0,0 +1,485 @@ +#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}); + } + +}