#include #include #include #include #include #include #include #include #include #include #include #include #include using namespace psemek; static const math::vector x_axis {1.f, 0.f}; static const math::vector y_axis {0.5f, std::sqrt(0.75f)}; static const math::vector z_axis = y_axis - x_axis; static const int N = 64; static const float dt = 0.1f; static const float dx = 1.f; static float const g = 10.f; static float const friction = std::pow(0.875f, dt); math::point to_world(int x, int y) { return math::point{0.f, 0.f} + x_axis * (x - N / 2.f) + y_axis * (y - N / 2.f); } math::point to_grid(math::point const & p) { static auto matrix = *math::inverse(math::by_columns(x_axis, y_axis)); static auto zero = math::point{0.f, 0.f}; return zero + matrix * (p - zero) + math::vector{N / 2.f, N / 2.f}; } struct water_2d_hex_app : app::application_base { water_2d_hex_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; void stop() override; private: bool vsync_on_ = true; std::function set_vsync_; random::generator rng_{random::device{}}; float aspect_ratio_ = 1.f; math::vector screen_size_; math::box view_area_; gfx::painter painter_; bool paused_ = false; float time_ = 0.f; bool show_velocity_ = false; util::ndarray bed_; util::ndarray water_; util::ndarray flow_x_; // flow_x(x, y) is (x-1, y) => (x, y) util::ndarray flow_y_; // flow_y(x, y) is (x, y-1) => (x, y) util::ndarray flow_z_; // flow_z(x, y) is (x, y-1) => (x-1, y) }; water_2d_hex_app::water_2d_hex_app(options const &, context const & ctx) : set_vsync_(ctx.vsync) { set_vsync_(vsync_on_); bed_.resize({N + 1, N + 1}, 0.f); water_.resize({N + 1, N + 1}, 0.f); flow_x_.resize({N + 2, N + 1}, 0.f); flow_y_.resize({N + 1, N + 2}, 0.f); flow_z_.resize({N + 2, N + 2}, 0.f); random::uniform_sphere_vector_distribution d_grad; util::ndarray, 2> perlin_grad({17, 17}); for (auto & v : perlin_grad) v = d_grad(rng_); pcg::perlin noise(std::move(perlin_grad)); for (int y = 0; y <= N; ++y) { for (int x = 0; x <= N; ++x) { auto q = (to_world(x, y) - math::point{0.f, 0.f}) / (1.f * N); auto p = q + math::vector{0.5f, 0.5f}; (void)p; // Canyon bed_(x, y) = std::max(0.f, 10.f * std::abs(2.f * noise(p) - 1.f) - 1.5f); // Islands // bed_(x, y) = 5.f * math::smoothstep(math::clamp(math::unlerp({0.45f, 0.55f}, noise(p) - 1.f * math::length(q)), {0.f, 1.f})); water_(x, y) = 0.f; } } } void water_2d_hex_app::update() { { float y_extent = (N / 2) * y_axis[1]; view_area_[1] = {- y_extent, y_extent}; view_area_[0] = {- y_extent * aspect_ratio_, y_extent * aspect_ratio_}; } if (state().mouse_button_down.contains(app::mouse_button::left)) { auto m = math::lerp(view_area_, math::vector{state().mouse[0] * 1.f / screen_size_[0], 1.f - state().mouse[1] * 1.f / screen_size_[1]}); auto p = to_grid(m); int const R = 4; for (int dy = -R; dy <= R; ++dy) { for (int dx = -R; dx <= R; ++dx) { if (dx + dy < -R || dx + dy > R) continue; int mx = std::round(p[0]) + dx; int my = std::round(p[1]) + dy; if (mx >= 0 && mx <= N && my >= 0 && my <= N) { auto c = to_world(mx, my); water_(mx, my) += 10.f * std::exp(- 0.25f * math::distance_sqr(c, m)) * dt; } } } } if (paused_) return; prof::profiler prof("update"); time_ += dt; // Init boundary flows for (int i = 0; i <= N / 2; ++i) if (bed_(N / 2 - i, i) < 0.5f) flow_x_(N / 2 - i, i) = 300.f / N; // flow_x_(N / 2 - i, i) = 0*std::pow(std::sin(0.5f * time_) * std::sin(math::pi * (i * 4.f / N)), 5.f) * 300.f / N; for (int i = 0; i <= N / 2; ++i) if (bed_(N - i, N / 2 + i) < 0.5f) flow_x_(N - i + 1, N / 2 + i) = 300.f / N; // Update X flows for (int y = 0; y <= N; ++y) // for (int x = 1; x <= N; ++x) for (int x = std::max(1, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 - y); ++x) // if (x + y - 1 >= N / 2 && x + y <= 3 * N / 2) flow_x_(x, y) = friction * flow_x_(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) for (int x = std::max(0, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 - y); ++x) // if (x + y - 1 >= N / 2 && x + y <= 3 * N / 2) flow_y_(x, y) = friction * flow_y_(x, y) + g * dt * (water_(x, y - 1) + bed_(x, y - 1) - water_(x, y) - bed_(x, y)); // Update Z flows for (int y = 1; y <= N; ++y) // for (int x = 1; x <= N; ++x) for (int x = std::max(1, N / 2 + 1 - y); x <= std::min(N, 3 * N / 2 + 1 - y); ++x) // if (x + y - 1 >= N / 2 && x + y - 1 <= 3 * N / 2) flow_z_(x, y) = friction * flow_z_(x, y) + g * dt * (water_(x, y - 1) + bed_(x, y - 1) - water_(x - 1, y) - bed_(x - 1, y)); // Scale flows for (int y = 0; y <= N; ++y) { // for (int x = 0; x <= N; ++x) for (int x = std::max(0, N / 2 - y); x <= std::min(N, 3 * N / 2 - y); ++x) { // if (x + y >= N / 2 && x + y <= 3 * N / 2) { float outflow = 0.f; float & fin1 = flow_x_(x , y ); float & fin2 = flow_y_(x , y ); float & fin3 = flow_z_(x + 1, y ); float & fout1 = flow_x_(x + 1, y ); float & fout2 = flow_y_(x , y + 1); float & fout3 = flow_z_(x , y + 1); outflow += std::max(0.f, -fin1); outflow += std::max(0.f, -fin2); outflow += std::max(0.f, -fin3); outflow += std::max(0.f, fout1); outflow += std::max(0.f, fout2); outflow += std::max(0.f, fout3); if (outflow > 0.f) { float max_outflow = water_(x, y) * dx * dx / dt; float scale = std::min(1.f, max_outflow / outflow); fin1 *= (fin1 < 0.f ? scale : 1.f); fin2 *= (fin2 < 0.f ? scale : 1.f); fin3 *= (fin3 < 0.f ? scale : 1.f); fout1 *= (fout1 > 0.f ? scale : 1.f); fout2 *= (fout2 > 0.f ? scale : 1.f); fout3 *= (fout3 > 0.f ? scale : 1.f); } } } } // Update water for (int y = 0; y <= N; ++y) // for (int x = 0; x <= N; ++x) for (int x = std::max(0, N / 2 - y); x <= std::min(N, 3 * N / 2 - y); ++x) // if (x + y >= N / 2 && x + y <= 3 * N / 2) water_(x, y) += dt / dx / dx * (flow_x_(x, y) + flow_y_(x, y) + flow_z_(x + 1,y) - flow_x_(x + 1, y) - flow_y_(x, y + 1) - flow_z_(x, y + 1)); } void water_2d_hex_app::present() { gl::ClearColor(0.f, 0.f, 0.f, 0.f); gl::Clear(gl::COLOR_BUFFER_BIT); for (int y = 0; y < N; ++y) { for (int x = 0; x < N; ++x) { auto color = [this](int x, int y) { auto bed = gfx::color_4f{0.9f, 0.7f, 0.5f, - std::expm1(- bed_(x, y))}; auto water = gfx::color_4f{0.0f, 0.25f, 1.f, - std::expm1(- water_(x, y))}; auto color = gfx::color_4f{0.f, 0.f, 0.f, 0.f}; color = gfx::blend(color, bed); color = gfx::blend(color, water); return gfx::to_coloru8(color); }; auto p00 = to_world(x, y); auto p01 = to_world(x + 1, y); auto p10 = to_world(x, y + 1); auto p11 = to_world(x + 1, y + 1); auto b00 = color(x, y); auto b01 = color(x + 1, y); auto b10 = color(x, y + 1); auto b11 = color(x + 1, y + 1); if (x + y >= N / 2 && x + y < (3 * N) / 2) painter_.triangle(p00, p01, p10, b00, b01, b10); if (x + y + 1 >= N / 2 && x + y + 1 < (3 * N) / 2) painter_.triangle(p10, p01, p11, b10, b01, b11); } } if (show_velocity_) for (int y = 0; y <= N; ++y) { for (int x = 0; x <= N; ++x) { if (x + y >= N / 2 && x + y <= (3 * N) / 2) { auto p = to_world(x, y); auto vx = (flow_x_(x, y) + flow_x_(x + 1, y)) * 0.5f * x_axis; auto vy = (flow_y_(x, y) + flow_y_(x, y + 1)) * 0.5f * y_axis; auto vz = (flow_z_(x + 1, y) + flow_z_(x, y + 1)) * 0.5f * z_axis; auto v = (vx + vy + vz); auto M = 1.f; auto l = math::length(v); float s = std::min(1.f, l / M); float c = -std::expm1(- 0.1f * l); auto color = gfx::to_coloru8(gfx::color_4f{1.f, 1.f - c, 1.f - c, 1.f}); painter_.line(p, p + v * (s / l), 0.25f * s, 0.f, color, color, true); } } } painter_.render(math::orthographic_camera{view_area_}.transform()); } void water_2d_hex_app::on_event(app::resize_event const & event) { gl::Viewport(0, 0, event.size[0], event.size[1]); screen_size_ = event.size; aspect_ratio_ = (event.size[0] * 1.f) / event.size[1]; } void water_2d_hex_app::on_event(app::key_event const & event) { if (event.down) { switch (event.key) { case app::keycode::V: vsync_on_ ^= true; set_vsync_(vsync_on_); break; case app::keycode::C: show_velocity_ ^= true; break; case app::keycode::SPACE: paused_ ^= true; break; default: break; } } } void water_2d_hex_app::stop() { app::application_base::stop(); prof::dump(); } namespace psemek::app { std::unique_ptr make_application_factory() { return default_application_factory({.name = "Water 2D hex example", .multisampling = 4}); } }