#include "engine.hpp" #include #include #include #include #include #include #include #include #include "GLFW/glfw3.h" #include "glm/ext/matrix_clip_space.hpp" #include "glm/ext/matrix_transform.hpp" #include 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>(); } [[nodiscard]] std::pair bounds_of(const trb::game::scene &scene) { auto min = glm::vec3(std::numeric_limits::infinity()); auto max = glm::vec3(-std::numeric_limits::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(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 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(entity, components::render_id(render_id)); return entity; }; return scene.meshes | std::views::transform(load_mesh) | std::ranges::to>(); } 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(eid); request = request.add_mesh(static_cast(render_id)).commit(); } return request; } render::request engine::add_playing_bounds_to_render_request(render::request request) const { auto lines = std::vector(); 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(); 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(x) * cell_size); const auto min_y = start.z + (static_cast(y) * cell_size); const auto max_x = start.x + (static_cast(x + 1) * cell_size); const auto max_y = start.z + (static_cast(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(); 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(tile.x) * cell_size); const auto min_y = start.z + (static_cast(tile.y) * cell_size); const auto max_x = start.x + (static_cast(tile.x + 1) * cell_size); const auto max_y = start.z + (static_cast(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)); } std::optional engine::calculate_intersected_tile(glm::mat4 view, glm::mat4 projection, glm::vec2 mouse) const { const float width = static_cast(display.extent().x); const float height = static_cast(display.extent().y); float x = (2.0f * mouse.x) / width - 1.0f; float y = 1.0f - (2.0f * mouse.y) / height; glm::vec4 near_clip = glm::vec4(x, y, -1.0f, 1.0f); glm::vec4 far_clip = glm::vec4(x, y, 1.0f, 1.0f); glm::vec4 near_world = glm::inverse(projection * view) * near_clip; glm::vec4 far_world = glm::inverse(projection * view) * far_clip; near_world /= near_world.w; far_world /= far_world.w; glm::vec3 ray_world = glm::normalize(glm::vec3(far_world - near_world)); glm::vec3 cam_pos = glm::vec3(near_world); if (std::abs(ray_world.y) < 1e-5f) { return std::nullopt; } float t = -cam_pos.y / ray_world.y; glm::vec3 hit = cam_pos + ray_world * t; const auto 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 = area_origin - area_size; int tile_x = static_cast((hit.x - start.x) / cell_width); int tile_y = static_cast((hit.z - start.z) / cell_depth); tile_x = std::clamp(tile_x, 0, 21); tile_y = std::clamp(tile_y, 0, 15); return glm::uvec2(tile_x, tile_y); } 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::infinity()); auto max = glm::vec3(-std::numeric_limits::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() .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(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(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(coordinate.x) * cell_width) + (cell_width * 0.5f); float world_z = start_point.z + (static_cast(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); static int wall_type = 0; ImGui::Combo("Wall Type", &wall_type, "cork\0solid\0\0"); level_editing_data.type_to_write = static_cast(wall_type); static int wall_height = 0; ImGui::Combo("Wall Height", &wall_height, "half\0full\0\0"); level_editing_data.height_to_write = static_cast(wall_height); if (level_editing_data.intersected_tile.has_value()) { // Todo: Check for validity based on same types if (ImGui::Button("Add Wall To Tile")) { auto entity = current_level->get_or_create_entity_at( *level_editing_data.intersected_tile); auto &wall = current_level->get_entities() ->get_or_emplace(entity); wall.type = level_editing_data.type_to_write; wall.heights[wall.wall_count++] = level_editing_data.height_to_write; } } 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(); 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); } if (debug_drawing.level_editor) { if (!ImGui::GetIO().WantCaptureMouse) { const auto mx = ImGui::GetIO().MousePos; if (ImGui::GetIO().MouseDown[0]) { level_editing_data.intersected_tile = calculate_intersected_tile(view, projection, {mx.x, mx.y}); } 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