PortalGame/src/gfx/renderer.cpp
2025-08-09 18:01:53 +02:00

263 lines
7.5 KiB
C++

#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
}