From 1b0af6cd8b33b0ef355570ef25e11f1eb42e0b48 Mon Sep 17 00:00:00 2001 From: bailehuni Date: Mon, 18 Mar 2024 01:04:33 +0000 Subject: [PATCH] Get player controller wall collisions working --- TODO => TODO.txt | 0 cloc | 71 ------ include/application.h | 2 + include/renderer.h | 1 + include/systems/collisions.h | 14 +- res/engine/shaders/debug.frag | 4 +- res/engine/shaders/debug.vert | 4 + res/engine/shaders/fancy.frag | 4 +- res/engine/shaders/fancy.vert | 2 +- src/application.cpp | 74 +++++- src/renderer.cpp | 28 ++- src/systems/collisions.cpp | 395 +++++++++++++++++++-------------- src/util/gltf_loader.cpp | 13 +- test/res/models/MY_AXES.glb | Bin 0 -> 68056 bytes test/res/models/stairs.glb | Bin 0 -> 111924 bytes test/src/camera_controller.cpp | 145 ++++++++---- test/src/camera_controller.hpp | 12 +- test/src/game.cpp | 20 +- testfile.txt | 1 - 19 files changed, 468 insertions(+), 322 deletions(-) rename TODO => TODO.txt (100%) delete mode 100644 cloc create mode 100644 test/res/models/MY_AXES.glb create mode 100644 test/res/models/stairs.glb delete mode 100644 testfile.txt diff --git a/TODO b/TODO.txt similarity index 100% rename from TODO rename to TODO.txt diff --git a/cloc b/cloc deleted file mode 100644 index d9b8137..0000000 --- a/cloc +++ /dev/null @@ -1,71 +0,0 @@ -include/application.h -include/components/collider.h -include/components/custom.h -include/components/mesh_renderable.h -include/components/transform.h -include/components/ui_renderable.h -include/ecs.h -include/engine_api.h -include/event_system.h -include/gfx.h -include/gfx_device.h -include/input_manager.h -include/inputs/keyboard.h -include/inputs/mouse.h -include/log.h -include/logger.h -include/renderer.h -include/resource_manager.h -include/resources/font.h -include/resources/material.h -include/resources/mesh.h -include/resources/shader.h -include/resources/texture.h -include/scene.h -include/scene_manager.h -include/systems/collisions.h -include/systems/custom_behaviour.h -include/systems/mesh_render_system.h -include/systems/transform.h -include/systems/ui_render_system.h -include/util.h -include/util/files.h -include/util/gltf_loader.h -include/util/model_loader.h -include/window.h -src/application.cpp -src/ecs.cpp -src/gfx_device_vulkan.cpp -src/input_manager.cpp -src/renderer.cpp -src/resources/font.cpp -src/resources/material.cpp -src/resources/mesh.cpp -src/resources/shader.cpp -src/resources/texture.cpp -src/scene.cpp -src/scene_manager.cpp -src/systems/collisions.cpp -src/systems/custom_behaviour.cpp -src/systems/mesh_render_system.cpp -src/systems/transform.cpp -src/systems/ui_render_system.cpp -src/util/files.cpp -src/util/gltf_loader.cpp -src/util/model_loader.cpp -src/vulkan/device.cpp -src/vulkan/device.h -src/vulkan/gpu_allocator.cpp -src/vulkan/gpu_allocator.h -src/vulkan/instance.cpp -src/vulkan/instance.h -src/vulkan/swapchain.cpp -src/vulkan/swapchain.h -src/window.cpp -test/src/camera_controller.cpp -test/src/camera_controller.hpp -test/src/game.cpp -test/src/game.hpp -test/src/main.cpp -test/src/meshgen.cpp -test/src/meshgen.hpp diff --git a/include/application.h b/include/application.h index efdcc00..bffde9b 100644 --- a/include/application.h +++ b/include/application.h @@ -66,6 +66,8 @@ class Application { std::string GetResourcePath(const std::string relative_path) const { return (resources_path_ / relative_path).string(); } + std::vector debug_lines{}; + private: std::unique_ptr window_; std::unique_ptr input_manager_; diff --git a/include/renderer.h b/include/renderer.h index 651be90..6b3945c 100644 --- a/include/renderer.h +++ b/include/renderer.h @@ -27,6 +27,7 @@ struct UniformDescriptor { struct Line { glm::vec3 pos1; glm::vec3 pos2; + glm::vec3 color; }; class Renderer : private ApplicationComponent { diff --git a/include/systems/collisions.h b/include/systems/collisions.h index 57040cd..7c76ea8 100644 --- a/include/systems/collisions.h +++ b/include/systems/collisions.h @@ -18,11 +18,14 @@ struct Ray { struct Raycast { glm::vec3 location; - Entity hit_entity; + glm::vec3 normal; + Entity hit_entity; // broken float distance; bool hit; }; +enum class AABBSide { Left, Right, Bottom, Top, Front, Back }; + class CollisionSystem : public System { public: CollisionSystem(Scene* scene); @@ -59,7 +62,14 @@ class CollisionSystem : public System { size_t colliders_size_last_update_ = 0; size_t colliders_size_now_ = 0; - bool RaycastTreeNode(const Ray& ray, const BiTreeNode& node, glm::vec3& location, float& t, Entity& object_index); + struct RaycastTreeNodeResult { + glm::vec3 location; + Entity object_index; + AABBSide side; + float t; + bool hit; // if this is false, all other values are undefined + }; + RaycastTreeNodeResult RaycastTreeNode(const Ray& ray, const BiTreeNode& node); static int BuildNode(std::vector& prims, std::vector& tree_nodes); }; diff --git a/res/engine/shaders/debug.frag b/res/engine/shaders/debug.frag index 7dd43fe..2b10c24 100644 --- a/res/engine/shaders/debug.frag +++ b/res/engine/shaders/debug.frag @@ -1,7 +1,9 @@ #version 450 +layout(location = 0) in vec3 inColor; + layout(location = 0) out vec4 outColor; void main() { - outColor = vec4(1.0); + outColor = vec4(inColor, 1.0); } \ No newline at end of file diff --git a/res/engine/shaders/debug.vert b/res/engine/shaders/debug.vert index f2e5b6a..8503945 100644 --- a/res/engine/shaders/debug.vert +++ b/res/engine/shaders/debug.vert @@ -2,9 +2,13 @@ layout( push_constant ) uniform Constants { vec4 positions[2]; + vec3 color; } constants; +layout(location = 0) out vec3 color; + void main() { + color = constants.color; gl_Position = constants.positions[gl_VertexIndex]; gl_Position.y *= -1.0; } diff --git a/res/engine/shaders/fancy.frag b/res/engine/shaders/fancy.frag index 7d5f27d..2583306 100644 --- a/res/engine/shaders/fancy.frag +++ b/res/engine/shaders/fancy.frag @@ -29,8 +29,8 @@ float GGXDist(float alpha_2, float N_dot_H) { void main() { const vec3 metallic_roughness = vec3(texture(materialSetMetallicRoughnessSampler, fragUV)); - const float metallic = metallic_roughness.g; - const float roughness = metallic_roughness.b; + const float metallic = metallic_roughness.b; + const float roughness = metallic_roughness.g; // roughness of zero is completely black? const float roughness_2 = roughness * roughness; const vec3 light_colour = vec3(1.0, 1.0, 1.0) * 2.4; diff --git a/res/engine/shaders/fancy.vert b/res/engine/shaders/fancy.vert index ec61072..d1c79d1 100644 --- a/res/engine/shaders/fancy.vert +++ b/res/engine/shaders/fancy.vert @@ -34,7 +34,7 @@ void main() { fragUV = inUV; fragPosTangentSpace = worldToTangentSpace * vec3(worldPosition); fragViewPosTangentSpace = worldToTangentSpace * vec3(inverse(frameSetUniformBuffer.view) * vec4(0.0, 0.0, 0.0, 1.0)); - fragLightPosTangentSpace = worldToTangentSpace * vec3(10000.0, 0000.0, 59000.0); + fragLightPosTangentSpace = worldToTangentSpace * vec3(10000.0, 0000.0, 20000.0); gl_Position.y *= -1.0; } diff --git a/src/application.cpp b/src/application.cpp index 20672e0..970292e 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -168,7 +168,7 @@ Application::Application(const char* appName, const char* appVersion, gfx::Graph GetResourceManager()->AddPersistent("builtin.normal", std::move(normalTexture)); } { - const uint8_t pixel[4] = {255, 0, 127, 255}; + const uint8_t pixel[4] = {255, 127, 0, 255}; // AO, roughness, metallic gfx::SamplerInfo samplerInfo{}; samplerInfo.minify = gfx::Filter::kNearest; samplerInfo.magnify = gfx::Filter::kNearest; @@ -208,7 +208,8 @@ void Application::GameLoop() struct DebugMenuState { bool menu_active = false; - bool show_info_window = true; + bool show_aabbs = false; + bool show_info_window = false; } debug_menu_state; // single-threaded game loop @@ -245,6 +246,7 @@ void Application::GameLoop() if (ImGui::Begin("debugMenu", 0)) { ImGui::Text("Test!"); ImGui::Text("FPS: %.3f", std::roundf(avg_fps)); + ImGui::Checkbox("Show AABBs?", &debug_menu_state.show_aabbs); } ImGui::End(); } @@ -285,8 +287,73 @@ void Application::GameLoop() const RenderList* static_list = nullptr; const RenderList* dynamic_list = nullptr; glm::mat4 camera_transform{1.0f}; - std::vector debug_lines{}; if (scene) { + if (debug_menu_state.show_aabbs) { + if (CollisionSystem* colsys = scene->GetSystem()) { + for (const auto& node : colsys->bvh_) { + if (node.type1 == CollisionSystem::BiTreeNode::Type::Entity) { + const glm::vec3 col = + (node.type1 == CollisionSystem::BiTreeNode::Type::BoundingVolume) ? glm::vec3{ 1.0f, 0.0f, 0.0f } : glm::vec3{ 0.0f, 1.0f, 0.0f }; + Line line1{ glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.min.z}, glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.min.z}, col }; + debug_lines.push_back(line1); + Line line2{ glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.min.z}, glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.min.z}, col }; + debug_lines.push_back(line2); + Line line3{ glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.min.z}, glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.min.z}, col }; + debug_lines.push_back(line3); + Line line4{ glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.min.z}, glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.min.z}, col }; + debug_lines.push_back(line4); + + Line line5{ glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.min.z}, glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.max.z}, col }; + debug_lines.push_back(line5); + Line line6{ glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.min.z}, glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.max.z}, col }; + debug_lines.push_back(line6); + Line line7{ glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.min.z}, glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.max.z}, col }; + debug_lines.push_back(line7); + Line line8{ glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.min.z}, glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.max.z}, col }; + debug_lines.push_back(line8); + + Line line9{ glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.max.z}, glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.max.z}, col }; + debug_lines.push_back(line9); + Line line10{ glm::vec3{node.box1.min.x, node.box1.min.y, node.box1.max.z}, glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.max.z}, col }; + debug_lines.push_back(line10); + Line line11{ glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.max.z}, glm::vec3{node.box1.max.x, node.box1.min.y, node.box1.max.z}, col }; + debug_lines.push_back(line11); + Line line12{ glm::vec3{node.box1.max.x, node.box1.max.y, node.box1.max.z}, glm::vec3{node.box1.min.x, node.box1.max.y, node.box1.max.z}, col }; + debug_lines.push_back(line12); + } + if (node.type2 == CollisionSystem::BiTreeNode::Type::Entity) { + const glm::vec3 col = + (node.type2 == CollisionSystem::BiTreeNode::Type::BoundingVolume) ? glm::vec3{ 1.0f, 0.0f, 0.0f } : glm::vec3{ 0.0f, 1.0f, 0.0f }; + Line line1{ glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.min.z}, glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.min.z}, col }; + debug_lines.push_back(line1); + Line line2{ glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.min.z}, glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.min.z}, col }; + debug_lines.push_back(line2); + Line line3{ glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.min.z}, glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.min.z}, col }; + debug_lines.push_back(line3); + Line line4{ glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.min.z}, glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.min.z}, col }; + debug_lines.push_back(line4); + + Line line5{ glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.min.z}, glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.max.z}, col }; + debug_lines.push_back(line5); + Line line6{ glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.min.z}, glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.max.z}, col }; + debug_lines.push_back(line6); + Line line7{ glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.min.z}, glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.max.z}, col }; + debug_lines.push_back(line7); + Line line8{ glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.min.z}, glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.max.z}, col }; + debug_lines.push_back(line8); + + Line line9{ glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.max.z}, glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.max.z}, col }; + debug_lines.push_back(line9); + Line line10{ glm::vec3{node.box2.min.x, node.box2.min.y, node.box2.max.z}, glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.max.z}, col }; + debug_lines.push_back(line10); + Line line11{ glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.max.z}, glm::vec3{node.box2.max.x, node.box2.min.y, node.box2.max.z}, col }; + debug_lines.push_back(line11); + Line line12{ glm::vec3{node.box2.max.x, node.box2.max.y, node.box2.max.z}, glm::vec3{node.box2.min.x, node.box2.max.y, node.box2.max.z}, col }; + debug_lines.push_back(line12); + } + } + } + } camera_transform = scene->GetComponent(scene->GetEntity("camera"))->world_matrix; auto mesh_render_system = scene->GetSystem(); static_list = mesh_render_system->GetStaticRenderList(); @@ -294,6 +361,7 @@ void Application::GameLoop() } renderer_->PreRender(window()->GetWindowResized(), camera_transform); renderer_->Render(static_list, dynamic_list, debug_lines); + debug_lines.clear(); // gets remade every frame :0 /* poll events */ window_->GetInputAndEvents(); diff --git a/src/renderer.cpp b/src/renderer.cpp index ab6acf5..996b8c1 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -121,24 +121,32 @@ void Renderer::Render(const RenderList* static_list, const RenderList* dynamic_l } } + struct DebugPush { + glm::vec4 pos1; + glm::vec4 pos2; + glm::vec3 color; + }; + // draw debug shit here device_->CmdBindPipeline(draw_buffer, debug_rendering_things_.pipeline); - glm::vec4 debug_positions[2] = {}; + DebugPush push{}; for (const Line& l : debug_lines) { - debug_positions[0] = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4(l.pos1, 1.0f); - debug_positions[1] = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4(l.pos2, 1.0f); - device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(glm::vec4) * 2, debug_positions); + push.pos1 = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4(l.pos1, 1.0f); + push.pos2 = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4(l.pos2, 1.0f); + push.color = l.color; + device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(DebugPush), &push); device_->CmdDraw(draw_buffer, 2, 1, 0, 0); } // also make a lil crosshair - debug_positions[0] = glm::vec4(-0.05f, 0.0f, 0.0f, 1.0f); - debug_positions[1] = glm::vec4(0.05f, 0.0f, 0.0f, 1.0f); - device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(glm::vec4) * 2, debug_positions); + push.color = glm::vec3{ 1.0f, 1.0f, 1.0f }; + push.pos1 = glm::vec4(-0.05f, 0.0f, 0.0f, 1.0f); + push.pos2 = glm::vec4(0.05f, 0.0f, 0.0f, 1.0f); + device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(DebugPush), &push); device_->CmdDraw(draw_buffer, 2, 1, 0, 0); - debug_positions[0] = glm::vec4(0.0f, -0.05f, 0.0f, 1.0f); - debug_positions[1] = glm::vec4(0.0f, 0.05f, 0.0f, 1.0f); - device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(glm::vec4) * 2, debug_positions); + push.pos1 = glm::vec4(0.0f, -0.05f, 0.0f, 1.0f); + push.pos2 = glm::vec4(0.0f, 0.05f, 0.0f, 1.0f); + device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(DebugPush), &push); device_->CmdDraw(draw_buffer, 2, 1, 0, 0); device_->CmdRenderImguiDrawData(draw_buffer, ImGui::GetDrawData()); diff --git a/src/systems/collisions.cpp b/src/systems/collisions.cpp index 74192f9..0d8eea2 100644 --- a/src/systems/collisions.cpp +++ b/src/systems/collisions.cpp @@ -29,7 +29,8 @@ static float GetBoxArea(const AABB& box) } // returns true on hit and tmin -static std::pair RayBoxIntersection(const Ray& ray, const AABB& box, float t) +// also modifies 'side' reference +static std::pair RayBoxIntersection(const Ray& ray, const AABB& box, float t, AABBSide& side) { // Thank you https://tavianator.com/cgit/dimension.git/tree/libdimension/bvh/bvh.c glm::vec3 n_inv{1.0f / ray.direction.x, 1.0f / ray.direction.y, 1.0f / ray.direction.z}; @@ -52,6 +53,20 @@ static std::pair RayBoxIntersection(const Ray& ray, const AABB& box tmin = fmaxf(tmin, fminf(tz1, tz2)); tmax = fminf(tmax, fmaxf(tz1, tz2)); + side = AABBSide::Left; + if (tmin == tx2) + side = AABBSide::Right; + else if (tmin == tx1) + side = AABBSide::Left; + else if (tmin == ty2) + side = AABBSide::Back; + else if (tmin == ty1) + side = AABBSide::Front; + else if (tmin == tz2) + side = AABBSide::Top; + else if (tmin == tz1) + side = AABBSide::Bottom; + return std::make_pair((tmax >= fmaxf(0.0, tmin) && tmin < t), tmin); } @@ -117,13 +132,40 @@ void CollisionSystem::OnUpdate(float ts) Raycast CollisionSystem::GetRaycast(Ray ray) { + ray.direction = glm::normalize(ray.direction); + Raycast res{}; res.hit = false; - ray.direction = glm::normalize(ray.direction); - if (!bvh_.empty()) { - res.hit = RaycastTreeNode(ray, bvh_.back(), res.location, res.distance, res.hit_entity); + const RaycastTreeNodeResult tree_node_cast_res = RaycastTreeNode(ray, bvh_.back()); + if (tree_node_cast_res.hit) { + res.hit = true; + res.distance = tree_node_cast_res.t; + res.location = tree_node_cast_res.location; + res.hit_entity = tree_node_cast_res.object_index; + // find normal + switch (tree_node_cast_res.side) { + case AABBSide::Left: + res.normal = glm::vec3{-1.0, 0.0f, 0.0f}; + break; + case AABBSide::Right: + res.normal = glm::vec3{1.0, 0.0f, 0.0f}; + break; + case AABBSide::Bottom: + res.normal = glm::vec3{0.0, 0.0f, -1.0f}; + break; + case AABBSide::Top: + res.normal = glm::vec3{0.0, 0.0f, 1.0f}; + break; + case AABBSide::Front: + res.normal = glm::vec3{0.0, -1.0f, 0.0f}; + break; + case AABBSide::Back: + res.normal = glm::vec3{0.0, 1.0f, 0.0f}; + break; + } + } } return res; @@ -139,38 +181,17 @@ int CollisionSystem::BuildNode(std::vector& prims, std::vector, 3> centroid_tests{ - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.centroid.x < p2.centroid.x); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.centroid.y < p2.centroid.y); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.centroid.z < p2.centroid.z); - } - }; + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.centroid.x < p2.centroid.x); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.centroid.y < p2.centroid.y); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.centroid.z < p2.centroid.z); }}; std::array, 3> box_min_tests{ - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.min.x < p2.box.min.x); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.min.y < p2.box.min.y); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.min.z < p2.box.min.z); - } - }; + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.min.x < p2.box.min.x); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.min.y < p2.box.min.y); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.min.z < p2.box.min.z); }}; std::array, 3> box_max_tests{ - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.max.x < p2.box.max.x); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.max.y < p2.box.max.y); - }, - [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { - return (p1.box.max.z < p2.box.max.z); - } - }; + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.max.x < p2.box.max.x); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.max.y < p2.box.max.y); }, + [](const PrimitiveInfo& p1, const PrimitiveInfo& p2) -> bool { return (p1.box.max.z < p2.box.max.z); }}; BiTreeNode node{}; @@ -180,7 +201,7 @@ int CollisionSystem::BuildNode(std::vector& prims, std::vector().infinity(); // surface area heuristic + float sah = std::numeric_limits().infinity(); // surface area heuristic // try along each axis for (int axis = 0; axis < 3; axis++) { @@ -190,79 +211,91 @@ int CollisionSystem::BuildNode(std::vector& prims, std::vector(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[axis])->box.min))[axis]; - float box1_main_max = reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[axis])->box.max))[axis]; - float box2_main_min = reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[axis])->box.min))[axis]; - float box2_main_max = reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[axis])->box.max))[axis]; + float box1_main_min = + reinterpret_cast(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[axis])->box.min))[axis]; + float box1_main_max = + reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[axis])->box.max))[axis]; + float box2_main_min = + reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[axis])->box.min))[axis]; + float box2_main_max = + reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[axis])->box.max))[axis]; - float box1_min1 = reinterpret_cast(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis1])->box.min))[other_axis1]; - float box1_max1 = reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis1])->box.max))[other_axis1]; - float box1_min2 = reinterpret_cast(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis2])->box.min))[other_axis2]; - float box1_max2 = reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis2])->box.max))[other_axis2]; + float box1_min1 = + reinterpret_cast(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis1])->box.min))[other_axis1]; + float box1_max1 = + reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis1])->box.max))[other_axis1]; + float box1_min2 = + reinterpret_cast(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis2])->box.min))[other_axis2]; + float box1_max2 = + reinterpret_cast(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis2])->box.max))[other_axis2]; - float box2_min1 = reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis1])->box.min))[other_axis1]; - float box2_max1 = reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[other_axis1])->box.max))[other_axis1]; - float box2_min2 = reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis2])->box.min))[other_axis2]; - float box2_max2 = reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[other_axis2])->box.max))[other_axis2]; + float box2_min1 = + reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis1])->box.min))[other_axis1]; + float box2_max1 = + reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[other_axis1])->box.max))[other_axis1]; + float box2_min2 = + reinterpret_cast(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis2])->box.min))[other_axis2]; + float box2_max2 = + reinterpret_cast(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[other_axis2])->box.max))[other_axis2]; AABB box1{}, box2{}; switch (axis) { - case 0: - // x - box1.min.x = box1_main_min; - box1.min.y = box1_min1; - box1.min.z = box1_min2; - box1.max.x = box1_main_max; - box1.max.y = box1_max1; - box1.max.z = box1_max2; - box2.min.x = box2_main_min; - box2.min.y = box2_min1; - box2.min.z = box2_min2; - box2.max.x = box2_main_max; - box2.max.y = box2_max1; - box2.max.z = box2_max2; - break; - case 1: - // y - box1.min.x = box1_min2; - box1.min.y = box1_main_min; - box1.min.z = box1_min1; - box1.max.x = box1_max2; - box1.max.y = box1_main_max; - box1.max.z = box1_max1; - box2.min.x = box2_min2; - box2.min.y = box2_main_min; - box2.min.z = box2_min1; - box2.max.x = box2_max2; - box2.max.y = box2_main_max; - box2.max.z = box2_max1; - break; - case 2: - // z - box1.min.x = box1_min1; - box1.min.y = box1_min2; - box1.min.z = box1_main_min; - box1.max.x = box1_max1; - box1.max.y = box1_max2; - box1.max.z = box1_main_max; - box2.min.x = box2_min1; - box2.min.y = box2_min2; - box2.min.z = box2_main_min; - box2.max.x = box2_max1; - box2.max.y = box2_max2; - box2.max.z = box2_main_max; - break; + case 0: + // x + box1.min.x = box1_main_min; + box1.min.y = box1_min1; + box1.min.z = box1_min2; + box1.max.x = box1_main_max; + box1.max.y = box1_max1; + box1.max.z = box1_max2; + box2.min.x = box2_main_min; + box2.min.y = box2_min1; + box2.min.z = box2_min2; + box2.max.x = box2_main_max; + box2.max.y = box2_max1; + box2.max.z = box2_max2; + break; + case 1: + // y + box1.min.x = box1_min2; + box1.min.y = box1_main_min; + box1.min.z = box1_min1; + box1.max.x = box1_max2; + box1.max.y = box1_main_max; + box1.max.z = box1_max1; + box2.min.x = box2_min2; + box2.min.y = box2_main_min; + box2.min.z = box2_min1; + box2.max.x = box2_max2; + box2.max.y = box2_main_max; + box2.max.z = box2_max1; + break; + case 2: + // z + box1.min.x = box1_min1; + box1.min.y = box1_min2; + box1.min.z = box1_main_min; + box1.max.x = box1_max1; + box1.max.y = box1_max2; + box1.max.z = box1_main_max; + box2.min.x = box2_min1; + box2.min.y = box2_min2; + box2.min.z = box2_main_min; + box2.max.x = box2_max1; + box2.max.y = box2_max2; + box2.max.z = box2_main_max; + break; } const float combined_surface_area = (GetBoxArea(box1) * (i + 1)) + (GetBoxArea(box2) * (prims.size() - (i + 1))); if (combined_surface_area < sah) { @@ -271,7 +304,7 @@ int CollisionSystem::BuildNode(std::vector& prims, std::vector& prims, std::vector& prims, std::vector::infinity(); float t2 = std::numeric_limits::infinity(); + AABBSide side1; + AABBSide side2; + if (node.type1 != Type::Empty) { - auto [is_hit, t] = RayBoxIntersection(ray, node.box1, T); + auto [is_hit, t] = RayBoxIntersection(ray, node.box1, T, side1); is_hit1 = is_hit; t1 = t; } if (node.type2 != Type::Empty) { - auto [is_hit, t] = RayBoxIntersection(ray, node.box2, T); + auto [is_hit, t] = RayBoxIntersection(ray, node.box2, T, side2); is_hit2 = is_hit; t2 = t; } // possible outcomes: - // neither hit - if (!is_hit1 && !is_hit2) return false; - - // when both hit - if (is_hit1 && is_hit2) { + if (!is_hit1 && !is_hit2) { + res.hit = false; + return res; + } + else if (is_hit1 && is_hit2) { // if 1 is a BV and 2 a gameobject, see if gameobject is in front if (node.type1 == Type::BoundingVolume && node.type2 == Type::Entity) { if (t2 < t1) { - location = (ray.direction * t2) + ray.origin; - t = t2; - object_index = node.index2; - return true; + res.location = (ray.direction * t2) + ray.origin; + res.t = t2; + res.object_index = node.index2; + res.side = side2; + res.hit = true; + return res; } else - return RaycastTreeNode(ray, bvh_.at(node.index1), location, t, object_index); + return RaycastTreeNode(ray, bvh_.at(node.index1)); } // if 1 is a gameobject and 2 a BV, see if gameobject is in front if (node.type1 == Type::Entity && node.type2 == Type::BoundingVolume) { if (t1 < t2) { - location = (ray.direction * t1) + ray.origin; - t = t1; - object_index = node.index1; - return true; + res.location = (ray.direction * t1) + ray.origin; + res.t = t1; + res.object_index = node.index1; + res.side = side1; + res.hit = true; + return res; } else - return RaycastTreeNode(ray, bvh_.at(node.index2), location, t, object_index); + return RaycastTreeNode(ray, bvh_.at(node.index2)); } // if 1 is a BV and 2 is a BV if (node.type1 == Type::BoundingVolume && node.type2 == Type::BoundingVolume) { - float node1_t{}; - glm::vec3 location1{}; - Entity object_index1{}; - bool node1_intersects = RaycastTreeNode(ray, bvh_.at(node.index1), location1, node1_t, object_index1); - float node2_t{}; - glm::vec3 location2{}; - Entity object_index2; - bool node2_intersects = RaycastTreeNode(ray, bvh_.at(node.index2), location2, node2_t, object_index2); - if (node1_intersects && node2_intersects) { - if (node1_t < node2_t) { - location = location1; - t = node1_t; - object_index = object_index1; - return true; + auto node1_intersection = RaycastTreeNode(ray, bvh_.at(node.index1)); + auto node2_intersection = RaycastTreeNode(ray, bvh_.at(node.index2)); + if (node1_intersection.hit && node2_intersection.hit) { + if (node1_intersection.t < node2_intersection.t) { + res.location = node1_intersection.location; + res.t = node1_intersection.t; + res.object_index = node1_intersection.object_index; + res.side = node1_intersection.side; + res.hit = true; + return res; } else { - location = location2; - t = node2_t; - object_index = object_index1; - return true; + res.location = node2_intersection.location; + res.t = node2_intersection.t; + res.object_index = node2_intersection.object_index; + res.side = node2_intersection.side; + res.hit = true; + return res; } } - else if (node1_intersects) { - t = node1_t; - location = location1; - object_index = object_index1; - return true; + else if (node1_intersection.hit) { + res.location = node1_intersection.location; + res.t = node1_intersection.t; + res.object_index = node1_intersection.object_index; + res.side = node1_intersection.side; + res.hit = true; + return res; } - else if (node2_intersects) { - t = node2_t; - location = location2; - object_index = object_index2; - return true; + else if (node2_intersection.hit) { + res.location = node2_intersection.location; + res.t = node2_intersection.t; + res.object_index = node2_intersection.object_index; + res.side = node2_intersection.side; + res.hit = true; + return res; } else { - return false; + res.hit = false; + return res; } } // if 1 is a gameobject and 2 is a gameobject if (node.type1 == Type::Entity && node.type2 == Type::Entity) { if (t1 < t2) { - location = (ray.direction * t1) + ray.origin; - t = t1; - object_index = node.index1; + res.location = (ray.direction * t1) + ray.origin; + res.t = t1; + res.object_index = node.index1; + res.side = side1; + res.hit = true; + return res; } else { - location = (ray.direction * t2) + ray.origin; - t = t2; - object_index = node.index2; + res.location = (ray.direction * t2) + ray.origin; + res.t = t2; + res.object_index = node.index2; + res.side = side2; + res.hit = true; + return res; } - return true; } } - - // only 1 hits - if (is_hit1) { + else if (is_hit1) { switch (node.type1) { case Type::BoundingVolume: - return RaycastTreeNode(ray, bvh_.at(node.index1), location, t, object_index); + return RaycastTreeNode(ray, bvh_.at(node.index1)); case Type::Entity: - location = (ray.direction * t1) + ray.origin; - t = t1; - object_index = node.index1; - return true; + res.location = (ray.direction * t1) + ray.origin; + res.t = t1; + res.object_index = node.index1; + res.side = side1; + res.hit = true; + return res; + } + } + else if (is_hit2) { + switch (node.type2) { + case Type::BoundingVolume: + return RaycastTreeNode(ray, bvh_.at(node.index2)); + case Type::Entity: + res.location = (ray.direction * t2) + ray.origin; + res.t = t2; + res.object_index = node.index2; + res.side = side2; + res.hit = true; + return res; } } - // only 2 hits - if (is_hit2) { - switch (node.type2) { - case Type::BoundingVolume: - return RaycastTreeNode(ray, bvh_.at(node.index2), location, t, object_index); - case Type::Entity: - location = (ray.direction * t2) + ray.origin; - t = t2; - object_index = node.index2; - return true; - } - } + throw std::runtime_error("RaycastTreeNode() hit end of function. This should not be possible"); } } // namespace engine diff --git a/src/util/gltf_loader.cpp b/src/util/gltf_loader.cpp index e7093f6..4d2023d 100644 --- a/src/util/gltf_loader.cpp +++ b/src/util/gltf_loader.cpp @@ -209,28 +209,23 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) for (const tg::Material& material : model.materials) { if (material.alphaMode != "OPAQUE") { LOG_WARN("Material {} contains alphaMode {} which isn't supported yet", material.name, material.alphaMode); - LOG_WARN("Material will be opaque"); } if (material.doubleSided == true) { LOG_WARN("Material {} specifies double-sided mesh rendering which isn't supported yet", material.name); - LOG_WARN("Material will be single-sided."); } if (material.emissiveTexture.index != -1 || material.emissiveFactor[0] != 0.0 || material.emissiveFactor[1] != 0.0 || material.emissiveFactor[2] != 0.0) { LOG_WARN("Material {} contains an emissive texture or non-zero emissive factor. Emission is currently unsupported.", material.name); - LOG_WARN("Material will be created without emission."); } const auto& baseColorFactor4 = material.pbrMetallicRoughness.baseColorFactor; if (baseColorFactor4[0] != 1.0 || baseColorFactor4[1] != 1.0 || baseColorFactor4[2] != 1.0 || baseColorFactor4[3] != 1.0) { if (material.pbrMetallicRoughness.baseColorTexture.index != -1) { LOG_WARN("Material {} contains a base color multiplier which isn't supported yet.", material.name); - LOG_WARN("The material's base color texture will be used as-is."); } } if (material.pbrMetallicRoughness.metallicFactor != 1.0 || material.pbrMetallicRoughness.roughnessFactor != 1.0) { if (material.pbrMetallicRoughness.metallicRoughnessTexture.index != -1) { - LOG_WARN("Material {} contains a metallic and/or roughness multiplier which isn't supported yet.", material.name); - LOG_WARN("The material's metallic-roughness texture will be used as-is."); + LOG_WARN("Material {} contains a metallic and/or roughness multiplier which isn't supported yet.", material.name); } } @@ -244,7 +239,6 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) } else { LOG_WARN("Material {} base color texture specifies a UV channel other than zero which is unsupported.", material.name); - LOG_WARN("Material will be created with a white base color"); } } else if (baseColorFactor4[0] != 1.0 || baseColorFactor4[1] != 1.0 || baseColorFactor4[2] != 1.0 || baseColorFactor4[3] != 1.0) { @@ -271,12 +265,11 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) } else { LOG_WARN("Material {} metallic roughness texture specifies a UV channel other than zero which is unsupported.", material.name); - LOG_WARN("Material will be created with a default metallic roughness"); } } else { LOG_INFO("Creating a metallic-roughness texture..."); - const std::vector mr_values{1.0f, material.pbrMetallicRoughness.metallicFactor, material.pbrMetallicRoughness.roughnessFactor, 1.0f}; + const std::vector mr_values{1.0f, material.pbrMetallicRoughness.roughnessFactor, material.pbrMetallicRoughness.metallicFactor, 1.0f}; Color mr(mr_values); if (metal_rough_textures.contains(mr) == false) { const uint8_t pixel[4] = {mr.r, mr.g, mr.b, mr.a}; @@ -299,7 +292,6 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) } else { LOG_WARN("Material {} occlusion texture specifies a UV channel other than zero which is unsupported.", material.name); - LOG_WARN("Material will be created with no ambient occlusion"); } } @@ -311,7 +303,6 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) } else { LOG_WARN("Material {} normal texture specifies a UV channel other than zero which is unsupported.", material.name); - LOG_WARN("Material will be created with no normal map"); } } } diff --git a/test/res/models/MY_AXES.glb b/test/res/models/MY_AXES.glb new file mode 100644 index 0000000000000000000000000000000000000000..0d1006ee6ee8accd66fef0a55a6fb7ba096f7bbb GIT binary patch literal 68056 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
=pNo~z8omppb``%AxGQK*Jj8qe&pv}IoN}Q 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"movex"); - float dy = scene_->app()->input_manager()->GetAxis("movey"); + float dx = scene_->app()->input_manager()->GetAxis("movex") * CameraControllerComponent::kSpeedStrafe; + float dy = scene_->app()->input_manager()->GetAxis("movey") * CameraControllerComponent::kSpeedForwardBack; // calculate new pitch and yaw - constexpr float kMaxPitch = glm::pi(); - constexpr float kMinPitch = 0.0f; - - float d_pitch = scene_->app()->input_manager()->GetAxis("looky") * -1.0f * c->kCameraSensitivity; + float d_pitch = scene_->app()->input_manager()->GetAxis("looky") * -1.0f * CameraControllerComponent::kCameraSensitivity; c->pitch += d_pitch; - if (c->pitch <= kMinPitch || c->pitch >= kMaxPitch) { + if (c->pitch <= CameraControllerComponent::kMinPitch || c->pitch >= CameraControllerComponent::kMaxPitch) { c->pitch -= d_pitch; } - c->yaw += scene_->app()->input_manager()->GetAxis("lookx") * -1.0f * c->kCameraSensitivity; + c->yaw += scene_->app()->input_manager()->GetAxis("lookx") * -1.0f * CameraControllerComponent::kCameraSensitivity; // update position relative to camera direction in xz plane const glm::vec3 d2x_rotated = glm::rotateZ(glm::vec3{dx, 0.0f, 0.0f}, c->yaw); const glm::vec3 d2y_rotated = glm::rotateZ(glm::vec3{0.0f, dy, 0.0f}, c->yaw); glm::vec3 h_vel = (d2x_rotated + d2y_rotated); c->vel.x = h_vel.x; - c->vel.y = h_vel.y; - // keep vel.z as gravity can increase it every frame + c->vel.y = h_vel.y; + // keep vel.z as gravity can increase it every frame // gravity stuff here: - constexpr float g = -9.81f; // constant velocity gravity??? - constexpr float player_height = 71.0f * 25.4f / 1000.0f; - c->vel.z += g * dt; + c->vel.z += CameraControllerComponent::kGravAccel * dt; - if (scene_->app()->input_manager()->GetButtonPress("jump")) { - c->vel.z += 4.4f; // m/s - } - - // update position with velocity: - - // check for collision during next frame and push back and remove velocity in the normal of the collision direction if so - engine::Ray ray{}; - ray.origin = t->position; - ray.origin.z -= player_height; // check for collision from the player's feet - - const glm::vec3 dX = c->vel * dt; // where player will end up with no collision - ray.direction = glm::normalize(dX); - const engine::Raycast raycast = scene_->GetSystem()->GetRaycast(ray); - if (raycast.hit && raycast.distance < glm::length(dX)) { - // will collide - t->position -= raycast.distance * ray.direction; // push out of collision zone - c->vel.z = 0.0f; // remove velocity normal to collision surface + // jumping + if (scene_->app()->input_manager()->GetButtonPress("jump") && c->grounded) { + c->vel.z += 4.4f; // m/s } - - t->position += c->vel * dt; - constexpr float kMaxDistanceFromOrigin = 10000.0f; + // update position with velocity: - if (glm::length(t->position) > kMaxDistanceFromOrigin) { - t->position = {0.0f, 0.0f, 10.0f}; + // check horizontal collisions first as otherwise the player may be teleported above a wall instead of colliding against it + if (c->vel.x != 0.0f || c->vel.y != 0.0f) { // just in case, to avoid a ray with direction = (0,0,0) + engine::Ray horiz_ray{}; + horiz_ray.origin = t->position; // set origin to 'MaxStairHeight' units above player's feet + horiz_ray.origin.z += CameraControllerComponent::kMaxStairHeight - CameraControllerComponent::kPlayerHeight; + horiz_ray.direction.x = c->vel.x; + horiz_ray.direction.y = c->vel.y; // this is normalized by GetRayCast() + horiz_ray.direction.z = 0.0f; + const engine::Raycast horiz_raycast = scene_->GetSystem()->GetRaycast(horiz_ray); + if (horiz_raycast.hit) { + const glm::vec2 norm_xy = glm::normalize(glm::vec2{horiz_raycast.normal.x, horiz_raycast.normal.y}) * -1.0f; // make it point towards object + const glm::vec2 vel_xy = glm::vec2{ c->vel.x, c->vel.y }; + // find the extent of the player's velocity in the direction of the wall's normal vector + const glm::vec2 partial_vel = norm_xy * glm::dot(norm_xy, vel_xy); + const glm::vec2 partial_dX = partial_vel * dt; + if (glm::length(partial_dX) > horiz_raycast.distance - CameraControllerComponent::kPlayerCollisionRadius) { + // player will collide with wall + // push player out of collision zone + const glm::vec2 push_vector = glm::normalize(vel_xy) * fmaxf(CameraControllerComponent::kPlayerCollisionRadius, horiz_raycast.distance); + t->position.x = horiz_raycast.location.x - push_vector.x; + t->position.y = horiz_raycast.location.y - push_vector.y; + c->vel.x -= partial_vel.x; + c->vel.y -= partial_vel.y; + } + } + } + + // check falling collisions + if (c->vel.z < 0.0f) { // if falling + engine::Ray fall_ray{}; + fall_ray.origin = t->position; + fall_ray.origin.z += CameraControllerComponent::kMaxStairHeight - CameraControllerComponent::kPlayerHeight; + fall_ray.direction = {0.0f, 0.0f, -1.0f}; // down + const engine::Raycast fall_raycast = scene_->GetSystem()->GetRaycast(fall_ray); + if (fall_raycast.hit) { // there is ground below player + // find how far the player will move (downwards) if velocity is applied without collision + const float mag_dz = fabsf(c->vel.z * dt); + // check if the player will be less than 'height' units above the collided ground + if (mag_dz > fall_raycast.distance - CameraControllerComponent::kMaxStairHeight) { + LOG_INFO("HIT"); + // push player up to ground level and set as grounded + t->position.z = fall_raycast.location.z + CameraControllerComponent::kPlayerHeight; + c->vel.z = 0.0f; + c->grounded = true; + } + else { // there is ground below player, however they are not grounded + c->grounded = false; + } + } + else { // there is no ground below player (THEY ARE FALLING INTO THE VOID) + c->grounded = false; + } + } + else if (c->vel.z > 0.0f) { + c->grounded = false; // they are jumping + } + + if (c->was_grounded != c->grounded) { + LOG_INFO("GROUNDED? {}", c->grounded); + c->was_grounded = c->grounded; + } + + t->position += c->vel * dt; + + if (glm::length(t->position) > CameraControllerComponent::kMaxDistanceFromOrigin) { + t->position = {0.0f, 0.0f, 100.0f}; } /* ROTATION STUFF */ @@ -122,6 +164,9 @@ void CameraControllerSystem::OnUpdate(float ts) if (scene_->app()->window()->GetKeyPress(engine::inputs::Key::K_R)) { t->position = {0.0f, 0.0f, 10.0f}; + c->vel = {0.0f, 0.0f, 0.0f}; + c->pitch = glm::half_pi(); + c->yaw = 0.0f; } if (scene_->app()->input_manager()->GetButtonPress("fullscreen")) { @@ -136,14 +181,38 @@ void CameraControllerSystem::OnUpdate(float ts) scene_->app()->scene_manager()->SetActiveScene(next_scene_); } - if (scene_->app()->window()->GetButtonPress(engine::inputs::MouseButton::M_LEFT)) { + static std::vector perm_lines{}; + + if (scene_->app()->window()->GetButton(engine::inputs::MouseButton::M_LEFT)) { engine::Ray ray{}; ray.origin = t->position; ray.direction = glm::vec3(glm::mat4_cast(t->rotation) * glm::vec4{0.0f, 0.0f, -1.0f, 1.0f}); engine::Raycast cast = scene_->GetSystem()->GetRaycast(ray); + if (cast.hit) { - LOG_INFO("Raycast hit {}", scene_->GetComponent(cast.hit_entity)->tag); LOG_INFO("Distance: {} m", cast.distance); + LOG_INFO("Location: {} {} {}", cast.location.x, cast.location.y, cast.location.z); + LOG_INFO("Normal: {} {} {}", cast.normal.x, cast.normal.y, cast.normal.z); + LOG_INFO("Ray direction: {} {} {}", ray.direction.x, ray.direction.y, ray.direction.z); + LOG_INFO("Hit Entity: {}", scene_->GetComponent(cast.hit_entity)->tag); + perm_lines.emplace_back(ray.origin, cast.location, glm::vec3{0.0f, 0.0f, 1.0f}); } } + if (scene_->app()->window()->GetButtonPress(engine::inputs::MouseButton::M_RIGHT)) { + engine::Ray horiz_ray{}; + horiz_ray.origin = t->position; // set origin to 'MaxStairHeight' units above player's feet + horiz_ray.origin.z += CameraControllerComponent::kMaxStairHeight - CameraControllerComponent::kPlayerHeight; + horiz_ray.direction.x = c->vel.x; + horiz_ray.direction.y = c->vel.y; // this is normalized by GetRayCast() + horiz_ray.direction.z = 0.0f; + const engine::Raycast cast = scene_->GetSystem()->GetRaycast(horiz_ray); + if (cast.hit) { + LOG_INFO("Distance: {} m", cast.distance); + LOG_INFO("Location: {} {} {}", cast.location.x, cast.location.y, cast.location.z); + LOG_INFO("Normal: {} {} {}", cast.normal.x, cast.normal.y, cast.normal.z); + perm_lines.emplace_back(horiz_ray.origin, cast.location, glm::vec3{ 0.0f, 0.0f, 1.0f }); + } + } + + scene_->app()->debug_lines.insert(scene_->app()->debug_lines.end(), perm_lines.begin(), perm_lines.end()); } diff --git a/test/src/camera_controller.hpp b/test/src/camera_controller.hpp index a68da9b..80eefb3 100644 --- a/test/src/camera_controller.hpp +++ b/test/src/camera_controller.hpp @@ -7,11 +7,21 @@ #include "ecs.h" struct CameraControllerComponent { - static constexpr float kWalkSpeed = 4.0f; + static constexpr float kSpeedForwardBack = 4.0f; + static constexpr float kSpeedStrafe = 4.0f; static constexpr float kCameraSensitivity = 0.007f; + static constexpr float kMaxDistanceFromOrigin = 10000.0f; + static constexpr float kGravAccel = -9.81f; + static constexpr float kPlayerHeight = 71.0f * 25.4f / 1000.0f; + static constexpr float kMaxStairHeight = 0.2f; + static constexpr float kPlayerCollisionRadius = 0.2f; + static constexpr float kMaxPitch = glm::pi(); + static constexpr float kMinPitch = 0.0f; float yaw = 0.0f; float pitch = glm::half_pi(); glm::vec3 vel{ 0.0f, 0.0f, 0.0f }; + bool grounded = false; + bool was_grounded = false; }; class CameraControllerSystem diff --git a/test/src/game.cpp b/test/src/game.cpp index 30304c8..a820224 100644 --- a/test/src/game.cpp +++ b/test/src/game.cpp @@ -127,18 +127,20 @@ void PlayGame(GameSettings settings) skybox_renderable->material->SetAlbedoTexture(app.GetResource("builtin.black")); engine::Entity helmet = engine::util::LoadGLTF(*main_scene, app.GetResourcePath("models/DamagedHelmet.glb")); - main_scene->GetPosition(helmet) += glm::vec3{20.0f, 10.0f, 5.0f}; + main_scene->GetPosition(helmet) += glm::vec3{5.0f, 5.0f, 1.0f}; + main_scene->GetScale(helmet) *= 3.0f; engine::Entity toycar = engine::util::LoadGLTF(*main_scene, app.GetResourcePath("models/ToyCar.glb")); main_scene->GetScale(toycar) *= 100.0f; - auto car_spin = main_scene->AddComponent(toycar); - car_spin->onInit = []() -> void {}; - car_spin->onUpdate = [&](float dt) -> void { - static float yaw = 0.0f; - yaw += dt; - main_scene->GetRotation(toycar) = glm::angleAxis(yaw, glm::vec3{0.0f, 0.0f, 1.0f}); - main_scene->GetRotation(toycar) *= glm::angleAxis(glm::half_pi(), glm::vec3{1.0f, 0.0f, 0.0f}); - }; + main_scene->GetPosition(toycar).z += 1.8f; + + engine::Entity stairs = engine::util::LoadGLTF(*main_scene, app.GetResourcePath("models/stairs.glb")); + main_scene->GetPosition(stairs) += glm::vec3{-8.0f, -5.0f, 0.1f}; + main_scene->GetRotation(stairs) = glm::angleAxis(glm::half_pi(), glm::vec3{ 0.0f, 0.0f, 1.0f }); + main_scene->GetRotation(stairs) *= glm::angleAxis(glm::half_pi(), glm::vec3{ 1.0f, 0.0f, 0.0f }); + + engine::Entity axes = engine::util::LoadGLTF(*main_scene, app.GetResourcePath("models/MY_AXES.glb")); + main_scene->GetPosition(axes) += glm::vec3{-40.0f, -40.0f, 1.0f}; } start_scene->GetSystem()->next_scene_ = main_scene; diff --git a/testfile.txt b/testfile.txt deleted file mode 100644 index 0527e6b..0000000 --- a/testfile.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test