diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 12b4eadc..50c7ffac 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,12 +2,17 @@ find_package(ZLIB REQUIRED) file(GLOB PSEMEK_EXAMPLES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +find_package(OpenMP) + foreach(example ${PSEMEK_EXAMPLES}) get_filename_component(TARGET_NAME "${example}" NAME_WLE) set(TARGET_NAME psemek-example-${TARGET_NAME}) psemek_add_application(${TARGET_NAME} ${example}) if(TARGET ${TARGET_NAME}) target_link_libraries(${TARGET_NAME} PUBLIC psemek ZLIB::ZLIB) + if(OpenMP_CXX_FOUND) + target_link_libraries(${TARGET_NAME} PUBLIC OpenMP::OpenMP_CXX) + endif() target_compile_definitions(${TARGET_NAME} PUBLIC -DPSEMEK_EXAMPLES_DIR="${CMAKE_CURRENT_SOURCE_DIR}") endif() endforeach() diff --git a/examples/soft_creatures_2d.cpp b/examples/soft_creatures_2d.cpp new file mode 100644 index 00000000..2b650cc8 --- /dev/null +++ b/examples/soft_creatures_2d.cpp @@ -0,0 +1,955 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace psemek; + +static float const sim_dt = 0.005f; +static float const gravity = 25.f; +static float const air_friction = 0.1f; +static float const ground_friction = 100.f; +static float const hills_friction = 100.f; +static float const spring_damping = 10.f; +static float const max_spring_force = 25000.f; + +static geom::interval const cell_size_range{1.f, 1.f}; +static geom::interval const cell_period_range{0.25f, 4.f}; +static float const muscle_expand_extent = 0.5f; + +static int const population_size = 4 * 1024; + +static float const simulation_time = 30.f; +static float const display_simulation_time = 30.f; + +enum class cell_type +{ + hard_tissue, + soft_tissue, + muscle, + dead, +}; + +struct cell_info +{ + cell_type type; + float size = 1.f; + float phase = 0.f; + float period = 1.f; +}; + +float spring_force(cell_type type) +{ + switch (type) + { + case cell_type::soft_tissue: + return 2000.f; + case cell_type::hard_tissue: + return 5000.f; + case cell_type::muscle: + return 2000.f; + case cell_type::dead: + return 100.f; + } + + return 0.f; +} + +static geom::vector const neighbours[4] = +{ + {-1, 0}, + { 1, 0}, + { 0, -1}, + { 0, 1}, +}; + +struct map +{ + geom::vector ground_normal{0.f, 1.f}; + + std::vector> ground_points; +}; + +using creature_blueprint = std::unordered_map, cell_info>; + +struct creature +{ + struct cell + { + std::uint32_t indices[4]; + cell_info info; + }; + + creature_blueprint blueprint; + int generation = 0; + std::optional score = std::nullopt; + + std::vector> positions; + std::vector> velocities; + std::vector cells; + + std::vector> outer_edges; + + geom::point center() const; + geom::vector center_velocity() const; + void translate(geom::vector const & delta); + void push(geom::vector const & delta_velocity); + + bool dead() const; + + void update(float dt); + void collide(map const & map, float dt); + + void compute_outer_edges(); +}; + +geom::point creature::center() const +{ + geom::vector sum{0.f, 0.f}; + for (int i = 1; i < positions.size(); ++i) + { + sum += positions[i] - positions[0]; + } + return positions[0] + sum / (1.f * positions.size()); +} + +geom::vector creature::center_velocity() const +{ + geom::vector sum{0.f, 0.f}; + for (int i = 1; i < velocities.size(); ++i) + { + sum += velocities[i]; + } + return sum / (1.f * velocities.size()); +} + +void creature::translate(geom::vector const & delta) +{ + for (auto & pos : positions) + pos += delta; +} + +void creature::push(geom::vector const & delta_velocity) +{ + for (auto & vel : velocities) + vel += delta_velocity; +} + +bool creature::dead() const +{ + for (auto const & cell : cells) + if (cell.info.type == cell_type::dead) + return true; + return false; +} + +void creature::update(float dt) +{ + for (auto & vel : velocities) + vel[1] -= gravity * dt; + + for (auto & vel : velocities) + { + auto v = geom::length(vel); + vel -= air_friction * v * vel * dt; + } + + for (int i = 0; i < positions.size(); ++i) + positions[i] += velocities[i] * dt; + + static geom::vector const deltas[4] + { + {-0.5f, -0.5f}, + { 0.5f, -0.5f}, + { 0.5f, 0.5f}, + {-0.5f, 0.5f}, + }; + + for (auto & cell : cells) + { + float size = cell.info.size; + + if (cell.info.type == cell_type::muscle) + { + cell.info.phase += dt / cell.info.period; + while (cell.info.phase >= 1.f) + cell.info.phase -= 1.f; + size *= 1.f + muscle_expand_extent * std::sin(cell.info.phase * 2.f * geom::pi); + } + + geom::point center{0.f, 0.f}; + for (auto idx : cell.indices) + { + center += (positions[idx] - geom::point{0.f, 0.f}) * 0.25f; + } + + float A = 0.f; + float B = 0.f; + + for (int i = 0; i < 4; ++i) + { + auto const p = positions[cell.indices[i]] - center; + auto const q = deltas[i] * size; + + A += geom::dot(p, q); + B += geom::det(p, q); + } + + float const angle = - std::atan2(B, A); + + for (int i = 0; i < 4; ++i) + { + auto const target = center + geom::rotate(deltas[i] * size, angle); + + auto force = spring_force(cell.info.type) * (target - positions[cell.indices[i]]); + if (auto f = geom::length(force); f > max_spring_force) + cell.info.type = cell_type::dead; + velocities[cell.indices[i]] += force * dt; + } + + auto cmvel = geom::vector{0.f, 0.f}; + auto rotation = 0.f; + for (auto idx : cell.indices) + { + cmvel += velocities[idx] * 0.25f; + rotation += geom::det(positions[idx] - center, velocities[idx]) * (0.25f / geom::length_sqr(positions[idx] - center)); + } + + for (auto idx : cell.indices) + { + auto target_vel = cmvel + geom::ort(positions[idx] - center) * rotation; + + velocities[idx] += (target_vel - velocities[idx]) * (1.f - std::exp(- spring_damping * dt)); + } + } +} + +void creature::collide(map const & map, float dt) +{ + for (int i = 0; i < positions.size(); ++i) + { + auto delta = positions[i] - geom::point{0.f, 0.f}; + auto dist = geom::dot(delta, map.ground_normal); + if (dist < 0.f) + { + positions[i] -= dist * map.ground_normal; + + auto tangent = geom::ort(map.ground_normal); + auto vn = geom::dot(velocities[i], map.ground_normal); + auto vt = geom::dot(velocities[i], tangent); + + vn = std::max(0.f, vn); + vt *= std::exp(- dt * ground_friction); + + velocities[i] = map.ground_normal * vn + tangent * vt; + } + + auto it = std::upper_bound(map.ground_points.begin(), map.ground_points.end(), positions[i][0], [](float v, auto const & p){ return v < p[0]; }); + if (it != map.ground_points.begin() && it != map.ground_points.end()) + { + auto j = (it - map.ground_points.begin()) - 1; + + auto n = geom::normalized(geom::ort(map.ground_points[j + 1] - map.ground_points[j])); + float dist = geom::dot(positions[i] - map.ground_points[j], n); + + if (dist < 0.f) + { + positions[i] -= dist * n; + + auto tangent = geom::ort(n); + auto vn = geom::dot(velocities[i], n); + auto vt = geom::dot(velocities[i], tangent); + + vn = std::max(0.f, vn); + vt *= std::exp(- dt * hills_friction); + + velocities[i] = n * vn + tangent * vt; + } + } + } +} + +void creature::compute_outer_edges() +{ + util::hash_set> edge_set; + + for (auto const & cell : cells) + { + for (int i = 0; i < 4; ++i) + { + geom::segment segment; + segment[0] = cell.indices[i]; + segment[1] = cell.indices[(i + 1) % 4]; + edge_set.insert(segment); + } + } + + outer_edges.clear(); + for (auto const & edge : edge_set) + { + auto dual = edge; + std::swap(dual[0], dual[1]); + if (!edge_set.contains(dual)) + outer_edges.push_back(edge); + } +} + +gfx::color_rgba cell_color(cell_type type) +{ + switch (type) + { + case cell_type::soft_tissue: + return {255, 255, 255, 255}; + case cell_type::hard_tissue: + return {127, 127, 127, 255}; + case cell_type::muscle: + return {255, 127, 127, 255}; + case cell_type::dead: + return {0, 0, 0, 0}; + } + + return {255, 0, 255, 255}; +} + +void draw(gfx::painter & painter, creature const & creature) +{ + for (auto const & cell : creature.cells) + { + geom::point center{0.f, 0.f}; + for (auto idx : cell.indices) + center += 0.25f * (creature.positions[idx] - geom::point{0.f, 0.f}); + + gfx::color_rgba const color = cell_color(cell.info.type); + gfx::color_rgba const bgcolor = gfx::dark(color, 0.25f); + + geom::point ps[4]; + for (int i = 0; i < 4; ++i) + ps[i] = creature.positions[cell.indices[i]]; + + painter.triangle(ps[0], ps[1], ps[2], bgcolor); + painter.triangle(ps[2], ps[0], ps[3], bgcolor); + + for (int i = 0; i < 4; ++i) + ps[i] = geom::lerp(ps[i], center, 0.25f); + + painter.triangle(ps[0], ps[1], ps[2], color); + painter.triangle(ps[2], ps[0], ps[3], color); + } + + for (auto const & edge : creature.outer_edges) + { + gfx::color_rgba const color{0, 0, 0, 255}; + float const width = 0.125f; + painter.line(creature.positions[edge.points[0]], creature.positions[edge.points[1]], width, color, false); + } +} + +struct creature_builder +{ + creature_builder() = default; + + creature_builder(creature_blueprint blueprint) + : blueprint_(std::move(blueprint)) + {} + + void add(geom::point const position, cell_info const & info) + { + blueprint_[position] = info; + } + + bool contains(geom::point const & position) const + { + return blueprint_.contains(position); + } + + creature build(int generation) + { + static geom::vector const vertex_delta[4] + { + {0, 0}, + {1, 0}, + {1, 1}, + {0, 1}, + }; + + creature result; + result.generation = generation; + + util::hash_map, std::uint32_t> vertex_id; + + for (auto const & cell : blueprint_) + { + auto & cell_out = result.cells.emplace_back(); + cell_out.info = cell.second; + for (int i = 0; i < 4; ++i) + { + geom::point vertex = cell.first + vertex_delta[i]; + if (auto it = vertex_id.find(vertex); it != vertex_id.end()) + { + cell_out.indices[i] = it->second; + } + else + { + cell_out.indices[i] = (vertex_id[vertex] = result.positions.size()); + result.positions.push_back(geom::cast(vertex)); + result.velocities.push_back(geom::vector::zero()); + } + } + } + + result.blueprint = std::move(blueprint_); + result.compute_outer_edges(); + return result; + } + + bool connected() const + { + if (blueprint_.empty()) + return false; + + util::hash_set> visited; + + std::deque> queue; + queue.push_back(blueprint_.begin()->first); + visited.insert(queue.back()); + + while (!queue.empty()) + { + auto cur = queue.front(); + queue.pop_front(); + + for (auto n : neighbours) + { + auto nn = cur + n; + if (blueprint_.contains(nn) && !visited.contains(nn)) + { + visited.insert(nn); + queue.push_back(nn); + } + } + } + + return visited.size() == blueprint_.size(); + } + +private: + creature_blueprint blueprint_; +}; + +int const display_creature_count = 4; + +cell_info random_cell(random::generator & rng) +{ + cell_info result; + result.size = random::uniform(rng, cell_size_range); + if (auto t = random::uniform(rng, 0, 2); t == 0) + result.type = cell_type::hard_tissue; + else if (t == 1) + result.type = cell_type::soft_tissue; + else + { + result.type = cell_type::muscle; + result.phase = random::uniform(rng); + result.period = random::uniform(rng, cell_period_range); + } + return result; +} + +struct soft_creatures_2d_app + : app::application_base +{ + soft_creatures_2d_app(options const &, context const &) + { + random::generator rng{random::device{}}; + +// map_.ground_points.push_back({5.f, 0.f}); +// for (int x = 10; x <= 200; x += 1) +// map_.ground_points.push_back({x / 2.f, random::uniform(rng, 0.f, std::min(x, 200) / 200.f * 4.f)}); +// map_.ground_points.push_back({x, std::min(2.f, geom::sqr(x - 10) * 0.25f)}); +// map_.ground_points.push_back({x, random::uniform(rng, 0.f, 4.f)}); + + for (int species = 0; species < population_size; ++species) + { + while (true) + { + int x_size = random::uniform(rng, 2, 7); + int y_size = random::uniform(rng, 2, 5); + + x_size = 1; + y_size = 1; + + creature_builder builder; + for (int x = 0; x < x_size; ++x) + { + for (int y = 0; y < y_size; ++y) + { + if (random::uniform(rng) > 0.75f) + continue; + + builder.add({x, y}, random_cell(rng)); + } + } + + if (builder.connected()) + { + population_.push_back(builder.build(0)); + break; + } + } + + if (species < display_creature_count) + display_creatures_.push_back(population_.back()); + } + + simulator_thread_ = util::thread([this]{ run_simulation(); }); + } + + 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 stop() override + { + app::application_base::stop(); + stopping_.store(true); + simulator_thread_.join(); + } + + void update() override + { + if (state().key_down.contains(app::keycode::ESCAPE)) + stop(); + + if (state().key_down.contains(app::keycode::RIGHT) || simulation_time_ >= display_simulation_time) + get_next_display_creatures(); + + float const frame_dt = frame_clock_.restart().count(); + + if (!paused_) physics_lag_ += frame_dt; + + while (physics_lag_ >= sim_dt) + { + physics_lag_ -= sim_dt; + simulation_time_ += sim_dt; + for (auto & creature : display_creatures_) + { + creature.update(sim_dt); + creature.collide(map_, sim_dt); + } + } + + float max_view_x = 0.f; + for (int c = 0; c < display_creatures_.size(); ++c) + for (auto const & p : display_creatures_[c].positions) + geom::make_max(max_view_x, p[0]); + max_view_x = std::max(max_view_x + 10.f, max_view_x_); + + max_view_x_ += (max_view_x - max_view_x_) * (1.f - std::exp(- 4.f * frame_dt)); + } + + void present() override + { + gl::ClearColor(0.5f, 0.6f, 0.8f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + for (int c = 0; c < display_creatures_.size(); ++c) + { + float aspect_ratio = state().size[0] * 1.f / state().size[1] * display_creatures_.size(); + geom::box view_box = {{{0.f, 0.f}, {0.f, 0.f}}}; + view_box[0] = {max_view_x_ - display_width, max_view_x_}; + view_box[1] = {0.f, view_box[0].length() / aspect_ratio}; + view_box[1] -= 2.f; + + geom::box ground_box; + ground_box[0] = geom::interval::full(); + ground_box[1] = {geom::limits::min(), 0.f}; + ground_box = ground_box & view_box; + gfx::color_rgba ground_color{127, 91, 65, 255}; + painter_.rect(ground_box, ground_color); + + gfx::color_rgba hills_color{91, 127, 65, 255}; + for (int i = 0; i + 1 < map_.ground_points.size(); ++i) + { + float x0 = map_.ground_points[i ][0]; + float x1 = map_.ground_points[i + 1][0]; + + float y0 = map_.ground_points[i ][1]; + float y1 = map_.ground_points[i + 1][1]; + + painter_.triangle({x0, 0.f}, {x1, 0.f}, {x1, y1}, hills_color); + painter_.triangle({x0, 0.f}, {x1, y1}, {x0, y0}, hills_color); + } + + int ruler_min = std::ceil(view_box[0].min / 5.f); + int ruler_max = std::floor(view_box[0].max / 5.f); + + for (int r = ruler_min; r <= ruler_max; ++r) + { + float x = r * 5.f; + float y = ((r % 2) == 0) ? -1.f : -0.5f; + y = std::max(y, view_box[1].min); + + float scale = 2.f * view_box[0].length() / state().size[0]; + + painter_.line({x, 0.f}, {x, y}, 0.125f, {255, 255, 255, 255}, false); + painter_.text3d({x + 0.1f, -0.5f, 0.f}, util::to_string(r * 5), {.scale = scale, .x = gfx::painter::x_align::left, .c = {255, 255, 255, 127}}, geom::scale({1.f, -1.f, 1.f}).linear_matrix()); + } + + draw(painter_, display_creatures_[c]); + + float height = view_box[1].length(); + view_box[1].max += c * height; + view_box[1].min = view_box[1].max - display_creatures_.size() * height; + painter_.render(geom::orthographic_camera(view_box).transform()); + } + + int text_row = 0; + auto put_line = [&](std::string const & line) + { + painter_.text({13.f, 10.f + text_row * 24.f}, line, {.scale = 2.f, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); + painter_.text({12.f, 9.f + text_row * 24.f}, line, {.scale = 2.f, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + ++text_row; + }; + + int simulated_generation = 0; + float best_score = 0.f; + int best_generation = 0; + float avg_score = 0.f; + { + std::lock_guard lock{next_display_mutex_}; + simulated_generation = next_display_generation_ + 1; + best_score = generation_best_score_; + best_generation = generation_best_generation_; + avg_score = generation_avg_score_; + } + + put_line(util::to_string("Simulating generation: ", simulated_generation)); + put_line(util::to_string("Best score: ", best_score, " (gen ", best_generation, ")")); + put_line(util::to_string("Average score: ", avg_score)); + put_line(util::to_string("Display generation: ", display_generation_)); + put_line(util::to_string("Time: ", simulation_time_)); + + for (int c = 0; c < display_creatures_.size(); ++c) + { + float x = state().size[0] - 12.f; + float y = (c * state().size[1] * 1.f) / display_creatures_.size() + 9.f; + + auto text = util::to_string("Gen ", display_creatures_[c].generation); + painter_.text({x + 1.f, y + 1.f}, text, {.scale = 2.f, .x = gfx::painter::x_align::right, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); + painter_.text({x, y}, text, {.scale = 2.f, .x = gfx::painter::x_align::right, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + + if (display_creatures_[c].score) + { + y += 24.f; + auto text = util::to_string("Score ", *display_creatures_[c].score); + painter_.text({x + 1.f, y + 1.f}, text, {.scale = 2.f, .x = gfx::painter::x_align::right, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); + painter_.text({x, y}, text, {.scale = 2.f, .x = gfx::painter::x_align::right, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + } + } + + for (int i = 1; i < display_creature_count; ++i) + { + float y = (state().size[1] * i * 1.f) / display_creature_count; + painter_.line({0.f, y}, {state().size[0], y}, 4.f, {0, 0, 0, 255}, false); + } + + painter_.render(geom::window_camera(state().size[0], state().size[1]).transform()); + } + +private: + gfx::painter painter_; + + util::clock<> frame_clock_; + + map map_; + + util::thread simulator_thread_; + std::vector population_; + + std::mutex next_display_mutex_; + std::vector next_display_creatures_; + bool has_next_creatures_ = false; + int next_display_generation_ = 0; + float generation_best_score_ = 0.f; + float generation_avg_score_ = 0.f; + int generation_best_generation_ = 0; + + float simulation_time_ = 0.f; + float physics_lag_ = 0.f; + bool paused_ = false; + std::vector display_creatures_; + int display_generation_ = 0; + float const display_width = 70.f; + float max_view_x_ = display_width - 5.f; + + std::atomic stopping_{false}; + + void get_next_display_creatures() + { + bool new_creatures = false; + + std::lock_guard lock{next_display_mutex_}; + if (has_next_creatures_) + { + display_creatures_ = std::move(next_display_creatures_); + physics_lag_ = 0.f; + simulation_time_ = 0.f; + has_next_creatures_ = false; + display_generation_ = next_display_generation_; + max_view_x_ = display_width - 5.f; + new_creatures = true; + } + + if (new_creatures) + { + for (auto & creature : display_creatures_) + { + auto center = creature.center(); + float radius = -std::numeric_limits::infinity(); + for (auto const & pos : creature.positions) + geom::make_max(radius, geom::distance(pos, center)); + + float angle = 0.f; + + for (auto & pos : creature.positions) + pos = geom::vector{0.f, radius} + center + geom::rotate(pos - center, angle); + } + } + } + + void run_simulation() + { + static thread_local random::generator rng{random::device{}}; + random::normal_distribution randn; + + int generation = 0; + + while (!stopping_) + { + int const test_iterations = std::round(simulation_time / sim_dt); + + ++generation; + + std::vector evaluated_creatures(population_.size()); + + #pragma omp parallel for + for (int i = 0; i < population_.size(); ++i) + { + auto & creature = population_[i]; + if (!creature.score) + { + auto sim_creature = creature; + + float muscle_power = 0.f; + for (auto const & cell : sim_creature.cells) + if (cell.info.type == cell_type::muscle) + muscle_power += std::pow(cell.info.period, -1.f); + + auto center = sim_creature.center(); + float radius = -std::numeric_limits::infinity(); + for (auto const & pos : sim_creature.positions) + geom::make_max(radius, geom::distance(pos, center)); + + auto angle = random::uniform(rng, -1.f, 1.f) * geom::rad(30.f); + angle = 0.f; + + for (auto & pos : sim_creature.positions) + pos = geom::vector{0.f, radius} + center + geom::rotate(pos - center, angle); + + auto start_pos = sim_creature.center(); + float max_y = start_pos[1]; + float sum_speed_x = 0.f; + float sum_speed_x2 = 0.f; + + for (int iteration = 0; iteration < test_iterations; ++iteration) + { + sim_creature.update(sim_dt); + sim_creature.collide(map_, sim_dt); + geom::make_max(max_y, sim_creature.center()[1]); + + auto speed = sim_creature.center_velocity(); + sum_speed_x += speed[0]; + sum_speed_x2 += speed[0] * speed[0]; + } + auto end_pos = sim_creature.center(); + + float avg_speed = sum_speed_x / test_iterations; + float var_speed = sum_speed_x2 / test_iterations - avg_speed * avg_speed; + + float total_speed = (end_pos[0] - start_pos[0]) / simulation_time; + + (void)total_speed; + (void)avg_speed; + (void)var_speed; + (void)muscle_power; + + float score = muscle_power > 0.f ? (total_speed) : 0.f; +// float score = muscle_power > 0.f ? (total_speed / std::sqrt(muscle_power)) : 0.f; +// float score = muscle_power > 0.f ? (total_speed * (sim_creature.cells.size())) : 0.f; +// float score = muscle_power > 0.f ? (total_speed / std::sqrt(muscle_power) * std::sqrt(sim_creature.cells.size())) : 0.f; + // float score = muscle_power > 0.f ? (total_speed / muscle_power / std::sqrt(sim_creature.cells.size())) : 0.f; + + if (sim_creature.dead()) + score = 0.f; + + creature.score = score; + } + evaluated_creatures[i] = std::move(creature); + } + + if (stopping_) + break; + + int const best = population_.size() / 4; + int const children_count = population_.size() - best; + + population_.clear(); + + std::partial_sort(evaluated_creatures.begin(), evaluated_creatures.begin() + best, evaluated_creatures.end(), [](auto const & a, auto const & b){ return *a.score > *b.score; }); + evaluated_creatures.resize(best); + population_ = std::move(evaluated_creatures); + + float best_score = *population_.front().score; + int best_generation = population_.front().generation; + + float avg_score = 0.f; + for (auto const & p : population_) + avg_score += *p.score; + avg_score /= population_.size(); + + float const mutation_boost = 1.f; //std::exp2(std::min(5.f, (generation - best_generation) / 10.f)); + + float const change_type_probability = 1.f / 8.f * mutation_boost; + float const erase_probability = 1.f / 8.f * mutation_boost; + float const grow_probability = 1.f / 8.f * mutation_boost; + float const sex_probability = 0.9f * mutation_boost; + + std::vector next_display; + for (int i = 0; i < display_creature_count; ++i) +// next_display.push_back(creatures_with_score[i * creatures_with_score.size() / display_creature_count].first); + next_display.push_back(population_[i]); + + std::vector scores(population_.size()); + for (int i = 0; i < population_.size(); ++i) + scores[i] = *population_[i].score; + + random::weighted_distribution random_parent(std::move(scores)); + + for (int i = 0; i < children_count; ++i) + { + auto const & parent1 = population_[random_parent(rng)]; + auto const & parent2 = population_[random_parent(rng)]; + + creature_blueprint blueprint = parent1.blueprint; + + if (random::uniform(rng) < sex_probability) + { + for (auto const & cell : parent2.blueprint) + if (random::uniform(rng, 0, 1) == 0) + blueprint[cell.first] = cell.second; + } + + std::vector> erase_cells; + for (auto const & cell : blueprint) + { + if (random::uniform(rng) < erase_probability) + erase_cells.push_back(cell.first); + } + + for (auto cell : erase_cells) + blueprint.erase(cell); + + std::vector, cell_info>> grow_cells; + for (auto const & cell : blueprint) + { + for (auto n : neighbours) + { + auto ncell = cell.first + n; + + if (blueprint.contains(ncell)) + continue; + + if (random::uniform(rng) < grow_probability) + grow_cells.push_back({ncell, random_cell(rng)}); + } + } + + for (auto cell : grow_cells) + blueprint.insert(cell); + + creature new_creature; + + { + creature_builder builder(std::move(blueprint)); + if (builder.connected()) + new_creature = builder.build(generation); + else + new_creature = parent1; + } + + for (auto & cell : new_creature.cells) + { + if (random::uniform(rng) < change_type_probability) + { + cell.info = random_cell(rng); + new_creature.score = std::nullopt; + new_creature.generation = generation; + } + + if (cell.info.type == cell_type::muscle) + { + cell.info.size += randn(rng) * 0.1f * mutation_boost; + cell.info.phase += randn(rng) * 0.1f * mutation_boost; + cell.info.period += randn(rng) * 0.1f * mutation_boost; + + cell.info.size = geom::clamp(cell.info.size, cell_size_range); + cell.info.phase = std::fmod(cell.info.phase, 1.f); + cell.info.period = geom::clamp(cell.info.period, cell_period_range); + + new_creature.score = std::nullopt; + new_creature.generation = generation; + } + } + + population_.push_back(std::move(new_creature)); + } + + std::lock_guard lock{next_display_mutex_}; + next_display_creatures_ = std::move(next_display); + next_display_generation_ = generation; + has_next_creatures_ = true; + generation_best_score_ = best_score; + generation_avg_score_ = avg_score; + generation_best_generation_ = best_generation; + } + } +}; + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Soft-body creatures", .multisampling = 4}); + } + +} diff --git a/examples/soft_plants_2d.cpp b/examples/soft_plants_2d.cpp new file mode 100644 index 00000000..8751312f --- /dev/null +++ b/examples/soft_plants_2d.cpp @@ -0,0 +1,1071 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace psemek; + +static float const sim_dt = 0.005f; +static float const gravity = 25.f; +static float const air_friction = 1.f; +static float const ground_friction = 100.f; +static float const hills_friction = 100.f; +static float const spring_damping = 10.f; +static float const max_spring_force = 25000.f; +static float const collision_force = 1000.f; +static float const fixed_root_force = 1000.f; +static float const creature_lifetime = 9.5f; + +struct hard_tissue +{}; + +struct soft_tissue +{}; + +struct leaf +{}; + +struct root +{ + bool fixed = false; +}; + +struct flower +{}; + +using cell_data = std::variant; + +float spring_force(cell_data const & type) +{ + return std::visit(util::overload( + [](hard_tissue const &){ return 5000.f; }, + [](soft_tissue const &){ return 2000.f; }, + [](leaf const &){ return 500.f; }, + [](root const &){ return 5000.f; }, + [](flower const &){ return 500.f; } + ), type); +} + +float size(cell_data const & type) +{ + return std::visit(util::overload( + [](auto const &){ return 1.f; } + ), type); +} + +float lifetime(cell_data const & type) +{ + return std::visit(util::overload( + [](hard_tissue const &){ return creature_lifetime; }, + [](soft_tissue const &){ return creature_lifetime; }, + [](leaf const &){ return creature_lifetime; }, + [](root const &){ return creature_lifetime; }, + [](flower const &){ return creature_lifetime; } + ), type); +} + +float opacity(cell_data const & type) +{ + return std::visit(util::overload( + [](hard_tissue const &){ return 1.f; }, + [](soft_tissue const &){ return 0.75f; }, + [](leaf const &){ return 0.25f; }, + [](root const &){ return 1.f; }, + [](flower const &){ return 0.5f; } + ), type); +} + +gfx::color_rgba cell_color(cell_data type) +{ + return std::visit(util::overload( + [](hard_tissue const &){ return gfx::color_rgba{127, 127, 127, 255}; }, + [](soft_tissue const &){ return gfx::color_rgba{255, 255, 255, 255}; }, + [](leaf const &){ return gfx::color_rgba{127, 255, 65, 255}; }, + [](root const &){ return gfx::color_rgba{63, 0, 0, 255}; }, + [](flower const &){ return gfx::color_rgba{255, 191, 63, 255}; } + ), type); +} + +static geom::vector const neighbours[4] = +{ + {-1, 0}, + { 1, 0}, + { 0, -1}, + { 0, 1}, +}; + +struct map +{ + struct wall + { + geom::point origin; + geom::vector normal; + + bool contains(geom::point const & p) const + { + return geom::dot(p - origin, normal) <= 0.f; + } + }; + + std::vector walls; + + std::vector> ground_points; + + std::optional radius; +}; + +using cell_map = std::unordered_map, cell_data>; + +struct genome +{ + cell_map cells; +}; + +struct creature +{ + struct cell + { + std::uint32_t indices[4]; + cell_data data; + float lifetime = 0.f; + float received_light = 0.f; + }; + + struct genome genome; + int generation = 0; + + std::vector> positions; + std::vector> velocities; + std::vector collided; + std::vector>> fixed; + std::vector cells; + + float energy = 0.f; + + std::vector> outer_edges; + + geom::point center() const; + geom::point cell_center(cell const & cell) const; + geom::vector center_velocity() const; + void translate(geom::vector const & delta); + void push(geom::vector const & delta_velocity); + + bool dead() const; + + std::vector update(float dt, random::generator & rng); + std::optional create_offspring(cell const & cell, random::generator & rng); + void collide(map const & map, float dt); + + void finalize_creation(); +}; + +geom::point creature::center() const +{ + geom::vector sum{0.f, 0.f}; + for (int i = 1; i < positions.size(); ++i) + { + sum += positions[i] - positions[0]; + } + return positions[0] + sum / (1.f * positions.size()); +} + +geom::point creature::cell_center(cell const & cell) const +{ + geom::point center{0.f, 0.f}; + for (auto idx : cell.indices) + center += (positions[idx] - geom::point{0.f, 0.f}) * 0.25f; + return center; +} + +geom::vector creature::center_velocity() const +{ + geom::vector sum{0.f, 0.f}; + for (int i = 1; i < velocities.size(); ++i) + { + sum += velocities[i]; + } + return sum / (1.f * velocities.size()); +} + +void creature::translate(geom::vector const & delta) +{ + for (auto & pos : positions) + pos += delta; +} + +void creature::push(geom::vector const & delta_velocity) +{ + for (auto & vel : velocities) + vel += delta_velocity; +} + +bool creature::dead() const +{ + return cells.empty(); +} + +std::vector creature::update(float dt, random::generator & rng) +{ + for (int i = 0; i < velocities.size(); ++i) + { +// velocities[i] -= gravity * dt * geom::normalized(positions[i] - geom::point{0.f, 0.f}); + velocities[i][1] -= gravity * dt; + if (fixed[i]) + velocities[i] += (*fixed[i] - positions[i]) * fixed_root_force * dt; + } + + for (auto & vel : velocities) + { +// auto v = geom::length(vel); +// vel -= air_friction * v * vel * dt; + vel *= std::exp(- air_friction * dt); + } + + for (int i = 0; i < positions.size(); ++i) + positions[i] += velocities[i] * dt; + + static geom::vector const deltas[4] + { + {-0.5f, -0.5f}, + { 0.5f, -0.5f}, + { 0.5f, 0.5f}, + {-0.5f, 0.5f}, + }; + + bool erased = false; + + float const reproduction_energy = genome.cells.size(); + float fixed_root_count = 0.f; + + for (int i = 0; i < cells.size(); ++i) + { + auto & cell = cells[i]; + + std::visit(util::overload( + [](auto const &){}, + [&](root const & root){ + if (root.fixed) + fixed_root_count += 1.f; + } + ), cell.data); + } + + std::vector offspring; + + for (int i = 0; i < cells.size();) + { + auto & cell = cells[i]; + + bool kill_cell = false; + + std::visit(util::overload( + [](auto const &){}, + [&](leaf const &){ +// energy += dt * cell.received_light * fixed_root_count; + energy += dt * cell.received_light; + }, + [&](root const & root){ + if (root.fixed) + energy += dt * 0.5f; + }, + [&](flower const &){ + if (energy >= reproduction_energy) + { + energy -= reproduction_energy; + if (auto child = create_offspring(cell, rng)) + offspring.push_back(std::move(*child)); + } + } + ), cell.data); + + cell.lifetime += dt; + + if (cell.lifetime >= lifetime(cell.data)) + kill_cell = true; + + if (kill_cell) + { + cells.erase(cells.begin() + i); + erased = true; + } + else + ++i; + } + + if (erased) + finalize_creation(); + + for (auto & cell : cells) + { + float size = ::size(cell.data); + + auto center = cell_center(cell); + + float A = 0.f; + float B = 0.f; + + for (int i = 0; i < 4; ++i) + { + auto const p = positions[cell.indices[i]] - center; + auto const q = deltas[i] * size; + + A += geom::dot(p, q); + B += geom::det(p, q); + } + + float const angle = - std::atan2(B, A); + + for (int i = 0; i < 4; ++i) + { + auto const target = center + geom::rotate(deltas[i] * size, angle); + + auto force = spring_force(cell.data) * (target - positions[cell.indices[i]]); +// if (auto f = geom::length(force); f > max_spring_force) +// ; + velocities[cell.indices[i]] += force * dt; + } + + auto cmvel = geom::vector{0.f, 0.f}; + auto rotation = 0.f; + for (auto idx : cell.indices) + { + cmvel += velocities[idx] * 0.25f; + rotation += geom::det(positions[idx] - center, velocities[idx]) * (0.25f / geom::length_sqr(positions[idx] - center)); + } + + for (auto idx : cell.indices) + { + auto target_vel = cmvel + geom::ort(positions[idx] - center) * rotation; + + velocities[idx] += (target_vel - velocities[idx]) * (1.f - std::exp(- spring_damping * dt)); + } + } + + return offspring; +} + +void creature::collide(map const & map, float dt) +{ + collided.assign(positions.size(), false); + + for (int i = 0; i < positions.size(); ++i) + { + if (map.radius) + { + auto delta = positions[i] - geom::point{0.f, 0.f}; + auto dist = geom::length(delta); + auto normal = delta / dist; + dist -= *map.radius; + + if (dist < 0.f) + { + positions[i] -= dist * normal; + + auto tangent = geom::ort(normal); + auto vn = geom::dot(velocities[i], normal); + auto vt = geom::dot(velocities[i], tangent); + + vn = std::max(0.f, vn); + vt *= std::exp(- dt * ground_friction); + + velocities[i] = normal * vn + tangent * vt; + + collided[i] = true; + } + } + + for (auto const & wall : map.walls) + { + auto delta = positions[i] - wall.origin; + auto dist = geom::dot(delta, wall.normal); + if (dist < 0.f) + { + positions[i] -= dist * wall.normal; + + auto tangent = geom::ort(wall.normal); + auto vn = geom::dot(velocities[i], wall.normal); + auto vt = geom::dot(velocities[i], tangent); + + vn = std::max(0.f, vn); + vt *= std::exp(- dt * ground_friction); + + velocities[i] = wall.normal * vn + tangent * vt; + + collided[i] = true; + } + } + + auto it = std::upper_bound(map.ground_points.begin(), map.ground_points.end(), positions[i][0], [](float v, auto const & p){ return v < p[0]; }); + if (it != map.ground_points.begin() && it != map.ground_points.end()) + { + auto j = (it - map.ground_points.begin()) - 1; + + auto n = geom::normalized(geom::ort(map.ground_points[j + 1] - map.ground_points[j])); + float dist = geom::dot(positions[i] - map.ground_points[j], n); + + if (dist < 0.f) + { + positions[i] -= dist * n; + + auto tangent = geom::ort(n); + auto vn = geom::dot(velocities[i], n); + auto vt = geom::dot(velocities[i], tangent); + + vn = std::max(0.f, vn); + vt *= std::exp(- dt * hills_friction); + + velocities[i] = n * vn + tangent * vt; + } + } + } + + for (auto & cell : cells) + { + if (auto root = std::get_if<::root>(&cell.data)) + { + for (auto idx : cell.indices) + if (collided[idx]) + { + root->fixed = true; + if (!fixed[idx]) + fixed[idx] = positions[idx]; + } + } + } +} + +void creature::finalize_creation() +{ + collided.assign(positions.size(), false); + fixed.assign(positions.size(), std::nullopt); + + util::hash_set> edge_set; + + for (auto const & cell : cells) + { + for (int i = 0; i < 4; ++i) + { + geom::segment segment; + segment[0] = cell.indices[i]; + segment[1] = cell.indices[(i + 1) % 4]; + edge_set.insert(segment); + } + } + + outer_edges.clear(); + for (auto const & edge : edge_set) + { + auto dual = edge; + std::swap(dual[0], dual[1]); + if (!edge_set.contains(dual)) + outer_edges.push_back(edge); + } +} + +void collide(std::vector & creatures, float dt) +{ + struct cell_data + { + int creature; + int cell; + geom::point center; + float radius; + }; + + util::hash_map, std::vector> bins; + float bin_size = 1.f; + + for (int c = 0; c < creatures.size(); ++c) + { + for (int i = 0; i < creatures[c].cells.size(); ++i) + { + auto const & cell = creatures[c].cells[i]; + + cell_data data{c, i, creatures[c].cell_center(cell), size(cell.data)}; + geom::point bin_id{std::floor(data.center[0] / bin_size), std::floor(data.center[1] / bin_size)}; + bins[bin_id].push_back(data); + } + } + + auto collide_cells = [&](cell_data const & data1, cell_data const & data2) + { + auto delta = data2.center - data1.center; + auto distance = geom::length(delta); + + float min_distance = data1.radius + data2.radius; + + if (data1.creature == data2.creature) + min_distance *= 0.5f; + else + min_distance *= 0.75f; + + if (0.f < distance && distance < min_distance) + { + auto impulse = delta * ((min_distance - distance) / distance * collision_force * dt); + + auto & creature1 = creatures[data1.creature]; + auto & creature2 = creatures[data2.creature]; + auto & cell1 = creature1.cells[data1.cell]; + auto & cell2 = creature2.cells[data2.cell]; + + for (auto idx : cell1.indices) + creature1.velocities[idx] -= impulse; + + for (auto idx : cell2.indices) + creature2.velocities[idx] += impulse; + } + }; + + for (auto const & bin : bins) + { + for (int i = 0; i < bin.second.size(); ++i) + for (int j = i + 1; j < bin.second.size(); ++j) + collide_cells(bin.second[i], bin.second[j]); + + for (int x = -1; x <= 1; ++x) + { + for (int y = -1; y <= 1; ++y) + { + if (x == 0 && y == 0) continue; + + auto nid = bin.first + geom::vector{x, y}; + + if (bin.first < nid) continue; + + if (auto it = bins.find(nid); it != bins.end()) + for (auto const & data1 : bin.second) + for (auto const & data2 : it->second) + collide_cells(data1, data2); + } + } + } +} + +void enlighten(std::vector & creatures) +{ + struct cell_data + { + int creature; + int cell; + geom::point center; + float radius; + }; + + util::hash_map> bins; + float bin_size = 1.f; + + for (int c = 0; c < creatures.size(); ++c) + { + for (int i = 0; i < creatures[c].cells.size(); ++i) + { + auto & cell = creatures[c].cells[i]; + cell.received_light = 0.f; + + float radius = size(cell.data) * 0.5f; + + cell_data data{c, i, creatures[c].cell_center(cell), radius}; + int bin_min = std::floor((data.center[0] - radius) / bin_size); + int bin_max = std::floor((data.center[0] + radius) / bin_size); + for (int bin_id = bin_min; bin_id <= bin_max; ++bin_id) + bins[bin_id].push_back(data); + } + } + + for (auto & bin : bins) + { + std::sort(bin.second.begin(), bin.second.end(), [](cell_data const & d1, cell_data const & d2){ + return std::tie(d1.center[1], d1.center[0]) < std::tie(d2.center[1], d2.center[0]); + }); + + geom::interval bin_extent{bin.first * bin_size, (bin.first + 1) * bin_size}; + + float received_light = 1.f; + for (int i = bin.second.size(); i --> 0;) + { + auto const & data = bin.second[i]; + auto & cell = creatures[data.creature].cells[data.cell]; + geom::interval cell_extent{data.center[0] - data.radius, data.center[0] + data.radius}; + + auto portion = std::min(1.f, (cell_extent & bin_extent).length() / cell_extent.length()); + + cell.received_light += received_light * portion; + + // Opacity, Portion -> Factor + // 0, 0 -> 1 + // 0, 1 -> 1 + // 1, 0 -> 1 + // 1, 1 -> 0 + received_light *= 1.f - opacity(cell.data) * portion; + } + } +} + +void draw(gfx::painter & painter, creature const & creature, float lag) +{ + auto point = [&](auto idx){ + return creature.positions[idx] + creature.velocities[idx] * lag; + }; + + for (auto const & cell : creature.cells) + { + geom::point center{0.f, 0.f}; + for (auto idx : cell.indices) + center += 0.25f * (creature.positions[idx] - geom::point{0.f, 0.f}); + + gfx::color_rgba const color = gfx::dark(cell_color(cell.data), 0.75f * (1.f - cell.received_light)); + gfx::color_rgba const bgcolor = gfx::dark(color, 0.25f); + + geom::point ps[4]; + for (int i = 0; i < 4; ++i) + ps[i] = point(cell.indices[i]); + + painter.triangle(ps[0], ps[1], ps[2], bgcolor); + painter.triangle(ps[2], ps[0], ps[3], bgcolor); + + for (int i = 0; i < 4; ++i) + ps[i] = geom::lerp(ps[i], center, 0.25f); + + painter.triangle(ps[0], ps[1], ps[2], color); + painter.triangle(ps[2], ps[0], ps[3], color); + } + + for (auto const & edge : creature.outer_edges) + { + gfx::color_rgba const color{0, 0, 0, 255}; + float const width = 0.125f; + painter.line(point(edge.points[0]), point(edge.points[1]), width, color, false); + } +} + +struct creature_builder +{ + creature_builder() = default; + + creature_builder(genome genome) + : genome_(std::move(genome)) + {} + + void add(geom::point const position, cell_data const & data) + { + genome_.cells[position] = data; + } + + bool contains(geom::point const & position) const + { + return genome_.cells.contains(position); + } + + creature build(int generation) + { + static geom::vector const vertex_delta[4] + { + {0, 0}, + {1, 0}, + {1, 1}, + {0, 1}, + }; + + creature result; + result.generation = generation; + + util::hash_map, std::uint32_t> vertex_id; + + for (auto const & cell : genome_.cells) + { + auto & cell_out = result.cells.emplace_back(); + cell_out.data = cell.second; + for (int i = 0; i < 4; ++i) + { + geom::point vertex = cell.first + vertex_delta[i]; + if (auto it = vertex_id.find(vertex); it != vertex_id.end()) + { + cell_out.indices[i] = it->second; + } + else + { + cell_out.indices[i] = (vertex_id[vertex] = result.positions.size()); + result.positions.push_back(geom::cast(vertex)); + result.velocities.push_back(geom::vector::zero()); + } + } + } + + result.genome = std::move(genome_); + result.finalize_creation(); + return result; + } + + bool connected() const + { + if (genome_.cells.empty()) + return false; + + util::hash_set> visited; + + std::deque> queue; + queue.push_back(genome_.cells.begin()->first); + visited.insert(queue.back()); + + while (!queue.empty()) + { + auto cur = queue.front(); + queue.pop_front(); + + for (auto n : neighbours) + { + auto nn = cur + n; + if (genome_.cells.contains(nn) && !visited.contains(nn)) + { + visited.insert(nn); + queue.push_back(nn); + } + } + } + + return visited.size() == genome_.cells.size(); + } + +private: + genome genome_; +}; + +cell_data random_cell(random::generator & rng) +{ + if (auto t = random::uniform(rng, 0, 4); t == 0) + return hard_tissue{}; + else if (t == 1) + return soft_tissue{}; + else if (t == 2) + return leaf{}; + else if (t == 3) + return root{}; + else + return flower{}; +} + +void mutate(genome & genome, random::generator & rng) +{ + float const change_type_probability = 1.f / 32.f; + float const erase_probability = 1.f / 64.f; + float const grow_probability = 1.f / 64.f; + + for (auto & cell : genome.cells) + if (random::uniform(rng) < change_type_probability) + cell.second = random_cell(rng); + + std::vector> erase_cells; + for (auto const & cell : genome.cells) + if (random::uniform(rng) < erase_probability) + erase_cells.push_back(cell.first); + + for (auto const & cell : erase_cells) + genome.cells.erase(cell); + + std::vector, cell_data>> grow_cells; + for (auto const & cell : genome.cells) + for (auto n : neighbours) + { + auto ncell = cell.first + n; + if (!genome.cells.contains(ncell) && random::uniform(rng) < grow_probability) + grow_cells.push_back({ncell, random_cell(rng)}); + } + + for (auto const & cell : grow_cells) + genome.cells.insert(cell); +} + +std::optional creature::create_offspring(cell const & cell, random::generator & rng) +{ + auto genome = this->genome; + mutate(genome, rng); + + creature_builder builder(std::move(genome)); + if (!builder.connected()) + return std::nullopt; + auto result = builder.build(generation + 1); + result.translate(cell_center(cell) - result.center()); + + auto center = result.center(); + auto angle = random::uniform_angle(rng); + for (auto & position : result.positions) + position = center + geom::rotate(position - center, angle); + + auto push = random::uniform_hemisphere_vector_distribution({0.f, 1.f})(rng); + result.translate(push * 1.f); +// this->translate(-push * 1.f); +// result.push(10.f * push); + + return result; +} + +struct soft_creatures_2d_app + : app::application_base +{ + soft_creatures_2d_app(options const &, context const &) + { +// map_.ground_points.push_back({5.f, 0.f}); +// for (int x = 10; x <= 200; x += 1) +// map_.ground_points.push_back({x / 2.f, random::uniform(rng, 0.f, std::min(x, 200) / 200.f * 4.f)}); +// map_.ground_points.push_back({x, std::min(2.f, geom::sqr(x - 10) * 0.25f)}); +// map_.ground_points.push_back({x, random::uniform(rng, 0.f, 4.f)}); + + map_.walls.push_back({{0.f, 0.f}, {0.f, 1.f}}); +// map_.walls.push_back({{-40.f, 0.f}, {1.f, 0.f}}); +// map_.walls.push_back({{40.f, 0.f}, {-1.f, 0.f}}); + map_.walls.push_back({{-20.f, 0.f}, geom::normalized(geom::vector{1.f, 1.f})}); + map_.walls.push_back({{20.f, 0.f}, geom::normalized(geom::vector{-1.f, 1.f})}); + +// map_.radius = 30.f; + + int initial_population_size = 1; + + for (int c = 0; c < initial_population_size; ++c) + { + while (true) + { + int x_size = random::uniform(rng_, 2, 4); + int y_size = random::uniform(rng_, 2, 4); + + x_size = 2; + y_size = 1; + + creature_builder builder; + +// builder.add({0, 0}, root{}); + builder.add({0, 1}, flower{}); + builder.add({0, 2}, leaf{}); + + if(false) + for (int x = 0; x < x_size; ++x) + { + for (int y = 0; y < y_size; ++y) + { + if (random::uniform(rng_) > 0.75f) + continue; + + builder.add({x, y}, random_cell(rng_)); + } + } + + if (builder.connected()) + { + creatures_.push_back(builder.build(0)); + creatures_.back().translate({0.f, 5.f}); +// creatures_.back().translate({0.f, map_.radius + 5.f}); + break; + } + } + } + + enlighten(creatures_); + } + + 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 + { + if (state().key_down.contains(app::keycode::ESCAPE)) + stop(); + + float const frame_dt = frame_clock_.restart().count(); + + if (!paused_) physics_lag_ += frame_dt; + + while (physics_lag_ >= sim_dt) + { + physics_lag_ -= sim_dt; + simulation_time_ += sim_dt; + + std::vector alive_creatures; + + enlighten(creatures_); + + for (auto & creature : creatures_) + { + auto children = creature.update(sim_dt, rng_); + + for (auto child : children) + if (!child.dead()) + alive_creatures.push_back(std::move(child)); + + creature.collide(map_, sim_dt); + + if (!creature.dead()) + alive_creatures.push_back(std::move(creature)); + } + + creatures_ = std::move(alive_creatures); + + collide(creatures_, sim_dt); + } + + if (state().key_down.contains(app::keycode::LEFT)) + view_center_tgt_[0] -= 50.f * frame_dt; + + if (state().key_down.contains(app::keycode::RIGHT)) + view_center_tgt_[0] += 50.f * frame_dt; + + if (state().key_down.contains(app::keycode::UP)) + view_center_tgt_[1] += 50.f * frame_dt; + + if (state().key_down.contains(app::keycode::DOWN)) + view_center_tgt_[1] -= 50.f * frame_dt; + + view_center_ += (view_center_tgt_ - view_center_) * (1.f - std::exp(- 20.f * frame_dt)); + } + + void present() override + { + gl::ClearColor(0.5f, 0.6f, 0.8f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + float aspect_ratio = state().size[0] * 1.f / state().size[1]; + geom::box view_box = {{{0.f, 0.f}, {0.f, 0.f}}}; + view_box[0] = {-50.f, 50.f}; + view_box[1] = {0.f, view_box[0].length() / aspect_ratio}; + view_box[1] -= view_box[1].length() / 2.f; + view_box[0] += view_center_[0]; + view_box[1] += view_center_[1]; + + gfx::color_rgba ground_color{127, 91, 65, 255}; + + if (map_.radius) + painter_.circle({0.f, 0.f}, *map_.radius, ground_color, 72); + + for (auto const & wall : map_.walls) + { + std::vector> points; + for (float x = 0; x <= 1; ++x) + { + for (float y = 0; y <= 1; ++y) + { + auto p = view_box.corner(x, y); + if (wall.contains(p)) + points.push_back(p); + } + } + + for (int d = 0; d < 2; ++d) + { + for (int i = 0; i < 2; ++i) + { + auto p0 = (d == 0) ? view_box.corner(0.f, i) : view_box.corner(i, 0.f); + auto p1 = (d == 0) ? view_box.corner(1.f, i) : view_box.corner(i, 1.f); + + // (p0 + t * dp - o) * n = 0 + // t (dp*n) + (p0-o)*n = 0 + float a = geom::dot(p1 - p0, wall.normal); + if (std::abs(a) < 1e-4f) + continue; + + float t = - geom::dot(p0 - wall.origin, wall.normal) / a; + + if (t < 0.f || t > 1.f) + continue; + + points.push_back(p0 + (p1 - p0) * t); + } + } + + std::vector>::iterator> hull; + if (!points.empty()) + cg::graham_convex_hull(points.begin(), points.end(), std::back_inserter(hull)); + + for (int i = 1; i + 1 < hull.size(); ++i) + painter_.triangle(*hull[0], *hull[i], *hull[i + 1], ground_color); + } + + { + gfx::color_rgba hills_color{91, 127, 65, 255}; + for (int i = 0; i + 1 < map_.ground_points.size(); ++i) + { + float x0 = map_.ground_points[i ][0]; + float x1 = map_.ground_points[i + 1][0]; + + float y0 = map_.ground_points[i ][1]; + float y1 = map_.ground_points[i + 1][1]; + + painter_.triangle({x0, 0.f}, {x1, 0.f}, {x1, y1}, hills_color); + painter_.triangle({x0, 0.f}, {x1, y1}, {x0, y0}, hills_color); + } + } + + for (auto const & creature : creatures_) + draw(painter_, creature, physics_lag_); + + painter_.render(geom::orthographic_camera(view_box).transform()); + + int text_row = 0; + auto put_line = [&](std::string const & line) + { + painter_.text({13.f, 10.f + text_row * 24.f}, line, {.scale = 2.f, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {0, 0, 0, 255}}); + painter_.text({12.f, 9.f + text_row * 24.f}, line, {.scale = 2.f, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = {255, 255, 255, 255}}); + ++text_row; + }; + + int cells = 0; + for (auto const & creature : creatures_) + cells += creature.cells.size(); + + put_line(util::to_string("Time: ", simulation_time_)); + put_line(util::to_string("Creatures: ", creatures_.size())); + put_line(util::to_string("Cells: ", cells)); + put_line(util::to_string("Cell/cr.: ", cells * 1.f / creatures_.size())); + + painter_.render(geom::window_camera(state().size[0], state().size[1]).transform()); + } + +private: + gfx::painter painter_; + + util::clock<> frame_clock_; + + geom::point view_center_{0.f, 20.f}; + geom::point view_center_tgt_ = view_center_; + + random::generator rng_{random::device{}}; + + map map_; + + std::vector creatures_; + + float simulation_time_ = 0.f; + float physics_lag_ = 0.f; + bool paused_ = false; +}; + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + return default_application_factory({.name = "Soft-body creatures", .multisampling = 4}); + } + +}