Files
tanks-reborn/source/game/engine.cpp

517 lines
16 KiB
C++

#include "engine.hpp"
#include <backends/imgui_impl_glfw.h>
#include <exception>
#include <game/components.hpp>
#include <glm/trigonometric.hpp>
#include <imgui.h>
#include <limits>
#include <ranges>
#include <vuk/extra/ImGuiIntegration.hpp>
#include "GLFW/glfw3.h"
#include "glm/ext/matrix_clip_space.hpp"
#include "glm/ext/matrix_transform.hpp"
#include <glm/gtc/matrix_transform.hpp>
namespace {
[[nodiscard]] auto load_scene_to_renderer(trb::render::renderer &renderer,
trb::game::scene &scene) {
const auto upload_mesh_node = [&](const auto &mesh) {
return renderer.register_mesh(
mesh,
scene.textures[scene.materials[mesh.material_index].albedo_texture]);
};
return scene.meshes | std::views::transform(upload_mesh_node) |
std::ranges::to<std::vector<std::uint32_t>>();
}
[[nodiscard]] std::pair<glm::vec3, glm::vec3>
bounds_of(const trb::game::scene &scene) {
auto min = glm::vec3(std::numeric_limits<float>::infinity());
auto max = glm::vec3(-std::numeric_limits<float>::infinity());
for (const auto &mesh : scene.meshes) {
const auto transform = [mesh](glm::vec3 p) {
return glm::vec3(mesh.transform * glm::vec4(p, 1.0f));
};
for (const auto [position, normal, uv] : mesh.vertices) {
min = glm::min(min, transform(position));
max = glm::max(max, transform(position));
}
}
return std::make_pair(min, max);
}
struct level_view {
glm::mat4 view;
glm::mat4 projection;
};
[[nodiscard]] level_view calculate_camera_matrices(glm::vec3 min, glm::vec3 max,
float aspectRatio,
float zoom = 1.0f,
bool debug = false,
float timeSeconds = 0.0f) {
glm::vec3 center = glm::vec3((min.x + max.x) * 0.5f, (min.y + max.y) * 0.5f,
(min.z + max.z) * 0.5f);
glm::mat4 view;
glm::mat4 projection;
if (debug) {
float bboxSize = glm::length(max - min);
float baseDistance = bboxSize * 1.5f * 0.1f;
float orbitSpeed = 0.2f;
float bobSpeed = 1.0f;
float bobAmount = bboxSize * 0.08f;
float angle = timeSeconds * orbitSpeed;
glm::vec3 offset =
glm::vec3(glm::cos(angle) * baseDistance * 1.05f,
glm::sin(timeSeconds * bobSpeed) * bobAmount +
baseDistance * 0.9f, // up/down bob + base height
glm::sin(angle) * baseDistance * 1.15f);
glm::vec3 cameraPos = center + offset;
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
view = glm::lookAt(cameraPos, center, up);
float fov = 45.0f;
float nearPlane = baseDistance * 0.1f;
float farPlane = baseDistance * 4.0f;
projection =
glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
} else {
glm::vec3 cameraPos = center + glm::vec3(0.0f, 100.0f, 100.0f);
view = glm::lookAt(cameraPos, center, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 corners[8] = {{min.x, min.y, min.z}, {max.x, min.y, min.z},
{min.x, max.y, min.z}, {max.x, max.y, min.z},
{min.x, min.y, max.z}, {max.x, min.y, max.z},
{min.x, max.y, max.z}, {max.x, max.y, max.z}};
float vMinX = FLT_MAX, vMaxX = -FLT_MAX;
float vMinY = FLT_MAX, vMaxY = -FLT_MAX;
for (const auto &corner : corners) {
glm::vec4 vSpace = view * glm::vec4(corner, 1.0f);
vMinX = glm::min(vMinX, vSpace.x);
vMaxX = glm::max(vMaxX, vSpace.x);
vMinY = glm::min(vMinY, vSpace.y);
vMaxY = glm::max(vMaxY, vSpace.y);
}
float width = (vMaxX - vMinX) / zoom;
float height = (vMaxY - vMinY) / zoom;
float midX = (vMinX + vMaxX) * 0.5f;
float midY = (vMinY + vMaxY) * 0.5f;
if (aspectRatio > (width / height)) {
width = height * aspectRatio;
} else {
height = width / aspectRatio;
}
projection = glm::ortho(midX - width * 0.5f, midX + width * 0.5f,
midY - height * 0.5f, midY + height * 0.5f,
-1000.0f, 1000.0f);
}
return {view, projection};
}
} // namespace
namespace trb::game {
engine::game_models engine::load_models(std::filesystem::path model_path) {
return {
.boundary = load_glb(model_path / "level_scene.glb"),
.boxes = load_glb(model_path / "boxes.glb"),
};
}
engine::map_boxes engine::load_boxes() {
auto boxes = map_boxes();
const auto upload_mesh_to_ecs =
[this](const game::mesh_node &mesh) -> std::uint32_t {
const auto entity = entities.create();
const auto render_id = renderer.register_mesh(
mesh, models.boxes.textures[models.boxes.materials[mesh.material_index]
.albedo_texture]);
entities.emplace<components::render_id>(entity,
components::render_id(render_id));
return entity;
};
for (const auto &mesh : models.boxes.meshes) {
if (mesh.name == "wall.full") {
boxes.solid_full = upload_mesh_to_ecs(mesh);
} else if (mesh.name == "wall.half") {
boxes.solid_half = upload_mesh_to_ecs(mesh);
} else if (mesh.name == "explodable.full") {
boxes.cork_full = upload_mesh_to_ecs(mesh);
} else if (mesh.name == "explodable.half") {
boxes.cork_half = upload_mesh_to_ecs(mesh);
} else {
throw std::runtime_error("unexpected mesh found in box model");
}
}
return boxes;
}
std::vector<std::uint32_t> engine::load_scene_to_ecs(game::scene &scene) {
const auto load_mesh =
[this, &scene](const game::mesh_node &mesh) -> std::uint32_t {
const auto entity = entities.create();
const auto render_id = renderer.register_mesh(
mesh,
scene.textures[scene.materials[mesh.material_index].albedo_texture]);
entities.emplace<components::render_id>(entity,
components::render_id(render_id));
return entity;
};
return scene.meshes | std::views::transform(load_mesh) |
std::ranges::to<std::vector<std::uint32_t>>();
}
render::request
engine::add_level_bounds_to_render_request(render::request request) const {
for (const auto eid : level_border_entity_ids) {
const auto render_id = entities.get<components::render_id>(eid);
request = request.add_mesh(static_cast<std::uint32_t>(render_id)).commit();
}
return request;
}
render::request
engine::add_playing_bounds_to_render_request(render::request request) const {
auto lines = std::vector<glm::vec3>();
const float xmin = area_origin.x - area_size.x;
const float xmax = area_origin.x + area_size.x;
const float zmin = area_origin.z - area_size.z;
const float zmax = area_origin.z + area_size.z;
const auto p1 = glm::vec3(xmin, 0.0f, zmin);
const auto p2 = glm::vec3(xmax, 0.0f, zmin);
const auto p3 = glm::vec3(xmax, 0.0f, zmax);
const auto p4 = glm::vec3(xmin, 0.0f, zmax);
lines.push_back(p1);
lines.push_back(p2);
lines.push_back(p2);
lines.push_back(p3);
lines.push_back(p3);
lines.push_back(p4);
lines.push_back(p4);
lines.push_back(p1);
return request.add_lines(lines);
}
render::request
engine::add_tiles_to_render_request(render::request request) const {
auto lines = std::vector<glm::vec3>();
const auto full_dimensions = area_size * 2.0f;
const auto cell_size = full_dimensions.x / 22.0f;
const auto start = area_origin - area_size;
for (int x = 0; x < 22; x++) {
for (int y = 0; y < 16; y++) {
const auto min_x = start.x + (static_cast<float>(x) * cell_size);
const auto min_y = start.z + (static_cast<float>(y) * cell_size);
const auto max_x = start.x + (static_cast<float>(x + 1) * cell_size);
const auto max_y = start.z + (static_cast<float>(y + 1) * cell_size);
const auto p1 = glm::vec3(min_x, 0.0f, min_y);
const auto p2 = glm::vec3(max_x, 0.0f, min_y);
const auto p3 = glm::vec3(max_x, 0.0f, max_y);
const auto p4 = glm::vec3(min_x, 0.0f, max_y);
lines.push_back(p1);
lines.push_back(p2);
lines.push_back(p2);
lines.push_back(p3);
lines.push_back(p3);
lines.push_back(p4);
lines.push_back(p4);
lines.push_back(p1);
}
}
return request.add_lines(lines);
}
render::request
engine::add_single_tile_to_render_request(render::request request,
glm::uvec2 tile) const {
auto lines = std::vector<glm::vec3>();
const auto full_dimensions = area_size * 2.0f;
const auto cell_size = full_dimensions.x / 22.0f;
const auto start = area_origin - area_size;
const auto min_x = start.x + (static_cast<float>(tile.x) * cell_size);
const auto min_y = start.z + (static_cast<float>(tile.y) * cell_size);
const auto max_x = start.x + (static_cast<float>(tile.x + 1) * cell_size);
const auto max_y = start.z + (static_cast<float>(tile.y + 1) * cell_size);
const auto p1 = glm::vec3(min_x, 0.0f, min_y);
const auto p2 = glm::vec3(max_x, 0.0f, min_y);
const auto p3 = glm::vec3(max_x, 0.0f, max_y);
const auto p4 = glm::vec3(min_x, 0.0f, max_y);
lines.push_back(p1);
lines.push_back(p2);
lines.push_back(p2);
lines.push_back(p3);
lines.push_back(p3);
lines.push_back(p4);
lines.push_back(p4);
lines.push_back(p1);
return request.add_lines(lines, glm::vec3(1.0f, 1.0f, 0.0f));
}
engine::mesh_bounds engine::compute_size_of_mesh(const mesh_node &mesh) {
auto bounds = mesh_bounds();
const auto project = [&mesh](const glm::vec3 &v) {
return glm::vec3(mesh.transform * glm::vec4(v, 1.0f));
};
auto min = glm::vec3(std::numeric_limits<float>::infinity());
auto max = glm::vec3(-std::numeric_limits<float>::infinity());
for (const auto &v : mesh.vertices) {
min = glm::min(project(v.position), min);
max = glm::max(project(v.position), max);
}
return {
.min = min,
.max = max,
.size = max - min,
};
}
void engine::calculate_game_plane_size() {
const auto &game_plane_mesh = [&]() {
for (const auto &mesh : models.boundary.meshes) {
if (mesh.name == "Plane.001") {
return mesh;
}
}
throw std::runtime_error("couldn't find game mesh plane");
}();
const auto [min, max, size] = compute_size_of_mesh(game_plane_mesh);
area_origin = min + (max - min) * 0.5f;
area_size = (max - min) * 0.5f;
area_size *= 0.85f;
tile_size = area_size / glm::vec3(22.0f, 1.0f, 16.0f);
auto normalized_ts = tile_size / glm::vec3(tile_size.x, 1.0f, tile_size.x);
area_size.z /= normalized_ts.z;
tile_size = area_size / glm::vec3(22.0f, 1.0f, 16.0f);
tile_size.y = 1.0f;
}
void engine::calculate_box_size() {
const auto &full_box = [&]() {
for (const auto &mesh : models.boxes.meshes) {
if (mesh.name == "wall.full") {
return mesh;
}
}
throw std::runtime_error("couldn't find game mesh plane");
}();
const auto &half_box = [&]() {
for (const auto &mesh : models.boxes.meshes) {
if (mesh.name == "wall.half") {
return mesh;
}
}
throw std::runtime_error("couldn't find game mesh plane");
}();
const auto [full_max, full_min, full_size] = compute_size_of_mesh(full_box);
const auto [half_max, half_min, half_size] = compute_size_of_mesh(half_box);
full_box_size = full_size;
half_box_size = half_size;
const auto scale_required = (tile_size * 1.8f) / full_box_size;
const auto s = glm::vec3(scale_required.x, 1.0f, scale_required.z);
box_transform = glm::scale(glm::mat4(1.0f), s);
}
render::request engine::add_level_to_render_request(render::request request,
level *level) const {
for (const auto [eid, position, wall] :
level->get_entities()
->view<components::tile_position, components::wall_definition>()
.each()) {
const auto coord_transform = transform_given_coordinate(position);
auto current_height =
(wall.heights[0] == components::wall_height::full ? full_box_size.y
: half_box_size.y) *
0.5f;
current_height = 0.0f;
for (int i = 0; i < wall.wall_count; i++) {
const auto wall_entity_id =
wall.type == components::wall_type::cork
? (wall.heights[i] == components::wall_height::full
? box_entities.cork_full
: box_entities.cork_half)
: (wall.heights[i] == components::wall_height::full
? box_entities.solid_full
: box_entities.solid_half);
const auto wall_tile_render_id =
entities.get<components::render_id>(wall_entity_id);
const float half_extent =
(wall.heights[i] == components::wall_height::full ? full_box_size.y
: half_box_size.y) *
0.5f;
const auto height = glm::translate(
glm::mat4(1.0f), glm::vec3(0.0f, current_height + half_extent, 0.0f));
request =
request.add_mesh(static_cast<std::uint32_t>(wall_tile_render_id))
.with_transform(coord_transform * height * box_transform)
.commit();
current_height +=
(wall.heights[i] == components::wall_height::full ? full_box_size.y
: half_box_size.y);
}
}
return request;
}
glm::mat4 engine::transform_given_coordinate(glm::uvec2 coordinate) const {
const glm::vec3 full_dimensions = area_size * 2.0f;
const float cell_width = full_dimensions.x / 22.0f;
const float cell_depth = full_dimensions.z / 16.0f;
const auto start_point = area_origin - area_size;
float world_x = start_point.x +
(static_cast<float>(coordinate.x) * cell_width) +
(cell_width * 0.5f);
float world_z = start_point.z +
(static_cast<float>(coordinate.y) * cell_depth) +
(cell_depth * 0.5f);
return glm::translate(glm::mat4(1.0f), glm::vec3(world_x, 0.0f, world_z));
}
void engine::do_level_editor_ui() {
if (!debug_drawing.level_editor) {
return;
}
ImGui::Begin("Level Editor");
ImGui::Checkbox("Enabled", &is_editing_level);
ImGui::End();
}
void engine::do_debug_ui() {
ImGui::Begin("Debug");
ImGui::Checkbox("Playing Bounds", &debug_drawing.playing_bounds);
ImGui::Checkbox("Individual Tiles", &debug_drawing.individual_tiles);
ImGui::Checkbox("Level Editor", &debug_drawing.level_editor);
ImGui::End();
}
engine::engine()
: display(&input, {1920, 1080}, "Tanks Reborn"), renderer(&display),
models(load_models("../assets/models")),
level_border_entity_ids(load_scene_to_ecs(models.boundary)),
box_entities(load_boxes()) {
calculate_game_plane_size();
calculate_box_size();
}
void engine::run() {
while (!display.should_close()) {
display.poll_events();
try {
const auto [map_min, map_max] = ::bounds_of(models.boundary);
const auto [view, projection] = ::calculate_camera_matrices(
map_min, map_max, 1920.0f / 1080.0f, 3.0f);
renderer.do_ui([this] {
do_level_editor_ui();
do_debug_ui();
});
auto request = render::request();
if (!ImGui::GetIO().WantCaptureMouse) {
const auto mx = ImGui::GetIO().MousePos;
std::println("mouse {}, {}", mx.x, mx.y);
// request = request.add_circle({mx.x, mx.y}, 1.0f);
}
request = add_level_bounds_to_render_request(request);
if (current_level.has_value()) {
request = add_level_to_render_request(request, &(*current_level));
}
if (debug_drawing.playing_bounds) {
request = add_playing_bounds_to_render_request(request);
}
if (debug_drawing.individual_tiles) {
request = add_tiles_to_render_request(request);
}
level_editing_data.intersected_tile = glm::uvec2(5, 5);
if (level_editing_data.intersected_tile.has_value()) {
request = add_single_tile_to_render_request(
request, *level_editing_data.intersected_tile);
}
renderer.render(request, view, projection);
} catch (const vuk::VkException &ex) {
if (ex.what() != std::string("Out of date.")) {
std::rethrow_exception(std::current_exception());
}
}
}
}
} // namespace trb::game