From f73f536d8d01204063f9aee30edf677f4be3dcf3 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Fri, 15 Jan 2021 18:04:21 +0300 Subject: [PATCH] Add 2D creatures neural animation example --- examples/animation_2d.cpp | 1486 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1486 insertions(+) create mode 100644 examples/animation_2d.cpp diff --git a/examples/animation_2d.cpp b/examples/animation_2d.cpp new file mode 100644 index 00000000..f96a5236 --- /dev/null +++ b/examples/animation_2d.cpp @@ -0,0 +1,1486 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace psemek; + +namespace +{ + +struct bone +{ + geom::point position; + geom::vector direction; + geom::vector velocity; + float angular_velocity; + float length; + float width; + float mass = 0.f; + float inertia = 0.f; +}; + +struct joint +{ + std::size_t i0, i1; + float s0, s1; + // angle range max should be in [0, 2*pi] + // angle range min should be less or equal to angle range max + std::optional> angle_range = std::nullopt; +}; + +template +struct constraint +{ + std::size_t bone[Bones]; + geom::matrix jacobian; + geom::vector bias; + geom::interval range = geom::interval::full(); +}; + +struct system +{ + float const dt = 0.01f; + + std::vector bones; + std::vector joints; + + struct selection + { + std::size_t index; + float pos; + geom::vector delta; + }; + + void advance(float time, std::optional sel, util::function(system const &)> torque_provider); + +private: + float time_lag_ = 0.f; + std::vector old_torque_; + + void step(std::optional sel, util::function(system const &)> const & torque_provider); + + template + void solve_constraint(constraint const & c); +}; + +void system::advance(float time, std::optional sel, util::function(system const &)> torque_provider) +{ + time_lag_ += time; + + while (time_lag_ >= dt) + { + time_lag_ -= dt; + step(sel, torque_provider); + } +} + +void system::step(std::optional sel, util::function(system const &)> const & torque_provider) +{ + if (old_torque_.size() != joints.size()) + old_torque_.assign(joints.size(), 0.f); + + geom::vector const gravity { 0.f, -10.f }; + + for (std::size_t i = 0; i < bones.size(); ++i) + { + auto & b = bones[i]; + + b.velocity += gravity * dt; + } + + auto torque = torque_provider(*this); + + for (std::size_t i = 0; i < joints.size(); ++i) + { + auto const & j = joints[i]; + + auto & b0 = bones[j.i0]; + auto & b1 = bones[j.i1]; + + float const max_torque_change = 250.f * dt; + + float d = std::max(std::abs(torque[i] - old_torque_[i]) / max_torque_change, 1.f); + + old_torque_[i] = old_torque_[i] + (torque[i] - old_torque_[i]) / d; + + b0.angular_velocity -= old_torque_[i] * dt / b0.inertia; + b1.angular_velocity += old_torque_[i] * dt / b1.inertia; + } + + for (std::size_t i = 0; i < bones.size(); ++i) + { + auto & b = bones[i]; + + b.position += b.velocity * dt; + b.direction = geom::normalized(b.direction + geom::ort(b.direction) * b.angular_velocity * dt); + } + + std::vector> constraints_1_1; + std::vector> constraints_1_2; + std::vector> constraints_2_1; + std::vector> constraints_2_2; + + if (sel) + { + auto & b = bones[sel->index]; + + constraint<1, 2> & c = constraints_1_2.emplace_back(); + c.bone[0] = sel->index; + + c.jacobian[0][0] = 1.f; + c.jacobian[0][1] = 0.f; + c.jacobian[0][2] = sel->pos * b.length / 2.f; + c.jacobian[0][3] = 0.f; + + c.jacobian[1][0] = 0.f; + c.jacobian[1][1] = 1.f; + c.jacobian[1][2] = 0.f; + c.jacobian[1][3] = sel->pos * b.length / 2.f; + + c.bias = sel->delta * 0.5f / dt; + } + + for (std::size_t i = 0; i < bones.size(); ++i) + { + auto & b = bones[i]; + + float const depth_factor = 0.5f / dt; + + float const bounce = 0.05f; + float const friction = 0.05f; + + auto push_1 = [&](float depth, float s, geom::vector const & v){ + constraint<1, 1> & c = constraints_1_1.emplace_back(); + + c.bone[0] = i; + + c.jacobian[0][0] = 0.f; + c.jacobian[0][1] = 1.f; + c.jacobian[0][2] = 0.f; + c.jacobian[0][3] = b.length / 2.f * s; + + c.bias[0] = v[1] - depth * depth_factor; + + c.range.min = 0.f; + + constraint<1, 1> & fc = constraints_1_1.emplace_back(); + + fc.bone[0] = i; + + fc.jacobian[0][0] = 1.f; + fc.jacobian[0][1] = 0.f; + fc.jacobian[0][2] = s * b.length / 2.f; + fc.jacobian[0][3] = 0.f; + + fc.bias = fc.bias.zero(); + + fc.range.min = -friction; + fc.range.max = friction; + }; + + std::optional d0, d1; + + auto p0 = b.position - b.direction * b.length / 2.f; + auto p1 = b.position + b.direction * b.length / 2.f; + + auto v0 = b.velocity - geom::ort(b.direction) * b.angular_velocity * b.length / 2.f; + auto v1 = b.velocity + geom::ort(b.direction) * b.angular_velocity * b.length / 2.f; + + if (p0[1] < b.width / 2.f) + { + float d = b.width / 2.f - p0[1]; + if (d > 0.f) + { + d0 = d; + } + } + + if (p1[1] < b.width / 2.f) + { + float d = b.width / 2.f - p1[1]; + if (d > 0.f) + { + d1 = d; + } + } + + if (d0 && d1) + { + constraint<1, 2> & c = constraints_1_2.emplace_back(); + + c.bone[0] = i; + + c.jacobian[0][0] = 0.f; + c.jacobian[0][1] = 1.f; + c.jacobian[0][2] = 0.f; + c.jacobian[0][3] = -b.length / 2.f; + c.jacobian[1][0] = 0.f; + c.jacobian[1][1] = 1.f; + c.jacobian[1][2] = 0.f; + c.jacobian[1][3] = b.length / 2.f; + + c.bias[0] = - (*d0) * depth_factor + v0[1] * bounce; + c.bias[1] = - (*d1) * depth_factor + v1[1] * bounce; + + c.range.min = 0.f; + + constraint<1, 2> & fc = constraints_1_2.emplace_back(); + + fc.bone[0] = i; + + fc.jacobian[0][0] = 1.f; + fc.jacobian[0][1] = 0.f; + fc.jacobian[0][2] = -b.length / 2.f; + fc.jacobian[0][3] = 0.f; + fc.jacobian[1][0] = 1.f; + fc.jacobian[1][1] = 0.f; + fc.jacobian[1][2] = b.length / 2.f; + fc.jacobian[1][3] = 0.f; + + fc.range.min = -friction; + fc.range.max = friction; + + fc.bias = fc.bias.zero(); + + } + else if (d0) + { + push_1(*d0, -1.f, v0 * bounce); + } + else if (d1) + { + push_1(*d1, 1.f, v1 * bounce); + } + } + + for (auto const & j : joints) + { + constraint<2, 2> & c = constraints_2_2.emplace_back(); + + auto & b0 = bones[j.i0]; + auto & b1 = bones[j.i1]; + + c.bone[0] = j.i0; + c.bone[1] = j.i1; + + c.jacobian = c.jacobian.zero(); + c.jacobian[0][0] = -1.f; + c.jacobian[0][2] = -j.s0 * b0.length / 2.f; + c.jacobian[0][4] = 1.f; + c.jacobian[0][6] = j.s1 * b1.length / 2.f; + c.jacobian[1][1] = -1.f; + c.jacobian[1][3] = -j.s0 * b0.length / 2.f; + c.jacobian[1][5] = 1.f; + c.jacobian[1][7] = j.s1 * b1.length / 2.f; + + c.bias = ((b1.position + b1.direction * j.s1 * b1.length / 2.f) - (b0.position + b0.direction * j.s0 * b0.length / 2.f)) / dt; + + if (j.angle_range) + { + float x = geom::dot(b0.direction, b1.direction); + float y = geom::det(b0.direction, b1.direction); + + // a \in [-pi, pi] + float a = std::atan2(y, x); + + // j.angle_range->max \in [0, 2*pi] + // j.angle_range->min \in [-2*pi, 2*pi] + // j.angle_range->center() \in [-pi, 2*pi] + // delta \in [-3*pi, 2*pi] + float delta = a - j.angle_range->center(); + if (delta > geom::pi) + delta -= 2.f * geom::pi; + if (delta < -geom::pi) + delta += 2.f * geom::pi; + // delta in [-pi, pi] + + float const half_length = j.angle_range->length() / 2.f; + + if (std::abs(delta) > half_length) + { + float const factor = 2.f; + + constraint<2, 1> & c = constraints_2_1.emplace_back(); + + c.bone[0] = j.i0; + c.bone[1] = j.i1; + + c.jacobian = c.jacobian.zero(); + c.jacobian[0][2] = - y * b1.direction[0] + x * b1.direction[1]; + c.jacobian[0][3] = - y * b1.direction[1] - x * b1.direction[0]; + c.jacobian[0][6] = - y * b0.direction[0] - x * b0.direction[1]; + c.jacobian[0][7] = - y * b0.direction[1] + x * b0.direction[0]; + + if (delta > 0.f) + { + c.bias[0] = (delta - half_length) * factor / dt; + c.range.max = 0.f; + } + else + { + c.bias[0] = (delta + half_length) * factor / dt; + c.range.min = 0.f; + } + } + } + } + + std::size_t const steps = 1; + + for (std::size_t step = 0; step < steps; ++step) + { + for (auto const & c : constraints_1_1) + { + solve_constraint(c); + } + + for (auto const & c : constraints_1_2) + { + solve_constraint(c); + } + + for (auto const & c : constraints_2_1) + { + solve_constraint(c); + } + + for (auto const & c : constraints_2_2) + { + solve_constraint(c); + } + } +} + +template +void system::solve_constraint(constraint const & c) +{ + geom::vector v; + auto j_inv_mass = c.jacobian; + + for (std::size_t i = 0; i < B; ++i) + { + bone const & b = bones[c.bone[i]]; + + v[4 * i + 0] = b.velocity[0]; + v[4 * i + 1] = b.velocity[1]; + v[4 * i + 2] = -b.angular_velocity * b.direction[1]; + v[4 * i + 3] = b.angular_velocity * b.direction[0]; + + for (std::size_t j = 0; j < C; ++j) + { + j_inv_mass[j][4 * i + 0] /= b.mass; + j_inv_mass[j][4 * i + 1] /= b.mass; + j_inv_mass[j][4 * i + 2] /= b.inertia; + j_inv_mass[j][4 * i + 3] /= b.inertia; + } + } + + geom::vector l = *geom::solve(j_inv_mass * geom::transpose(c.jacobian), - c.jacobian * v - c.bias); + for (std::size_t j = 0; j < C; ++j) + l[j] = geom::clamp(l[j], c.range); + + auto p = geom::transpose(j_inv_mass) * l; + + for (std::size_t i = 0; i < B; ++i) + { + bone & b = bones[c.bone[i]]; + + b.velocity[0] += p[4 * i + 0]; + b.velocity[1] += p[4 * i + 1]; + b.angular_velocity += geom::det(b.direction, geom::vector{p[4 * i + 2], p[4 * i + 3]}); + } +} + +struct controller +{ + // inputs should be equal to number of bones * 7 + // outputs should be equal to number of joints + + static constexpr std::size_t inputs = 12 * 7; + static constexpr std::size_t user_inputs = 0; + static constexpr std::size_t outputs = 11; + + static constexpr std::size_t layer1 = outputs; +// static constexpr std::size_t layer2 = 4; +// static constexpr std::size_t layer3 = outputs; + +// static constexpr std::size_t param_count = layer1 * (inputs + 1) + layer2 * (layer1 + 1) + layer3 * (layer2 + 1); + static constexpr std::size_t param_count = layer1 * (inputs + user_inputs + 1); + + static constexpr float max_output = 20.f; + + geom::matrix m1; + geom::vector t1; + +// geom::matrix m1; +// geom::vector t1; +// geom::matrix m2; +// geom::vector t2; +// geom::matrix m3; +// geom::vector t3; + + // DIRTY HACKERY!!!!! + float * params() { return (float *)&m1; } + + int generation = 0; + + template + void randomize(RNG && rng, float amplitude); + + template + void mutate(RNG && rng, float amplitude); + + void reset(); + + geom::vector apply(system const & s, geom::vector const & ui) const; + + void read(std::istream & is); + void write(std::ostream & os) const; + + static float activation(float x); + + template + static geom::vector activation(geom::vector v); + + template + static void read(std::istream & is, T & x); + + template + static void write(std::ostream & os, T const & x); +}; + +template +void controller::randomize(RNG && rng, float amplitude) +{ + generation = 0; + + random::uniform_distribution d{-1.f, 1.f}; + + auto visit_m = [&rng, &d, amplitude](auto & m) + { + for (std::size_t i = 0; i < m.rows; ++i) + { + for (std::size_t j = 0; j < m.columns; ++j) + { + m[i][j] = d(rng) * amplitude; + } + } + }; + + auto visit_v = [&rng, &d, amplitude](auto & v) + { + for (std::size_t i = 0; i < v.dimension; ++i) + { + v[i] = d(rng) * amplitude; + } + }; + + visit_m(m1); +// visit_m(m2); +// visit_m(m3); + visit_v(t1); +// visit_v(t2); +// visit_v(t3); +} + +template +void controller::mutate(RNG && rng, float amplitude) +{ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + random::normal_distribution d{}; + + auto visit_m = [&rng, &d, amplitude](auto & m) + { + for (std::size_t i = 0; i < m.rows; ++i) + { + for (std::size_t j = 0; j < m.columns; ++j) + { + m[i][j] += d(rng) * amplitude; + } + } + }; + + auto visit_v = [&rng, &d, amplitude](auto & v) + { + for (std::size_t i = 0; i < v.dimension; ++i) + { + v[i] += d(rng) * amplitude; + } + }; + + visit_m(m1); +// visit_m(m2); +// visit_m(m3); + visit_v(t1); +// visit_v(t2); +// visit_v(t3); +#pragma GCC diagnostic pop +} + +void controller::reset() +{} + +geom::vector controller::apply(system const & sys, geom::vector const & ui) const +{ + if (sys.bones.size() * 7 != inputs) + throw std::runtime_error(util::to_string("Wrong number of inputs: should be ", sys.bones.size() * 7)); + if (sys.joints.size() != outputs) + throw std::runtime_error(util::to_string("Wrong number of outputs: should be ", sys.joints.size())); + + geom::vector input; + for (std::size_t i = 0; i < inputs / 7; ++i) + { + input[7 * i + 0] = sys.bones[i].position[0] - sys.bones[0].position[0]; + input[7 * i + 1] = sys.bones[i].position[1]; + input[7 * i + 2] = sys.bones[i].direction[0]; + input[7 * i + 3] = sys.bones[i].direction[1]; + input[7 * i + 4] = sys.bones[i].velocity[0]; + input[7 * i + 5] = sys.bones[i].velocity[1]; + input[7 * i + 6] = sys.bones[i].angular_velocity; + } + for (std::size_t i = 0; i < user_inputs; ++i) + input[inputs + i] = ui[i]; + + auto r1 = activation(m1 * input + t1); +// auto r2 = activation(m2 * r1 + t2); +// auto r3 = activation(m3 * r2 + t3); + + for (std::size_t i = 0; i < sys.joints.size(); ++i) +// r3[i] *= 0.5f * (sys.bones[sys.joints[i].i0].mass + sys.bones[sys.joints[i].i1].mass); + r1[i] *= 0.5f * (sys.bones[sys.joints[i].i0].mass + sys.bones[sys.joints[i].i1].mass); + + return r1 * max_output; +} + +template +void controller::read(std::istream & is, T & x) +{ + is.read(reinterpret_cast(&x), sizeof(x)); +} + +template +void controller::write(std::ostream & os, T const & x) +{ + os.write(reinterpret_cast(&x), sizeof(x)); +} + +void controller::read(std::istream & is) +{ + read(is, m1); + read(is, t1); +// read(is, m2); +// read(is, t2); +// read(is, m3); +// read(is, t3); +} + +void controller::write(std::ostream & os) const +{ + write(os, m1); + write(os, t1); +// write(os, m2); +// write(os, t2); +// write(os, m3); +// write(os, t3); +} + +float controller::activation(float x) +{ + return 2.f / (1.f + std::exp(- 2.f * x)) - 1.f; +} + +template +geom::vector controller::activation(geom::vector v) +{ + for (std::size_t i = 0; i < N; ++i) + v[i] = activation(v[i]); + return v; +} + +controller lerp(controller const & c1, controller const & c2, float t) +{ + controller c; + c.m1 = geom::lerp(c1.m1, c2.m1, t); +// c.m2 = geom::lerp(c1.m2, c2.m2, t); +// c.m3 = geom::lerp(c1.m3, c2.m3, t); + c.t1 = geom::lerp(c1.t1, c2.t1, t); +// c.t2 = geom::lerp(c1.t2, c2.t2, t); +// c.t3 = geom::lerp(c1.t3, c2.t3, t); + c.generation = std::max(c1.generation, c2.generation) + 1; + return c; +} + +struct animation_2d_app + : app::app +{ + animation_2d_app(); + + void on_resize(int width, int height) override; + void on_mouse_wheel(int delta) override; + + void on_key_down(SDL_Keycode key) override; + + void update() override; + void present() override; + + void update_camera(); + + geom::box view_bbox; + bool centered = false; + + gfx::painter painter; + + util::clock<> frame_clock; + util::clock<> test_clock; + + system physics; + + std::optional selected; + float selected_s = 0.f; + + random::generator rng{random::random_device{}}; + + async::threadpool bg{"bg"}; + + enum class mode + { + train, + optimize, + test, + } mode = mode::train; + + std::vector population; + std::size_t const population_size = 256; + std::size_t const max_train_frames = 60.f / physics.dt; + std::size_t const max_train_variations = 1; + float const position_variation_amplitude = 0.f; + float const angle_variation_amplitude = 0.f; + + std::size_t train_iterations = 0; + std::size_t const max_train_iterations = 1024*8; + float const randomize_amplitude = 10.f; + static constexpr auto mutation_amplitude = [](float t){ return 10.f * geom::lerp(1.f, 0.01f, t); }; +// static constexpr auto mutation_amplitude = [](float){ return 0.00001f; }; + + std::size_t optimize_iterations = 0; + std::size_t const max_optimize_iterations = 1; + float const optimize_amplitude = 0.1f; + + float best_score = 0.f; + bool const warm_start = false; + bool const enable_testing = true; + bool testing_control = true; + std::string const cache_location = "/home/lisyarus/runner"; + + std::size_t test_id = 0; + std::vector test_speeds; + + void reset_state(system & sys) const; + + float eval_score(controller const & c, random::generator rng) const; + void do_train(); + void do_optimize(); + void do_test(); +}; + +animation_2d_app::animation_2d_app() + : app("Animation 2D") +{ + view_bbox[1] = {-1.f, 15.f}; + + vsync(false); +} + +void animation_2d_app::on_resize(int width, int height) +{ + app::on_resize(width, height); + + update_camera(); +} + +void animation_2d_app::on_mouse_wheel(int delta) +{ + float p = std::pow(0.8f, delta); + view_bbox[1].max *= p; + + update_camera(); +} + +void animation_2d_app::update_camera() +{ + float ratio = static_cast(width()) / height(); + + float cx = 0.f; + if (centered) + { + float m = 0.f; + for (auto const & b : physics.bones) + { + cx += b.position[0] * b.mass; + m += b.mass; + } + cx /= m; + } + else + { + cx = ratio * view_bbox[1].length() / 2.f; + } + + view_bbox[0] = geom::expand(geom::interval::singleton(cx), ratio * view_bbox[1].length() / 2.f); +} + +void animation_2d_app::on_key_down(SDL_Keycode key) +{ + if (key == SDLK_c) + { + centered = !centered; + update_camera(); + } + + if (key == SDLK_p) + { + testing_control = !testing_control; + } + + if (mode == mode::train) + { + if (key == SDLK_s) + { + train_iterations = max_train_iterations; + } + } + if (mode == mode::test) + { + bool reset = false; + if (key == SDLK_LEFT) + { + reset = true; + test_id = (test_id + population.size() - 1) % population.size(); + } + if (key == SDLK_RIGHT) + { + reset = true; + test_id = (test_id + 1) % population.size(); + } + + if (reset) + { + reset_state(physics); + test_clock.restart(); + + float shiftx = random::uniform_distribution{-1.f, 1.f}(rng) * position_variation_amplitude; + float shifty = random::uniform_distribution{0.f, 1.f}(rng) * position_variation_amplitude; + float angle = random::uniform_distribution{-1.f, 1.f}(rng) * geom::rad(angle_variation_amplitude); + + geom::plane_rotation rot{0, 1, angle}; + + float miny = 0.f; + for (auto & b : physics.bones) + { + b.position = rot(b.position) + geom::vector{shiftx, shifty}; + b.direction = rot(b.direction); + miny = std::min(miny, b.position[1] - std::abs(b.direction[1]) * b.length / 2.f - b.width / 2.f); + } + for (auto & b : physics.bones) + b.position[1] -= miny; + + population[test_id].reset(); + test_speeds.clear(); + } + } +} + +void animation_2d_app::update() +{ + if (mode == mode::train) + { + do_train(); + if (train_iterations >= max_train_iterations) + { + mode = mode::optimize; + } + else if (train_iterations >= max_train_iterations / 2 && best_score < -1000.f) + { +// train_iterations = 0; + } + } + else if (mode == mode::optimize) + { + do_optimize(); + if (optimize_iterations >= max_optimize_iterations) + { + mode = mode::test; + frame_clock.restart(); + test_clock.restart(); + reset_state(physics); + + std::ofstream os(cache_location, std::ios::binary); + population[0].write(os); + + population[0].reset(); + + vsync(true); + } + } + else if (mode == mode::test) + { + if (!enable_testing) + { + stop(); + return; + } + + do_test(); + } +} + +void animation_2d_app::reset_state(system & sys) const +{ + sys.bones.clear(); + sys.joints.clear(); + +/* + // Jumper + sys.bones.push_back(bone{{0.5f, 0.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.25f, 0.5f}, {0.5f, 1.f}, {0.f, 0.f}, 0.f, std::sqrt(1.25f), 0.25f}); + sys.bones.push_back(bone{{0.75f, 1.5f}, {0.5f, 1.f}, {0.f, 0.f}, 0.f, std::sqrt(1.25f), 0.25f}); + sys.joints.push_back(joint{0, 1, -1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(-30.f), geom::rad(30.f)}}); +//*/ + +/* + // Dachshund + float s = 0.5f; + float h = 0.3f; + sys.bones.push_back(bone{{ -s, h*0.5f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, h, 0.25f}); + sys.bones.push_back(bone{{ -s, h*1.5f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, h, 0.25f}); + sys.bones.push_back(bone{{ 0.f, h*2.f }, {1.f, 0.f}, {0.f, 0.f}, 0.f, 2.f, 0.25f}); + sys.bones.push_back(bone{{ s, h*1.5f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, h, 0.25f}); + sys.bones.push_back(bone{{ s, h*0.5f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, h, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad(45.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -s, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{2, 3, s, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{3, 4, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad(45.f)}}); +//*/ + +/* + // Cube + sys.bones.push_back(bone{{0.5f, 0.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{1.f, 0.5f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.5f, 1.f}, {-1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.f, 0.5f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{3, 0, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); +//*/ + +/* + // Cube with tiny legs + sys.bones.push_back(bone{{0.5f, 0.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{1.f, 0.5f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.5f, 1.f}, {-1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.f, 0.5f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); + sys.joints.push_back(joint{3, 0, 1.f, -1.f, geom::interval{geom::rad(60.f), geom::rad(120.f)}}); +//*/ + +/* + // Roller + int n = 2; + for (int i = 0; i < n; ++i) + sys.bones.push_back(bone{{i + 0.5f, 0.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{n + 0.25f, std::sqrt(3.f) * 0.25f}, {0.5f, std::sqrt(3.f) * 0.5}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{n + 0.25f, std::sqrt(3.f) * 0.75f}, {-0.5f, std::sqrt(3.f) * 0.5}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + for (int i = 0; i < n; ++i) + sys.bones.push_back(bone{{n - i - 0.5f, std::sqrt(3.f)}, {-1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{-0.25f, std::sqrt(3.f) * 0.75f}, {-0.5f, -std::sqrt(3.f) * 0.5}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{-0.25f, std::sqrt(3.f) * 0.25f}, {0.5f, -std::sqrt(3.f) * 0.5}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + + for (int i = 0; i < 2 * n + 4; ++i) + sys.joints.push_back(joint{i, (i + 1) % (2 * n + 4), 1.f, -1.f, geom::interval{geom::rad(0.f), geom::rad(60.f)}}); +//*/ + + + // Worm + + int n = 12; + for (int i = 0; i < n; ++i) + sys.bones.push_back(bone{{i + 0.5f, 0.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + for (int i = 0; i + 1 < n; ++i) + sys.joints.push_back(joint{i, i + 1, 1.f, -1.f, geom::interval{geom::rad(-30.f), geom::rad(30.f)}}); +//*/ + +/* + // Caterpillar + int n = 5; + for (int i = 0; i < n; ++i) + sys.bones.push_back(bone{{i + 0.5f, 0.5f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + for (int i = 0; i < n; ++i) + { + sys.bones.push_back(bone{{i + 0.25f, 0.25f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{i + 0.25f, 0.75f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + } + + for (int i = 0; i + 1 < n; ++i) + sys.joints.push_back(joint{i, i + 1, 1.f, -1.f, geom::interval{geom::rad(-30.f), geom::rad(30.f)}}); + for (int i = 0; i < n; ++i) + { + sys.joints.push_back(joint{i, n + 2 * i + 0, -0.5f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{i, n + 2 * i + 1, 0.5f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + } +//*/ + +/* + // Runner-1 + sys.bones.push_back(bone{{0.f, 0.5f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{0.5f, 1.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{1.5f, 1.f}, {1.f, 0.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.bones.push_back(bone{{2.f, 0.5f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 1.f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(-15.f), geom::rad( 15.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); +//*/ + +/* + // Runner: dog + sys.bones.push_back(bone{{-1.f, 0.25f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{-1.f, 0.75f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 0.f, 1.f }, {1.f, 0.f}, {0.f, 0.f}, 0.f, 2.f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.75f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.25f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad(45.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{3, 4, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad(45.f)}}); +//*/ + +/* + // Runner: goat + sys.bones.push_back(bone{{-1.f, 0.25f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{-1.f, 0.75f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 0.f, 1.f }, {1.f, 0.f}, {0.f, 0.f}, 0.f, 2.f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.75f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.25f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad(-45.f), geom::rad( 0.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{3, 4, 1.f, -1.f, geom::interval{geom::rad(-45.f), geom::rad( 0.f)}}); +//*/ + +/* + // Runner: left + sys.bones.push_back(bone{{-1.f, 0.25f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{-1.f, 0.75f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 0.f, 1.f }, {1.f, 0.f}, {0.f, 0.f}, 0.f, 2.f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.75f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.25f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad(-45.f), geom::rad( 0.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{3, 4, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad( 45.f)}}); +//*/ + +/* + // Runner: right + sys.bones.push_back(bone{{-1.f, 0.25f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{-1.f, 0.75f}, {0.f, 1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 0.f, 1.f }, {1.f, 0.f}, {0.f, 0.f}, 0.f, 2.f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.75f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.bones.push_back(bone{{ 1.f, 0.25f}, {0.f, -1.f}, {0.f, 0.f}, 0.f, 0.5f, 0.25f}); + sys.joints.push_back(joint{0, 1, 1.f, -1.f, geom::interval{geom::rad( 0.f), geom::rad( 45.f)}}); + sys.joints.push_back(joint{1, 2, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{2, 3, 1.f, -1.f, geom::interval{geom::rad(240.f), geom::rad(300.f)}}); + sys.joints.push_back(joint{3, 4, 1.f, -1.f, geom::interval{geom::rad(-45.f), geom::rad( 0.f)}}); +//*/ + + float const bone_density = 1.f; + for (auto & b : sys.bones) + { + b.position[1] += 0.125f; + b.direction = geom::normalized(b.direction); + b.mass = b.length * b.width * bone_density; + b.inertia = b.mass * (b.length * b.length + b.width * b.width) / 12.f; + } +} + +float animation_2d_app::eval_score(controller const & c, random::generator rng) const +{ + system physics; + std::optional sel; + + float score = 0.f; + + for (std::size_t variation = 0; variation < max_train_variations; ++variation) + { + reset_state(physics); + + float shiftx = random::uniform_distribution{-1.f, 1.f}(rng) * position_variation_amplitude; + float shifty = random::uniform_distribution{0.f, 1.f}(rng) * position_variation_amplitude; + float angle = random::uniform_distribution{-1.f, 1.f}(rng) * geom::rad(angle_variation_amplitude); + + geom::plane_rotation rot{0, 1, angle}; + + float miny = 0.f; + for (auto & b : physics.bones) + { + b.position = rot(b.position) + geom::vector{shiftx, shifty}; + b.direction = rot(b.direction); + miny = std::min(miny, b.position[1] - std::abs(b.direction[1]) * b.length / 2.f - b.width / 2.f); + } + for (auto & b : physics.bones) + b.position[1] -= miny; + + float const dt = physics.dt; + + bool failure = false; + float cur_score = 0.f; + float energy = 0.f; + float reward = 0.f; + + static std::vector> hit_floor_points = + { +// {2, 1.f}, +// {2, -1.f}, +// {2, 1.f}, +// {3, -1.f}, +// {4, -1.f}, +// {5, -1.f}, + }; + + std::size_t train_frames = max_train_frames * 1.0f; + + float time = 0.f; + + std::vector switch_times; + for (int i = 0; i < 10; ++i) + switch_times.push_back(random::uniform_distribution{0.f, train_frames * dt}(rng)); + std::sort(switch_times.begin(), switch_times.end()); + + for (std::size_t frame = 0; frame < train_frames; ++frame) + { + bool moving = false; + + for (auto t : switch_times) + { + if (time < t) break; + moving = !moving; + } + + physics.advance(dt, sel, [&c, &energy, dt](system const & physics){ + std::vector torque(physics.joints.size(), 0.f); + auto ctrl = c.apply(physics, {}); + for (std::size_t i = 0; i < ctrl.dimension; ++i) + torque[i] = ctrl[i]; + energy += geom::length(ctrl) * dt; + return torque; + }); + + for (auto f : hit_floor_points) + { + if (f.first >= physics.bones.size()) break; + + auto const & b = physics.bones[f.first]; + + auto const p = b.position + f.second * b.direction * b.length / 2.f; + + if (p[1] <= b.width / 2.f) + { + failure = true; + break; + } + } + +// penalty += geom::sqr((1.f - physics.bones[2].direction[0]) * dt); +// float const target_speed = 2.f; +// penalty += geom::sqr((physics.bones[2].velocity[0] - target_speed) / target_speed) * dt; +// penalty -= physics.bones[2].velocity[0] * dt / physics.bones; + + auto cm_vel = geom::vector::zero(); + float mass = 0.f; + for (auto const & b : physics.bones) + { + cm_vel += b.velocity * b.mass; + mass += b.mass; + } + cm_vel /= mass; + + reward += (cm_vel[0]) * dt; + +// reward -= energy * 0.1f; + + if (failure) + { + cur_score = 0.f; + break; + } + + if (frame + 1 == max_train_frames) + { + float mean_pos_x = 0.f; + float mass = 0.f; + for (auto const & b : physics.bones) + { + mean_pos_x += b.position[0] * b.mass; + mass += b.mass; + } + mean_pos_x /= mass; + cur_score = reward / max_train_variations; + } + + time += dt; + } + + (void)failure; + + score += cur_score; + } + + return score; +} + +void animation_2d_app::do_train() +{ + if (population.empty()) + { + population.resize(population_size); + std::ifstream is(cache_location, std::ios::binary); + if (warm_start && is) + { + controller w; + w.read(is); + for (auto & c : population) + { + c = w; + } + } + else + { + for (auto & c : population) + c.randomize(rng, randomize_amplitude); + } + } + else + { + std::size_t const preserved = population.size() / 16; + std::size_t const cross_parents = population.size() / 8; + std::size_t const cross_new = population.size() / 2; + + std::vector new_population(population.size()); + + for (std::size_t i = 0; i < preserved; ++i) + { + new_population[i] = population[i]; + } + + float const mu = mutation_amplitude((train_iterations * 1.f) / max_train_iterations); + + for (std::size_t i = preserved; i < preserved + cross_new; ++i) + { + auto p1 = random::uniform_distribution{0, cross_parents - 1}(rng); + auto p2 = random::uniform_distribution{0, cross_parents - 1}(rng); + + float t = random::uniform_distribution{}(rng); + new_population[i] = lerp(population[p1], population[p2], t); + new_population[i].mutate(rng, mu); + } + + for (std::size_t i = preserved + cross_new; i < population.size(); ++i) + new_population[i].randomize(rng, randomize_amplitude); + + population = std::move(new_population); + } + + std::vector> scores(population.size()); + std::vector> futures; + for (std::size_t i = 0; i < population.size(); ++i) + { + futures.push_back(bg.dispatch([&, i, rng = rng]() mutable { + scores[i] = {eval_score(population[i], rng), i}; + })); + } +// bg.wait(); +// while (trained.load() != population.size()); + + bg.wait_all(futures.begin(), futures.end()).get(); + + std::sort(scores.begin(), scores.end(), [](auto const & p1, auto const & p2){ return p1.first > p2.first; }); + + std::vector new_population(population.size()); + for (std::size_t i = 0; i < population.size(); ++i) + { + new_population[i] = population[scores[i].second]; + } + population = std::move(new_population); + ++train_iterations; + + best_score = scores.front().first; +} + +void animation_2d_app::do_optimize() +{ + auto & c = population[0]; + + auto rng = this->rng; + + float current_score = eval_score(c, rng); + + float eps = 0.1f; + + std::vector scores(c.param_count, 0.f); + std::atomic dispatched{0}; + for (std::size_t i = 0; i < c.param_count; ++i) + { + bg.dispatch([&, i, rng = rng]() mutable { + controller cc = c; + cc.params()[i] += eps; + scores[i] = eval_score(cc, rng); + ++dispatched; + }); + } +// bg.wait(); + while (dispatched.load() < scores.size()); + + for (auto & s : scores) + s = (s - current_score) / eps; + + float m = 0.f; + for (auto s : scores) + m = std::max(m, std::abs(s)); + + log::info() << "Max gradient: " << m; + + for (std::size_t k = 0; k < 30; ++k) + { + auto cc = c; + for (std::size_t i = 0; i < c.param_count; ++i) + cc.params()[i] += optimize_amplitude * scores[i]; + float new_score = eval_score(cc, rng); + + if (new_score > current_score) + { + log::info() << "k = " << k; + log::info() << "Score change: " << current_score << " -> " << new_score; + c = cc; + break; + } + + for (auto & s : scores) + s /= 2.f; + } + + ++optimize_iterations; +} + +void animation_2d_app::do_test() +{ + std::optional> m; + if (mouse()) + { + m = view_bbox.corner((*mouse())[0] * 1.f / width(), 1.f - (*mouse())[1] * 1.f / height()); + } + + if (!is_left_button_down()) + { + selected = std::nullopt; + + if (m) + { + float selected_distance = std::numeric_limits::infinity(); + + for (std::size_t i = 0; i < physics.bones.size(); ++i) + { + auto & b = physics.bones[i]; + + auto p0 = b.position - b.direction * b.length / 2.f; + auto p1 = b.position + b.direction * b.length / 2.f; + auto r = p1 - p0; + + auto d = *m - p0; + + float t = geom::dot(d, r) / geom::dot(r, r); + + float distance; + + if (0.f <= t && t <= 1.f) + { + distance = geom::length(d - r * t); + } + else + { + float d0 = geom::distance(p0, *m); + float d1 = geom::distance(p1, *m); + + if (d0 < d1) + { + t = 0.f; + } + else + { + t = 1.f; + } + + distance = std::min(d0, d1); + } + + if (distance < b.width / 2.f && distance < selected_distance) + { + selected_distance = distance; + selected = i; + selected_s = 2.f * t - 1.f; + } + } + } + } + + std::optional sel; + if (selected && is_left_button_down()) + { + auto const & b = physics.bones[*selected]; + auto delta = b.position + b.direction * selected_s * b.length / 2.f - *m; + sel = system::selection{*selected, selected_s, delta}; + } + + if (testing_control) + { + physics.advance(frame_clock.restart().count(), sel, [this](system const & physics){ + std::vector torque(physics.joints.size()); + auto ctrl = population[test_id].apply(physics, {}); + for (std::size_t i = 0; i < ctrl.dimension; ++i) + torque[i] = ctrl[i]; + return torque; + }); + } + else + { + physics.advance(frame_clock.restart().count(), sel, [this](system const & physics){ + return std::vector(physics.joints.size(), 0.f); + }); + } + + { + auto cm_vel = geom::vector::zero(); + float mass = 0.f; + for (auto const & b : physics.bones) + { + cm_vel += b.velocity * b.mass; + mass += b.mass; + } + cm_vel /= mass; + + test_speeds.push_back(physics.bones[2].velocity[0]); + } +} + +void animation_2d_app::present() +{ + update_camera(); + + gl::ClearColor(0.8f, 0.8f, 0.8f, 1.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + { + float ground_width = 0.1f; + painter.line({view_bbox[0].min, -ground_width/2.f}, {view_bbox[0].max, -ground_width/2.f}, ground_width, gfx::black); + } + + for (auto const & b : physics.bones) + { + auto c = gfx::dark(gfx::blue).as_color_rgba(); + painter.line(b.position - b.direction * b.length / 2.f, b.position + b.direction * b.length / 2.f, b.width, c); + } + + if (selected) + { + auto c = gfx::blue.as_color_rgba(); + + auto const & b = physics.bones[*selected]; + painter.line(b.position - b.direction * b.length / 2.f, b.position + b.direction * b.length / 2.f, b.width, c); + } + + for (auto const & b : physics.bones) + { + for (float s : {-1.f, 1.f}) + { + auto c = gfx::dark(gfx::red).as_color_rgba(); + auto p = b.position + s * b.direction * b.length / 2.f; + if (p[1] < b.width / 2.f) + c = gfx::yellow.as_color_rgba(); + painter.circle(p, b.width * 0.35f, c); + } + } + + painter.render(geom::orthographic_camera{view_bbox}.transform()); + + float avg_speed = 0.f; + + if (mode == mode::test) + { + for (float s : test_speeds) + avg_speed += s / test_speeds.size(); + + int margin = 40; + float const step = 1.f; + + int max_frames_shown = (width() - 2 * margin) / step; + int start = std::max(0, static_cast(test_speeds.size() - max_frames_shown)); + + for (std::size_t i = start; i + 1 < test_speeds.size(); ++i) + { + float const scale = 2.f; + geom::point p0{40.f + (i - start ) * step, 180.f - test_speeds[i ] * scale}; + geom::point p1{40.f + (i - start + 1) * step, 180.f - test_speeds[i + 1] * scale}; + painter.line(p0, p1, 2.f, gfx::red, false); + } + } + + { + gfx::painter::text_options opts; + opts.c = gfx::black; + opts.x = gfx::painter::x_align::left; + opts.y = gfx::painter::y_align::top; + opts.scale = 2.f; + painter.text({40.f, 40.f}, util::to_string(train_iterations, "/", max_train_iterations, " ", optimize_iterations, "/", max_optimize_iterations), opts); + painter.text({40.f, 64.f}, util::to_string("Best score: ", std::setprecision(10), best_score), opts); + painter.text({40.f, 88.f}, util::to_string("Model: ", test_id, "/", population.size(), ", gen ", population[test_id].generation), opts); + painter.text({40.f, 112.f}, util::to_string(util::pretty(test_clock.duration(), std::chrono::milliseconds{1})), opts); + painter.text({40.f, 136.f}, util::to_string("View height: ", view_bbox[1].length()), opts); + +// if (mode == mode::test && !test_speeds.empty()) painter.text({40.f, 136.f}, util::to_string("Speed: ", test_speeds.back()), opts); + } + + painter.render(geom::window_camera{width(), height()}.transform()); +} + +} + +int main() +{ + return app::main(); +}