Basic movement

This commit is contained in:
tovjemam 2025-08-12 14:40:32 +02:00
parent 30d70e178e
commit e4ee01de2e
16 changed files with 462 additions and 605 deletions

3
.gitmodules vendored
View File

@ -5,3 +5,6 @@
[submodule "external/glm"] [submodule "external/glm"]
path = external/glm path = external/glm
url = https://github.com/g-truc/glm.git url = https://github.com/g-truc/glm.git
[submodule "bullet3"]
path = bullet3
url = https://github.com/bulletphysics/bullet3.git

View File

@ -16,9 +16,9 @@ add_executable(PortalGame
"src/assets/sectordef.cpp" "src/assets/sectordef.cpp"
"src/assets/mesh.hpp" "src/assets/mesh.hpp"
"src/assets/mesh.cpp" "src/assets/mesh.cpp"
"src/collision/capsule_triangle_sweep.hpp"
"src/collision/capsule_triangle_sweep.cpp"
"src/collision/trianglemesh.cpp" "src/collision/trianglemesh.cpp"
"src/game/entity.hpp"
"src/game/entity.cpp"
"src/game/sector.hpp" "src/game/sector.hpp"
"src/game/sector.cpp" "src/game/sector.cpp"
"src/game/world.hpp" "src/game/world.hpp"
@ -83,3 +83,7 @@ endif()
add_subdirectory(external/glm) add_subdirectory(external/glm)
target_link_libraries(PortalGame PRIVATE glm) target_link_libraries(PortalGame PRIVATE glm)
target_include_directories(PortalGame PRIVATE "external/stb") 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")

View File

@ -1,7 +1,6 @@
#include "app.hpp" #include "app.hpp"
#include <iostream> #include <iostream>
#include "collision/capsule_triangle_sweep.hpp"
App::App() App::App()
{ {
@ -23,7 +22,11 @@ App::App()
world_.LinkPortals(s1, "NDoor", s2, "WDoor"); world_.LinkPortals(s1, "NDoor", s2, "WDoor");
world_.LinkPortals(s2, "NDoor", s2, "SDoor"); world_.LinkPortals(s2, "NDoor", s2, "SDoor");
player_ = world_.Spawn<game::Entity>(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<game::Entity>(s1, capsule_shape, glm::vec3(0.0f, 0.0f, 1.0f));
} }
void App::Frame() void App::Frame()
@ -58,9 +61,8 @@ void App::Frame()
//renderer_.DrawWorld(world_, 1, center + cameraPos, -cameraPos, up, aspect, 60.0f); //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) if (input_ & game::PI_FORWARD)
velocity.y += 1.0f; velocity.y += 1.0f;
@ -79,84 +81,51 @@ void App::Frame()
if (input_ & game::PI_CROUCH) if (input_ & game::PI_CROUCH)
velocity.z -= 1.0f; velocity.z -= 1.0f;
glm::vec3 forward = glm::vec3(
//velocity.z = -1.0f; cos(angles_.x) * cos(angles_.y),
sin(angles_.x) * cos(angles_.y),
sin(angles_.y)
glm::vec3 cameraDir = glm::vec3(1.0f, 0.0f, 0.0f);
auto tris = world_.GetSector(0).GetMesh()->GetCollisionMesh()->GetTriangles();
glm::vec3 c0 = position;
c0.z -= 0.1f;
glm::vec3 c1 = position;
c1.z += 0.1f;
bool found = false;
glm::vec3 u = velocity * delta_time;
if (glm::length(velocity) > 0.001f)
{
//velocity = glm::normalize(velocity);
//collision::ResolveCollisions(
// position,
// velocity,
// 0.5f,
// 0.2f,
// delta_time,
// tris
//);
Sweep sweep;
static std::vector<collision::Triangle> 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) velocity.z -= 0.1f * delta_time; // Apply gravity
{
position += velocity * delta_time;
}
printf("position: %.2f, %.2f, %.2f\n", position.x, position.y, position.z); glm::vec3 forward_xy = glm::normalize(glm::vec3(forward.x, forward.y, 0.0f));
printf("found: %s\n", found ? "true" : "false"); 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));
renderer_.DrawWorld(world_, 0, position, cameraDir, glm::vec3(0.0f, 0.0f, 1.0f), aspect, 60.0f); //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);
// //if (!hit)
// //{
// // position += u;
// //}
//}
player_->SetVelocity(velocity);
player_->Update(delta_time);
const auto& position = player_->GetOccurrence().GetPosition();
renderer_.DrawWorld(world_, 0, position, forward, glm::vec3(0.0f, 0.0f, 1.0f), aspect, 60.0f);
} }
void App::MouseMove(const glm::vec2& delta) 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() App::~App()

View File

@ -17,6 +17,8 @@ class App
game::World world_; game::World world_;
game::Entity* player_ = nullptr; game::Entity* player_ = nullptr;
glm::vec2 angles_ = { 0.0f, 0.0f }; // Pitch and yaw angles in radians
gfx::Renderer renderer_; gfx::Renderer renderer_;
public: public:

View File

@ -68,10 +68,12 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
{ {
MeshTriangle& tri = tris.emplace_back(); MeshTriangle& tri = tris.emplace_back();
glm::vec3 tri_verts[3]; 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; uint32_t vert_index;
iss >> 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); throw std::runtime_error("Vertex index out of bounds in mesh file: " + filename);
} }
tri.vert[i] = vert_index; tri.vert[i] = vert_index;
@ -114,6 +116,11 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
// Create the mesh object // Create the mesh object
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>(verts, tris, materials); std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>(verts, tris, materials);
if (load_collision)
{
collision_mesh->Build();
mesh->SetCollisionMesh(std::move(collision_mesh)); mesh->SetCollisionMesh(std::move(collision_mesh));
}
return mesh; return mesh;
} }

View File

@ -1,13 +0,0 @@
#pragma once
#include <glm/glm.hpp>
namespace collision
{
struct Capsule
{
glm::vec3 p0;
glm::vec3 p1;
float radius;
};
}

View File

@ -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<const collision::Triangle> 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;
}
}

View File

@ -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<const collision::Triangle> triangles);
}

View File

@ -1,14 +1,20 @@
#include "trianglemesh.hpp" #include "trianglemesh.hpp"
collision::TriangleMesh::TriangleMesh()
{
}
void collision::TriangleMesh::AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) void collision::TriangleMesh::AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2)
{ {
Triangle& tri = tris_.emplace_back(); btVector3 bt_v0(v0.x, v0.y, v0.z);
tri.verts[0] = v0; btVector3 bt_v1(v1.x, v1.y, v1.z);
tri.verts[1] = v1; btVector3 bt_v2(v2.x, v2.y, v2.z);
tri.verts[2] = v2; bt_mesh_.addTriangle(bt_v0, bt_v1, bt_v2, true);
}
for (size_t i = 0; i < 3; ++i)
{ void collision::TriangleMesh::Build()
tri.aabb.AddPoint(tri.verts[i]); {
} bt_shape_ = std::make_unique<btBvhTriangleMeshShape>(&bt_mesh_, true, true);
} }

View File

@ -5,25 +5,23 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "aabb.hpp" #include "aabb.hpp"
#include <memory>
#include <btBulletCollisionCommon.h>
namespace collision namespace collision
{ {
struct Triangle
{
glm::vec3 verts[3];
AABB3 aabb;
};
class TriangleMesh class TriangleMesh
{ {
std::vector<Triangle> tris_; btTriangleMesh bt_mesh_;
std::unique_ptr<btBvhTriangleMeshShape> bt_shape_;
public: public:
TriangleMesh() = default; TriangleMesh();
void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2); void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2);
void Build();
std::span<const Triangle> GetTriangles() const { btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); }
return std::span(tris_);
}
}; };
} }

View File

@ -1,6 +1,153 @@
#include "entity.hpp" #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
);
}

View File

@ -6,32 +6,81 @@
namespace game namespace game
{ {
class World; 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 class EntityOccurrence
{ {
const Sector* sector_; Entity* entity_;
Sector* sector_;
float radius_;
glm::vec3 position_; glm::vec3 position_;
glm::vec3 up_;
glm::mat3 trans_; float scale_;
glm::mat3 inv_trans_; 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 class Entity
{ {
World* world_; World* world_;
CapsuleShape capsule_;
std::unique_ptr<EntityOccurrence> occu_; std::unique_ptr<EntityOccurrence> occu_;
std::unique_ptr<EntityOccurrence> other_occu_; std::unique_ptr<EntityOccurrence> other_occu_;
const Portal* touching_portal_;
glm::vec3 velocity_; glm::vec3 velocity_;
public: 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<EntityOccurrence> CreateOccurrence(const CreateOccurrenceParams& params)
{
return std::make_unique<EntityOccurrence>(this, params);
}
private:
void CreateOtherOccurence(const Portal& portal);
}; };

View File

@ -9,7 +9,10 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
world_(world), world_(world),
idx_(idx), idx_(idx),
def_(std::move(def)), def_(std::move(def)),
mesh_(def_->GetMesh()) 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(); auto def_portals = def_->GetPortals();
size_t num_portals = def_portals.size(); size_t num_portals = def_portals.size();
@ -21,7 +24,6 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
Portal& portal = portals_.emplace_back(); Portal& portal = portals_.emplace_back();
portal.sector = this; portal.sector = this;
portal.def_sector = &def_portal;
portal.def = &assets::PortalDef::portal_defs[def_portal.def_name]; portal.def = &assets::PortalDef::portal_defs[def_portal.def_name];
glm::mat4 rotation(glm::quat(glm::radians(def_portal.angles))); glm::mat4 rotation(glm::quat(glm::radians(def_portal.angles)));
@ -29,6 +31,7 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(def_portal.scale)); glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(def_portal.scale));
portal.trans = translation * rotation * scale; portal.trans = translation * rotation * scale;
portal.scale = def_portal.scale;
portal.plane = glm::transpose(glm::inverse(portal.trans)) * glm::vec4(0.0f, -1.0f, 0.0f, 0.0f); portal.plane = glm::transpose(glm::inverse(portal.trans)) * glm::vec4(0.0f, -1.0f, 0.0f, 0.0f);
@ -41,6 +44,9 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
portal_map_[def_portal.name] = i; portal_map_[def_portal.name] = i;
} }
bt_mesh_col_obj_.setCollisionShape(mesh_->GetCollisionMesh()->GetShape());
bt_world_.addCollisionObject(&bt_mesh_col_obj_);
} }
void game::Sector::ComputePortalVertex(Portal& portal, size_t idx, const glm::vec2& base_vert) 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; 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) void game::Sector::LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2)
{ {
Portal& p1 = s1.portals_[idx1]; 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"); 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) if (&p1 != &p2)
{ {
p2.link = &p1; LinkPortal(p2, p1);
p2.link_trans = glm::inverse(p1_to_p2);
} }
} }
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<float>::max();
virtual btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace) override
{
void* ptr = convexResult.m_hitCollisionObject->getUserPointer();
CollisionObjectData* data = static_cast<CollisionObjectData*>(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<const btVector3*>(&start);
//const btVector3* bt_end = reinterpret_cast<const btVector3*>(&end);
//const btMatrix3x3* bt_basis = reinterpret_cast<const btMatrix3x3*>(&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;
}

View File

@ -3,28 +3,47 @@
#include "assets/sectordef.hpp" #include "assets/sectordef.hpp"
#include <vector> #include <vector>
#include <map> #include <map>
#include <btBulletCollisionCommon.h>
namespace game namespace game
{ {
class Sector; 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 struct Portal
{ {
Sector* sector; Sector* sector;
const assets::SectorPortalDef* def_sector;
const assets::PortalDef* def; const assets::PortalDef* def;
glm::mat4 trans; glm::mat4 trans;
float scale;
glm::vec4 plane; glm::vec4 plane;
glm::vec3 verts[4]; // Portal vertices in sector space glm::vec3 verts[4]; // Portal vertices in sector space
Portal* link; 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 class Sector
{ {
@ -37,6 +56,13 @@ namespace game
std::vector<Portal> portals_; std::vector<Portal> portals_;
std::map<std::string, size_t> portal_map_; // Maps portal name to index in portals_ std::map<std::string, size_t> 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: public:
Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef> def); Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef> def);
@ -52,6 +78,14 @@ namespace game
static void LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2); 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);
}; };

View File

@ -48,7 +48,7 @@ void gfx::Renderer::DrawWorld(
proj_ = glm::perspective( proj_ = glm::perspective(
glm::radians(fov), // FOV glm::radians(fov), // FOV
aspect, // Aspect ratio aspect, // Aspect ratio
0.1f, // Near plane 0.01f, // Near plane
1000.0f // Far 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.screen_aabb = params.screen_aabb.Intersection(portal_aabb);
new_params.recursion = params.recursion + 1; 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.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 // Draw the sector through the portal
DrawSector(new_params); DrawSector(new_params);

View File

@ -125,11 +125,22 @@ static void PollEvents()
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) while (SDL_PollEvent(&event))
{ {
if (event.type == SDL_QUIT) switch (event.type)
{ {
case SDL_QUIT:
s_quit = true; s_quit = true;
return; 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<float>(xrel), static_cast<float>(yrel)));
} }
break;
}
} }
} }
@ -186,6 +197,8 @@ static void Main() {
throw std::runtime_error("Failed to initialize OpenGL"); throw std::runtime_error("Failed to initialize OpenGL");
} }
SDL_SetRelativeMouseMode(SDL_TRUE);
s_app = std::make_unique<App>(); s_app = std::make_unique<App>();
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN