add bvh tree for collisions, begin basic debug rendering support

This commit is contained in:
bailehuni 2024-03-03 23:22:23 +00:00
parent 984941d546
commit d9ea79170f
22 changed files with 632 additions and 251 deletions

View File

@ -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"

View File

@ -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;

View File

@ -0,0 +1,30 @@
#pragma once
#include <string>
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

View File

@ -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

View File

@ -25,7 +25,7 @@ class IComponentArray {
template <typename T>
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);
}

View File

@ -3,6 +3,7 @@
#include <cstdint>
#include <functional>
#include <string>
#include <type_traits>
#include <vector>
@ -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<const DescriptorSetLayout*> descriptor_set_layouts;
};

View File

@ -7,6 +7,7 @@
#include <glm/mat4x4.hpp>
#include <glm/trigonometric.hpp>
#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<glm::mat4> global_uniform; // rarely updates; set 0
UniformDescriptor<glm::mat4> 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);
};

View File

@ -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;

View File

@ -63,11 +63,11 @@ class Scene {
}
template <typename T>
T* AddComponent(Entity entity) {
T* AddComponent(Entity entity, const T& comp = T{}) {
size_t hash = typeid(T).hash_code();
auto array = GetComponentArray<T>();
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);

View File

@ -11,49 +11,56 @@
namespace engine {
class PhysicsSystem : public System {
public:
PhysicsSystem(Scene* scene);
struct Ray {
glm::vec3 origin;
glm::vec3 direction;
};
void OnUpdate(float ts) override;
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;
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
};
void OnUpdate(float ts) override;
Raycast GetRaycast(Ray ray);
private:
// dynamic arrays to avoid realloc on every frame
// entity, aabb, is_trigger
std::vector<std::tuple<Entity, AABB, bool>> static_aabbs_{};
std::vector<std::tuple<Entity, AABB, bool>> dynamic_aabbs_{};
// 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
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;
// 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);
};
std::vector<PossibleCollision> possible_collisions_{};
std::vector<std::pair<Entity, CollisionEvent>>
collision_infos_{}; // target entity, event info
size_t colliders_size_last_update_ = 0;
size_t colliders_size_now_ = 0;
std::vector<BiTreeNode> bvh_{};
bool RaycastTreeNode(const Ray& ray, const BiTreeNode& node, glm::vec3& location, float& t, Entity& object_index);
static int BuildNode(std::vector<PrimitiveInfo>& prims, std::vector<BiTreeNode>& tree_nodes);
};
} // namespace engine

View File

@ -0,0 +1,7 @@
#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0);
}

View File

@ -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;
}

View File

@ -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<Window>(appName, true, false);
input_manager_ = std::make_unique<InputManager>(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<Renderer>(appName, appVersion, window_->GetHandle(), graphicsSettings);
renderer_ = std::make_unique<Renderer>(*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<Shader>(renderer(), GetResourcePath("engine/shaders/fancy.vert").c_str(),
GetResourcePath("engine/shaders/fancy.frag").c_str(), shaderSettings);
auto fancyShader = std::make_unique<Shader>(renderer(), GetResourcePath("engine/shaders/fancy.vert"),
GetResourcePath("engine/shaders/fancy.frag"), shaderSettings);
GetResourceManager<Shader>()->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<Shader>(renderer(), GetResourcePath("engine/shaders/skybox.vert").c_str(),
GetResourcePath("engine/shaders/skybox.frag").c_str(), shaderSettings);
auto skyboxShader = std::make_unique<Shader>(renderer(), GetResourcePath("engine/shaders/skybox.vert"),
GetResourcePath("engine/shaders/skybox.frag"), shaderSettings);
GetResourceManager<Shader>()->AddPersistent("builtin.skybox", std::move(skyboxShader));
}

View File

@ -0,0 +1,23 @@
#include "application_component.h"
#include "application.h"
#include "window.h"
#include <string>
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

View File

@ -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{};

View File

@ -1,33 +1,18 @@
#include "renderer.h"
#include "application_component.h"
#include <glm/mat4x4.hpp>
#include <glm/trigonometric.hpp>
#include <glm/ext/matrix_clip_space.hpp>
#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<GFXDevice>(app_name, app_version, window, settings);
device_ = std::make_unique<GFXDevice>(GetAppName(), GetAppVersion(), GetWindowHandle(), settings);
// sort out descriptor set layouts:
std::vector<gfx::DescriptorSetLayoutBinding> 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);

View File

@ -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());

View File

@ -27,7 +27,7 @@ Scene::Scene(Application* app) : app_(app) {
// Order here matters:
RegisterSystem<TransformSystem>();
RegisterSystem<PhysicsSystem>();
RegisterSystem<CollisionSystem>();
RegisterSystem<CustomBehaviourSystem>();
RegisterSystem<MeshRenderSystem>();
RegisterSystem<UIRenderSystem>();

View File

@ -2,164 +2,420 @@
#include "components/transform.h"
#include "components/collider.h"
#include "components/mesh_renderable.h"
#include "scene.h"
#include "log.h"
#include <array>
#include <iostream>
namespace engine {
// static functions
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 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 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;
}
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<float, 6> distances { PXdistance, NXdistance, PYdistance, NYdistance, PZdistance, NZdistance };
const auto minDistance = std::min_element(distances.begin(), distances.end());
const int index = static_cast<int>(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");
}
}
// returns true on hit and tmin
static std::pair<bool, float> 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};
// class methods
float tx1 = (box.min.x - ray.origin.x) * n_inv.x;
float tx2 = (box.max.x - ray.origin.x) * n_inv.x;
PhysicsSystem::PhysicsSystem(Scene* scene)
: System(scene, { typeid(TransformComponent).hash_code(), typeid(ColliderComponent).hash_code() })
{
scene_->event_system()->RegisterEventType<CollisionEvent>();
}
float tmin = fminf(tx1, tx2);
float tmax = fmaxf(tx1, tx2);
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);
}
float ty1 = (box.min.y - ray.origin.y) * n_inv.y;
float ty2 = (box.max.y - ray.origin.y) * n_inv.y;
void PhysicsSystem::OnUpdate(float ts)
{
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;
static_aabbs_.clear();
dynamic_aabbs_.clear();
possible_collisions_.clear();
collision_infos_.clear();
if (colliders_size_last_update_ != colliders_size_now_) {
std::vector<PrimitiveInfo> prims{};
prims.reserve(entities_.size());
for (Entity entity : entities_) {
const auto t = scene_->GetComponent<TransformComponent>(entity);
const auto c = scene_->GetComponent<ColliderComponent>(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));
}
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);
}
/* BROAD PHASE */
bvh_.clear();
bvh_.reserve(entities_.size() * 3 / 2); // from testing, bvh is usually 20% larger than number of objects
BuildNode(prims, bvh_);
// 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
);
}
}
}
}
LOG_INFO("BUILT BVH!");
// 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);
colliders_size_last_update_ = colliders_size_now_;
}
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<CollisionEvent>(EventSubscriberKind::kEntity, entity, info);
}
}
}
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<PrimitiveInfo>& prims, std::vector<BiTreeNode>& tree_nodes)
{
if (prims.size() == 0) throw std::runtime_error("Cannot build BVH with no primitives!");
std::array<std::function<bool(const PrimitiveInfo&, const PrimitiveInfo&)>, 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<std::function<bool(const PrimitiveInfo&, const PrimitiveInfo&)>, 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<std::function<bool(const PrimitiveInfo&, const PrimitiveInfo&)>, 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<PrimitiveInfo> sorted_prims;
sorted_prims.reserve(prims.size());
int sorted_prims_split_index = 0;
float sah = std::numeric_limits<float>().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<const float*>(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[axis])->box.min))[axis];
float box1_main_max =
reinterpret_cast<const float*>(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[axis])->box.max))[axis];
float box2_main_min =
reinterpret_cast<const float*>(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[axis])->box.min))[axis];
float box2_main_max =
reinterpret_cast<const float*>(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[axis])->box.max))[axis];
float box1_min1 =
reinterpret_cast<const float*>(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis1])->box.min))[other_axis1];
float box1_max1 =
reinterpret_cast<const float*>(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis1])->box.max))[other_axis1];
float box1_min2 =
reinterpret_cast<const float*>(&(std::min_element(prims.begin(), prims.begin() + i + 1, box_min_tests[other_axis2])->box.min))[other_axis2];
float box1_max2 =
reinterpret_cast<const float*>(&(std::max_element(prims.begin(), prims.begin() + i + 1, box_max_tests[other_axis2])->box.max))[other_axis2];
float box2_min1 =
reinterpret_cast<const float*>(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis1])->box.min))[other_axis1];
float box2_max1 =
reinterpret_cast<const float*>(&(std::max_element(prims.begin() + i + 1, prims.end(), box_max_tests[other_axis1])->box.max))[other_axis1];
float box2_min2 =
reinterpret_cast<const float*>(&(std::min_element(prims.begin() + i + 1, prims.end(), box_min_tests[other_axis2])->box.min))[other_axis2];
float box2_max2 =
reinterpret_cast<const float*>(&(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<PrimitiveInfo> prims1(sorted_prims.begin(), sorted_prims.begin() + sorted_prims_split_index + 1);
int index1 = BuildNode(prims1, tree_nodes);
std::vector<PrimitiveInfo> 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<int>(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<float>::infinity();
float t2 = std::numeric_limits<float>::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

View File

@ -8,7 +8,8 @@
#include "libs/tiny_gltf.h"
#include "components/mesh_renderable.h"
#include <components/transform.h>
#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> mesh;
std::shared_ptr<Material> material;
AABB aabb;
};
std::vector<std::vector<EnginePrimitive>> 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<size_t>(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<Material>("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<MeshRenderableComponent>(e);
meshren->mesh = primitives.front().mesh;
meshren->material = primitives.front().material;
auto collider = scene.AddComponent<ColliderComponent>(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<MeshRenderableComponent>(prim_entity);
meshren->mesh = prim.mesh;
meshren->material = prim.material;
auto collider = scene.AddComponent<ColliderComponent>(e);
collider->aabb = prim.aabb;
++i;
}
}

View File

@ -13,6 +13,7 @@
#include "scene.h"
#include "scene_manager.h"
#include "window.h"
#include <systems/collisions.h>
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<engine::CollisionSystem>()->GetRaycast(ray);
LOG_INFO("Raycast success? {}", cast.hit ? "YES" : "NO");
}
}

View File

@ -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<engine::TransformComponent>(camera);
camera_transform->position = {0.0f, 0.0f, 10.0f};
start_scene->RegisterComponent<CameraControllerComponent>();
start_scene->RegisterSystem<CameraControllerSystem>();
start_scene->AddComponent<CameraControllerComponent>(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<engine::Texture>("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"));