diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f6cf4e..5433386 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ project(engine LANGUAGES CXX C # from command: find . -regex "^\.\/.*" | sort set(SRC_FILES "src/application.cpp" + "src/application_component.cpp" "src/ecs.cpp" "src/gfx_device_vulkan.cpp" "src/imgui/imconfig.h" @@ -67,6 +68,7 @@ set(SRC_FILES set(INCLUDE_FILES "include/application.h" + "include/application_component.h" "include/components/collider.h" "include/components/custom.h" "include/components/mesh_renderable.h" diff --git a/include/application.h b/include/application.h index 85de224..efdcc00 100644 --- a/include/application.h +++ b/include/application.h @@ -21,6 +21,9 @@ class Application { bool enable_frame_limiter; }; + const char* const app_name; + const char* const app_version; + Application(const char* app_name, const char* app_version, gfx::GraphicsSettings graphics_settings, Configuration configuration); ~Application(); Application(const Application&) = delete; @@ -53,7 +56,7 @@ class Application { /* methods */ void GameLoop(); - void SetFrameLimiter(bool on) { configuration_.enable_frame_limiter = on; } + void SetFrameLimiter(bool on) { configuration_.enable_frame_limiter = on; } /* getters */ Window* window() { return window_.get(); } diff --git a/include/application_component.h b/include/application_component.h new file mode 100644 index 0000000..0818708 --- /dev/null +++ b/include/application_component.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +struct SDL_Window; // forward-dec + +namespace engine { + +class Application; // forward-dec + +class ApplicationComponent { + // a class that extends many classes in the engine to expose 'global' functionality to lower-tier classes + + // if multithreading is ever implemented, this class must do synchronization as its instantiations may run concurrently + private: + Application& app_; + + protected: + ApplicationComponent(Application& app) : app_(app) {} + + std::string GetResourcePath(const std::string& relative_path) const; + SDL_Window* GetWindowHandle() const; + const char* GetAppName() const; + const char* GetAppVersion() const; + + public: + ApplicationComponent() = delete; +}; + +} // namespace engine \ No newline at end of file diff --git a/include/components/collider.h b/include/components/collider.h index 45d4caf..19de85d 100644 --- a/include/components/collider.h +++ b/include/components/collider.h @@ -5,14 +5,12 @@ namespace engine { struct AABB { - glm::vec3 pos1; - glm::vec3 pos2; + glm::vec3 min; + glm::vec3 max; }; struct ColliderComponent { - bool is_static; - bool is_trigger; // entity receives an event on collision enter and exit - AABB aabb; // broad phase + AABB aabb; }; } // namespace engine \ No newline at end of file diff --git a/include/ecs.h b/include/ecs.h index 8b4c89c..85bb895 100644 --- a/include/ecs.h +++ b/include/ecs.h @@ -25,7 +25,7 @@ class IComponentArray { template class ComponentArray : public IComponentArray { public: - void InsertData(Entity entity, T component) { + void InsertData(Entity entity, const T& component) { if (component_array_.size() < entity + 1) { component_array_.resize(entity + 1); } diff --git a/include/gfx.h b/include/gfx.h index 8e5d83a..cf30344 100644 --- a/include/gfx.h +++ b/include/gfx.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -115,12 +116,13 @@ struct VertexFormat { }; struct PipelineInfo { - const char* vert_shader_path; - const char* frag_shader_path; + std::string vert_shader_path; + std::string frag_shader_path; VertexFormat vertex_format; bool alpha_blending; bool backface_culling; bool write_z; + bool line_primitives; // false for triangles, true for lines std::vector descriptor_set_layouts; }; diff --git a/include/renderer.h b/include/renderer.h index 487e9b2..c393083 100644 --- a/include/renderer.h +++ b/include/renderer.h @@ -7,6 +7,7 @@ #include #include +#include "application_component.h" #include "gfx_device.h" #include "systems/mesh_render_system.h" @@ -23,10 +24,9 @@ struct UniformDescriptor { gfx::UniformBuffer* uniform_buffer; }; -class Renderer { +class Renderer : private ApplicationComponent { public: - Renderer(const char* app_name, const char* app_version, SDL_Window* window, - gfx::GraphicsSettings settings); + Renderer(Application& app, gfx::GraphicsSettings settings); ~Renderer(); @@ -88,12 +88,19 @@ class Renderer { UniformDescriptor global_uniform; // rarely updates; set 0 UniformDescriptor frame_uniform; // updates once per frame; set 1 // in fragment shader - const gfx::DescriptorSetLayout* material_set_layout; // set 2 + const gfx::DescriptorSetLayout* material_set_layout; // set 2; set bound per material float viewport_aspect_ratio_ = 1.0f; const gfx::Pipeline* last_bound_pipeline_ = nullptr; + struct DebugRenderingThings { + const gfx::Pipeline* pipeline = nullptr; + // have a simple vertex buffer with 2 points that draws a line + const gfx::Buffer* vertex_buffer = nullptr; + // shader will take 2 clip space xyzw coords as push constants to define the line + } debug_rendering_things_{}; + void DrawRenderList(gfx::DrawBuffer* draw_buffer, const RenderList& render_list); }; diff --git a/include/resources/shader.h b/include/resources/shader.h index e44d5d7..50737f0 100644 --- a/include/resources/shader.h +++ b/include/resources/shader.h @@ -26,7 +26,7 @@ class Shader { static constexpr int kHighestRenderOrder = 1; - Shader(Renderer* renderer, const char* vert_path, const char* frag_path, + Shader(Renderer* renderer, const std::string& vert_path, const std::string& frag_path, const ShaderSettings& settings); ~Shader(); Shader(const Shader&) = delete; diff --git a/include/scene.h b/include/scene.h index 086fc59..88f707e 100644 --- a/include/scene.h +++ b/include/scene.h @@ -63,11 +63,11 @@ class Scene { } template - T* AddComponent(Entity entity) { + T* AddComponent(Entity entity, const T& comp = T{}) { size_t hash = typeid(T).hash_code(); auto array = GetComponentArray(); - array->InsertData(entity, T{}); // errors if entity already exists in array + array->InsertData(entity, comp); // errors if entity already exists in array // set the component bit for this entity size_t signature_position = component_signature_positions_.at(hash); diff --git a/include/systems/collisions.h b/include/systems/collisions.h index 44cb06b..d3f8b38 100644 --- a/include/systems/collisions.h +++ b/include/systems/collisions.h @@ -11,51 +11,58 @@ namespace engine { -class PhysicsSystem : public System { - public: - PhysicsSystem(Scene* scene); - - void OnUpdate(float ts) override; - - void OnComponentInsert(Entity entity) override; - - struct CollisionEvent { - bool is_collision_enter; // false == collision exit - Entity collided_entity; // the entity that this entity collided with - glm::vec3 normal; // the normal of the surface this entity collided with; - // ignored on collision exit - glm::vec3 point; // where the collision was detected - }; - - private: - // dynamic arrays to avoid realloc on every frame - // entity, aabb, is_trigger - std::vector> static_aabbs_{}; - std::vector> dynamic_aabbs_{}; - - struct PossibleCollision { - PossibleCollision(Entity static_entity, AABB static_aabb, - bool static_trigger, Entity dynamic_entity, - AABB dynamic_aabb, bool dynamic_trigger) - : static_entity(static_entity), - static_aabb(static_aabb), - static_trigger(static_trigger), - dynamic_entity(dynamic_entity), - dynamic_aabb(dynamic_aabb), - dynamic_trigger(dynamic_trigger) {} - - Entity static_entity; - AABB static_aabb; - bool static_trigger; - Entity dynamic_entity; - AABB dynamic_aabb; - bool dynamic_trigger; - }; - std::vector possible_collisions_{}; - std::vector> - collision_infos_{}; // target entity, event info +struct Ray { + glm::vec3 origin; + glm::vec3 direction; }; -} // namespace engine +struct Raycast { + glm::vec3 location; + Entity hit_entity; + float distance; + bool hit; +}; + +class CollisionSystem : public System { + public: + CollisionSystem(Scene* scene); + + void OnComponentInsert(Entity entity) override; + + void OnUpdate(float ts) override; + + Raycast GetRaycast(Ray ray); + + private: + // one node of the BVH + struct BiTreeNode { + enum class Type : uint8_t { BoundingVolume, Entity, Empty }; + AABB box1; + AABB box2; + uint32_t index1; // index for either tree entry or entity, depending on type + uint32_t index2; + Type type1; + Type type2; + }; // 60 bytes with alignment of 4 allows for tight packing + + // an array of these is used to build the BVH + struct PrimitiveInfo { + Entity entity; + AABB box; + glm::vec3 centroid; + PrimitiveInfo(const AABB& aabb, Entity entity_idx); + }; + + size_t colliders_size_last_update_ = 0; + size_t colliders_size_now_ = 0; + + std::vector bvh_{}; + + bool RaycastTreeNode(const Ray& ray, const BiTreeNode& node, glm::vec3& location, float& t, Entity& object_index); + + static int BuildNode(std::vector& prims, std::vector& tree_nodes); +}; + +} // namespace engine #endif \ No newline at end of file diff --git a/res/engine/shaders/debug.frag b/res/engine/shaders/debug.frag new file mode 100644 index 0000000..7dd43fe --- /dev/null +++ b/res/engine/shaders/debug.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0); +} \ No newline at end of file diff --git a/res/engine/shaders/debug.vert b/res/engine/shaders/debug.vert new file mode 100644 index 0000000..f2e5b6a --- /dev/null +++ b/res/engine/shaders/debug.vert @@ -0,0 +1,10 @@ +#version 450 + +layout( push_constant ) uniform Constants { + vec4 positions[2]; +} constants; + +void main() { + gl_Position = constants.positions[gl_VertexIndex]; + gl_Position.y *= -1.0; +} diff --git a/src/application.cpp b/src/application.cpp index 91ae4bd..8bde9aa 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -74,7 +74,7 @@ static std::filesystem::path getResourcesPath() } Application::Application(const char* appName, const char* appVersion, gfx::GraphicsSettings graphicsSettings, Configuration configuration) - : configuration_(configuration) + : app_name(appName), app_version(appVersion), configuration_(configuration) { window_ = std::make_unique(appName, true, false); input_manager_ = std::make_unique(window_.get()); @@ -94,7 +94,7 @@ Application::Application(const char* appName, const char* appVersion, gfx::Graph // ImGuiIO& io = ImGui::GetIO() ImGui_ImplSDL2_InitForVulkan(window_->GetHandle()); - renderer_ = std::make_unique(appName, appVersion, window_->GetHandle(), graphicsSettings); + renderer_ = std::make_unique(*this, graphicsSettings); /* default fonts */ { @@ -114,8 +114,8 @@ Application::Application(const char* appName, const char* appVersion, gfx::Graph shaderSettings.cull_backface = true; shaderSettings.write_z = true; shaderSettings.render_order = 0; - auto fancyShader = std::make_unique(renderer(), GetResourcePath("engine/shaders/fancy.vert").c_str(), - GetResourcePath("engine/shaders/fancy.frag").c_str(), shaderSettings); + auto fancyShader = std::make_unique(renderer(), GetResourcePath("engine/shaders/fancy.vert"), + GetResourcePath("engine/shaders/fancy.frag"), shaderSettings); GetResourceManager()->AddPersistent("builtin.fancy", std::move(fancyShader)); } { @@ -129,8 +129,8 @@ Application::Application(const char* appName, const char* appVersion, gfx::Graph shaderSettings.cull_backface = true; shaderSettings.write_z = false; shaderSettings.render_order = 1; - auto skyboxShader = std::make_unique(renderer(), GetResourcePath("engine/shaders/skybox.vert").c_str(), - GetResourcePath("engine/shaders/skybox.frag").c_str(), shaderSettings); + auto skyboxShader = std::make_unique(renderer(), GetResourcePath("engine/shaders/skybox.vert"), + GetResourcePath("engine/shaders/skybox.frag"), shaderSettings); GetResourceManager()->AddPersistent("builtin.skybox", std::move(skyboxShader)); } diff --git a/src/application_component.cpp b/src/application_component.cpp new file mode 100644 index 0000000..158075e --- /dev/null +++ b/src/application_component.cpp @@ -0,0 +1,23 @@ +#include "application_component.h" + +#include "application.h" +#include "window.h" + +#include + +namespace engine { + +std::string ApplicationComponent::GetResourcePath(const std::string& relative_path) const { return app_.GetResourcePath(relative_path); } + +SDL_Window* ApplicationComponent::GetWindowHandle() const { + return app_.window()->GetHandle(); +} + +const char* ApplicationComponent::GetAppName() const { + return app_.app_name; +} +const char* ApplicationComponent::GetAppVersion() const { + return app_.app_version; +} + +} // namespace engine \ No newline at end of file diff --git a/src/gfx_device_vulkan.cpp b/src/gfx_device_vulkan.cpp index a6ed49d..ca2ebf3 100644 --- a/src/gfx_device_vulkan.cpp +++ b/src/gfx_device_vulkan.cpp @@ -900,13 +900,14 @@ gfx::Pipeline* GFXDevice::CreatePipeline(const gfx::PipelineInfo& info) VkShaderModule vertShaderModule; VkShaderModule fragShaderModule; + // be careful with these .c_str() calls. It is OK here because 'info' exists for the duration of CreatePipeline() { - auto vertShaderCode = util::ReadTextFile(info.vert_shader_path); - vertShaderModule = compileShader(pimpl->device.device, shaderc_vertex_shader, vertShaderCode->data(), info.vert_shader_path); + auto vertShaderCode = util::ReadTextFile(info.vert_shader_path.c_str()); + vertShaderModule = compileShader(pimpl->device.device, shaderc_vertex_shader, vertShaderCode->data(), info.vert_shader_path.c_str()); } { - auto fragShaderCode = util::ReadTextFile(info.frag_shader_path); - fragShaderModule = compileShader(pimpl->device.device, shaderc_fragment_shader, fragShaderCode->data(), info.frag_shader_path); + auto fragShaderCode = util::ReadTextFile(info.frag_shader_path.c_str()); + fragShaderModule = compileShader(pimpl->device.device, shaderc_fragment_shader, fragShaderCode->data(), info.frag_shader_path.c_str()); } // get vertex attrib layout: @@ -952,7 +953,7 @@ gfx::Pipeline* GFXDevice::CreatePipeline(const gfx::PipelineInfo& info) VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; - inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + inputAssembly.topology = info.line_primitives ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE; VkViewport viewport{}; diff --git a/src/renderer.cpp b/src/renderer.cpp index 9df9a3d..227e7fa 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,33 +1,18 @@ #include "renderer.h" +#include "application_component.h" + #include #include #include #include "imgui/imgui.h" -[[maybe_unused]] static glm::mat4 GenPerspectiveMatrix(float vertical_fov_radians, float aspect_ratio, float znear, float zfar) -{ - float g = 1.0f / tanf(vertical_fov_radians * 0.5f); - float k1 = zfar / (zfar - znear); - float k2 = -(zfar * znear) / (znear - zfar); - glm::mat4 m{1.0f}; - - m[0][0] = g / aspect_ratio; - m[1][1] = g; - m[2][2] = k1; - m[2][3] = -1.0f; - m[3][2] = k2; - m[3][3] = 0.0f; - - return m; -} - namespace engine { -Renderer::Renderer(const char* app_name, const char* app_version, SDL_Window* window, gfx::GraphicsSettings settings) +Renderer::Renderer(Application& app, gfx::GraphicsSettings settings) : ApplicationComponent(app) { - device_ = std::make_unique(app_name, app_version, window, settings); + device_ = std::make_unique(GetAppName(), GetAppVersion(), GetWindowHandle(), settings); // sort out descriptor set layouts: std::vector globalSetBindings; @@ -65,10 +50,28 @@ Renderer::Renderer(const char* app_name, const char* app_version, SDL_Window* wi material_set_layout = device_->CreateDescriptorSetLayout(materialSetBindings); device_->SetupImguiBackend(); + + gfx::VertexFormat debug_vertex_format{}; + debug_vertex_format.stride = 0; + // debug_vertex_format.vertex_attrib_descriptions = empty + + gfx::PipelineInfo debug_pipeline_info{}; + debug_pipeline_info.vert_shader_path = GetResourcePath("engine/shaders/debug.vert"); + debug_pipeline_info.frag_shader_path = GetResourcePath("engine/shaders/debug.frag"); + debug_pipeline_info.vertex_format = debug_vertex_format; + debug_pipeline_info.alpha_blending = false; + debug_pipeline_info.backface_culling = false; // probably ignored for line rendering + debug_pipeline_info.write_z = false; // lines don't need the depth buffer + //debug_pipeline_info.descriptor_set_layouts = empty; + debug_pipeline_info.line_primitives = true; + + debug_rendering_things_.pipeline = device_->CreatePipeline(debug_pipeline_info); }; Renderer::~Renderer() { + device_->DestroyPipeline(debug_rendering_things_.pipeline); + for (const auto& [info, sampler] : samplers) { device_->DestroySampler(sampler); } @@ -118,6 +121,14 @@ void Renderer::Render(const RenderList* static_list, const RenderList* dynamic_l } } + // draw debug shit here + device_->CmdBindPipeline(draw_buffer, debug_rendering_things_.pipeline); + glm::vec4 debug_positions[2] = {}; + debug_positions[0] = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4{ 0.0f, 0.0f, 0.0f, 1.0f }; + debug_positions[1] = global_uniform.uniform_buffer_data.data * frame_uniform.uniform_buffer_data.data * glm::vec4{ 0.0f, 0.0f, 1.0f, 1.0f }; + device_->CmdPushConstants(draw_buffer, debug_rendering_things_.pipeline, 0, sizeof(glm::vec4) * 2, debug_positions); + device_->CmdDraw(draw_buffer, 2, 1, 0, 0); + device_->CmdRenderImguiDrawData(draw_buffer, ImGui::GetDrawData()); device_->FinishRender(draw_buffer); diff --git a/src/resources/shader.cpp b/src/resources/shader.cpp index 2449f2b..a00e73b 100644 --- a/src/resources/shader.cpp +++ b/src/resources/shader.cpp @@ -8,7 +8,7 @@ namespace engine { -Shader::Shader(Renderer* renderer, const char* vertPath, const char* fragPath, +Shader::Shader(Renderer* renderer, const std::string& vertPath, const std::string& fragPath, const ShaderSettings& settings) : gfx_(renderer->GetDevice()), render_order_(settings.render_order) { assert(settings.render_order <= kHighestRenderOrder && @@ -50,6 +50,7 @@ Shader::Shader(Renderer* renderer, const char* vertPath, const char* fragPath, info.alpha_blending = settings.alpha_blending; info.backface_culling = settings.cull_backface; info.write_z = settings.write_z; + info.line_primitives = false; info.descriptor_set_layouts.push_back(renderer->GetGlobalSetLayout()); info.descriptor_set_layouts.push_back(renderer->GetFrameSetLayout()); info.descriptor_set_layouts.push_back(renderer->GetMaterialSetLayout()); diff --git a/src/scene.cpp b/src/scene.cpp index a59d3f5..3952c54 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -27,7 +27,7 @@ Scene::Scene(Application* app) : app_(app) { // Order here matters: RegisterSystem(); - RegisterSystem(); + RegisterSystem(); RegisterSystem(); RegisterSystem(); RegisterSystem(); diff --git a/src/systems/collisions.cpp b/src/systems/collisions.cpp index cfe4c5c..b3351c7 100644 --- a/src/systems/collisions.cpp +++ b/src/systems/collisions.cpp @@ -2,164 +2,420 @@ #include "components/transform.h" #include "components/collider.h" -#include "components/mesh_renderable.h" #include "scene.h" #include "log.h" #include +#include namespace engine { - // static functions - - static bool checkCollisionFast(AABB a, AABB b) - { - return ( - a.pos1.x <= b.pos2.x && - a.pos2.x >= b.pos1.x && - a.pos1.y <= b.pos2.y && - a.pos2.y >= b.pos1.y && - a.pos1.z <= b.pos2.z && - a.pos2.z >= b.pos1.z - ); - } - - static glm::vec3 getAABBNormal(AABB subject, AABB object) - { - // get centre position of static entity: - glm::vec3 subjectCentre = subject.pos1 + ((subject.pos2 - subject.pos1) / glm::vec3{ 2.0f, 2.0f, 2.0f }); - // find which face the centre is closest to: - const float PXdistance = glm::abs(subjectCentre.x - object.pos2.x); - const float NXdistance = glm::abs(subjectCentre.x - object.pos1.x); - const float PYdistance = glm::abs(subjectCentre.y - object.pos2.y); - const float NYdistance = glm::abs(subjectCentre.y - object.pos1.y); - const float PZdistance = glm::abs(subjectCentre.z - object.pos2.z); - const float NZdistance = glm::abs(subjectCentre.z - object.pos1.z); - const std::array distances { PXdistance, NXdistance, PYdistance, NYdistance, PZdistance, NZdistance }; - const auto minDistance = std::min_element(distances.begin(), distances.end()); - const int index = static_cast(minDistance - distances.begin()); - switch (index) { - case 0: - // P_X - return {1.0f, 0.0f, 0.0f}; - case 1: - // N_X - return {-1.0f, 0.0f, 0.0f}; - case 2: - // P_Y - return {0.0f, 1.0f, 0.0f}; - case 3: - // N_Y - return {0.0f, -1.0f, 0.0f}; - case 4: - // P_Z - return {0.0f, 0.0f, 1.0f}; - case 5: - // N_Z - return {0.0f, 0.0f, -1.0f}; - default: - throw std::runtime_error("wtf"); - } - } - - // class methods - - PhysicsSystem::PhysicsSystem(Scene* scene) - : System(scene, { typeid(TransformComponent).hash_code(), typeid(ColliderComponent).hash_code() }) - { - scene_->event_system()->RegisterEventType(); - } - - void PhysicsSystem::OnComponentInsert(Entity entity) - { - (void)entity; - const size_t size = entities_.size(); - static_aabbs_.reserve(size); - dynamic_aabbs_.reserve(size); - possible_collisions_.reserve(size); - collision_infos_.reserve(size); - LOG_TRACE("added entity {} to collider system", entity); - } - - void PhysicsSystem::OnUpdate(float ts) - { - (void)ts; - - static_aabbs_.clear(); - dynamic_aabbs_.clear(); - possible_collisions_.clear(); - collision_infos_.clear(); - - for (Entity entity : entities_) { - const auto t = scene_->GetComponent(entity); - const auto c = scene_->GetComponent(entity); - - const glm::vec3 globalPosition = t->world_matrix[3]; - const AABB localBoundingBox = c->aabb; - AABB globalBoundingBox{}; - globalBoundingBox.pos1 = globalPosition + localBoundingBox.pos1; - globalBoundingBox.pos2 = globalPosition + localBoundingBox.pos2; - - auto& a = globalBoundingBox; - if (a.pos1.x > a.pos2.x) std::swap(a.pos1.x, a.pos2.x); - if (a.pos1.y > a.pos2.y) std::swap(a.pos1.y, a.pos2.y); - if (a.pos1.z > a.pos2.z) std::swap(a.pos1.z, a.pos2.z); - - if (c->is_static) { - static_aabbs_.emplace_back(std::make_tuple(entity, globalBoundingBox, c->is_trigger)); - } else { - dynamic_aabbs_.emplace_back(std::make_tuple(entity, globalBoundingBox, c->is_trigger)); - } - } - - /* BROAD PHASE */ - - // Check every static collider against every dynamic collider, and every dynamic collider against every other one - // This technique is inefficient for many entities. - for (const auto& [staticEntity, staticAABB, staticTrigger] : static_aabbs_) { - for (const auto& [dynamicEntity, dynamicAABB, dynamicTrigger] : dynamic_aabbs_) { - if (checkCollisionFast(staticAABB, dynamicAABB)) { - if (staticTrigger || dynamicTrigger) { // only check collisions involved with triggers - possible_collisions_.emplace_back( - staticEntity, staticAABB, staticTrigger, - dynamicEntity, dynamicAABB, dynamicTrigger - ); - } - } - } - } - - // get collision details and submit events - for (const auto& possibleCollision : possible_collisions_) { - if (possibleCollision.static_trigger) { - CollisionEvent info{}; - info.is_collision_enter = true; - info.collided_entity = possibleCollision.dynamic_entity; - info.normal = getAABBNormal(possibleCollision.static_aabb, possibleCollision.dynamic_aabb); - - AABB object = possibleCollision.dynamic_aabb; - info.point = object.pos2; - - collision_infos_.emplace_back(possibleCollision.static_entity, info); - } - if (possibleCollision.dynamic_trigger) { - CollisionEvent info{}; - info.is_collision_enter = true; - info.collided_entity = possibleCollision.static_entity; - info.normal = getAABBNormal(possibleCollision.dynamic_aabb, possibleCollision.static_aabb); - - AABB object = possibleCollision.static_aabb; - info.point = object.pos2; - - collision_infos_.emplace_back(possibleCollision.dynamic_entity, info); - } - } - - for (const auto& [entity, info] : collision_infos_) { - scene_->event_system()->QueueEvent(EventSubscriberKind::kEntity, entity, info); - } - } - +static glm::vec3 GetBoxCentroid(const AABB& box) +{ + glm::vec3 v{}; + v.x = (box.min.x + box.max.x) * 0.5f; + v.y = (box.min.y + box.max.y) * 0.5f; + v.z = (box.min.z + box.max.z) * 0.5f; + return v; } +static float GetBoxArea(const AABB& box) +{ + float front_back = fabsf(box.max.x - box.min.x) * fabs(box.max.y - box.min.y) * 2.0f; + float left_right = fabsf(box.max.z - box.min.z) * fabs(box.max.y - box.min.y) * 2.0f; + float top_bottom = fabsf(box.max.x - box.min.x) * fabs(box.max.z - box.min.z) * 2.0f; + return front_back + left_right + top_bottom; +} + +// returns true on hit and tmin +static std::pair RayBoxIntersection(const Ray& ray, const AABB& box, float t) +{ + // 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}; + + float tx1 = (box.min.x - ray.origin.x) * n_inv.x; + float tx2 = (box.max.x - ray.origin.x) * n_inv.x; + + float tmin = fminf(tx1, tx2); + float tmax = fmaxf(tx1, tx2); + + float ty1 = (box.min.y - ray.origin.y) * n_inv.y; + float ty2 = (box.max.y - ray.origin.y) * n_inv.y; + + tmin = fmaxf(tmin, fminf(ty1, ty2)); + tmax = fminf(tmax, fmaxf(ty1, ty2)); + + float tz1 = (box.min.z - ray.origin.z) * n_inv.z; + float tz2 = (box.max.z - ray.origin.z) * n_inv.z; + + tmin = fmaxf(tmin, fminf(tz1, tz2)); + tmax = fminf(tmax, fmaxf(tz1, tz2)); + + return std::make_pair((tmax >= fmaxf(0.0, tmin) && tmin < t), tmin); +} + +// class methods + +CollisionSystem::CollisionSystem(Scene* scene) : System(scene, {typeid(TransformComponent).hash_code(), typeid(ColliderComponent).hash_code()}) {} + +void CollisionSystem::OnComponentInsert(Entity entity) { ++colliders_size_now_; } + +void CollisionSystem::OnUpdate(float ts) +{ + (void)ts; + + if (colliders_size_last_update_ != colliders_size_now_) { + + std::vector prims{}; + prims.reserve(entities_.size()); + for (Entity entity : entities_) { + const auto t = scene_->GetComponent(entity); + const auto c = scene_->GetComponent(entity); + + AABB transformed_box{}; + transformed_box.min = t->world_matrix * glm::vec4(c->aabb.min, 1.0f); + transformed_box.max = t->world_matrix * glm::vec4(c->aabb.max, 1.0f); + // if a mesh is not rotated a multiple of 90 degrees, the box will not fit the mesh + // TODO fix it + prims.emplace_back(c->aabb, entity); + } + + bvh_.clear(); + bvh_.reserve(entities_.size() * 3 / 2); // from testing, bvh is usually 20% larger than number of objects + BuildNode(prims, bvh_); + + LOG_INFO("BUILT BVH!"); + + colliders_size_last_update_ = colliders_size_now_; + } +} + +Raycast CollisionSystem::GetRaycast(Ray ray) +{ + Raycast res{}; + + ray.direction = glm::normalize(ray.direction); + + res.hit = RaycastTreeNode(ray, bvh_.back(), res.location, res.distance, res.hit_entity); + + return res; +} + +CollisionSystem::PrimitiveInfo::PrimitiveInfo(const AABB& aabb, Entity entity_idx) : entity(entity_idx), box(aabb), centroid(GetBoxCentroid(aabb)) {} + +// returns the index of the node just added +// 'prims' will be sorted randomly and should be treated as garbage +int CollisionSystem::BuildNode(std::vector& prims, std::vector& tree_nodes) +{ + + if (prims.size() == 0) throw std::runtime_error("Cannot build BVH with no primitives!"); + + std::array, 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); }}; + 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); }}; + 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); }}; + + BiTreeNode node{}; + + if (prims.size() > 2) { + AABB optimal_box1{}, optimal_box2{}; + std::vector sorted_prims; + sorted_prims.reserve(prims.size()); + int sorted_prims_split_index = 0; + + float sah = std::numeric_limits().infinity(); // surface area heuristic + + // try along each axis + for (int axis = 0; axis < 3; axis++) { + + int other_axis1 = (axis + 1) % 3; + int other_axis2 = (axis + 2) % 3; + + std::sort(prims.begin(), prims.end(), centroid_tests[axis]); + + // std::cout << "Sorting on axis " << axis << "...\n"; + for (const auto& p : prims) { + // std::cout << p.centroid.x << "\t" << p.centroid.y << "\t" << p.centroid.z << "\n"; + } + // std::cout << "\n\n\n"; + + // break; + + // split the boxes + for (int i = 0; i < prims.size() - 1; i++) { + + 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 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; + } + const float combined_surface_area = (GetBoxArea(box1) * (i + 1)) + (GetBoxArea(box2) * (prims.size() - (i + 1))); + if (combined_surface_area < sah) { + sah = combined_surface_area; + optimal_box1 = box1; + optimal_box2 = box2; + sorted_prims = prims; // VECTOR COPY! + sorted_prims_split_index = i; + std::cout << "picking new boxes, axis: " << axis << " i: " << i << " sah: " << combined_surface_area << "\n"; + } + } + } + + // now we have: + // a vector of sorted prims + // the index where these primitives are split + // the two bounding boxes + node.box1 = optimal_box1; + node.box2 = optimal_box2; + std::vector prims1(sorted_prims.begin(), sorted_prims.begin() + sorted_prims_split_index + 1); + int index1 = BuildNode(prims1, tree_nodes); + std::vector prims2(sorted_prims.begin() + sorted_prims_split_index + 1, sorted_prims.end()); + int index2 = BuildNode(prims2, tree_nodes); + node.index1 = index1; + node.index2 = index2; + node.type1 = BiTreeNode::Type::BoundingVolume; + node.type2 = BiTreeNode::Type::BoundingVolume; + } + else { + if (prims.size() == 2) { + node.box1 = prims[0].box; + node.box2 = prims[1].box; + node.index1 = prims[0].entity; + node.index2 = prims[1].entity; + node.type1 = BiTreeNode::Type::Entity; + node.type2 = BiTreeNode::Type::Entity; + } + else { + // prims.size() == 1 + node.box1 = prims[0].box; + node.index1 = prims[0].entity; + node.type1 = BiTreeNode::Type::Entity; + node.type2 = BiTreeNode::Type::Empty; + } + } + + tree_nodes.push_back(node); + int node_index = static_cast(tree_nodes.size() - 1); + + return node_index; +} + +// returns true on ray hit +bool CollisionSystem::RaycastTreeNode(const Ray& ray, const BiTreeNode& node, glm::vec3& location, float& t, Entity& object_index) +{ + + using Type = BiTreeNode::Type; + + constexpr float T = 1000.0f; + + bool is_hit1 = false; + bool is_hit2 = false; + float t1 = std::numeric_limits::infinity(); + float t2 = std::numeric_limits::infinity(); + + if (node.type1 != Type::Empty) { + auto [is_hit, t] = RayBoxIntersection(ray, node.box1, T); + is_hit1 = is_hit; + t1 = t; + } + if (node.type2 != Type::Empty) { + auto [is_hit, t] = RayBoxIntersection(ray, node.box2, T); + 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 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; + } + else + return RaycastTreeNode(ray, bvh_.at(node.index1), location, t, object_index); + } + + // 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; + } + else + return RaycastTreeNode(ray, bvh_.at(node.index2), location, t, object_index); + } + + // 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; + } + else { + location = location2; + t = node2_t; + object_index = object_index1; + return true; + } + } + else if (node1_intersects) { + t = node1_t; + location = location1; + object_index = object_index1; + return true; + } + else if (node2_intersects) { + t = node2_t; + location = location2; + object_index = object_index2; + return true; + } + else { + return false; + } + } + + // 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; + } + else { + location = (ray.direction * t2) + ray.origin; + t = t2; + object_index = node.index2; + } + return true; + } + } + + // only 1 hits + if (is_hit1) { + switch (node.type1) { + case Type::BoundingVolume: + return RaycastTreeNode(ray, bvh_.at(node.index1), location, t, object_index); + case Type::Entity: + location = (ray.direction * t1) + ray.origin; + t = t1; + object_index = node.index1; + return true; + } + } + + // 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; + } + } +} + +} // namespace engine diff --git a/src/util/gltf_loader.cpp b/src/util/gltf_loader.cpp index 700d2ca..e7093f6 100644 --- a/src/util/gltf_loader.cpp +++ b/src/util/gltf_loader.cpp @@ -8,7 +8,8 @@ #include "libs/tiny_gltf.h" #include "components/mesh_renderable.h" -#include +#include "components/transform.h" +#include "components/collider.h" struct Color { uint8_t r, g, b, a; @@ -320,6 +321,7 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) struct EnginePrimitive { std::shared_ptr mesh; std::shared_ptr material; + AABB aabb; }; std::vector> primitive_arrays{}; // sub-array is all primitives for a given mesh primitive_arrays.reserve(model.meshes.size()); @@ -368,7 +370,6 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) tangents.stride = static_cast(tang_accessor.ByteStride(tang_bufferview)); } else { - // TODO: use MikkTSpace to generate tangents generate_tangents = true; } @@ -546,7 +547,16 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) engine_material = scene.app()->GetResource("builtin.default"); } - primitive_array.emplace_back(engine_mesh, engine_material); + // get AABB + AABB box{}; + box.min.x = pos_accessor.minValues.at(0); + box.min.y = pos_accessor.minValues.at(1); + box.min.z = pos_accessor.minValues.at(2); + box.max.x = pos_accessor.maxValues.at(0); + box.max.y = pos_accessor.maxValues.at(1); + box.max.z = pos_accessor.maxValues.at(2); + + primitive_array.emplace_back(engine_mesh, engine_material, box); } else { // skip primitive's rendering @@ -613,6 +623,8 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) auto meshren = scene.AddComponent(e); meshren->mesh = primitives.front().mesh; meshren->material = primitives.front().material; + auto collider = scene.AddComponent(e); + collider->aabb = primitives.front().aabb; } else { for (const EnginePrimitive& prim : primitives) { @@ -620,6 +632,8 @@ engine::Entity LoadGLTF(Scene& scene, const std::string& path, bool isStatic) auto meshren = scene.AddComponent(prim_entity); meshren->mesh = prim.mesh; meshren->material = prim.material; + auto collider = scene.AddComponent(e); + collider->aabb = prim.aabb; ++i; } } diff --git a/test/src/camera_controller.cpp b/test/src/camera_controller.cpp index 0f662d0..459d6e6 100644 --- a/test/src/camera_controller.cpp +++ b/test/src/camera_controller.cpp @@ -13,6 +13,7 @@ #include "scene.h" #include "scene_manager.h" #include "window.h" +#include CameraControllerSystem::CameraControllerSystem(engine::Scene* scene) : System(scene, {typeid(engine::TransformComponent).hash_code(), typeid(CameraControllerComponent).hash_code()}) @@ -89,8 +90,9 @@ void CameraControllerSystem::OnUpdate(float ts) /* user interface inputs */ if (scene_->app()->window()->GetKeyPress(engine::inputs::Key::K_P)) { - std::string pos_string{"x: " + std::to_string(t->position.x) + " y: " + std::to_string(t->position.y) + " z: " + std::to_string(t->position.z)}; + std::string pos_string{"x: " + std::to_string(t->world_matrix[3][0]) + " y: " + std::to_string(t->world_matrix[3][1]) + " z: " + std::to_string(t->world_matrix[3][2])}; LOG_INFO("position {}", pos_string); + LOG_INFO("rotation w: {} x: {} y: {} z: {}", t->rotation.w, t->rotation.x, t->rotation.y, t->rotation.z); } if (scene_->app()->window()->GetKeyPress(engine::inputs::Key::K_R)) { @@ -108,4 +110,14 @@ void CameraControllerSystem::OnUpdate(float ts) if (scene_->app()->window()->GetKeyPress(engine::inputs::Key::K_F)) { scene_->app()->scene_manager()->SetActiveScene(next_scene_); } + + if (scene_->app()->window()->GetButtonPress(engine::inputs::MouseButton::M_LEFT)) { + engine::Ray ray{}; + ray.origin.x = t->world_matrix[3][0]; + ray.origin.y = t->world_matrix[3][1]; + ray.origin.z = t->world_matrix[3][2]; + ray.direction = glm::vec3{ 0.0f, 0.0f, -1.0f }; + engine::Raycast cast = scene_->GetSystem()->GetRaycast(ray); + LOG_INFO("Raycast success? {}", cast.hit ? "YES" : "NO"); + } } \ No newline at end of file diff --git a/test/src/game.cpp b/test/src/game.cpp index 1084ba2..fec985e 100644 --- a/test/src/game.cpp +++ b/test/src/game.cpp @@ -67,9 +67,6 @@ void PlayGame(GameSettings settings) /* as of right now, the entity with tag 'camera' is used to build the view * matrix */ - auto camera_transform = start_scene->GetComponent(camera); - camera_transform->position = {0.0f, 0.0f, 10.0f}; - start_scene->RegisterComponent(); start_scene->RegisterSystem(); start_scene->AddComponent(camera); @@ -105,7 +102,7 @@ void PlayGame(GameSettings settings) floor_renderable->material->SetNormalTexture(floor_normal); floor_renderable->material->SetMetallicRoughnessTexture(floor_mr); floor_renderable->material->SetOcclusionTexture(app.GetResource("builtin.white")); - floor_renderable->visible = false; + floor_renderable->visible = true ; engine::Entity normal_map_test = engine::util::LoadGLTF(*main_scene, app.GetResourcePath("models/normalmaptest.glb"));