From f2ccdff4991b3bc2a7d2e80ffca756205da1360d Mon Sep 17 00:00:00 2001 From: John McCardle Date: Wed, 4 Feb 2026 23:45:43 -0500 Subject: [PATCH] Frustum culling --- src/3d/Math3D.h | 110 ++++++++++++++++++++++++++++++++++++++++++ src/3d/Viewport3D.cpp | 34 +++++++++++++ 2 files changed, 144 insertions(+) diff --git a/src/3d/Math3D.h b/src/3d/Math3D.h index c337343..5f6d9f8 100644 --- a/src/3d/Math3D.h +++ b/src/3d/Math3D.h @@ -610,6 +610,116 @@ struct quat { } }; +// ============================================================================= +// Frustum - View frustum for culling +// ============================================================================= + +struct Plane { + vec3 normal; + float distance; + + Plane() : normal(0, 1, 0), distance(0) {} + Plane(const vec3& n, float d) : normal(n), distance(d) {} + + // Distance from plane to point (positive = in front, negative = behind) + float distanceToPoint(const vec3& point) const { + return normal.dot(point) + distance; + } +}; + +struct Frustum { + // Six planes: left, right, bottom, top, near, far + Plane planes[6]; + + // Extract frustum planes from view-projection matrix + // Uses Gribb/Hartmann method + void extractFromMatrix(const mat4& viewProj) { + const float* m = viewProj.m; + + // Left plane + planes[0].normal.x = m[3] + m[0]; + planes[0].normal.y = m[7] + m[4]; + planes[0].normal.z = m[11] + m[8]; + planes[0].distance = m[15] + m[12]; + + // Right plane + planes[1].normal.x = m[3] - m[0]; + planes[1].normal.y = m[7] - m[4]; + planes[1].normal.z = m[11] - m[8]; + planes[1].distance = m[15] - m[12]; + + // Bottom plane + planes[2].normal.x = m[3] + m[1]; + planes[2].normal.y = m[7] + m[5]; + planes[2].normal.z = m[11] + m[9]; + planes[2].distance = m[15] + m[13]; + + // Top plane + planes[3].normal.x = m[3] - m[1]; + planes[3].normal.y = m[7] - m[5]; + planes[3].normal.z = m[11] - m[9]; + planes[3].distance = m[15] - m[13]; + + // Near plane + planes[4].normal.x = m[3] + m[2]; + planes[4].normal.y = m[7] + m[6]; + planes[4].normal.z = m[11] + m[10]; + planes[4].distance = m[15] + m[14]; + + // Far plane + planes[5].normal.x = m[3] - m[2]; + planes[5].normal.y = m[7] - m[6]; + planes[5].normal.z = m[11] - m[10]; + planes[5].distance = m[15] - m[14]; + + // Normalize all planes + for (int i = 0; i < 6; i++) { + float len = planes[i].normal.length(); + if (len > 0.0001f) { + planes[i].normal = planes[i].normal / len; + planes[i].distance /= len; + } + } + } + + // Test if a point is inside the frustum + bool containsPoint(const vec3& point) const { + for (int i = 0; i < 6; i++) { + if (planes[i].distanceToPoint(point) < 0) { + return false; + } + } + return true; + } + + // Test if a sphere intersects or is inside the frustum + bool containsSphere(const vec3& center, float radius) const { + for (int i = 0; i < 6; i++) { + if (planes[i].distanceToPoint(center) < -radius) { + return false; // Sphere is completely behind this plane + } + } + return true; + } + + // Test if an axis-aligned bounding box intersects the frustum + bool containsAABB(const vec3& min, const vec3& max) const { + for (int i = 0; i < 6; i++) { + // Find the positive vertex (furthest along plane normal) + vec3 pVertex; + pVertex.x = (planes[i].normal.x >= 0) ? max.x : min.x; + pVertex.y = (planes[i].normal.y >= 0) ? max.y : min.y; + pVertex.z = (planes[i].normal.z >= 0) ? max.z : min.z; + + // If positive vertex is behind plane, box is outside + if (planes[i].distanceToPoint(pVertex) < 0) { + return false; + } + } + return true; + } +}; + // ============================================================================= // Utility constants and functions // ============================================================================= diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index b5985b8..1997e08 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -515,10 +515,23 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) { #ifdef MCRF_HAS_GL if (!entities_ || !shader_ || !shader_->isValid()) return; + // Extract frustum for culling + mat4 viewProj = proj * view; + Frustum frustum; + frustum.extractFromMatrix(viewProj); + // Render non-skeletal entities first shader_->bind(); for (auto& entity : *entities_) { if (entity && entity->isVisible()) { + // Frustum culling - use entity position with generous bounding radius + vec3 pos = entity->getWorldPos(); + float boundingRadius = entity->getScale().x * 2.0f; // Approximate bounding sphere + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; // Skip this entity - outside view frustum + } + auto model = entity->getModel(); if (!model || !model->hasSkeleton()) { entity->render(view, proj, shader_->getProgram()); @@ -554,6 +567,14 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) { for (auto& entity : *entities_) { if (entity && entity->isVisible()) { + // Frustum culling for skeletal entities too + vec3 pos = entity->getWorldPos(); + float boundingRadius = entity->getScale().x * 2.0f; + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; + } + auto model = entity->getModel(); if (model && model->hasSkeleton()) { entity->render(view, proj, skinnedShader_->getProgram()); @@ -594,6 +615,11 @@ void Viewport3D::renderBillboards(const mat4& view, const mat4& proj) { #ifdef MCRF_HAS_GL if (!billboards_ || billboards_->empty() || !shader_ || !shader_->isValid()) return; + // Extract frustum for culling + mat4 viewProj = proj * view; + Frustum frustum; + frustum.extractFromMatrix(viewProj); + shader_->bind(); unsigned int shaderProgram = shader_->getProgram(); vec3 cameraPos = camera_.getPosition(); @@ -607,6 +633,14 @@ void Viewport3D::renderBillboards(const mat4& view, const mat4& proj) { for (auto& billboard : *billboards_) { if (billboard && billboard->isVisible()) { + // Frustum culling for billboards + vec3 pos = billboard->getPosition(); + float boundingRadius = billboard->getScale() * 2.0f; // Approximate + + if (!frustum.containsSphere(pos, boundingRadius)) { + continue; // Skip - outside frustum + } + billboard->render(shaderProgram, view, proj, cameraPos); } }