psemek/examples/particle_life_2d.cpp

441 lines
11 KiB
C++

#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/contains.hpp>
#include <psemek/cg/kdtree.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/random/uniform_box.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/ndarray.hpp>
#include <psemek/log/log.hpp>
#include <format>
using namespace psemek;
struct particle
{
math::point<float, 2> position;
math::vector<float, 2> velocity;
float mass;
float radius;
int type;
};
struct particle_life_2d_app
: app::application_base
{
particle_life_2d_app(options const &, context const &)
{
float S = 5.f;
float W = 160.f * S;
float H = 90.f * S;
area_ = {{{-W, W}, {-H, H}}};
types_ = 10;
auto seed = random::device{}();
// Explosions
// types_ = 6;
// seed = 3940378904;
// Cells
// types_ = 8;
// seed = 3071915692;
// Cells v2
// types_ = 6;
// seed = 3337663839;
// Cool cells!
types_ = 10;
seed = 388834085;
// One cell, use central gravity
// types_ = 10;
// seed = 175098945;
// Mitochondria cell
// types_ = 10;
// seed = 1763331406;
// Just fun
// types_ = 10;
// seed = 1676103531;
// Another cell
// types_ = 10;
// seed = 2396049785;
log::info() << "Seed: " << seed;
random::generator rng{seed, 0};
random::uniform_distribution<int> dcolor(63, 255);
random::uniform_distribution<int> dtype(0, types_ - 1);
random::uniform_box_point_distribution<float, 2> darea(area_);
for (int i = 0; i < types_; ++i)
colors_.push_back({dcolor(rng), dcolor(rng), dcolor(rng), 255});
for (int i = 0; i < 4 * 1024; ++i)
{
particles_.push_back({
.position = darea(rng),
.velocity = {0.f, 0.f},
.mass = 1.f,
.radius = 1.f,
.type = dtype(rng),
// .type = random::uniform_from(rng, {4, 6}),
});
}
auto dforce = [](auto & rng)
{
return random::uniform(rng, 20.f, 100.f) * (random::uniform<bool>(rng) ? 1.f : -1.f);
};
random::uniform_distribution<float> ddistance(3.f, 20.f);
force_constants_.resize({types_, types_});
force_distance_.resize({types_, types_});
collision_distance_.resize({types_, types_});
for (int i = 0; i < types_; ++i)
for (int j = 0; j < types_; ++j)
force_constants_(i, j) = dforce(rng);
for (int i = 0; i < types_; ++i)
for (int j = 0; j < types_; ++j)
force_distance_(i, j) = ddistance(rng);
for (int i = 0; i < types_; ++i)
for (int j = 0; j < types_; ++j)
collision_distance_(i, j) = random::uniform(rng, 0.125f, 0.75f) * force_distance_(i, j);
// for (int i = 0; i < types_; ++i)
// for (int j = 0; j < types_; ++j)
// force_constants_(i, j) = -100.f * ((i == j) ? 1.f : 2.f);
max_force_distance_.resize(types_, 2.f);
for (int i = 0; i < types_; ++i)
for (int j = 0; j < types_; ++j)
math::make_max(max_force_distance_[i], force_distance_(i, j));
}
void on_event(app::mouse_wheel_event const & event) override
{
app::application_base::on_event(event);
scale_target_ *= std::pow(1.25f, -event.delta);
}
void on_event(app::key_event const & event) override
{
app::application_base::on_event(event);
if (event.down && event.key == app::keycode::SPACE)
paused_ ^= true;
}
void update() override
{
float const dt = 0.02f;
scale_ += (scale_target_ - scale_) * (- std::expm1(- 20.f * dt));
auto visible_area = area_;
visible_area = math::expand(visible_area, 0.5f * (scale_ - 1.f) * visible_area.dimensions());
float window_aspect_ratio = state().size[0] * 1.f / state().size[1];
float area_aspect_ratio = visible_area[0].length() / visible_area[1].length();
if (area_aspect_ratio < window_aspect_ratio)
{
view_area_[1] = visible_area[1];
view_area_[0] = math::expand(math::interval<float>::singleton(visible_area[0].center()), visible_area[1].length() * window_aspect_ratio * 0.5f);
}
else
{
view_area_[0] = visible_area[0];
view_area_[1] = math::expand(math::interval<float>::singleton(visible_area[1].center()), visible_area[0].length() / window_aspect_ratio * 0.5f);
}
if (!paused_)
for (int step = 0; step < 5; ++step)
{
cg::kdtree<float, 2, int> kdtree;
for (int i = 0; i < particles_.size(); ++i)
kdtree.insert({particles_[i].position, i});
float const collision_strength = 200.f;
mouseover_particle_ = std::nullopt;
if (state().mouse_button_down.contains(app::mouse_button::left))
{
math::point<float, 2> m;
m[0] = math::lerp(view_area_[0], state().mouse[0] * 1.f / state().size[0]);
m[1] = math::lerp(view_area_[1], 1.f - state().mouse[1] * 1.f / state().size[1]);
mouseover_particle_ = kdtree.closest(m).data;
}
#pragma omp parallel for
for (int i = 0; i < particles_.size(); ++i)
{
// ======== kd-tree based algorithm ========
auto & pi = particles_[i];
auto f = math::vector{0.f, 0.f};
kdtree.closer_than_map(pi.position, max_force_distance_[pi.type], [&](auto const & value){
auto j = value.data;
if (i == j)
return;
auto const & pj = particles_[j];
auto r = pj.position - pi.position;
float l = math::length(r);
auto n = r / l;
// ==== 1/R forces ====
// float lg = std::max(0.5f, l);
// auto g = n / (lg * lg);
// if (l < force_distance_(pi.type, pj.type))
// f += force_constants_(pi.type, pj.type) * g;
// float collision_distance = pi.radius + pj.radius;
// if (l < collision_distance)
// {
// float d = std::max(0.25f, l / collision_distance);
// f -= collision_strength * n * (1.f / (d * d) - 1.f);
// }
// ==== Linear forces ====
if (l < force_distance_(pi.type, pj.type))
f += force_constants_(pi.type, pj.type) * n * (1.f - l / force_distance_(pi.type, pj.type));
if (l < collision_distance_(pi.type, pj.type))
f -= 10.f * std::abs(force_constants_(pi.type, pj.type)) * n * (1.f - l / collision_distance_(pi.type, pj.type));
(void)collision_strength;
});
pi.velocity += f * dt / pi.mass;
// ======== Naive algorithm v2 ========
// auto & pi = particles_[i];
// auto f = math::vector{0.f, 0.f};
// for (int j = 0; j < particles_.size(); ++j)
// {
// if (i == j)
// continue;
// auto const & pj = particles_[j];
// auto r = pj.position - pi.position;
// float l = math::length(r);
// auto n = r / l;
// auto g = n / (l * l);
// if (l < force_distance_(pi.type, pj.type))
// f += force_constants_(pi.type, pj.type) * g;
// if (l < pi.radius + pj.radius)
// {
// float d = l / (pi.radius + pj.radius);
// f -= collision_strength * n * (1.f / (d * d) - 1.f);
// }
// }
// pi.velocity += f * dt / pi.mass;
// ======== Naive algorithm ========
// for (int j = i + 1; j < particles_.size(); ++j)
// {
// auto & pi = particles_[i];
// auto & pj = particles_[j];
// auto fi = math::vector{0.f, 0.f};
// auto fj = math::vector{0.f, 0.f};
// auto r = pj.position - pi.position;
// float l = math::length(r);
// auto n = r / l;
// auto g = n / (l * l);
// if (l < force_distance_(pi.type, pj.type))
// fi += force_constants_(pi.type, pj.type) * g;
// if (l < force_distance_(pj.type, pi.type))
// fj -= force_constants_(pj.type, pi.type) * g;
// if (l < pi.radius + pj.radius)
// {
// float d = l / (pi.radius + pj.radius);
// auto f = collision_strength * n * (1.f / (d * d) - 1.f);
// fi -= f;
// fj += f;
// }
// pi.velocity += fi * dt / pi.mass;
// pj.velocity += fj * dt / pj.mass;
// }
}
float const friction = 10.f;
float const friction_factor = std::exp(- friction * dt);
float const wall_distance = 0.f;
float const wall_force = 10.f;
float const gravity = 0.f;
float const center_gravity = 0.f;
float const line_gravity = 0.f;
float const circle_gravity = 0.f;
float const circle_radius = 100.f;
bool const circle_wall = false;
float const circle_wall_radius = 100.f;
for (auto & p : particles_)
{
auto r = p.position - p.position.zero();
p.velocity[1] -= gravity * dt;
p.velocity[1] -= line_gravity * p.position[1] * dt;
p.velocity -= center_gravity * r * dt;
p.velocity += circle_gravity * r / math::length(r) * (circle_radius - math::length(r)) * dt;
p.velocity *= friction_factor;
p.position += p.velocity * dt;
for (int d : {0, 1})
{
// if (p.position[d] < area_[d].min + p.radius)
// {
// p.position[d] = area_[d].min + p.radius;
// p.velocity[d] *= -1.f;
// }
// if (p.position[d] > area_[d].max - p.radius)
// {
// p.position[d] = area_[d].max - p.radius;
// p.velocity[d] *= -1.f;
// }
if (circle_wall)
{
auto r = p.position - p.position.zero();
auto l = math::length(r);
if (l > circle_wall_radius)
{
p.velocity -= wall_force * r / l * (l - circle_wall_radius) * dt;
}
}
else
{
if (p.position[d] < area_[d].min + wall_distance)
{
p.velocity[d] += wall_force * (area_[d].min + wall_distance - p.position[d]) * dt;
}
if (p.position[d] > area_[d].max - wall_distance)
{
p.velocity[d] -= wall_force * (p.position[d] - (area_[d].max - wall_distance)) * dt;
}
}
}
}
}
}
void present() override
{
gl::Viewport(0, 0, state().size[0], state().size[1]);
gl::ClearColor(0.02f, 0.02f, 0.02f, 1.f);
gl::Clear(gl::COLOR_BUFFER_BIT);
for (auto const & p : particles_)
{
auto c0 = colors_[p.type];
c0[3] = 15;
auto c1 = colors_[p.type];
c1[3] = 0;
painter_.circle(p.position, p.radius * 8.f, c0, c1);
}
for (auto const & p : particles_)
painter_.circle(p.position, p.radius, colors_[p.type]);
painter_.render(math::orthographic_camera{view_area_}.transform());
if (mouseover_particle_)
{
int type = particles_[*mouseover_particle_].type;
painter_.text({state().size[0] / 2.f, state().size[1] - 20.f}, std::format("Species #{}", type), {
.scale = {2.f, 2.f},
.y = gfx::painter::y_align::bottom,
.c = colors_[type],
});
}
painter_.render(math::window_camera{state().size[0], state().size[1]}.transform());
}
private:
gfx::painter painter_;
util::clock<std::chrono::duration<float>, std::chrono::high_resolution_clock> clock_;
math::box<float, 2> area_;
math::box<float, 2> view_area_;
float scale_ = 1.f;
float scale_target_ = 1.f;
int types_;
std::vector<gfx::color_rgba> colors_;
util::ndarray<float, 2> force_constants_;
util::ndarray<float, 2> force_distance_;
util::ndarray<float, 2> collision_distance_;
std::vector<float> max_force_distance_;
std::vector<particle> particles_;
std::optional<int> mouseover_particle_;
bool paused_ = true;
};
namespace psemek::app
{
std::unique_ptr<application::factory> make_application_factory()
{
return default_application_factory<particle_life_2d_app>({.name = "Particle Life 2D"});
}
}