Stuff 9.8.2025

This commit is contained in:
tovjemam 2025-08-09 18:01:53 +02:00
parent 21fa4c6d6f
commit d10ffb17f0
36 changed files with 12609 additions and 98 deletions

View File

@ -12,9 +12,25 @@ add_executable(PortalGame
"src/app.cpp"
"src/gl.hpp"
"src/utils.hpp"
"src/assets/sectordef.hpp"
"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/sector.hpp"
"src/game/sector.cpp"
"src/game/world.hpp"
"src/game/world.cpp"
"src/gfx/buffer_object.cpp"
"src/gfx/buffer_object.hpp"
"src/gfx/renderer.hpp"
"src/gfx/renderer.cpp"
"src/gfx/shader.hpp"
"src/gfx/shader.cpp"
"src/gfx/shader_sources.hpp"
"src/gfx/shader_sources.cpp"
"src/gfx/texture.cpp"
"src/gfx/texture.hpp"
"src/gfx/vertex_array.cpp"
@ -35,14 +51,16 @@ if (CMAKE_SYSTEM_NAME STREQUAL Emscripten)
target_compile_options(PortalGame PRIVATE
"-sUSE_SDL=2"
"-sNO_DISABLE_EXCEPTION_CATCHING=1"
)
target_link_options(PortalGame PRIVATE
"-sUSE_SDL=2"
"-sASYNCIFY"
"-sUSE_WEBGL2=1"
"-sNO_DISABLE_EXCEPTION_CATCHING=1"
"--shell-file" "${CMAKE_SOURCE_DIR}/shell.html"
"--preload-file" "${CMAKE_SOURCE_DIR}/assets"
"--preload-file" "${CMAKE_SOURCE_DIR}/assets/@/"
)
else()
@ -63,5 +81,5 @@ else()
endif()
add_subdirectory(external/glm)
# target_include_directories(PortalGame PRIVATE "external/glm")
target_link_libraries(PortalGame PRIVATE glm)
target_include_directories(PortalGame PRIVATE "external/stb")

View File

@ -27,7 +27,7 @@
"description": "Build with Emscripten toolchain using Multi-Config generator",
"generator": "Ninja Multi-Config",
"binaryDir": "${sourceDir}/build/wasm",
"toolchainFile": "c:/dev/emsdk/cmake/Modules/Platform/Emscripten.cmake",
"toolchainFile": "c:/dev/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}

View File

@ -1 +0,0 @@
Test

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7988
external/stb/stb_image.h vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,51 @@
#include "app.hpp"
#include <glm/glm.hpp>
#include "gl.hpp"
#include <iostream>
App::App()
{
std::cout << "Initializing App..." << std::endl;
std::cout << "Loading shader..." << std::endl;
std::cout << "Loading sector defs..." << std::endl;
// Door frame portal def
assets::PortalDef& portal100x200 = assets::PortalDef::portal_defs["100x200"];
portal100x200.size = glm::vec2(1.0f, 2.0f); // 1m x 2m portal
portal100x200.half_frame_thickness = 0.1f; // 10cm offset towards the second sector
auto room001 = assets::SectorDef::LoadFromFile("data/room_001");
size_t s1 = world_.AddSector(room001);
size_t s2 = world_.AddSector(room001);
world_.LinkPortals(s1, "NDoor", s2, "WDoor");
world_.LinkPortals(s2, "NDoor", s2, "SDoor");
}
void App::Frame()
{
glm::vec3 clear_color(glm::abs(glm::sin(m_time)), 0.2f, 0.3f);
glClearColor(clear_color.r, clear_color.g, clear_color.b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
float aspect = static_cast<float>(m_viewport_size.x) / static_cast<float>(m_viewport_size.y);
renderer_.Begin(m_viewport_size.x, m_viewport_size.y);
float cameraDistance = 2.0f;
float angle = m_time * 0.1f; // Rotate over time
glm::vec3 cameraPos = glm::vec3(
cameraDistance * cos(angle),
cameraDistance * sin(angle),
cameraDistance * 0.13f
);
glm::vec3 center(0.0f, 0.0f, 1.5f);
glm::vec3 up(0.0f, 0.0f, 1.0f);
renderer_.DrawWorld(world_, 1, center + cameraPos, -cameraPos, up, aspect, 60.0f);
}
App::~App()
{
}

View File

@ -1,8 +1,18 @@
#pragma once
#include "assets/sectordef.hpp"
#include "game/world.hpp"
#include "gfx/renderer.hpp"
class App
{
float m_time = 0.0f;
glm::ivec2 m_viewport_size = { 800, 600 };
game::World world_;
gfx::Renderer renderer_;
public:
App();
@ -10,6 +20,8 @@ public:
void Frame();
void SetTime(float time) { m_time = time; }
void SetViewportSize(int width, int height) { m_viewport_size = { width, height }; }
~App();
};

View File

@ -5,8 +5,21 @@
#include <vector>
#include <map>
std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& filename)
assets::Mesh::Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials) :
va_(gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV | gfx::VA_LIGHTMAP_UV, gfx::VF_CREATE_EBO)
{
va_.SetVBOData(verts.data(), verts.size_bytes());
va_.SetIndices(reinterpret_cast<const GLuint*>(tris.data()), tris.size() * 3);
materials_.assign(materials.begin(), materials.end());
}
std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& filename, bool load_collision)
{
std::shared_ptr<collision::TriangleMesh> collision_mesh;
if (load_collision)
collision_mesh = std::make_shared<collision::TriangleMesh>();
std::ifstream file(filename, std::ios::binary);
if (!file.is_open())
@ -16,7 +29,7 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
std::vector<MeshVertex> verts;
std::vector<MeshTriangle> tris;
std::vector<MeshMaterialSlot> materials;
std::vector<MeshMaterial> materials;
std::string line;
@ -42,11 +55,11 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
{
if (materials.size() > 0)
{
MeshMaterialSlot& last_material = materials.back();
MeshMaterial& last_material = materials.back();
last_material.num_tris = tris.size() - last_material.first_tri;
}
MeshMaterialSlot& material = materials.emplace_back();
MeshMaterial& material = materials.emplace_back();
iss >> material.name;
material.first_tri = tris.size();
@ -54,20 +67,53 @@ std::shared_ptr<assets::Mesh> assets::Mesh::LoadFromFile(const std::string& file
else if (command == "f") // Face
{
MeshTriangle& tri = tris.emplace_back();
iss >> tri.vert[0] >> tri.vert[1] >> tri.vert[2];
glm::vec3 tri_verts[3];
for (size_t i = 0; i < 3; ++i) {
uint32_t vert_index;
iss >> vert_index;
if (vert_index >= verts.size()) {
throw std::runtime_error("Vertex index out of bounds in mesh file: " + filename);
}
tri.vert[i] = vert_index;
tri_verts[i] = verts[vert_index].pos;
}
if (load_collision)
{
// Add triangle to collision mesh
collision_mesh->AddTriangle(tri_verts[0], tri_verts[1], tri_verts[2]);
}
}
}
if (materials.size() > 0)
{
MeshMaterialSlot& last_material = materials.back();
MeshMaterial& last_material = materials.back();
last_material.num_tris = tris.size() - last_material.first_tri;
}
file.close();
if (verts.empty() || tris.empty())
{
throw std::runtime_error("Mesh file is empty or malformed: " + filename);
}
return std::make_shared<Mesh>(verts, tris, materials);
// Load textures for materials
for (MeshMaterial& material : materials)
{
if (!material.name.empty())
{
try {
material.texture = gfx::Texture::LoadFromFile("data/" + material.name + ".png");
} catch (const std::exception& e) {
throw std::runtime_error("Failed to load texture for material '" + material.name + "': " + e.what());
}
}
}
// Create the mesh object
std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>(verts, tris, materials);
mesh->SetCollisionMesh(std::move(collision_mesh));
return mesh;
}

View File

@ -7,6 +7,7 @@
#include "gfx/vertex_array.hpp"
#include "gfx/texture.hpp"
#include "collision/trianglemesh.hpp"
namespace assets
{
@ -23,7 +24,7 @@ namespace assets
uint32_t vert[3];
};
struct MeshMaterialSlot
struct MeshMaterial
{
std::string name;
std::shared_ptr<gfx::Texture> texture;
@ -34,11 +35,23 @@ namespace assets
class Mesh
{
gfx::VertexArray va_;
std::vector<MeshMaterialSlot> materials_;
std::vector<MeshMaterial> materials_;
std::shared_ptr<collision::TriangleMesh> collision_mesh_;
public:
Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterialSlot> materials);
Mesh(std::span<MeshVertex> verts, std::span<MeshTriangle> tris, std::span<MeshMaterial> materials);
static std::shared_ptr<Mesh> LoadFromFile(const std::string& filename);
void SetCollisionMesh(std::shared_ptr<collision::TriangleMesh> mesh) {
collision_mesh_ = std::move(mesh);
}
const std::shared_ptr<collision::TriangleMesh>& GetCollisionMesh() const {
return collision_mesh_;
}
const gfx::VertexArray& GetVA() { return va_; }
const std::span<MeshMaterial> GetMaterials() { return materials_; }
static std::shared_ptr<Mesh> LoadFromFile(const std::string& filename, bool load_collision);
};
}

View File

@ -1,8 +0,0 @@
#pragma once
class RoomDef
{
};

65
src/assets/sectordef.cpp Normal file
View File

@ -0,0 +1,65 @@
#include "sectordef.hpp"
#include <stdexcept>
#include <fstream>
#include <sstream>
std::map<std::string, assets::PortalDef> assets::PortalDef::portal_defs;
assets::SectorDef::SectorDef(std::shared_ptr<Mesh> mesh, std::span<SectorPortalDef> portals)
{
mesh_ = std::move(mesh);
portals_.assign(portals.begin(), portals.end());
}
std::shared_ptr<assets::SectorDef> assets::SectorDef::LoadFromFile(const std::string& base_name)
{
std::string mesh_file = base_name + ".mesh";
auto mesh = Mesh::LoadFromFile(mesh_file, true);
if (!mesh)
{
throw std::runtime_error("Failed to load mesh from file: " + mesh_file);
}
std::string info_file = base_name + ".info";
std::vector<SectorPortalDef> portals;
std::ifstream info_stream(info_file);
if (!info_stream.is_open())
{
throw std::runtime_error("Failed to open sector info file: " + info_file);
}
std::string line;
while (std::getline(info_stream, line))
{
if (line.empty() || line[0] == '#') // Skip empty lines and comments
continue;
std::istringstream iss(line);
std::string command;
iss >> command;
if (command == "portal")
{
SectorPortalDef portal;
iss >> portal.def_name >> portal.name;
iss >> portal.origin.x >> portal.origin.y >> portal.origin.z;
iss >> portal.angles.x >> portal.angles.y >> portal.angles.z;
iss >> portal.scale;
if (iss.fail())
{
throw std::runtime_error("Failed to parse portal definition in file: " + info_file);
}
portals.push_back(std::move(portal));
}
else
{
throw std::runtime_error("Unknown command in sector info file: " + command);
}
}
return std::make_shared<SectorDef>(std::move(mesh), portals);
}

44
src/assets/sectordef.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <string>
#include <memory>
#include <span>
#include <map>
#include "mesh.hpp"
namespace assets
{
struct PortalDef
{
glm::vec2 size = glm::vec2(1.0f, 1.0f); // Size of the portal in meters
float half_frame_thickness = 0.1f; // Offset of the portal towards second sector
// Portal def map
static std::map<std::string, PortalDef> portal_defs;
};
struct SectorPortalDef
{
std::string def_name;
std::string name;
glm::vec3 origin;
glm::vec3 angles; // Euler angles in degrees
float scale = 1.0f;
};
class SectorDef
{
std::shared_ptr<Mesh> mesh_;
std::vector<SectorPortalDef> portals_;
public:
SectorDef(std::shared_ptr<Mesh> mesh, std::span<SectorPortalDef> portals);
const std::shared_ptr<Mesh>& GetMesh() const { return mesh_; }
std::span<const SectorPortalDef> GetPortals() const { return portals_; }
static std::shared_ptr<SectorDef> LoadFromFile(const std::string& base_name);
};
}

55
src/collision/aabb.hpp Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include <glm/glm.hpp>
#include <limits>
namespace collision
{
template <size_t dim>
struct AABB
{
static constexpr float C_INFINITY = std::numeric_limits<float>::infinity();
static constexpr float C_DEFAULT_MARGIN = 0.01f; // Margin for AABBs
using Vec = glm::vec<dim, float, glm::defaultp>;
Vec min = Vec(C_INFINITY);
Vec max = Vec(-C_INFINITY);
AABB() = default;
AABB(const Vec& min, const Vec& max) : min(min), max(max) {}
void AddPoint(const Vec& point, float margin = C_DEFAULT_MARGIN)
{
min = glm::min(min, point - margin);
max = glm::max(max, point + margin);
}
bool CollidesWith(const AABB<dim>& other) const;
AABB<dim> Intersection(const AABB<dim>& other) const
{
Vec new_min = glm::max(min, other.min);
Vec new_max = glm::min(max, other.max);
return AABB<dim>(new_min, new_max);
}
};
template <>
inline bool AABB<2>::CollidesWith(const AABB<2>& other) const
{
return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y);
}
template <>
inline bool AABB<3>::CollidesWith(const AABB<3>& other) const
{
return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y) &&
(min.z <= other.max.z && max.z >= other.min.z);
}
using AABB2 = AABB<2>;
using AABB3 = AABB<3>;
}

13
src/collision/capsule.hpp Normal file
View File

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

View File

@ -0,0 +1,447 @@
#include "capsule_triangle_sweep.hpp"
#define EPSILON 1e-5f // Used to test if float is close to 0. Tweak this if you get problems.
struct Sweep
{
float time; // Non-negative time of first contact.
float depth; // Non-negative penetration depth if objects start initially colliding.
glm::vec3 point; // Point of first-contact. Only updated when contact occurs.
glm::vec3 normal; // Unit-length collision normal. Only updated when contact occurs.
};
// 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][0];
const glm::vec3& p1 = faces[i][1];
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
static 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)
{
// 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 ResolveCollisions(glm::vec3& p, glm::vec3& v, float h, float r, float dt, std::span<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

@ -0,0 +1,9 @@
#pragma once
#include "capsule.hpp"
#include "trianglemesh.hpp"
namespace collision
{
}

View File

@ -0,0 +1,14 @@
#include "trianglemesh.hpp"
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]);
}
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include <span>
#include <glm/glm.hpp>
#include "aabb.hpp"
namespace collision
{
struct Triangle
{
glm::vec3 verts[3];
AABB3 aabb;
};
class TriangleMesh
{
std::vector<Triangle> tris_;
public:
TriangleMesh() = default;
void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2);
std::span<const Triangle> GetTriangles() const {
return std::span(tris_);
}
};
}

11
src/game/entity.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
namespace game
{
class Entity
{
};
}

88
src/game/sector.cpp Normal file
View File

@ -0,0 +1,88 @@
#include "sector.hpp"
#include <stdexcept>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef> def) :
world_(world),
idx_(idx),
def_(std::move(def)),
mesh_(def_->GetMesh())
{
auto def_portals = def_->GetPortals();
size_t num_portals = def_portals.size();
portals_.reserve(num_portals);
for (size_t i = 0; i < num_portals; ++i)
{
const assets::SectorPortalDef& def_portal = def_portals[i];
Portal& portal = portals_.emplace_back();
portal.sector = this;
portal.def_sector = &def_portal;
portal.def = &assets::PortalDef::portal_defs[def_portal.def_name];
glm::mat4 rotation(glm::quat(glm::radians(def_portal.angles)));
glm::mat4 translation = glm::translate(glm::mat4(1.0f), def_portal.origin);
glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(def_portal.scale));
portal.trans = translation * rotation * scale;
portal.plane = glm::transpose(glm::inverse(portal.trans)) * glm::vec4(0.0f, -1.0f, 0.0f, 0.0f);
ComputePortalVertex(portal, 0, glm::vec2(-1.0f, -1.0f)); // Bottom-left
ComputePortalVertex(portal, 1, glm::vec2(-1.0f, 1.0f)); // Top-left
ComputePortalVertex(portal, 3, glm::vec2(1.0f, -1.0f)); // Bottom-right
ComputePortalVertex(portal, 2, glm::vec2(1.0f, 1.0f)); // Top-right
portal.link = nullptr;
portal_map_[def_portal.name] = i;
}
}
void game::Sector::ComputePortalVertex(Portal& portal, size_t idx, const glm::vec2& base_vert)
{
glm::vec2 vert_xz = base_vert * portal.def->size * 0.5f; // Scale to portal size
glm::vec3 vert(vert_xz.x, 0.0f, vert_xz.y); // Convert to 3D vector
portal.verts[idx] = glm::vec3(portal.trans * glm::vec4(vert, 1.0f));
}
int game::Sector::GetPortalIndex(const std::string& name) const
{
auto it = portal_map_.find(name);
if (it != portal_map_.end())
{
return static_cast<int>(it->second);
}
return -1;
}
void game::Sector::LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2)
{
Portal& p1 = s1.portals_[idx1];
Portal& p2 = s2.portals_[idx2];
if (p1.link || p2.link)
{
throw std::runtime_error("One of the portals is already linked");
}
// 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);
}
}

58
src/game/sector.hpp Normal file
View File

@ -0,0 +1,58 @@
#pragma once
#include "assets/sectordef.hpp"
#include <vector>
#include <map>
namespace game
{
class Sector;
struct Portal
{
Sector* sector;
const assets::SectorPortalDef* def_sector;
const assets::PortalDef* def;
glm::mat4 trans;
glm::vec4 plane;
glm::vec3 verts[4]; // Portal vertices in sector space
Portal* link;
glm::mat4 link_trans;
};
class World;
class Sector
{
World* world_;
size_t idx_;
std::shared_ptr<assets::SectorDef> def_;
std::shared_ptr<assets::Mesh> mesh_;
std::vector<Portal> portals_;
std::map<std::string, size_t> portal_map_; // Maps portal name to index in portals_
public:
Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef> def);
private:
void ComputePortalVertex(Portal& portal, size_t idx, const glm::vec2& base_vert);
public:
const std::shared_ptr<assets::Mesh>& GetMesh() const { return mesh_; }
int GetPortalIndex(const std::string& name) const;
const Portal& GetPortal(size_t idx) const { return portals_[idx]; }
const size_t GetNumPortals() const { return portals_.size(); }
static void LinkPortals(Sector& s1, size_t idx1, Sector& s2, size_t idx2);
};
}

29
src/game/world.cpp Normal file
View File

@ -0,0 +1,29 @@
#include "world.hpp"
#include <stdexcept>
game::World::World()
{
}
size_t game::World::AddSector(std::shared_ptr<assets::SectorDef> def)
{
size_t idx = sectors_.size();
sectors_.emplace_back(std::make_unique<Sector>(this, idx, std::move(def)));
return idx;
}
void game::World::LinkPortals(size_t sector1, const std::string& portal_name1, size_t sector2, const std::string& portal_name2)
{
Sector& s1 = *sectors_[sector1];
Sector& s2 = *sectors_[sector2];
int p1 = s1.GetPortalIndex(portal_name1);
int p2 = s2.GetPortalIndex(portal_name2);
if (p1 < 0 || p2 < 0)
{
throw std::runtime_error("Invalid portal names for linking");
}
Sector::LinkPortals(s1, p1, s2, p2);
}

37
src/game/world.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <vector>
#include <map>
#include <memory>
#include "assets/sectordef.hpp"
#include "sector.hpp"
#include "entity.hpp"
namespace game
{
class World
{
std::vector<std::unique_ptr<Sector>> sectors_;
std::vector<std::unique_ptr<Entity>> entities_;
public:
World();
size_t AddSector(std::shared_ptr<assets::SectorDef> def);
void LinkPortals(size_t sector1, const std::string& portal_name1,
size_t sector2, const std::string& portal_name2);
template<class T, class... TArgs>
T* Spawn(TArgs&&... args)
{
auto& entity = entities_.emplace_back(std::make_unique<T>(this, std::forward<TArgs>(args)...));
return entity.get();
}
Sector& GetSector(size_t idx) { return *sectors_[idx]; }
const Sector& GetSector(size_t idx) const { return *sectors_[idx]; }
};
}

262
src/gfx/renderer.cpp Normal file
View File

@ -0,0 +1,262 @@
#include "renderer.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "gl.hpp"
#include "gfx/shader_sources.hpp"
gfx::Renderer::Renderer()
{
ShaderSources::MakeShader(sector_shader_, SS_SECTOR_MESH_VERT, SS_SECTOR_MESH_FRAG);
ShaderSources::MakeShader(portal_shader_, SS_PORTAL_VERT, SS_PORTAL_FRAG);
proj_ = glm::mat4(1.0f); // Initialize projection matrix to identity
SetupPortalVAO();
}
void gfx::Renderer::SetupPortalVAO()
{
const glm::vec3 portal_verts[4] = {
{ -0.5f, 0.0f, 0.5f }, // Top-left
{ -0.5f, 0.0f, -0.5f }, // Bottom-left
{ 0.5f, 0.0f, -0.5f }, // Bottom-right
{ 0.5f, 0.0f, 0.5f }, // Top-right
};
portal_vao_ = std::make_shared<VertexArray>(VA_POSITION, 0);
portal_vao_->SetVBOData(portal_verts, sizeof(portal_verts));
}
void gfx::Renderer::Begin(size_t width, size_t height)
{
glViewport(0, 0, width, height);
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}
void gfx::Renderer::DrawWorld(
const game::World& world,
size_t sector_idx,
const glm::vec3& eye,
const glm::vec3& dir,
const glm::vec3& up,
float aspect,
float fov)
{
proj_ = glm::perspective(
glm::radians(fov), // FOV
aspect, // Aspect ratio
0.1f, // Near plane
1000.0f // Far plane
);
glm::mat4 view = glm::lookAt(
eye, // Camera position
eye + dir, // Look at point (camera position + look direction)
up // Up vector
);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 0, 0xFF); // Always pass stencil test
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glStencilMask(0xFF);
const game::Sector& sector = world.GetSector(sector_idx);
DrawSectorParams params;
params.sector = &sector;
params.entry_portal = nullptr; // No entry portal for the first sector
params.clip_plane = glm::vec4(0.0f, 0.0f, 1.0f, 1000.0f); // Default clip plane
params.screen_aabb = collision::AABB2(glm::vec2(-1.0f), glm::vec2(1.0f)); // Full screen
params.recursion = 0; // Start with recursion level 0
params.view = view;
params.view_proj = proj_ * view;
params.eye = eye;
DrawSector(params);
//glDisable(GL_STENCIL_TEST);
//glDisable(GL_CULL_FACE);
//glDisable(GL_DEPTH_TEST);
}
void gfx::Renderer::DrawSector(const DrawSectorParams& params)
{
assets::Mesh& mesh = *params.sector->GetMesh();
glm::mat4 model = glm::mat4(1.0f); // Identity matrix for model
// #ifndef PG_GLES
// glEnable(GL_CLIP_DISTANCE0);
// #else
// glEnable(GL_CLIP_DISTANCE0_EXT);
// #endif
glUseProgram(sector_shader_->GetId());
glUniformMatrix4fv(sector_shader_->U(gfx::SU_VIEW_PROJ), 1, GL_FALSE, &params.view_proj[0][0]);
glUniformMatrix4fv(sector_shader_->U(gfx::SU_MODEL), 1, GL_FALSE, &model[0][0]);
glUniform4fv(sector_shader_->U(gfx::SU_CLIP_PLANE), 1, &params.clip_plane[0]);
glBindVertexArray(mesh.GetVA().GetVAOId());
for (auto mesh_materials = mesh.GetMaterials(); const auto& mat : mesh_materials) {
glActiveTexture(GL_TEXTURE0);
if (mat.texture) {
glBindTexture(GL_TEXTURE_2D, mat.texture->GetId());
glUniform1i(sector_shader_->U(gfx::SU_TEX), 0); // Bind texture to sampler
}
else {
glBindTexture(GL_TEXTURE_2D, 0); // No texture
}
glDrawElements(GL_TRIANGLES, mat.num_tris * 3, GL_UNSIGNED_INT, (void*)(mat.first_tri * 3 * sizeof(uint32_t)));
}
glBindVertexArray(0);
for (size_t i = 0; i < params.sector->GetNumPortals(); ++i)
{
const game::Portal& portal = params.sector->GetPortal(i);
DrawPortal(params, portal);
}
}
void gfx::Renderer::DrawPortal(const DrawSectorParams& params, const game::Portal& portal)
{
if (params.recursion > 10)
{
return; // Recursion limit reached
}
if (&portal == params.entry_portal)
{
return; // Skip the portal this sector was rendered through
// This should NEVER happen due to portal plane check, but would cause cyclic traversal if it did.
}
if (!portal.link)
{
return; // Portal not linked, skip
}
// Signed distance of eye to the portal plane
float eye_plane_sd = glm::dot(glm::vec3(portal.plane), params.eye) + portal.plane.w;
if (eye_plane_sd < 0.0f)
{
return; // Eye is behind the portal plane, skip
}
collision::AABB2 portal_aabb;
if (!ComputePortalScreenAABB(portal, params.view_proj, portal_aabb))
{
return; // Portal is fully behind the camera, skip
}
if (!portal_aabb.CollidesWith(params.screen_aabb))
{
return; // Portal AABB does not intersect with the screen AABB, skip
}
// Open portal
glStencilFunc(GL_EQUAL, params.recursion, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
DrawPortalPlane(params, portal, false); // Mark the portal plane in stencil buffer
glStencilFunc(GL_EQUAL, params.recursion + 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glDepthFunc(GL_ALWAYS);
DrawPortalPlane(params, portal, true); // Clear the depth buffer for nested sector
glDepthFunc(GL_LESS);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
const game::Portal& other_portal = *portal.link;
const game::Sector& other_sector = *other_portal.sector;
DrawSectorParams new_params;
new_params.sector = &other_sector;
new_params.entry_portal = &other_portal;
new_params.clip_plane = other_portal.plane; // Use the portal plane as the clip plane
// Compute the new screen AABB based on the portal AABB and the current screen AABB
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_proj = proj_ * new_params.view;
new_params.eye = portal.link_trans * glm::vec4(params.eye, 1.0f);
// Draw the sector through the portal
DrawSector(new_params);
// Close portal
glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glStencilFunc(GL_EQUAL, params.recursion + 1, 0xFF);
DrawPortalPlane(params, portal, false);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}
bool gfx::Renderer::ComputePortalScreenAABB(const game::Portal& portal, const glm::mat4 view_proj, collision::AABB2& aabb)
{
size_t num_behind = 0;
for (size_t i = 0; i < 4; ++i)
{
glm::vec4 vert = glm::vec4(portal.verts[i], 1.0f);
glm::vec4 clip = view_proj * vert;
if (clip.w <= 0.0f)
{
num_behind++;
continue; // Skip vertices behind the camera
}
glm::vec2 ndc = clip;
ndc /= clip.w;
aabb.AddPoint(ndc, 0.0f);
}
if (num_behind == 4)
{
return false; // All vertices are behind the camera, no visible portal
}
if (num_behind > 0)
{
// Some vertices are behind the camera, cannot use the AABB
aabb = collision::AABB2(glm::vec2(-1.0f), glm::vec2(1.0f)); // Full screen AABB
}
return true;
}
void gfx::Renderer::DrawPortalPlane(const DrawSectorParams& params, const game::Portal& portal, bool clear_depth)
{
if (!clear_depth) // first draw
{
glUseProgram(portal_shader_->GetId());
glUniformMatrix4fv(portal_shader_->U(gfx::SU_VIEW_PROJ), 1, GL_FALSE, &params.view_proj[0][0]);
glUniformMatrix4fv(portal_shader_->U(gfx::SU_MODEL), 1, GL_FALSE, &portal.trans[0][0]);
glUniform4fv(portal_shader_->U(gfx::SU_CLIP_PLANE), 1, &params.clip_plane[0]);
glUniform2fv(portal_shader_->U(gfx::SU_PORTAL_SIZE), 1, &portal.def->size[0]);
glUniform1i(portal_shader_->U(gfx::SU_CLEAR_DEPTH), 0);
glBindVertexArray(portal_vao_->GetVAOId());
}
else // assume uniforms are already set from previous draw
{
glUniform1i(portal_shader_->U(gfx::SU_CLEAR_DEPTH), 1);
}
glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // Draw the portal as a quad
}

54
src/gfx/renderer.hpp Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <memory>
#include "gfx/shader.hpp"
#include "game/world.hpp"
#include "game/sector.hpp"
namespace gfx
{
struct DrawSectorParams
{
const game::Sector* sector;
const game::Portal* entry_portal;
glm::vec4 clip_plane;
collision::AABB2 screen_aabb;
size_t recursion;
glm::mat4 view;
glm::mat4 view_proj;
glm::vec3 eye;
};
class Renderer
{
public:
Renderer();
void Begin(size_t width, size_t height);
void DrawWorld(
const game::World& world,
size_t sector_idx,
const glm::vec3& eye,
const glm::vec3& dir,
const glm::vec3& up,
float aspect,
float fov);
private:
std::unique_ptr<Shader> sector_shader_;
std::unique_ptr<Shader> portal_shader_;
std::shared_ptr<VertexArray> portal_vao_;
void SetupPortalVAO();
glm::mat4 proj_;
void DrawSector(const DrawSectorParams& params);
void DrawPortal(const DrawSectorParams& params, const game::Portal& portal);
static bool ComputePortalScreenAABB(const game::Portal& portal, const glm::mat4 view_proj, collision::AABB2& aabb);
void DrawPortalPlane(const DrawSectorParams& params, const game::Portal& portal, bool clear_depth);
};
}

87
src/gfx/shader.cpp Normal file
View File

@ -0,0 +1,87 @@
#include <stdexcept>
#include <string>
#include "shader.hpp"
#define LOG_LENGTH 1024
// Nazvy uniformnich promennych v shaderech
static const char* const s_uni_names[] = {
"u_model", // SU_MODEL
"u_view_proj", // SU_VIEW_PROJ
"u_tex", // SU_TEX
"u_clip_plane", // SU_CLIP_PLANE
"u_portal_size", // SU_PORTAL_SIZE
"u_clear_depth", // SU_CLEAR_DEPTH
};
// Vytvori shader z daneho zdroje
static GLuint CreateShader(const char* src, GLenum type) {
GLuint id = glCreateShader(type);
glShaderSource(id, 1, &src, NULL);
glCompileShader(id);
GLint is_compiled = 0;
glGetShaderiv(id, GL_COMPILE_STATUS, &is_compiled);
if (is_compiled == GL_FALSE) {
GLchar log[LOG_LENGTH];
glGetShaderInfoLog(id, LOG_LENGTH, NULL, log);
glDeleteShader(id);
throw std::runtime_error(std::string("Nelze zkompilovat shader: ") + log);
}
return id;
}
gfx::Shader::Shader(const char* vert_src, const char* frag_src) {
m_id = glCreateProgram();
if (!m_id)
throw std::runtime_error("Nelze vytvorit program!");
GLuint vert, frag;
// Vytvoreni vertex shaderu
vert = CreateShader(vert_src, GL_VERTEX_SHADER);
try {
// Vyvoreni fragment shaderu
frag = CreateShader(frag_src, GL_FRAGMENT_SHADER);
}
catch (std::exception& e) {
glDeleteShader(vert);
throw e;
}
// Pripojeni shaderu k programu
glAttachShader(m_id, vert);
glAttachShader(m_id, frag);
// Linknuti programu
glLinkProgram(m_id);
// Smazani shaderu
glDeleteShader(vert);
glDeleteShader(frag);
GLint is_linked = 0;
glGetProgramiv(m_id, GL_LINK_STATUS, &is_linked);
if (is_linked == GL_FALSE) {
GLchar log[LOG_LENGTH];
glGetProgramInfoLog(m_id, LOG_LENGTH, NULL, log);
glDeleteProgram(m_id);
throw std::runtime_error(std::string("Nelze linknout shader: ") + log);
}
// Ziskani lokaci uniformnich promennych
for (size_t i = 0; i < SU_COUNT; i++)
m_uni[i] = glGetUniformLocation(m_id, s_uni_names[i]);
}
gfx::Shader::~Shader() {
// Smazani programu
glDeleteProgram(m_id);
}

51
src/gfx/shader.hpp Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include "gl.hpp"
#include "utils.hpp"
namespace gfx
{
/**
* \brief Uniformy shaderu
*/
enum ShaderUniform
{
SU_MODEL,
SU_VIEW_PROJ,
SU_TEX,
SU_CLIP_PLANE,
SU_PORTAL_SIZE,
SU_CLEAR_DEPTH,
SU_COUNT
};
/**
* \brief Wrapper pro OpenGL shader program
*/
class Shader : public NonCopyableNonMovable
{
GLuint m_id;
GLint m_uni[SU_COUNT];
public:
Shader(const char* vert_src, const char* frag_src);
~Shader();
/**
* \brief Vrátí lokaci uniformy
*
* \param u Uniforma
* \return Lokace
*/
GLint U(ShaderUniform u) const { return m_uni[u]; }
/**
* \brief Vrátí OpenGL název shaderu
*
* \return Název shaderu
*/
GLuint GetId() const { return m_id; }
};
}

115
src/gfx/shader_sources.cpp Normal file
View File

@ -0,0 +1,115 @@
#include "shader_sources.hpp"
#ifndef EMSCRIPTEN
#define GLSL_VERSION \
"#version 330 core\n" \
"\n"
#else
#define GLSL_VERSION \
"#version 300 es\n" \
"precision mediump float;\n" \
"\n"
#endif
// Zdrojove kody shaderu
static const char* const s_srcs[] = {
// SS_SECTOR_MESH_VERT
GLSL_VERSION
R"GLSL(
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec3 a_normal;
layout (location = 3) in vec2 a_uv;
uniform mat4 u_model;
uniform mat4 u_view_proj;
uniform vec4 u_clip_plane;
out vec2 v_uv;
out float v_clip_distance;
void main() {
vec4 sector_pos = u_model * vec4(a_pos, 1.0);
gl_Position = u_view_proj * sector_pos;
// Clip against the plane
v_clip_distance = dot(sector_pos, u_clip_plane);
//v_normal = mat3(u_model) * a_normal;
v_uv = vec2(a_uv.x, 1.0 - a_uv.y);
}
)GLSL",
// SS_SECTOR_MESH_FRAG
GLSL_VERSION
R"GLSL(
in vec2 v_uv;
in float v_clip_distance;
uniform sampler2D u_tex;
layout (location = 0) out vec4 o_color;
void main() {
if (v_clip_distance < 0.0) {
discard; // Discard fragment if it is outside the clip plane
}
o_color = vec4(texture(u_tex, v_uv));
//o_color = vec4(1.0, 0.0, 0.0, 1.0);
}
)GLSL",
// SS_PORTAL_VERT
GLSL_VERSION
R"GLSL(
layout (location = 0) in vec3 a_pos;
uniform mat4 u_model; // Portal transform
uniform mat4 u_view_proj;
uniform vec4 u_clip_plane;
uniform vec2 u_portal_size; // Size of the portal in meters
out float v_clip_distance;
void main() {
vec4 pos = vec4(a_pos.x * u_portal_size.x, 0.0, a_pos.z * u_portal_size.y, 1.0);
vec4 sector_pos = u_model * pos;
gl_Position = u_view_proj * sector_pos;
// Clip against the plane
v_clip_distance = dot(sector_pos, u_clip_plane);
}
)GLSL",
// SS_PORTAL_FRAG
GLSL_VERSION
R"GLSL(
in float v_clip_distance;
uniform bool u_clear_depth;
void main() {
if (v_clip_distance < 0.0) {
discard; // Discard fragment if it is outside the clip plane
}
if (u_clear_depth) {
gl_FragDepth = 1.0;
}
else
{
gl_FragDepth = gl_FragCoord.z; // Use the default depth value
}
}
)GLSL",
};
// Vrati zdrojovy kod shaderu
const char* gfx::ShaderSources::Get(ShaderSource ss) {
return s_srcs[ss];
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <memory>
#include "shader.hpp"
namespace gfx
{
enum ShaderSource
{
SS_SECTOR_MESH_VERT,
SS_SECTOR_MESH_FRAG,
SS_PORTAL_VERT,
SS_PORTAL_FRAG,
};
class ShaderSources
{
public:
// Vrati zdrojovy kod shaderu
static const char* Get(ShaderSource ss);
static void MakeShader(std::unique_ptr<Shader>& shader, ShaderSource ss_vert, ShaderSource ss_frag) {
shader = std::make_unique<Shader>(
Get(ss_vert),
Get(ss_frag)
);
}
};
}

View File

@ -1,7 +1,10 @@
#include "texture.hpp"
#include <stdexcept>
gfx::Texture::Texture(GLuint width, GLuint height, GLint internalformat, GLenum format, GLenum type, GLenum filter) {
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
gfx::Texture::Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, GLenum filter) {
glGenTextures(1, &m_id);
if (!m_id)
@ -9,7 +12,7 @@ gfx::Texture::Texture(GLuint width, GLuint height, GLint internalformat, GLenum
glBindTexture(GL_TEXTURE_2D, m_id);
glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, type, NULL);
glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, type, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
@ -22,3 +25,24 @@ gfx::Texture::Texture(GLuint width, GLuint height, GLint internalformat, GLenum
gfx::Texture::~Texture() {
glDeleteTextures(1, &m_id);
}
std::shared_ptr<gfx::Texture> gfx::Texture::LoadFromFile(const std::string& filename)
{
int width, height, channels;
unsigned char* data = stbi_load(filename.c_str(), &width, &height, &channels, 4);
if (!data) {
throw std::runtime_error("Failed to load texture from file: " + filename);
}
std::shared_ptr<Texture> texture;
try {
texture = std::make_shared<Texture>(width, height, data, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR);
} catch (const std::exception& e) {
stbi_image_free(data);
throw; // Rethrow the exception after freeing the data
}
stbi_image_free(data);
return texture;
}

View File

@ -1,4 +1,6 @@
#pragma once
#include <memory>
#include <string>
#include "utils.hpp"
#include "gl.hpp"
@ -12,11 +14,13 @@ class Texture : public NonCopyableNonMovable
GLuint m_id;
public:
Texture(GLuint width, GLuint height, GLint internalformat, GLenum format, GLenum type, GLenum filter = GL_NEAREST);
Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, GLenum filter = GL_NEAREST);
~Texture();
GLuint GetId() const { return m_id; }
static std::shared_ptr<Texture> LoadFromFile(const std::string& filename);
};
}

View File

@ -14,10 +14,12 @@ static const VertexAttribInfo s_ATTR_INFO[] = {
{ 0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_POSITION
{ 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL
{ 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR
{ 3, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_UV
{ 4, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 } // VA_LIGHTMAP_UV
};
// Pocet typu vertex atributu
static const size_t s_ATTR_COUNT = 3;
static const size_t s_ATTR_COUNT = 5;
gfx::VertexArray::VertexArray(int attrs, int flags) : m_usage(GL_STATIC_DRAW), m_num_indices(0) {
glGenVertexArrays(1, &m_vao);

View File

@ -10,49 +10,51 @@
namespace gfx
{
// Vycet jednotlivych atributu vrcholu
enum VertexAttr
{
VA_POSITION = 1 << 0,
VA_NORMAL = 1 << 1,
VA_COLOR = 1 << 2,
};
// Vycet jednotlivych atributu vrcholu
enum VertexAttr
{
VA_POSITION = 1 << 0,
VA_NORMAL = 1 << 1,
VA_COLOR = 1 << 2,
VA_UV = 1 << 3,
VA_LIGHTMAP_UV = 1 << 4,
};
// Informace pro vytvoreni VertexArraye
enum VertexArrayFlags
{
VF_CREATE_EBO = 1 << 0,
VF_DYNAMIC = 1 << 1,
};
// Informace pro vytvoreni VertexArraye
enum VertexArrayFlags
{
VF_CREATE_EBO = 1 << 0,
VF_DYNAMIC = 1 << 1,
};
// VertexArray - trida pro praci s modely
class VertexArray : public NonCopyableNonMovable
{
GLuint m_vao;// , m_vbo, m_ebo;
std::unique_ptr<BufferObject> m_vbo;
std::unique_ptr<BufferObject> m_ebo;
size_t m_num_indices;
GLenum m_usage;
// VertexArray - trida pro praci s modely
class VertexArray : public NonCopyableNonMovable
{
GLuint m_vao;// , m_vbo, m_ebo;
std::unique_ptr<BufferObject> m_vbo;
std::unique_ptr<BufferObject> m_ebo;
size_t m_num_indices;
GLenum m_usage;
public:
VertexArray(int attrs, int flags);
~VertexArray();
public:
VertexArray(int attrs, int flags);
~VertexArray();
// Nastavi data do VBO
void SetVBOData(const void* data, size_t size);
// Nastavi data do VBO
void SetVBOData(const void* data, size_t size);
// Nastavi indexy do EBO
void SetIndices(const GLuint* data, size_t size);
// Nastavi indexy do EBO
void SetIndices(const GLuint* data, size_t size);
GLuint GetVAOId() const { return m_vao; }
GLuint GetVBOId() const { return m_vbo->GetId(); }
GLuint GetEBOId() const {
assert(m_ebo && "GetEBOId() ale !ebo");
return m_ebo->GetId();
}
GLuint GetVAOId() const { return m_vao; }
GLuint GetVBOId() const { return m_vbo->GetId(); }
GLuint GetEBOId() const {
assert(m_ebo && "GetEBOId() ale !ebo");
return m_ebo->GetId();
}
//size_t GetVBOSize() const { return m_vbo->; }
size_t GetNumIndices() const { return m_num_indices; }
};
//size_t GetVBOSize() const { return m_vbo->; }
size_t GetNumIndices() const { return m_num_indices; }
};
}

View File

@ -1,5 +1,8 @@
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#define PG_GLES
#include <GLES3/gl3.h>
#include <GLES3/gl2ext.h>
#else
#include <glad/glad.h>
#endif

View File

@ -5,6 +5,7 @@
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/html5_webgl.h>
#endif
#include "gl.hpp"
@ -16,12 +17,28 @@ static std::unique_ptr<App> s_app;
static bool InitSDL()
{
std::cout << "Initializing SDL..." << std::endl;
if (SDL_Init(SDL_INIT_VIDEO) != 0)
{
std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
return false;
}
#ifdef EMSCRIPTEN
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
std::cout << "Creating SDL window..." << std::endl;
s_window = SDL_CreateWindow("PortalGame", 100, 100, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!s_window)
{
@ -33,18 +50,23 @@ static bool InitSDL()
return true;
}
#ifndef EMSCRIPTEN
static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) {
//if (severity == 0x826b)
// return;
//
////std::cout << message << std::endl;
fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
(type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
type, severity, message);
}
#endif // EMSCRIPTEN
static bool InitGL()
{
#ifdef EMSCRIPTEN
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif
std::cout << "Creating OpenGL context..." << std::endl;
s_context = SDL_GL_CreateContext(s_window);
if (!s_context)
{
@ -62,14 +84,20 @@ static bool InitGL()
#ifndef EMSCRIPTEN
// Initialize GLAD
std::cout << "Initializing GLAD..." << std::endl;
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{
std::cerr << "Failed to initialize GLAD" << std::endl;
SDL_GL_DeleteContext(s_context);
return false;
}
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(GLDebugCallback, 0);
SDL_GL_SetSwapInterval(0);
#endif
return true;
}
@ -109,22 +137,27 @@ static void Frame()
{
Uint32 current_time = SDL_GetTicks();
s_app->SetTime(current_time / 1000.0f); // Set time in seconds
PollEvents();
int width, height;
SDL_GetWindowSize(s_window, &width, &height);
s_app->SetViewportSize(width, height);
s_app->Frame();
SDL_GL_SwapWindow(s_window);
}
int main(int argc, char *argv[])
{
static void Main() {
if (!InitSDL())
{
return 1;
throw std::runtime_error("Failed to initialize SDL");
}
if (!InitGL())
{
ShutdownSDL();
return 1;
throw std::runtime_error("Failed to initialize OpenGL");
}
s_app = std::make_unique<App>();
@ -142,5 +175,17 @@ int main(int argc, char *argv[])
ShutdownGL();
ShutdownSDL();
#endif
return 0;
}
int main(int argc, char *argv[])
{
try {
Main();
}
catch (const std::exception& e) {
std::cerr << "[ERROR] " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}