Frustum culling

This commit is contained in:
John McCardle 2026-02-04 23:45:43 -05:00
commit f2ccdff499
2 changed files with 144 additions and 0 deletions

View file

@ -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 // Utility constants and functions
// ============================================================================= // =============================================================================

View file

@ -515,10 +515,23 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
#ifdef MCRF_HAS_GL #ifdef MCRF_HAS_GL
if (!entities_ || !shader_ || !shader_->isValid()) return; if (!entities_ || !shader_ || !shader_->isValid()) return;
// Extract frustum for culling
mat4 viewProj = proj * view;
Frustum frustum;
frustum.extractFromMatrix(viewProj);
// Render non-skeletal entities first // Render non-skeletal entities first
shader_->bind(); shader_->bind();
for (auto& entity : *entities_) { for (auto& entity : *entities_) {
if (entity && entity->isVisible()) { 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(); auto model = entity->getModel();
if (!model || !model->hasSkeleton()) { if (!model || !model->hasSkeleton()) {
entity->render(view, proj, shader_->getProgram()); entity->render(view, proj, shader_->getProgram());
@ -554,6 +567,14 @@ void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
for (auto& entity : *entities_) { for (auto& entity : *entities_) {
if (entity && entity->isVisible()) { 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(); auto model = entity->getModel();
if (model && model->hasSkeleton()) { if (model && model->hasSkeleton()) {
entity->render(view, proj, skinnedShader_->getProgram()); entity->render(view, proj, skinnedShader_->getProgram());
@ -594,6 +615,11 @@ void Viewport3D::renderBillboards(const mat4& view, const mat4& proj) {
#ifdef MCRF_HAS_GL #ifdef MCRF_HAS_GL
if (!billboards_ || billboards_->empty() || !shader_ || !shader_->isValid()) return; if (!billboards_ || billboards_->empty() || !shader_ || !shader_->isValid()) return;
// Extract frustum for culling
mat4 viewProj = proj * view;
Frustum frustum;
frustum.extractFromMatrix(viewProj);
shader_->bind(); shader_->bind();
unsigned int shaderProgram = shader_->getProgram(); unsigned int shaderProgram = shader_->getProgram();
vec3 cameraPos = camera_.getPosition(); vec3 cameraPos = camera_.getPosition();
@ -607,6 +633,14 @@ void Viewport3D::renderBillboards(const mat4& view, const mat4& proj) {
for (auto& billboard : *billboards_) { for (auto& billboard : *billboards_) {
if (billboard && billboard->isVisible()) { 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); billboard->render(shaderProgram, view, proj, cameraPos);
} }
} }