#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace psemek::cg { template struct box; template box(geom::box) -> box; template struct box { box() = default; box(geom::box const & b); std::array, 8> vertices; }; template box::box(geom::box const & b) { for (std::size_t z = 0; z < 2; ++z) { for (std::size_t y = 0; y < 2; ++y) { for (std::size_t x = 0; x < 2; ++x) { std::size_t i = z * 4 + y * 2 + x; vertices[i][0] = (x == 0) ? b[0].min : b[0].max; vertices[i][1] = (y == 0) ? b[1].min : b[1].max; vertices[i][2] = (z == 0) ? b[2].min : b[2].max; } } } } template auto const & vertices(box const & b) { return b.vertices; } namespace detail { inline auto const & cubiod_edges() { static const std::array, 12> result = {{ { 0b000, 0b001 }, { 0b010, 0b011 }, { 0b100, 0b101 }, { 0b110, 0b111 }, { 0b000, 0b010 }, { 0b001, 0b011 }, { 0b100, 0b110 }, { 0b101, 0b111 }, { 0b000, 0b100 }, { 0b001, 0b101 }, { 0b010, 0b110 }, { 0b011, 0b111 }, }}; return result; } inline auto const & cubiod_faces() { static const std::array, 6> result = {{ {{ 0b000, 0b100, 0b110, 0b010 }}, {{ 0b001, 0b011, 0b111, 0b101 }}, {{ 0b000, 0b001, 0b101, 0b100 }}, {{ 0b010, 0b110, 0b111, 0b011 }}, {{ 0b000, 0b010, 0b011, 0b001 }}, {{ 0b100, 0b101, 0b111, 0b110 }}, }}; return result; } } template auto const & edges(box const &) { return detail::cubiod_edges(); } template auto const & faces(box const &) { return detail::cubiod_faces(); } template auto const & face_normals(box const &) { static const std::array, 6> result = {{ {-1, 0, 0}, { 1, 0, 0}, { 0, -1, 0}, { 0, 1, 0}, { 0, 0, -1}, { 0, 0, 1}, }}; return result; } template auto const & edge_directions(box const &) { static const std::array, 3> result = {{ { 1, 0, 0}, { 0, 1, 0}, { 0, 0, 1}, }}; return result; } template struct frustum; template frustum(geom::matrix) -> frustum; template struct frustum { frustum(geom::matrix const & m); std::array, 8> vertices; std::array, 6> face_normals; }; template frustum::frustum(geom::matrix const & m) { bool flip = (geom::det(m) < 0); for (std::size_t z = 0; z < 2; ++z) { for (std::size_t y = 0; y < 2; ++y) { for (std::size_t x = 0; x < 2; ++x) { std::size_t i = z * 4 + y * 2 + (flip ? 1 - x : x); geom::vector p; p[0] = (x == 0) ? -1 : 1; p[1] = (y == 0) ? -1 : 1; p[2] = (z == 0) ? -1 : 1; p[3] = 1; geom::gauss(m, p); vertices[i] = geom::as_point(p); } } } } template auto const & vertices(frustum const & f) { return f.vertices; } template auto const & edges(frustum const &) { return detail::cubiod_edges(); } template auto const & faces(frustum const &) { return detail::cubiod_faces(); } template struct triangular_prism { triangular_prism() = default; triangular_prism(geom::triangle> const & t, geom::vector const & d); std::array, 6> vertices; std::array, 4> edge_directions; }; template triangular_prism::triangular_prism(geom::triangle> const & t, geom::vector const & d) { for (std::size_t i = 0; i < 3; ++i) { vertices[i] = t[i]; vertices[i + 3] = t[i] + d; } if (geom::dot(geom::normal(vertices[0], vertices[1], vertices[2]), d) < 0) { std::swap(vertices[1], vertices[2]); std::swap(vertices[4], vertices[5]); } for (std::size_t i = 0; i < 3; ++i) { edge_directions[i] = geom::normalized(vertices[(i + 1) % 3] - vertices[i]); } edge_directions[3] = geom::normalized(d); } template auto const & vertices(triangular_prism const & p) { return p.vertices; } template auto const & edges(triangular_prism const &) { static const std::array, 9> result = {{ { 0, 1 }, { 1, 2 }, { 2, 0 }, { 3, 4 }, { 4, 5 }, { 5, 3 }, { 0, 3 }, { 1, 4 }, { 2, 5 }, }}; return result; } template auto const & faces(triangular_prism const &) { static std::array, 5> result = {{ { 0, 2, 1 }, { 3, 4, 5 }, { 0, 1, 4, 3 }, { 1, 2, 5, 4 }, { 2, 0, 3, 5 }, }}; return result; } template auto const & edge_directions(triangular_prism const & p) { return p.edge_directions; } namespace detail { template struct has_static_size : std::false_type {}; template struct has_static_size> : std::true_type {}; template constexpr bool has_static_size_v = has_static_size::value; template struct static_size; template struct static_size> { static constexpr std::size_t value = N; }; template constexpr std::size_t static_size_v = static_size::value; } template auto triangles(Body const & b) { auto const & fs = faces(b); using faces_type = std::remove_cvref_t; using face_type = std::remove_cvref_t; using index_type = std::remove_cvref_t; auto impl = [&fs](auto out) { for (auto const & f : fs) { auto it0 = std::begin(f); for (auto it = std::next(it0), jt = std::next(it); jt < std::end(f); it = jt++) { *out++ = {*it0, *it, *jt}; } } }; if constexpr (detail::has_static_size_v && detail::has_static_size_v) { constexpr std::size_t faces_count = detail::static_size_v; constexpr std::size_t face_size = detail::static_size_v; std::array, faces_count * (face_size - 2)> result; impl(result.begin()); return result; } else { std::vector> result; if constexpr (detail::has_static_size_v) { constexpr std::size_t face_size = detail::static_size_v; result.reserve(fs.size() * (face_size - 2)); } else { result.reserve(fs.size()); } impl(std::back_inserter(result)); return result; } } template auto edge_directions(Body const & b) { auto const & vs = vertices(b); auto const & es = edges(b); using edges_type = std::remove_cvref_t; using vector_type = std::remove_cvref_t; auto impl = [&es, &vs](auto out) { for (auto const & e : es) { *out++ = geom::normalized(vs[e[1]] - vs[e[0]]); } }; if constexpr (detail::has_static_size_v) { constexpr std::size_t edges_count = detail::static_size_v; std::array result; impl(result.begin()); return result; } else { std::vector result; result.reserve(es.size()); impl(std::back_inserter(result)); return result; } } template auto faces(Body const & b) { return triangles(b); } template auto face_normals(Body const & b) { auto const & vs = vertices(b); auto const & fs = faces(b); using faces_type = std::remove_cvref_t; using vector_type = std::remove_cvref_t; auto impl = [&fs, &vs](auto out) { for (auto const & f : fs) { *out++ = geom::normal(vs[f[0]], vs[f[1]], vs[f[2]]); } }; if constexpr (detail::has_static_size_v) { constexpr std::size_t faces_count = detail::static_size_v; std::array result; impl(result.begin()); return result; } else { std::vector result; result.reserve(fs.size()); impl(std::back_inserter(result)); return result; } } template bool inside(P const & p, Body const & body) { auto const & vs = vertices(body); auto const & fs = faces(body); for (auto const & f : fs) { if (geom::orientation(vs[f[0]], vs[f[1]], vs[f[2]], p) == geom::sign_t::negative) return false; } return true; } // returns pair(normalized vector from 1 to 2, signed distance) // distance <= 0 means intersection template auto separation(Body1 const & b1, Body2 const & b2) { auto const & vs1 = vertices(b1); auto const & vs2 = vertices(b2); auto const & fs1 = faces(b1); auto const & fs2 = faces(b2); auto const & eds1 = edge_directions(b1); auto const & eds2 = edge_directions(b2); using vector_type = std::remove_cvref_t; using scalar_type = std::remove_cvref_t; vector_type res_n = vector_type::zero(); auto res_d = -std::numeric_limits::infinity(); auto process_faces = [](auto const & vs1, auto const & fs1, auto const & vs2) { vector_type res_n = vector_type::zero(); scalar_type res_d = -std::numeric_limits::infinity(); for (auto const & f : fs1) { auto const face_n = geom::normal(vs1[f[0]], vs1[f[1]], vs1[f[2]]); scalar_type face_d = std::numeric_limits::infinity(); auto const face_p = vs1[f[0]]; for (auto const & v : vs2) { auto const d = geom::dot(face_n, v - face_p); face_d = std::min(d, face_d); } if (face_d == std::numeric_limits::infinity()) { throw 42; } if (face_d > res_d) { res_d = face_d; res_n = face_n; } } return std::make_pair(res_n, res_d); }; auto process_edges = [](auto const & vs1, auto const & eds1, auto const & vs2, auto const & eds2) { vector_type res_n = vector_type::zero(); scalar_type res_d = -std::numeric_limits::infinity(); for (auto const & ed1 : eds1) { for (auto const & ed2 : eds2) { auto edge_n = geom::cross(ed1, ed2); auto l = geom::length(edge_n); if (l == 0) continue; edge_n /= l; geom::interval i1, i2; for (auto const & v : vs1) { i1 |= geom::dot(geom::homogeneous(v), geom::homogeneous(edge_n)); } for (auto const & v : vs2) { i2 |= geom::dot(geom::homogeneous(v), geom::homogeneous(edge_n)); } auto edge_d12 = i2.min - i1.max; auto edge_d21 = i1.min - i2.max; scalar_type edge_d; if (edge_d12 > edge_d21) { edge_d = edge_d12; } else { edge_d = edge_d21; edge_n = -edge_n; } if (edge_d > res_d) { res_d = edge_d; res_n = edge_n; } } } return std::make_pair(res_n, res_d); }; { auto res12 = process_faces(vs1, fs1, vs2); if (res12.second > res_d) { res_d = res12.second; res_n = res12.first; } } { auto res21 = process_faces(vs2, fs2, vs1); if (res21.second > res_d) { res_d = res21.second; res_n = -res21.first; } } { auto rese = process_edges(vs1, eds1, vs2, eds2); if (rese.second > res_d) { res_d = rese.second; res_n = rese.first; } } return std::make_pair(res_n, res_d); } } using namespace psemek; template struct smooth_updater { smooth_updater(Value & value, T speed) : value_{value} , target_value_{value} , speed_{speed} {} smooth_updater & operator = (Value const & value) { target_value_ = value; return *this; } operator Value const & () const { return target_value_; } void update(T dt) { value_ += std::min(T{1}, dt * speed_) * (target_value_ - value_); } private: Value & value_; Value target_value_; T speed_; }; static const float icosa_a = 0.850650808; // phi / sqrt(1 + phi^2) static const float icosa_b = 0.525731112; // 1 / sqrt(1 + phi^2) static const float icosa_side = 1.05146222; // 2 / sqrt(phi * sqrt(5)) static const geom::vector icosa_vertices[12] = { {-icosa_a, -icosa_b, 0.0}, // 0 {-icosa_a, icosa_b, 0.0}, // 1 { icosa_a, -icosa_b, 0.0}, // 2 { icosa_a, icosa_b, 0.0}, // 3 {0.0, -icosa_a, -icosa_b}, // 4 {0.0, -icosa_a, icosa_b}, // 5 {0.0, icosa_a, -icosa_b}, // 6 {0.0, icosa_a, icosa_b}, // 7 {-icosa_b, 0.0, -icosa_a}, // 8 { icosa_b, 0.0, -icosa_a}, // 9 {-icosa_b, 0.0, icosa_a}, // 10 { icosa_b, 0.0, icosa_a}, // 11 }; static const std::size_t icosa_faces[20][3] = { {0, 1, 8,}, {0, 10, 1,}, {2, 9, 3,}, {2, 3, 11,}, {4, 5, 0,}, {4, 2, 5,}, {6, 1, 7,}, {6, 7, 3,}, {8, 9, 4,}, {8, 6, 9,}, {10, 5, 11,}, {10, 11, 7,}, {0, 8, 4,}, {0, 5, 10,}, {1, 6, 8,}, {1, 10, 7,}, {2, 4, 9,}, {2, 11, 5,}, {3, 9, 6,}, {3, 7, 11}, }; static char const tile_vs[] = R"(#version 330 uniform mat4 u_transform; uniform int u_icosa_face; uniform int u_N; uniform vec3 u_p0; uniform vec3 u_p1; uniform vec3 u_p2; uniform vec4 u_color; out vec3 color; void main() { int i = int(floor(0.5 * (sqrt(1.0 + 8.0 * gl_VertexID) - 1.0))); int j = gl_VertexID - (i * (i + 1)) / 2; float t0 = 1.0 - float(i) / float(u_N); float t1 = float(j) / float(u_N); float t2 = 1.0 - t0 - t1; vec3 p = normalize(u_p0 * t0 + u_p1 * t1 + u_p2 * t2); gl_Position = u_transform * vec4(p, 1.0); color = u_color.rgb; })"; static char const tile_fs[] = R"(#version 330 in vec3 color; out vec4 out_color; void main() { out_color = vec4(color, 1.0); })"; static char const test_vs[] = R"(#version 330 uniform mat4 u_transform; layout (location = 0) in vec4 in_position; out vec4 color; void main() { gl_Position = u_transform * in_position; color = vec4(in_position.xyz * 0.5 + vec3(0.5), 1.0); })"; static char const test_fs[] = R"(#version 330 in vec4 color; out vec4 out_color; void main() { out_color = color; })"; struct srtm_app : app::app { srtm_app(); void on_resize(int width, int height) override; void on_mouse_move(int x, int y, int dx, int dy) override; void update() override; void present() override; // geom::spherical_camera camera; geom::free_camera camera; smooth_updater camera_azimuthal_angle_updater{camera.azimuthal_angle, 20.f}; smooth_updater camera_elevation_angle_updater{camera.elevation_angle, 20.f}; bool camera_forward = false; static constexpr int tile_size_log2 = 8; static constexpr int tile_size = (1 << tile_size_log2); gfx::mesh tile_mesh[tile_size_log2 + 1]; gfx::program tile_program{tile_vs, tile_fs}; gfx::program test_program{test_vs, test_fs}; gfx::mesh test_mesh; struct test_object { cg::box body; gfx::mesh mesh; int x, y; }; int const test_object_count = 21; std::vector test_objects; util::clock> frame_clock; util::moving_average frame_dt_average{32}; gfx::painter painter; }; srtm_app::srtm_app() : app("SRTM", 4) { vsync(false); show_cursor(false); camera.fov_y = geom::rad(45.f); camera.near_clip = 0.0001f; camera.far_clip = 10.f; // camera.target = {0.f, 0.f, 0.f}; camera.pos = {0.f, -10.f, 0.f}; camera.azimuthal_angle = 0.f; camera.elevation_angle = 0.f; // camera.distance = 5.f; camera_azimuthal_angle_updater = camera.azimuthal_angle; camera_elevation_angle_updater = camera.elevation_angle; // camera_distance_updater = camera.distance; for (std::size_t N = 0; N <= tile_size_log2; ++N) { std::vector indices; std::size_t step = tile_size / (1 << N); auto idx = [step](std::size_t i, std::size_t j) -> std::uint16_t { i *= step; j *= step; return (i * (i + 1)) / 2 + j; }; for (std::size_t i = 0; i < (1 << N); ++i) { for (std::size_t j = 0; j <= i; ++j) { indices.push_back(idx(i + 1, j)); indices.push_back(idx(i, j)); } indices.push_back(idx(i + 1, i + 1)); indices.push_back(0xffffu); } tile_mesh[N].load_index(indices, gl::TRIANGLE_STRIP, gl::STATIC_DRAW); } for (int x = - test_object_count + 1; x <= test_object_count - 1; x += 2) { for (int y = - test_object_count + 1; y <= test_object_count - 1; y += 2) { auto & o = test_objects.emplace_back(); o.x = (x + test_object_count - 1) / 2; o.y = (y + test_object_count - 1) / 2; geom::box b {{ {x - 1.f, x + 1.f}, {y - 1.f, y + 1.f}, {0 - 1.f, 0 + 1.f} }}; o.body = cg::box(b); auto const & vertices = cg::vertices(o.body); auto const & edges = cg::edges(o.body); o.mesh.setup>(); o.mesh.load(vertices.data(), vertices.size(), edges.data(), edges.size()); } } } void srtm_app::on_resize(int width, int height) { app::on_resize(width, height); camera.set_fov(camera.fov_y, (1.f * width) / height); } void srtm_app::on_mouse_move(int x, int y, int dx, int dy) { app::on_mouse_move(x, y, dx, dy); camera_azimuthal_angle_updater = camera_azimuthal_angle_updater - 0.01f * dx; camera_elevation_angle_updater = camera_elevation_angle_updater + 0.01f * dy; } void srtm_app::update() { float dt = frame_clock.restart().count(); frame_dt_average.push(dt); if (is_key_down(SDLK_q)) { } if (is_key_down(SDLK_e)) { } camera_azimuthal_angle_updater.update(dt); camera_elevation_angle_updater.update(dt); float const camera_speed = std::min(5.f, geom::distance(camera.pos, geom::point::zero()) - 1.f); auto const camera_forward = camera.direction(); auto const camera_up = camera.axis_y(); auto const camera_right = camera.axis_x(); if (is_key_down(SDLK_w)) { camera.pos += camera_speed * dt * camera_forward; } if (is_key_down(SDLK_s)) { camera.pos -= camera_speed * dt * camera_forward; } if (is_key_down(SDLK_d)) { camera.pos += camera_speed * dt * camera_right; } if (is_key_down(SDLK_a)) { camera.pos -= camera_speed * dt * camera_right; } if (is_key_down(SDLK_LSHIFT)) { camera.pos += camera_speed * dt * camera_up; } if (is_key_down(SDLK_LCTRL)) { camera.pos -= camera_speed * dt * camera_up; } } namespace std { template std::ostream & operator << (std::ostream & os, std::vector const & v) { os << "["; bool first = true; for (auto const & x : v) { if (first) first = false; else os << ", "; os << x; } return os << "]"; } } void srtm_app::present() { std::vector info; gl::ClearColor(0.9f, 0.9f, 0.9f, 0.f); gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); gl::LineWidth(2.f); gl::PolygonMode(gl::FRONT_AND_BACK, gl::LINE); gl::PointSize(5.f); gl::Enable(gl::CULL_FACE); gl::Enable(gl::DEPTH_TEST); gl::DepthFunc(gl::LEQUAL); gl::Enable(gl::PRIMITIVE_RESTART); gl::PrimitiveRestartIndex(0xffffu); { auto d = geom::distance(camera.pos, geom::point::zero()); camera.far_clip = std::sqrt(d * d + 1.f); camera.near_clip = (d > 2.f) ? d - 2.f : 0.0001f; } auto const camera_transform = camera.transform(); auto const camera_pos = camera.position(); auto const camera_direction = camera.direction(); info.push_back(util::to_string("Camera height: ", (geom::distance(camera_pos, geom::point::zero()) - 1.f) * 6400000.f, " m")); auto const frustum = cg::frustum(camera_transform); tile_program.bind(); tile_program["u_transform"] = camera_transform; tile_program["u_N"] = static_cast(tile_size); info.push_back(util::to_string("Camera pos: ", camera_pos)); std::size_t rendered_tiles = 0; std::vector id; auto visit = util::recursive([&](auto & self, geom::vector const (&v)[3], int level = 0) -> void { auto const o = geom::point::zero(); auto m = (v[0] + v[1] + v[2]) / 3.f; m = geom::normalized(m) * (1.f + 1.f / 6400.f) - m; { bool culled = true; for (std::size_t i = 0; i < 3; ++i) { if (geom::dot(v[i], geom::normalized(camera_pos - o)) >= 0.f) { culled = false; break; } } if (culled) return; (void)culled; } geom::triangle> t{o + v[0], o + v[1], o + v[2]}; cg::triangular_prism body{t, m}; if (cg::separation(body, frustum).second > 0.f) return; bool const selected = geom::intersect(geom::ray{camera_pos, camera_direction}, t); float on_screen_unit; { auto edge = [](auto const & v0, auto const & v1, auto const & u) -> std::optional { auto const n = geom::normalized(geom::cross(v0, v1)); auto v = geom::normalized(u - n * dot(u, n)); if (geom::dot(geom::cross(v0, v), geom::cross(v, v1)) >= 0.f) return geom::length(v - u); return std::nullopt; }; float distance = std::numeric_limits::infinity(); auto c = camera_pos - o; if (geom::det(v[0], v[1], c) >= 0.f && geom::det(v[1], v[2], c) >= 0.f && geom::det(v[2], v[0], c) >= 0.f) distance = std::min(distance, geom::length(c) - 1.f); if (auto d = edge(v[0], v[1], c); d) distance = std::min(distance, *d); if (auto d = edge(v[1], v[2], c); d) distance = std::min(distance, *d); if (auto d = edge(v[2], v[0], c); d) distance = std::min(distance, *d); distance = std::min(distance, geom::length(c - v[0])); distance = std::min(distance, geom::length(c - v[1])); distance = std::min(distance, geom::length(c - v[2])); on_screen_unit = width() / distance / std::tan(camera.fov_x / 2.f); } assert(on_screen_unit > 0.f); float const max_triangle_size = 10.f; // pixels float const side_length = icosa_side / (1 << (level * 4)); int tile_n = std::ceil(std::log2(on_screen_unit * side_length / max_triangle_size)); if (level < 3 && tile_n > tile_size_log2) { std::size_t const C = 16; auto at = [&](std::size_t i, std::size_t j) { float t0 = 1.f - (1.f * i) / C; float t1 = (1.f * j) / C; float t2 = 1.f - t0 - t1; return geom::normalized(v[0] * t0 + v[1] * t1 + v[2] * t2); }; std::size_t child_id = 0; auto child = [&](std::size_t i0, std::size_t j0, std::size_t i1, std::size_t j1, std::size_t i2, std::size_t j2) { geom::vector v[3]; v[0] = at(i0, j0); v[1] = at(i1, j1); v[2] = at(i2, j2); id.push_back(child_id++); self(v, level + 1); id.pop_back(); }; for (std::size_t i = 0; i < C; ++i) { for (std::size_t j = 0; j < i; ++j) { child(i + 1, j, i, j, i + 1, j + 1); child(i + 1, j + 1, i, j, i, j + 1); } child(i + 1, i, i, i, i + 1, i + 1); } } else { tile_n = geom::clamp(tile_n, {0, tile_size_log2}); ++rendered_tiles; static gfx::color_4f colors[4] { gfx::black, gfx::dark(gfx::red).as_color_4f(), gfx::dark(gfx::green).as_color_4f(), gfx::blue }; tile_program["u_p0"] = v[0]; tile_program["u_p1"] = v[1]; tile_program["u_p2"] = v[2]; tile_program["u_color"] = colors[tile_n % 4]; tile_mesh[tile_n].draw(); if (selected) { gl::Disable(gl::DEPTH_TEST); tile_program["u_color"] = gfx::white.as_color_4f(); tile_mesh[0].draw(); gl::Enable(gl::DEPTH_TEST); info.push_back(util::to_string("Selected id: ", id)); info.push_back(util::to_string("Selected separation: ", cg::separation(body, frustum).second)); } } }); // if(false) // for (int f = 4; f < 5; ++f) for (int f = 0; f < 20; ++f) { geom::vector v[3]; v[0] = icosa_vertices[icosa_faces[f][0]]; v[1] = icosa_vertices[icosa_faces[f][1]]; v[2] = icosa_vertices[icosa_faces[f][2]]; id.push_back(f); visit(v); id.pop_back(); } info.push_back(util::to_string("Tiles: ", rendered_tiles)); if (false) { gl::PolygonMode(gl::FRONT_AND_BACK, gl::FILL); gl::Disable(gl::CULL_FACE); test_program.bind(); test_program["u_transform"] = camera_transform; int visible_count = 0; for (auto const & o : test_objects) { gfx::color_rgba color; if (cg::separation(o.body, frustum).second < 0.f) { ++visible_count; o.mesh.draw(); color = gfx::red; } else { color = gfx::black; } float sx = width() - 10 * test_object_count + o.x * 10; float sy = height() - 10 * test_object_count + (test_object_count - 1 - o.y) * 10; painter.rect({{{sx, sx + 10.f}, {sy, sy + 10.f}}}, color); } info.push_back(util::to_string("Visible count: ", visible_count)); } { float s = 10.f; painter.line({width() / 2.f - s, height() / 2.f}, {width() / 2.f + s, height() / 2.f}, 3.f, gfx::cyan, false); painter.line({width() / 2.f, height() / 2.f - s}, {width() / 2.f, height() / 2.f + s}, 3.f, gfx::cyan, false); } { info.insert(info.begin(), util::to_string("FPS: ", 1.f / frame_dt_average.average())); gfx::painter::text_options opts; opts.x = gfx::painter::x_align::left; opts.y = gfx::painter::y_align::top; opts.f = gfx::painter::font::font_9x12; opts.c = gfx::cyan; opts.scale = 2.f; for (int l = 0; l < info.size(); ++l) { painter.text({10.f + 1.f, 10.f + 24.f * l + 1.f}, info[l], opts); } opts.c = gfx::black; for (int l = 0; l < info.size(); ++l) { painter.text({10.f, 10.f + 24.f * l}, info[l], opts); } } gl::PolygonMode(gl::FRONT_AND_BACK, gl::FILL); gl::Enable(gl::BLEND); gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); gl::Disable(gl::DEPTH_TEST); painter.render(geom::window_camera{width(), height()}.transform()); } int main() { return app::main(); }