diff --git a/.gitmodules b/.gitmodules index c91f338..d95c6c3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "external/glm"] path = external/glm url = https://github.com/g-truc/glm.git +[submodule "bullet3"] + path = bullet3 + url = https://github.com/bulletphysics/bullet3.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 2992492..8ae3c9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,9 +16,9 @@ add_executable(PortalGame "src/assets/sectordef.cpp" "src/assets/mesh.hpp" "src/assets/mesh.cpp" - "src/collision/capsule_triangle_sweep.hpp" - "src/collision/capsule_triangle_sweep.cpp" "src/collision/trianglemesh.cpp" + "src/game/entity.hpp" + "src/game/entity.cpp" "src/game/sector.hpp" "src/game/sector.cpp" "src/game/world.hpp" @@ -82,4 +82,8 @@ endif() add_subdirectory(external/glm) target_link_libraries(PortalGame PRIVATE glm) -target_include_directories(PortalGame PRIVATE "external/stb") \ No newline at end of file +target_include_directories(PortalGame PRIVATE "external/stb") + +add_subdirectory(bullet3) +target_link_libraries(PortalGame PRIVATE BulletCollision LinearMath Bullet3Common) +target_include_directories(PortalGame PRIVATE "bullet3/src") diff --git a/src/app.cpp b/src/app.cpp index f1fd25a..0be14ab 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,7 +1,6 @@ #include "app.hpp" #include -#include "collision/capsule_triangle_sweep.hpp" App::App() { @@ -23,7 +22,11 @@ App::App() world_.LinkPortals(s1, "NDoor", s2, "WDoor"); world_.LinkPortals(s2, "NDoor", s2, "SDoor"); - player_ = world_.Spawn(s1, glm::vec3(0.0f, 0.0f, 1.0f)); + game::CapsuleShape capsule_shape; + capsule_shape.radius = 0.2f; // 20cm radius + capsule_shape.height = 0.3f; // 1.8m height + + player_ = world_.Spawn(s1, capsule_shape, glm::vec3(0.0f, 0.0f, 1.0f)); } void App::Frame() @@ -58,9 +61,8 @@ void App::Frame() //renderer_.DrawWorld(world_, 1, center + cameraPos, -cameraPos, up, aspect, 60.0f); - static glm::vec3 position(0.0f, 0.0f, 1.5f); + glm::vec3 velocity(0.0f, 0.0f, 0.0f); - glm::vec3 velocity(0.0f); if (input_ & game::PI_FORWARD) velocity.y += 1.0f; @@ -79,84 +81,51 @@ void App::Frame() if (input_ & game::PI_CROUCH) velocity.z -= 1.0f; + glm::vec3 forward = glm::vec3( + cos(angles_.x) * cos(angles_.y), + sin(angles_.x) * cos(angles_.y), + sin(angles_.y) + ); - //velocity.z = -1.0f; + velocity.z -= 0.1f * delta_time; // Apply gravity - - glm::vec3 cameraDir = glm::vec3(1.0f, 0.0f, 0.0f); - auto tris = world_.GetSector(0).GetMesh()->GetCollisionMesh()->GetTriangles(); + glm::vec3 forward_xy = glm::normalize(glm::vec3(forward.x, forward.y, 0.0f)); + glm::vec3 right = glm::normalize(glm::vec3(forward.y, -forward.x, 0.0f)); + velocity = glm::normalize(velocity.x * right + velocity.y * forward_xy + velocity.z * glm::vec3(0.0f, 0.0f, 1.0f)); - glm::vec3 c0 = position; - c0.z -= 0.1f; - glm::vec3 c1 = position; - c1.z += 0.1f; + //if (glm::length(velocity) > 0.1f) + //{ + // velocity = glm::normalize(velocity); // Normalize to prevent faster diagonal movement + // glm::vec3 u = velocity * delta_time * 2.0f; + // //bool hit = world_.GetSector(0).SweepCapsule(position, position + u); - bool found = false; + // //if (!hit) + // //{ + // // position += u; + // //} + //} - glm::vec3 u = velocity * delta_time; + player_->SetVelocity(velocity); + player_->Update(delta_time); - if (glm::length(velocity) > 0.001f) - { - //velocity = glm::normalize(velocity); + const auto& position = player_->GetOccurrence().GetPosition(); - //collision::ResolveCollisions( - // position, - // velocity, - // 0.5f, - // 0.2f, - // delta_time, - // tris - //); - - Sweep sweep; - - static std::vector sweep_tris; - sweep_tris.clear(); - - for (const auto& tri : tris) { - float dot = glm::dot(tri.verts[1] - tri.verts[0], tri.verts[2] - tri.verts[0]); - if (glm::abs(dot) < 0.001f) { - printf("degen!!!!!!!!!\n"); - continue; // Skip degenerate triangles - } - - //found = found || collision::SweepCapsuleTriangle( - // sweep, - // c0, - // c1, - // 0.2f, - // u, - // tri.verts[0], tri.verts[1], tri.verts[2] - //); - - sweep_tris.push_back(tri); - } - - collision::ResolveCollisions( - position, - velocity, - 0.5f, // Capsule radius - 0.2f, // Capsule height - delta_time, - sweep_tris - ); - } - - if (!found) - { - position += velocity * delta_time; - } - - printf("position: %.2f, %.2f, %.2f\n", position.x, position.y, position.z); - printf("found: %s\n", found ? "true" : "false"); - - renderer_.DrawWorld(world_, 0, position, cameraDir, glm::vec3(0.0f, 0.0f, 1.0f), aspect, 60.0f); + renderer_.DrawWorld(world_, 0, position, forward, glm::vec3(0.0f, 0.0f, 1.0f), aspect, 60.0f); } void App::MouseMove(const glm::vec2& delta) { + float sensitivity = 0.002f; // Sensitivity factor for mouse movement + angles_.x -= delta.x * sensitivity; // Yaw + angles_.y -= delta.y * sensitivity; // Pitch + // Clamp pitch to avoid gimbal lock + if (angles_.y > glm::radians(89.0f)) { + angles_.y = glm::radians(89.0f); + } else if (angles_.y < glm::radians(-89.0f)) { + angles_.y = glm::radians(-89.0f); + } } App::~App() diff --git a/src/app.hpp b/src/app.hpp index dffdd18..29e880d 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -6,7 +6,7 @@ #include "game/player_input.hpp" #include "gfx/renderer.hpp" -class App +class App { float time_ = 0.0f; glm::ivec2 viewport_size_ = { 800, 600 }; @@ -17,6 +17,8 @@ class App game::World world_; game::Entity* player_ = nullptr; + glm::vec2 angles_ = { 0.0f, 0.0f }; // Pitch and yaw angles in radians + gfx::Renderer renderer_; public: diff --git a/src/assets/mesh.cpp b/src/assets/mesh.cpp index 1881124..2a4a708 100644 --- a/src/assets/mesh.cpp +++ b/src/assets/mesh.cpp @@ -68,10 +68,12 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file { MeshTriangle& tri = tris.emplace_back(); glm::vec3 tri_verts[3]; - for (size_t i = 0; i < 3; ++i) { + for (size_t i = 0; i < 3; ++i) + { uint32_t vert_index; iss >> vert_index; - if (vert_index >= verts.size()) { + if (vert_index >= verts.size()) + { throw std::runtime_error("Vertex index out of bounds in mesh file: " + filename); } tri.vert[i] = vert_index; @@ -114,6 +116,11 @@ std::shared_ptr assets::Mesh::LoadFromFile(const std::string& file // Create the mesh object std::shared_ptr mesh = std::make_shared(verts, tris, materials); - mesh->SetCollisionMesh(std::move(collision_mesh)); + + if (load_collision) + { + collision_mesh->Build(); + mesh->SetCollisionMesh(std::move(collision_mesh)); + } return mesh; } diff --git a/src/collision/capsule.hpp b/src/collision/capsule.hpp deleted file mode 100644 index 553bee0..0000000 --- a/src/collision/capsule.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -namespace collision -{ - struct Capsule - { - glm::vec3 p0; - glm::vec3 p1; - float radius; - }; -} \ No newline at end of file diff --git a/src/collision/capsule_triangle_sweep.cpp b/src/collision/capsule_triangle_sweep.cpp deleted file mode 100644 index 0264acb..0000000 --- a/src/collision/capsule_triangle_sweep.cpp +++ /dev/null @@ -1,439 +0,0 @@ -#include "capsule_triangle_sweep.hpp" - -#define EPSILON 1e-5f // Used to test if float is close to 0. Tweak this if you get problems. - -// Return whether point P is contained inside 3D region delimited by triangle T0,T1,T2 edges. -static bool PointInsideTriangle(const glm::vec3& p, const glm::vec3& t0, const glm::vec3& t1, const glm::vec3& t2) -{ - // Real-Time Collision Detection: 3.4: Barycentric Coordinates (pages 46-52). - // - // The book also has a subsection dedicated to point inside triangle tests: - // Real-Time Collision Detection: 5.4.2: Testing Point in Triangle (pages 203-206). - // But those tests only work for CCW triangles. This seems to work for either orientation. - glm::vec3 t01 = t1 - t0; - glm::vec3 t02 = t2 - t0; - glm::vec3 t0p = p - t0; - float t01t01 = glm::dot(t01, t01); - float t01t02 = glm::dot(t01, t02); - float t02t02 = glm::dot(t02, t02); - float t0pt01 = glm::dot(t0p, t01); - float t0pt02 = glm::dot(t0p, t02); - float denom = t01t01 * t02t02 - t01t02 * t01t02; - - // Normally I would have to divide vd,wd by denom to get v,w. But divisions are - // expensive and cause troubles around 0. If denom isn't negative then we don't - // ever need to divide. If in the future it does turn out denom can be negative - // then we can always multiply by denom instead of dividing to keep sign the same. - float vd = t02t02 * t0pt01 - t01t02 * t0pt02; - float wd = t01t01 * t0pt02 - t01t02 * t0pt01; - return vd >= 0 && wd >= 0 && vd + wd <= denom; -} - -// Return whether point P is contained inside 3D region delimited by parallelogram P0,P1,P2 edges. -static bool PointInsideParallelogram(const glm::vec3& p, const glm::vec3& p0, const glm::vec3& p1, const glm::vec3& p2) -{ - // There may be a better way. - // https://math.stackexchange.com/questions/4381852/point-in-parallelogram-in-3d-space - glm::vec3 p3 = p2 + (p1 - p0); - return PointInsideTriangle(p, p0, p1, p2) || PointInsideTriangle(p, p1, p3, p2); -} - -// Return whether point P is contained inside a triangular prism A0,A1,A2-B0,B1,B2. -static bool PointInsideTriangularPrism( - const glm::vec3& p, - const glm::vec3& a0, - const glm::vec3& a1, - const glm::vec3& a2, - const glm::vec3& b0, - const glm::vec3& b1, - const glm::vec3& b2) -{ - glm::vec3 faces[5][3] = { { a0,a1,a2 }, { b0,b2,b1 }, { a0,b0,a1 }, { a1,b1,a2 }, { a2,b2,a0 } }; - float sgn = 0; - for (int i = 0; i < 5; i++) { - const glm::vec3& p0 = faces[i][1]; - const glm::vec3& p1 = faces[i][0]; - const glm::vec3& p2 = faces[i][2]; - - // Check which side of plane point is in. If it's always on the same side, it's colliding. - glm::vec3 p01 = p1 - p0; - glm::vec3 p02 = p2 - p0; - glm::vec3 n = glm::cross(p01, p02); - float d = glm::dot(n, p - p0); - if (i == 0) sgn = d; - if (sgn * d <= 0) - return false; - } - return true; -} - -// Sweep sphere C,r with velocity Sv against plane N of triangle T0,T1,T2, ignoring edges. -static bool SweepSphereTrianglePlane( - Sweep& sweep, - const glm::vec3& c, - float r, - const glm::vec3& v, - const glm::vec3& t0, - const glm::vec3& t1, - const glm::vec3& t2, - const glm::vec3& n) -{ - // Real-Time Collision Detection 5.5.3: Intersecting Moving Sphere Against Plane (pages 219-223). - float t; - float d = glm::dot(n, c - t0); - float pen = r - d; - if (pen > 0) - t = 0; // Sphere already starts coliding with triangle plane. - else { - // Sphere isn't immediately colliding with the plane. Check if it's moving away. - float denom = glm::dot(n, v); - if (denom >= 0) - return false; // Sphere is moving away from plane. - - // Sphere will collide with plane at some point. - t = (r - d) / denom; - pen = 0; - } - - // If sphere misses entire triangle plane, then it definitely misses the triangle too. - if (t >= sweep.time) - return false; - - // Is the plane collision point inside the triangle? - // Real-Time Collision Detection: 5.4.2: Testing Point in Triangle (pg 203-206). - glm::vec3 collision = c + t * v - r * n; - if (!PointInsideTriangle(collision, t0, t1, t2)) - return false; - - // Plane collision point is inside the triangle. So the sphere collides with the triangle. - sweep.time = t; - sweep.depth = pen; - sweep.point = collision; - sweep.normal = n; - return true; -} - -// Sweep sphere C,r with velocity V against plane N of parallelogram P0,P1,P2 ignoring edges. -static bool SweepSphereParallelogramPlane( - Sweep& sweep, - const glm::vec3& c, - float r, - const glm::vec3& v, - const glm::vec3& p0, - const glm::vec3& p1, - const glm::vec3& p2, - const glm::vec3& n) -{ - // Real-Time Collision Detection 5.5.3: Intersecting Moving Sphere Against Plane (pages 219-223). - float t; - float d = glm::dot(c, n - p0); - float pen = r - d; - if (pen > 0) - t = 0; // Sphere already starts coliding with the quad plane. - else { - // Sphere isn't immediately colliding with the plane. Check if it's moving away. - float denom = glm::dot(n, v); - if (denom >= 0) - return false; // Sphere is moving away from plane. - - // Sphere will collide with plane at some point. - t = (r - d) / denom; - pen = 0; - } - - // If sphere misses entire quad plane, then it definitely misses the quad too. - if (t >= sweep.time) - return false; - - // Is the plane collision point inside the quad? - // Real-Time Collision Detection: 5.4.2: Testing Point in Triangle (pages 203-206). - glm::vec3 collision = c + t * v - r * n; - if (!PointInsideParallelogram(collision, p0, p1, p2)) - return false; - - // Plane collision point is inside the quad. So the sphere collides with the quad. - sweep.time = t; - sweep.depth = pen; - sweep.point = collision; - sweep.normal = n; - return true; -} - -// Sweep point P with velocity V against sphere S,r. -static bool SweepPointSphere( - Sweep& sweep, - const glm::vec3& p, - const glm::vec3& v, - const glm::vec3& s, - float r, - const glm::vec3& fallbackNormal) -{ - // Real-Time Collision Detection 5.3.2: Intersecting Ray or Segment Against Sphere (pages 177-179). - - // Set up quadratic equation. - glm::vec3 d = p - s; - float b = glm::dot(d, v); - float c = glm::dot(d, d) - r * r; - if (c > 0 && b > 0) - return false; // Point starts outside (c > 0) and moves away from sphere (b > 0). - float a = glm::dot(v, v); - float discr = b * b - a * c; - if (discr < 0) - return false; // Point misses sphere. - - // Point hits sphere. Compute time of first impact. - float t = (-b - glm::sqrt(discr)) / a; - if (t >= sweep.time) - return false; - - // The sphere is the first thing the point hits so far. - t = glm::max(t, 0.0f); - glm::vec3 collision = p + t * v; - glm::vec3 vec = collision - s; - float len = glm::length(vec); - sweep.time = t; - sweep.depth = t > 0 ? 0 : r - len; - sweep.point = collision; - sweep.normal = len >= EPSILON ? vec / len : fallbackNormal; - return true; -} - -// Sweep point P with velocity V against cylinder C0,C1,r, ignoring the endcaps. -static bool SweepPointUncappedCylinder( - Sweep& sweep, - const glm::vec3& p, - const glm::vec3& v, - const glm::vec3& c0, - const glm::vec3& c1, - float r, - const glm::vec3& fallbackNormal) -{ - // Real-Time Collision Detection 5.3.7: Intersecting Ray or Segment Against Cylinder (pages 194-198). - - // Test if swept point is fully outside of either endcap. - glm::vec3 n = c1 - c0; - glm::vec3 d = p - c0; - float dn = glm::dot(d, n); - float vn = glm::dot(v, n); - float nn = glm::dot(n, n); - if (dn < 0 && dn + vn < 0) - return false; // Fully outside c0 end of cylinder. - if (dn > nn && dn + vn > nn) - return false; // Fully outside c1 end of cylinder. - - // Set up quadratic equations and check if sweep direction is parallel to cylinder. - float t; - float vv = glm::dot(v, v); - float dv = glm::dot(d, v); - float dd = glm::dot(d, d); - float a = nn * vv - vn * vn; - float c = nn * (dd - r * r) - dn * dn; - if (a < EPSILON) { - // Sweep direction is parallel to cylinder. - if (c > 0) - return false; // Point starts outside of cylinder, so it never collides. - if (dn < 0) - return false; // Point starts outside of c0 endcap. - if (dn > nn) - return false; // Point starts outside of c1 endcap. - t = 0; - } - else { - // Sweep direction is not parallel to cylinder. Solve for time of first contact. - float b = nn * dv - vn * dn; - float discr = b * b - a * c; - if (discr < 0) - return false; // Sweep misses cylinder. - t = (-b - glm::sqrt(discr)) / a; - } - - // Check if the sweep missed, or if it hits but another collision happens sooner. - if (t < 0 || t >= sweep.time) - return false; - - // This is the first collision. Find the closest point on the center of the cylinder. - glm::vec3 collision = p + t * v; - glm::vec3 center; - if (nn < EPSILON) - center = c0; // The cylinder is actually a circle. - else - center = c0 + (glm::dot(collision - c0, n) / nn) * n; - - // Update collision time, depth, and normal. - glm::vec3 vec = collision - center; - float len = glm::length(vec); - float depth = r - len; - sweep.time = t; - sweep.depth = t > 0 ? 0 : depth; - sweep.point = collision; - sweep.normal = len >= EPSILON ? vec / len : fallbackNormal; - return true; -} - -// Sweep a capsule C0,C1,Cr with velocity Cv against the triangle T0,T1,T2. -// c0,c1 capsule line segment endpoints -// r capsule radius -// v capsule velocity -// t0,t1,t2 3 triangle vertices -// returns whether the capsule and triangle intersect -bool collision::SweepCapsuleTriangle( - Sweep& s, - const glm::vec3& c0, - const glm::vec3& c1, - float r, - const glm::vec3& v, - const glm::vec3& t0, - const glm::vec3& t1, - const glm::vec3& t2) -{ - // Compute triangle plane equation. - glm::vec3 t01 = t1 - t0; - glm::vec3 t02 = t2 - t0; - glm::vec3 normal = glm::normalize(glm::cross(t01, t02)); - - // Extrude triangle along capsule direction. - glm::vec3 c01 = c1 - c0; - glm::vec3 a0 = t0; - glm::vec3 a1 = t1; - glm::vec3 a2 = t2; - glm::vec3 b0 = t0 - c01; - glm::vec3 b1 = t1 - c01; - glm::vec3 b2 = t2 - c01; - - // Test for initial collision with the extruded triangle prism. - if (PointInsideTriangularPrism(c0, a0, a1, a2, b0, b1, b2)) { - // Capsule starts off penetrating triangle. Push it out from the triangle plane. - float d0 = glm::dot(normal, c0 - t0); - float d1 = glm::dot(normal, c1 - t0); - float d = glm::abs(d0) <= glm::abs(d1) ? d0 : d1; - glm::vec3 n = d >= 0 ? normal : -normal; - s.time = 0; - s.depth = glm::abs(d) + r; - s.normal = n; - s.point = c0 + d0 * normal; - return true; - } - - // Decompose capsule triangle sweep into: 2 sphere-triangle + 3 sphere-parallelogram + 9 point-cylinder + 6 point-sphere sweeps. - bool hit = false; - glm::vec3 triangles[2][3] = { {a0,a1,a2}, {b0,b1,b2} }; - glm::vec3 parallelograms[3][3] = { {a0,a1,b0}, {a1,a2,b1}, {a2,a0,b2} }; - glm::vec3 cylinders[9][2] = { {a0,a1}, {a1,a2}, {a2,a0}, {b0,b1}, {b1,b2}, {b2,b0}, {a0,b0}, {a1,b1}, {a2,b2} }; - glm::vec3 spheres[6] = { a0, a1, a2, b0, b1, b2 }; - - // Do sphere-triangle sweeps. - glm::vec3 triangleNormals[2]; - for (int i = 0; i < 2; i++) { - glm::vec3 p0 = triangles[i][0]; - glm::vec3 p1 = triangles[i][1]; - glm::vec3 p2 = triangles[i][2]; - - // Compute triangle plane normal. - glm::vec3 n = normal; - if (glm::dot(n, c0 - p0) < 0) n = -n; // Orient towards sphere. - triangleNormals[i] = n; - - // Test for triangle-plane sphere intersection. - hit = hit || SweepSphereTrianglePlane(s, c0, r, v, p0, p1, p2, n); - } - - // Do sphere-parallelogram sweeps. - glm::vec3 parallelogramNormals[3]; - for (int i = 0; i < 3; i++) { - glm::vec3 p0 = parallelograms[i][0]; - glm::vec3 p1 = parallelograms[i][1]; - glm::vec3 p2 = parallelograms[i][2]; - - // Check if quad is degenerate. Happens when triangle edge completely parallel to capsule. - glm::vec3 p01 = p1 - p0; - glm::vec3 p02 = p2 - p0; - glm::vec3 c = glm::cross(p01, p02); - float len = glm::length(c); - if (len > EPSILON) { - // Compute quad plane equation. - glm::vec3 n = c / len; - if (glm::dot(n, c0 - p0) < 0) n = -n; // Orient towards sphere. - parallelogramNormals[i] = n; - - // Do the sweep test. - hit = hit || SweepSphereParallelogramPlane(s, c0, r, v, p0, p1, p2, n); - } - else parallelogramNormals[i] = triangleNormals[0]; - } - - // Do point-cylinder sweeps. - for (int i = 0; i < 9; i++) { - glm::vec3 p0 = cylinders[i][0]; - glm::vec3 p1 = cylinders[i][1]; - glm::vec3 n; - if (i < 6) - n = triangleNormals[i / 3]; - else - n = parallelogramNormals[i - 6]; - hit = hit || SweepPointUncappedCylinder(s, c0, v, p0, p1, r, n); - } - - // Do point-sphere sweeps. - for (int i = 0; i < 6; i++) { - glm::vec3 c = spheres[i]; - glm::vec3 n = triangleNormals[i / 3]; - hit = hit || SweepPointSphere(s, c0, v, c, r, n); - } - - return hit; -} - -// Move a capsule and resolve any triangle collisions encountered along the way. -// p - capsule base position -// v - capsule velocity -// h - capsule height -// r - capsule radius -// dt - time-step length -// triangles - list of triangles to collide with -void collision::ResolveCollisions(glm::vec3& p, glm::vec3& v, float h, float r, float dt, std::span triangles) { - // Store the leftover movement in this vector. - glm::vec3 u = dt * v; - - // Move and resolve collisions while there is still motion. But cap max iterations to ensure simulation terminates. - const int MAX_ITER = 16; - for (int iter = 0; iter < MAX_ITER && glm::dot(u, u) > 0.0f; iter++) { - // Compute capsule endpoints. - glm::vec3 c0 = p; - glm::vec3 c1 = p; - c0.y += r; - c1.y += h - r; - - // Perform the sweep test against all triangles. - Sweep s; - s.time = 1; - for (int i = 0; i < triangles.size(); i++) { - glm::vec3 t0 = triangles[i].verts[0]; - glm::vec3 t1 = triangles[i].verts[1]; - glm::vec3 t2 = triangles[i].verts[2]; - SweepCapsuleTriangle(s, c0, c1, r, u, t0, t1, t2); - } - - // Stop objects from intersecting. - if (s.depth > 0) - p += (s.depth + EPSILON) * s.normal; - - // Advance the cylinder until the first contact time. - glm::vec3 dp = s.time * u; - p += dp; - - // If there were no collisions, entire motion is complete and we can terminate early. - if (s.time >= 1) - break; - - // Cancel out motion parallel to the normal. This causes capsule to slide along surface. - u -= dp; - u += glm::dot(u, s.normal) * s.normal; - v += glm::dot(v, s.normal) * s.normal; - - // Nudge the position and velocity slightly away from surface to avoid another collision. - glm::vec3 offset = EPSILON * s.normal; - p += offset; - v += offset; - u += offset; - } -} \ No newline at end of file diff --git a/src/collision/capsule_triangle_sweep.hpp b/src/collision/capsule_triangle_sweep.hpp deleted file mode 100644 index 3f92a13..0000000 --- a/src/collision/capsule_triangle_sweep.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "capsule.hpp" -#include "trianglemesh.hpp" - -struct Sweep -{ - float time = 1.0f; // Non-negative time of first contact. - float depth = 0.0f; // Non-negative penetration depth if objects start initially colliding. - glm::vec3 point = glm::vec3(0.0f); // Point of first-contact. Only updated when contact occurs. - glm::vec3 normal = glm::vec3(0.0f); // Unit-length collision normal. Only updated when contact occurs. -}; - -namespace collision -{ - - bool SweepCapsuleTriangle( - Sweep& s, - const glm::vec3& c0, - const glm::vec3& c1, - float r, - const glm::vec3& v, - const glm::vec3& t0, - const glm::vec3& t1, - const glm::vec3& t2); - - void ResolveCollisions(glm::vec3& p, glm::vec3& v, float h, float r, float dt, std::span triangles); -} \ No newline at end of file diff --git a/src/collision/trianglemesh.cpp b/src/collision/trianglemesh.cpp index 34b6a8b..661e7d7 100644 --- a/src/collision/trianglemesh.cpp +++ b/src/collision/trianglemesh.cpp @@ -1,14 +1,20 @@ #include "trianglemesh.hpp" +collision::TriangleMesh::TriangleMesh() +{ + +} + void collision::TriangleMesh::AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) { - Triangle& tri = tris_.emplace_back(); - tri.verts[0] = v0; - tri.verts[1] = v1; - tri.verts[2] = v2; - - for (size_t i = 0; i < 3; ++i) - { - tri.aabb.AddPoint(tri.verts[i]); - } + btVector3 bt_v0(v0.x, v0.y, v0.z); + btVector3 bt_v1(v1.x, v1.y, v1.z); + btVector3 bt_v2(v2.x, v2.y, v2.z); + bt_mesh_.addTriangle(bt_v0, bt_v1, bt_v2, true); +} + +void collision::TriangleMesh::Build() +{ + bt_shape_ = std::make_unique(&bt_mesh_, true, true); + } diff --git a/src/collision/trianglemesh.hpp b/src/collision/trianglemesh.hpp index 1170ad8..72cb95b 100644 --- a/src/collision/trianglemesh.hpp +++ b/src/collision/trianglemesh.hpp @@ -5,25 +5,23 @@ #include #include "aabb.hpp" +#include + +#include + namespace collision { - struct Triangle - { - glm::vec3 verts[3]; - AABB3 aabb; - }; - class TriangleMesh { - std::vector tris_; - + btTriangleMesh bt_mesh_; + std::unique_ptr bt_shape_; + public: - TriangleMesh() = default; + TriangleMesh(); void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2); + void Build(); - std::span GetTriangles() const { - return std::span(tris_); - } + btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); } }; } \ No newline at end of file diff --git a/src/game/entity.cpp b/src/game/entity.cpp index 62f0bb6..d751a33 100644 --- a/src/game/entity.cpp +++ b/src/game/entity.cpp @@ -1,6 +1,153 @@ #include "entity.hpp" +#include "world.hpp" -game::Entity::Entity(World* world, size_t sector_idx, const glm::vec3& position) +game::Entity::Entity(World* world, size_t sector_idx, const CapsuleShape& capsule_shape, const glm::vec3& position) : + world_(world), + capsule_(capsule_shape), + velocity_(0.0f) +{ + CreateOccurrenceParams occu_params; + occu_params.sector = &world->GetSector(sector_idx); + occu_params.position = position; + occu_params.scale = 1.0f; // Default scale + occu_params.basis = glm::mat3(1.0f); // Identity basis + + // Create initial occurence in given sector + occu_ = CreateOccurrence(occu_params); +} + +void game::Entity::Move(glm::vec3& velocity, float dt) +{ + glm::vec3 u = velocity * dt; // Calculate the movement vector + + const int MAX_ITERS = 4; + for (size_t i = 0; i < MAX_ITERS && glm::dot(u, u) > 0.0f; ++i) + { + printf("Entity::Move: Iteration %zu, u = (%f, %f, %f)\n", i, u.x, u.y, u.z); + + // offset in occu's sector space + glm::vec3 occu_offset = occu_->basis_ * u; + glm::vec3 to = occu_->position_ + occu_offset; + + float occu_hit_fraction = 1.0f; + glm::vec3 occu_hit_normal; + const Portal* hit_portal = nullptr; + + bool hit = occu_->Sweep(to, occu_hit_fraction, occu_hit_normal, &hit_portal); + + if (hit_portal != touching_portal_) + { + touching_portal_ = hit_portal; + + if (hit_portal) + { + // Beginning to touch a portal, create other occurrence in the linked sector + CreateOtherOccurence(*hit_portal); + } + else + { + // Not touching the portal anymore, destroy the other occurrence + other_occu_.reset(); + } + } + + float other_hit_fraction = 1.0f; + glm::vec3 other_hit_normal; + + bool other_hit = false; + + if (other_occu_) + { + glm::vec3 other_offset = other_occu_->basis_ * u; + glm::vec3 other_to = other_occu_->position_ + other_offset; + + // Sweep the other occurrence if it exists + other_hit = other_occu_->Sweep(other_to, other_hit_fraction, other_hit_normal, nullptr); + } + + bool any_hit = hit || other_hit; + float hit_fraction = 1.0f; // Default to no hit + glm::vec3 hit_normal; + + if (hit && (!other_hit || occu_hit_fraction <= other_hit_fraction)) + { + // Primary occurrence hit firstly + hit_fraction = occu_hit_fraction; + hit_normal = occu_->inv_basis_ * occu_hit_normal; + } + else if (other_hit) + { + // Other occurrence hit firstly + hit_fraction = other_hit_fraction; + hit_normal = other_occu_->inv_basis_ * other_hit_normal; + } + + // Update the position based on the hit fraction + occu_->position_ += hit_fraction * occu_offset; + + if (any_hit) + { + occu_->position_ += hit_normal * 0.00001f; + } + + + if (other_occu_ && touching_portal_) + { + other_occu_->position_ = touching_portal_->tr_position * glm::vec4(occu_->position_, 1.0f); + } + + if (!any_hit) + { + break; + } + + //hit_normal *= -1.0f; // Invert the normal to point outwards + printf("Entity::Move: Hit detected, hit_fraction = %f, hit_normal = (%f, %f, %f)\n", hit_fraction, hit_normal.x, hit_normal.y, hit_normal.z); + + u -= hit_fraction * u; // Reduce the movement vector by the hit fraction + u -= glm::dot(u, hit_normal) * hit_normal; // Reflect the velocity along the hit normal + velocity -= glm::dot(velocity, hit_normal) * hit_normal; // Adjust the velocity + } + +} + +void game::Entity::Update(float dt) +{ + Move(velocity_, dt); +} + +void game::Entity::CreateOtherOccurence(const Portal& portal) +{ + CreateOccurrenceParams other_occu_params; + other_occu_params.sector = portal.link->sector; + other_occu_params.position = portal.tr_position * glm::vec4(occu_->position_, 1.0f); + other_occu_params.scale = occu_->scale_ * portal.tr_scale; + other_occu_params.basis = portal.tr_basis * occu_->basis_; + + other_occu_ = CreateOccurrence(other_occu_params); +} + +game::EntityOccurrence::EntityOccurrence(Entity* entity, const CreateOccurrenceParams& params) : + entity_(entity), + sector_(params.sector), + position_(params.position), + scale_(params.scale), + basis_(params.basis), + inv_basis_(glm::inverse(params.basis)), + bt_capsule_(entity->GetCapsuleShape().radius * params.scale, entity->GetCapsuleShape().height * params.scale) { } + +bool game::EntityOccurrence::Sweep(const glm::vec3& target_position, float& hit_fraction, glm::vec3& hit_normal, const Portal** hit_portal) +{ + return sector_->SweepCapsule( + bt_capsule_, + basis_, + position_, + target_position, + hit_fraction, + hit_normal, + hit_portal + ); +} diff --git a/src/game/entity.hpp b/src/game/entity.hpp index 54c6869..309674a 100644 --- a/src/game/entity.hpp +++ b/src/game/entity.hpp @@ -6,32 +6,81 @@ namespace game { class World; + class Entity; + + struct CapsuleShape + { + float radius; + float height; + }; + + struct CreateOccurrenceParams + { + Sector* sector; + glm::vec3 position; + float scale; + glm::mat3 basis; // Orthonormal + }; class EntityOccurrence { - const Sector* sector_; + Entity* entity_; + Sector* sector_; - float radius_; glm::vec3 position_; - glm::vec3 up_; - - glm::mat3 trans_; - glm::mat3 inv_trans_; + float scale_; + glm::mat3 basis_; // Orthonormal basis in sector space + glm::mat3 inv_basis_; + + btCapsuleShapeZ bt_capsule_; + + friend class Entity; + + public: + EntityOccurrence(Entity* entity, const CreateOccurrenceParams& params); + + // pos in sector space + bool Sweep(const glm::vec3& target_position, float& hit_fraction, glm::vec3& hit_normal, const Portal** hit_portal); + + const Sector& GetSector() const { return *sector_; } + const glm::vec3& GetPosition() const { return position_; } }; class Entity { World* world_; + CapsuleShape capsule_; + std::unique_ptr occu_; std::unique_ptr other_occu_; + const Portal* touching_portal_; glm::vec3 velocity_; public: - Entity(World* world, size_t sector_idx, const glm::vec3& position); + Entity(World* world, size_t sector_idx, const CapsuleShape& capsule_shape, const glm::vec3& position); + // offset in entity space + void Move(glm::vec3& velocity, float dt); + + void Update(float dt); + + void SetVelocity(const glm::vec3& velocity) { velocity_ = velocity; } + + const glm::vec3& GetVelocity() const { return velocity_; } + const CapsuleShape& GetCapsuleShape() const { return capsule_; } + EntityOccurrence& GetOccurrence() { return *occu_; } + + protected: + virtual std::unique_ptr CreateOccurrence(const CreateOccurrenceParams& params) + { + return std::make_unique(this, params); + } + + private: + void CreateOtherOccurence(const Portal& portal); }; diff --git a/src/game/sector.cpp b/src/game/sector.cpp index 9448639..ab1a45c 100644 --- a/src/game/sector.cpp +++ b/src/game/sector.cpp @@ -9,7 +9,10 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptrGetMesh()) + mesh_(def_->GetMesh()), + + bt_col_dispatcher_(&bt_col_cfg_), + bt_world_(&bt_col_dispatcher_, &bt_broad_phase_, &bt_col_cfg_) { auto def_portals = def_->GetPortals(); size_t num_portals = def_portals.size(); @@ -21,7 +24,6 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptrGetCollisionMesh()->GetShape()); + bt_world_.addCollisionObject(&bt_mesh_col_obj_); } void game::Sector::ComputePortalVertex(Portal& portal, size_t idx, const glm::vec2& base_vert) @@ -61,6 +67,26 @@ int game::Sector::GetPortalIndex(const std::string& name) const return -1; } +static void LinkPortal(game::Portal& p1, game::Portal& p2) { + // Calculate the traverse transformation + glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), glm::radians(180.0f), glm::vec3(0, 0, 1)); + glm::mat4 p1_to_p2 = p2.trans * rotation * glm::inverse(p1.trans); + + p1.link = &p2; + + // Transforms points from p1's sector space to p2's sector space + p1.tr_position = p1_to_p2; + + // Transforms directions from p1's sector space to p2's sector space + p1.tr_basis = glm::mat3(p1.tr_position); + + // get rid of scale + for (size_t i = 0; i < 3 ; i++) + p1.tr_orientation[i] = glm::normalize(p1.tr_basis[i]); + + p1.tr_scale = p2.scale / p1.scale; +} + void game::Sector::LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2) { Portal& p1 = s1.portals_[idx1]; @@ -71,18 +97,97 @@ void game::Sector::LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2) throw std::runtime_error("One of the portals is already linked"); } + LinkPortal(p1, p2); - // Calculate the link transformation - glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), glm::radians(180.0f), glm::vec3(0, 0, 1)); - glm::mat4 p1_to_p2 = p2.trans * rotation * glm::inverse(p1.trans); - - p1.link = &p2; - p1.link_trans = p1_to_p2; - if (&p1 != &p2) { - p2.link = &p1; - p2.link_trans = glm::inverse(p1_to_p2); + LinkPortal(p2, p1); } } + +namespace game +{ + struct PortalDetectingClosestConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback + { + using Super = btCollisionWorld::ClosestConvexResultCallback; + + PortalDetectingClosestConvexResultCallback(const btVector3& convexFromWorld, const btVector3& convexToWorld) : + Super(convexFromWorld, convexToWorld) + { + } + + const Portal* hit_portal = nullptr; + float hit_portal_fraction = std::numeric_limits::max(); + + virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) override + { + void* ptr = convexResult.m_hitCollisionObject->getUserPointer(); + CollisionObjectData* data = static_cast(ptr); + + if (data && data->type == CO_PORTAL) + { + if (convexResult.m_hitFraction < hit_portal_fraction) + { + hit_portal = data->portal; + hit_portal_fraction = convexResult.m_hitFraction; + } + } + else + { + Super::addSingleResult(convexResult, normalInWorldSpace); + } + + return 1.0f; // Continue processing other results + } + }; + +} + +bool game::Sector::SweepCapsule( + const btCapsuleShapeZ& capsule, + const glm::mat3& basis, + const glm::vec3& start, + const glm::vec3& end, + float& hit_fraction, + glm::vec3& hit_normal, + const Portal** hit_portal) +{ + //const btVector3* bt_start = reinterpret_cast(&start); + //const btVector3* bt_end = reinterpret_cast(&end); + //const btMatrix3x3* bt_basis = reinterpret_cast(&basis); + + btVector3 bt_start(start.x, start.y, start.z); + btVector3 bt_end(end.x, end.y, end.z); + btMatrix3x3 bt_basis( + basis[0][0], basis[0][1], basis[0][2], + basis[1][0], basis[1][1], basis[1][2], + basis[2][0], basis[2][1], basis[2][2] + ); + + btTransform start_transform(bt_basis, bt_start); + btTransform end_transform(bt_basis, bt_end); + + PortalDetectingClosestConvexResultCallback result_callback(bt_start, bt_end); + + bt_world_.convexSweepTest(&capsule, start_transform, end_transform, result_callback); + + if (hit_portal) + { + *hit_portal = result_callback.hit_portal; + } + + if (result_callback.hasHit()) + { + hit_fraction = result_callback.m_closestHitFraction; + hit_normal = glm::vec3( + result_callback.m_hitNormalWorld.x(), + result_callback.m_hitNormalWorld.y(), + result_callback.m_hitNormalWorld.z() + ); + + return true; + } + + return false; +} diff --git a/src/game/sector.hpp b/src/game/sector.hpp index 579de6b..b445c33 100644 --- a/src/game/sector.hpp +++ b/src/game/sector.hpp @@ -3,28 +3,47 @@ #include "assets/sectordef.hpp" #include #include +#include namespace game { class Sector; + class Portal; + class World; + + enum CollisionObjectType + { + CO_DEFAULT, + CO_PORTAL, + }; + + struct CollisionObjectData + { + CollisionObjectType type = CO_DEFAULT; + union + { + const Portal* portal; // CO_PORTAL + }; + }; struct Portal { Sector* sector; - - const assets::SectorPortalDef* def_sector; const assets::PortalDef* def; glm::mat4 trans; + float scale; glm::vec4 plane; glm::vec3 verts[4]; // Portal vertices in sector space Portal* link; - glm::mat4 link_trans; - }; - class World; + glm::mat4 tr_position; // this sector space to other sector space + glm::mat3 tr_basis; + glm::mat3 tr_orientation; + float tr_scale; + }; class Sector { @@ -37,6 +56,13 @@ namespace game std::vector portals_; std::map portal_map_; // Maps portal name to index in portals_ + btDefaultCollisionConfiguration bt_col_cfg_; + btCollisionDispatcher bt_col_dispatcher_; + btDbvtBroadphase bt_broad_phase_; + btCollisionWorld bt_world_; + + btCollisionObject bt_mesh_col_obj_; + public: Sector(World* world, size_t idx, std::shared_ptr def); @@ -52,6 +78,14 @@ namespace game static void LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2); + bool SweepCapsule( + const btCapsuleShapeZ& capsule, + const glm::mat3& basis, + const glm::vec3& start, + const glm::vec3& end, + float& hit_fraction, + glm::vec3& hit_normal, + const Portal** hit_portal); }; diff --git a/src/gfx/renderer.cpp b/src/gfx/renderer.cpp index 95451fa..57f7204 100644 --- a/src/gfx/renderer.cpp +++ b/src/gfx/renderer.cpp @@ -48,7 +48,7 @@ void gfx::Renderer::DrawWorld( proj_ = glm::perspective( glm::radians(fov), // FOV aspect, // Aspect ratio - 0.1f, // Near plane + 0.01f, // Near plane 1000.0f // Far plane ); @@ -190,9 +190,9 @@ void gfx::Renderer::DrawPortal(const DrawSectorParams& params, const game::Porta new_params.screen_aabb = params.screen_aabb.Intersection(portal_aabb); new_params.recursion = params.recursion + 1; - new_params.view = params.view * other_portal.link_trans; + new_params.view = params.view * other_portal.tr_position; new_params.view_proj = proj_ * new_params.view; - new_params.eye = portal.link_trans * glm::vec4(params.eye, 1.0f); + new_params.eye = portal.tr_position * glm::vec4(params.eye, 1.0f); // Draw the sector through the portal DrawSector(new_params); diff --git a/src/main.cpp b/src/main.cpp index 08bd590..0a54426 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -125,11 +125,22 @@ static void PollEvents() SDL_Event event; while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) + switch (event.type) { + case SDL_QUIT: s_quit = true; return; + + case SDL_MOUSEMOTION: + int xrel = event.motion.xrel; + int yrel = event.motion.yrel; + if (xrel != 0 || yrel != 0) + { + s_app->MouseMove(glm::vec2(static_cast(xrel), static_cast(yrel))); + } + break; } + } } @@ -186,6 +197,8 @@ static void Main() { throw std::runtime_error("Failed to initialize OpenGL"); } + SDL_SetRelativeMouseMode(SDL_TRUE); + s_app = std::make_unique(); #ifdef EMSCRIPTEN