#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace psemek; namespace psemek::util { template struct fixed_vector { T * begin() { return data_; } T * end() { return data_ + size_; } T const * begin() const { return data_; } T const * end() const { return data_ + size_; } void push_back(T const & value) { assert(size_ < Max); data_[size_++] = value; } private: T data_[Max]; std::size_t size_ = 0; }; } struct stick_model { std::vector> points; std::vector> vels; std::vector movable; std::vector> sticks; std::vector stick_length; std::vector stick_solid; void add_stick(std::size_t i, std::size_t j, bool solid = true); }; void stick_model::add_stick(std::size_t i, std::size_t j, bool solid) { sticks.push_back({i, j}); stick_length.push_back(math::distance(points[i], points[j])); stick_solid.push_back(solid); } static const float ball_radius = 0.1f; struct physics_demo_app : app::application_base { physics_demo_app(options const &, context const &); ~physics_demo_app(); void on_event(app::resize_event const & event) override; void on_event(app::mouse_button_event const & event) override; void on_event(app::mouse_move_event const & event) override; void on_event(app::key_event const & event) override; void on_event(app::mouse_wheel_event const & event) override; void update() override; void present() override; math::point screen_to_world(math::point const & p) const; math::point world_to_screen(math::point const & p) const; util::clock> frame_clock; float update_time_left = 0.f; bool paused = false; stick_model model; gfx::painter painter; math::box view_region; std::optional closest_point; std::optional closest_stick; std::optional> drag_delta; std::optional new_spring_start; std::string text; util::statistics physics_update_stats; }; physics_demo_app::physics_demo_app(options const &, context const &) { view_region[0] = {0.f, 0.f}; view_region[1] = {-2.f, 5.f}; int preset = 5; if (preset == 0) { int const N = 10; for (int i = 0; i <= N; ++i) { model.points.push_back({0.f, i * 0.5f}); if (i > 0) model.add_stick(i - 1, i); } } else if (preset == 1) { int N = 10; for (int i = 0; i <= N; ++i) { float t = 1.f - (i * 1.f) / N; model.points.push_back({-(0.25f + 0.f * t), i * 0.5f}); model.points.push_back({ (0.25f + 0.f * t), i * 0.5f}); model.add_stick(2 * i + 0, 2 * i + 1); if (i > 0) { model.add_stick(2 * i - 2, 2 * i + 0); model.add_stick(2 * i - 1, 2 * i + 1); model.add_stick(2 * i - 2, 2 * i + 1); model.add_stick(2 * i - 1, 2 * i + 0); } } } else if (preset == 2) { int N = 12; float y = 1.f; model.points.push_back({0.f, y}); for (int i = 0; i < N; ++i) { float a = (2.f * math::pi * i) / N; model.points.push_back({std::cos(a) * 0.5f, y + std::sin(a) * 0.5f}); } for (int i = 0; i < N; ++i) { model.add_stick(0, i + 1); model.add_stick(i + 1, 1 + ((i + 1) % N)); } } else if (preset == 3) { model.points.push_back({-5.f, -1.f}); model.points.push_back({-4.f, -1.f}); model.points.push_back({-5.f, 3.f}); model.points.push_back({-4.f, 3.f}); model.points.push_back({-4.5f, 2.8f}); model.add_stick(0, 1); model.add_stick(0, 2); model.add_stick(0, 3); model.add_stick(1, 2); model.add_stick(1, 3); model.add_stick(2, 3); model.add_stick(2, 4); model.add_stick(3, 4); } model.vels.assign(model.points.size(), math::vector::zero()); model.movable.assign(model.points.size(), true); } physics_demo_app::~physics_demo_app() { log::info() << "Update: " << physics_update_stats; } void physics_demo_app::on_event(app::resize_event const & event) { app::application_base::on_event(event); float aspect_ratio = static_cast(event.size[0]) / event.size[1]; float xc = view_region[0].center(); float yl = view_region[1].length(); view_region[0].min = xc - yl / 2.f * aspect_ratio; view_region[0].max = xc + yl / 2.f * aspect_ratio; } void physics_demo_app::on_event(app::mouse_button_event const & event) { app::application_base::on_event(event); if (event.button == app::mouse_button::left && event.down && closest_point) { drag_delta = world_to_screen(model.points[*closest_point]) - math::cast(state().mouse); model.vels[*closest_point] = math::vector::zero(); } if (event.button == app::mouse_button::left && !event.down) { drag_delta = std::nullopt; } if (event.button == app::mouse_button::right && event.down) { if (new_spring_start) { new_spring_start = std::nullopt; } else if (closest_point) { model.movable[*closest_point] = !model.movable[*closest_point]; model.vels[*closest_point] = math::vector::zero(); } } } void physics_demo_app::on_event(app::mouse_move_event const & event) { app::application_base::on_event(event); if (!drag_delta) { closest_point = std::nullopt; closest_stick = std::nullopt; { auto const m = math::cast(state().mouse); std::size_t closest = 0; float distance = std::numeric_limits::infinity(); for (std::size_t i = 0; i < model.points.size(); ++i) { auto const d = math::distance(m, world_to_screen(model.points[i])); if (d < distance) { distance = d; closest = i; } } if (distance < 20.f) { closest_point = closest; } if (!closest_point) { std::size_t closest = 0; float distance = std::numeric_limits::infinity(); for (std::size_t i = 0; i < model.sticks.size(); ++i) { auto const d = math::distance(m, math::simplex{world_to_screen(model.points[model.sticks[i][0]]), world_to_screen(model.points[model.sticks[i][1]])}); if (d < distance) { distance = d; closest = i; } } if (distance < 20.f) { closest_stick = closest; } } } } } void physics_demo_app::on_event(app::key_event const & event) { app::application_base::on_event(event); if (event.down && event.key == app::keycode::SPACE) { paused = !paused; } else if (event.down && event.key == app::keycode::X) { if (closest_point) { model.points.erase(model.points.begin() + *closest_point); model.vels.erase(model.vels.begin() + *closest_point); model.movable.erase(model.movable.begin() + *closest_point); std::vector> new_sticks; std::vector new_stick_length; std::vector new_stick_solid; for (std::size_t i = 0; i < model.sticks.size(); ++i) { if (model.sticks[i].points[0] == *closest_point) continue; if (model.sticks[i].points[1] == *closest_point) continue; auto s = model.sticks[i]; if (s[0] > *closest_point) --s[0]; if (s[1] > *closest_point) --s[1]; new_sticks.push_back(s); new_stick_length.push_back(model.stick_length[i]); new_stick_solid.push_back(model.stick_solid[i]); } model.sticks = std::move(new_sticks); model.stick_length = std::move(new_stick_length); model.stick_solid = std::move(new_stick_solid); closest_point = std::nullopt; } if (closest_stick) { model.sticks.erase(model.sticks.begin() + *closest_stick); model.stick_length.erase(model.stick_length.begin() + *closest_stick); model.stick_solid.erase(model.stick_solid.begin() + *closest_stick); closest_stick = std::nullopt; } } else if (event.down && event.key == app::keycode::C) { if (new_spring_start && closest_point) { if (new_spring_start != closest_point) { model.add_stick(*new_spring_start, *closest_point); new_spring_start = std::nullopt; } } else if (new_spring_start) { model.points.push_back(screen_to_world(math::cast(state().mouse))); model.vels.push_back(math::vector::zero()); model.movable.push_back(false); model.add_stick(*new_spring_start, model.points.size() - 1); new_spring_start = model.points.size() - 1; } else if (closest_point) { new_spring_start = *closest_point; closest_point = std::nullopt; } else { model.points.push_back(screen_to_world(math::cast(state().mouse))); model.vels.push_back(math::vector::zero()); model.movable.push_back(false); } } else if (event.down && event.key == app::keycode::N) { model.points.push_back(screen_to_world(math::cast(state().mouse))); model.vels.push_back(math::vector::zero()); model.movable.push_back(true); } else if (event.down && event.key == app::keycode::F) { if (closest_stick) { model.stick_solid[*closest_stick] = !model.stick_solid[*closest_stick]; } } else if (event.down && event.key == app::keycode::W) { std::uint32_t const base = model.points.size(); int N = 12; auto o = screen_to_world(math::cast(state().mouse)); model.points.push_back(o); model.vels.push_back(math::vector::zero()); model.movable.push_back(true); float r = 0.5f; for (int i = 0; i < N; ++i) { float a = (2.f * math::pi * i) / N; model.points.push_back({o[0] + std::cos(a) * r, o[1] + std::sin(a) * r}); model.vels.push_back(math::vector::zero()); model.movable.push_back(true); } for (int i = 0; i < N; ++i) { model.add_stick(base, base + i + 1, false); model.add_stick(base + i + 1, base + 1 + ((i + 1) % N)); } } else if (event.down && event.key == app::keycode::B) { std::uint32_t const base = model.points.size(); auto o = screen_to_world(math::cast(state().mouse)); float w = 0.25f; model.points.push_back({o[0] - w, o[1] - w}); model.points.push_back({o[0] + w, o[1] - w}); model.points.push_back({o[0] - w, o[1] + w}); model.points.push_back({o[0] + w, o[1] + w}); model.vels.push_back(math::vector::zero()); model.vels.push_back(math::vector::zero()); model.vels.push_back(math::vector::zero()); model.vels.push_back(math::vector::zero()); model.movable.push_back(true); model.movable.push_back(true); model.movable.push_back(true); model.movable.push_back(true); model.add_stick(base + 0, base + 1); model.add_stick(base + 0, base + 2); model.add_stick(base + 0, base + 3, false); model.add_stick(base + 1, base + 2, false); model.add_stick(base + 1, base + 3); model.add_stick(base + 2, base + 3); } } void physics_demo_app::on_event(app::mouse_wheel_event const & event) { app::application_base::on_event(event); if (closest_stick) { auto & l = model.stick_length[*closest_stick]; l += event.delta * ball_radius / 2.f; l = std::max(3.f * ball_radius, l); } } void physics_demo_app::update() { if (paused) { frame_clock.restart(); return; } float const dt = 0.001f; float const spring_constant = 10000.f; float const spring_damping_constant = 200.f; float const ground_friction = 100.f; float const ground_mu = 0.3f; float const ground_bounce = 0.5f; float const ball_friction = 0.f; float const ball_bounce = 0.5f; float const stick_friction = 500.f; float const stick_bounce = 0.9f; math::vector const gravity {0.f, -10.f}; update_time_left += frame_clock.restart().count(); int update_steps = 0; while (update_time_left >= dt) { ++update_steps; if (update_steps == 32) { log::warning() << "Can't keep up with physics, skipping " << std::round(update_time_left / dt) << " updates"; update_time_left = 0.f; break; } util::clock<> timer; update_time_left -= dt; float const cell_size = ball_radius * 2.f; std::vector, 8>> ball_cells(model.points.size()); util::ndarray, 2> cells({std::ceil(view_region[0].length() / cell_size), std::ceil(view_region[1].length() / cell_size)}); float dx = view_region[0].length() / cells.width(); float dy = view_region[1].length() / cells.height(); for (std::size_t i = 0; i < model.points.size(); ++i) { int x = std::floor(cells.width() * (model.points[i][0] - view_region[0].min) / view_region[0].length()); x = math::clamp(x, {0, cells.width() - 1}); int y = std::floor(cells.height() * (model.points[i][1] - view_region[1].min) / view_region[1].length()); y = math::clamp(y, {0, cells.height() - 1}); float cx = view_region[0].min + dx * x; float cy = view_region[1].min + dy * y; if (x > 0 && model.points[i][0] - ball_radius <= cx) { if (y > 0 && model.points[i][1] - ball_radius <= cy) { cells(x - 1, y - 1).push_back(i); ball_cells[i].push_back({x - 1, y - 1}); } cells(x - 1, y).push_back(i); ball_cells[i].push_back({x - 1, y}); if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy) { cells(x - 1, y + 1).push_back(i); ball_cells[i].push_back({x - 1, y + 1}); } } if (y > 0 && model.points[i][1] - ball_radius <= cy) { cells(x, y - 1).push_back(i); ball_cells[i].push_back({x, y - 1}); } cells(x, y).push_back(i); ball_cells[i].push_back({x, y}); if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy) { cells(x, y + 1).push_back(i); ball_cells[i].push_back({x, y + 1}); } if (x + 1 < cells.width() && model.points[i][0] + ball_radius >= cx + dx) { if (y > 0 && model.points[i][1] - ball_radius <= cy) { cells(x + 1, y - 1).push_back(i); ball_cells[i].push_back({x + 1, y - 1}); } cells(x + 1, y).push_back(i); ball_cells[i].push_back({x + 1, y}); if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy) { cells(x + 1, y + 1).push_back(i); ball_cells[i].push_back({x + 1, y + 1}); } } } std::optional fixed; if (closest_point && drag_delta) { auto target = screen_to_world(math::cast(state().mouse) + *drag_delta); auto & point = model.points[*closest_point]; auto & vel = model.vels[*closest_point]; vel = vel * 0.9f + (target - point) * 0.1f / dt; point = target; fixed = closest_point; } std::vector> force(model.points.size(), math::vector::zero()); for (std::size_t i = 0; i < model.points.size(); ++i) { force[i] += gravity; } for (std::size_t i = 0; i < model.sticks.size(); ++i) { auto const & s = model.sticks[i]; auto d = model.points[s[1]] - model.points[s[0]]; auto l = math::length(d); auto n = d / l; auto f = - spring_constant * (l - model.stick_length[i]) * n / 2.f; force[s[0]] -= f; force[s[1]] += f; } for (std::size_t i = 0; i < model.points.size(); ++i) { if (fixed && i == *fixed) continue; if (!model.movable[i]) continue; model.vels[i] += dt * force[i]; } std::vector> impulse(model.points.size(), math::vector::zero()); std::vector> offset(model.points.size(), math::vector::zero()); for (std::size_t i = 0; i < model.sticks.size(); ++i) { auto const & s = model.sticks[i]; auto rel = model.vels[s[1]] - model.vels[s[0]]; auto n = math::normalized(model.points[s[1]] - model.points[s[0]]); rel = n * math::dot(rel, n); impulse[s[0]] += rel / 2.f * spring_damping_constant * dt; impulse[s[1]] -= rel / 2.f * spring_damping_constant * dt; } for (std::size_t i = 0; i < model.points.size(); ++i) { bool collision = false; math::vector n; float d; if (model.points[i][0] < view_region[0].min + ball_radius && model.vels[i][0] < 0.f) { collision = true; d = view_region[0].min + ball_radius - model.points[i][0]; n = {1.f, 0.f}; } if (model.points[i][0] > view_region[0].max - ball_radius && model.vels[i][0] > 0.f) { collision = true; d = ball_radius + model.points[i][0] - view_region[0].max; n = {-1.f, 0.f}; } if (model.points[i][1] < view_region[1].min + ball_radius && model.vels[i][1] < 0.f) { collision = true; d = view_region[1].min + ball_radius - model.points[i][1]; n = {0.f, 1.f}; } if (model.points[i][1] > view_region[1].max - ball_radius && model.vels[i][1] > 0.f) { collision = true; d = ball_radius + model.points[i][1] - view_region[1].max; n = {0.f, -1.f}; } if (collision) { offset[i] += n * d; (void)d; auto nv = n * math::dot(n, model.vels[i]); auto tv = model.vels[i] - nv; auto jn = - (1.f + ground_bounce) * nv; auto jt = - tv * ground_friction * dt; unused(ground_mu); // if (math::length(jt) > ground_mu * math::length(jn)) // { // jt = math::normalized(jt) * ground_mu * math::length(jn); // } impulse[i] += jn + jt; } } std::vector checked; checked.reserve(16); for (std::size_t i = 0; i < model.points.size(); ++i) { checked.clear(); // for (std::size_t j = i + 1; j < model.points.size(); ++j) for (auto c : ball_cells[i]) for (auto j : cells(c[0], c[1])) { if (i >= j) continue; if (std::find(checked.begin(), checked.end(), j) != checked.end()) continue; checked.push_back(j); auto r = math::distance(model.points[i], model.points[j]); if (r < ball_radius * 2.f) { auto n = (model.points[j] - model.points[i]) / r; auto rel = model.vels[j] - model.vels[i]; auto d = math::dot(rel, n); if (d < 0.f) { auto nv = n * d; auto tv = rel - nv; impulse[i] += nv * (1.f + ball_bounce) / 2.f; impulse[j] -= nv * (1.f + ball_bounce) / 2.f; impulse[i] += tv * ball_friction * dt / 2.f; impulse[j] -= tv * ball_friction * dt / 2.f; offset[i] -= n * (2.f * ball_radius - r) / 2.f / 2.f; offset[j] += n * (2.f * ball_radius - r) / 2.f / 2.f; } } } } for (std::size_t i = 0; i < model.points.size(); ++i) { for (std::size_t j = 0; j < model.sticks.size(); ++j) { if (!model.stick_solid[j]) continue; auto const & s = model.sticks[j]; if (i == s[0] || i == s[1]) continue; auto d = math::normalized(model.points[s[1]] - model.points[s[0]]); auto t = math::dot(d, model.points[i] - model.points[s[0]]) / math::distance(model.points[s[1]], model.points[s[0]]); if (t < 0.f || t > 1.f) continue; auto v = math::lerp(model.vels[s[0]], model.vels[s[1]], t); auto rel = model.vels[i] - v; auto n = math::ort(d); float r = math::dot(n, model.points[i] - model.points[s[0]]); if (math::dot(rel, n) * r >= 0.f) continue; auto nv = n * math::dot(rel, n); auto tv = rel - nv; unused(stick_friction); unused(tv); if (std::abs(r) >= ball_radius) continue; math::vector off = (r < 0.f ? -1.f : 1.f) * n * (ball_radius - std::abs(r)); unused(off); math::vector imp = - nv * (1.f + stick_bounce) - tv * stick_friction * dt; offset[i] += off * 2.f / 3.f; offset[s[0]] -= off / 3.f * (1.f - t); offset[s[1]] -= off / 3.f * t; impulse[i] += imp * 2.f / 3.f; impulse[s[0]] -= imp / 3.f * (1.f - t); impulse[s[1]] -= imp / 3.f * t; } } for (std::size_t i = 0; i < model.points.size(); ++i) { if (fixed && i == *fixed) continue; if (!model.movable[i]) continue; model.vels[i] += impulse[i]; model.points[i] += dt * model.vels[i] + offset[i]; } physics_update_stats.push(timer.count()); } } void physics_demo_app::present() { gl::Viewport(0, 0, state().size[0], state().size[1]); gl::ClearColor(0.8f, 0.8f, 0.9f, 0.f); gl::Clear(gl::COLOR_BUFFER_BIT); gl::Enable(gl::BLEND); gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); for (std::size_t i = 0; i < model.sticks.size(); ++i) { painter.line(model.points[model.sticks[i][0]], model.points[model.sticks[i][1]], model.stick_solid[i] ? ball_radius / 2.f : ball_radius / 4.f, model.stick_solid[i] ? gfx::color_rgba{0, 0, 0, 255} : gfx::color_rgba{63, 63, 63, 255}); } if (new_spring_start) { painter.line(model.points[*new_spring_start], screen_to_world(math::cast(state().mouse)), ball_radius / 2.f, gfx::red); } if (closest_stick) { painter.line(model.points[model.sticks[*closest_stick][0]], model.points[model.sticks[*closest_stick][1]], ball_radius / 2.f, gfx::red); } for (std::size_t i = 0; i < model.points.size(); ++i) { painter.circle(model.points[i], ball_radius, model.movable[i] ? gfx::black : gfx::blue); } if (closest_point) { painter.circle(model.points[*closest_point], ball_radius, gfx::red); } painter.render(math::orthographic(math::box{{view_region[0], view_region[1], {-1.f, 1.f}}}).homogeneous_matrix()); text = util::to_string("Balls: ", model.points.size(), "\n", text); { gfx::painter::text_options opts; opts.x = gfx::painter::x_align::left; opts.y = gfx::painter::y_align::top; opts.c = gfx::black; opts.f = gfx::painter::font::font_9x12; opts.scale = {2.f, 2.f}; float y = 10.f; for (std::size_t i = 0;;) { std::size_t j = text.find_first_of('\n', i); if (j > i) { painter.text({10.f, y}, std::string_view(text).substr(i, j == std::string::npos ? std::string::npos : j - i), opts); } if (j == std::string::npos) break; i = j + 1; y += 24.f; } } text.clear(); painter.render(math::window_camera{state().size[0], state().size[1]}.transform()); } math::point physics_demo_app::screen_to_world(math::point const & p) const { return math::orthographic(view_region).inverse()(math::point{2.f * p[0] / state().size[0] - 1.f, 1.f - 2.f * p[1] / state().size[1]}); } math::point physics_demo_app::world_to_screen(math::point const & p) const { auto q = math::orthographic(view_region)(p); return {(q[0] + 1.f) / 2.f * state().size[0], (1.f - q[1]) / 2.f * state().size[1]}; } namespace psemek::app { std::unique_ptr make_application_factory() { return default_application_factory({.name = "Physics example"}); } }