diff --git a/common/math/Frustum.hpp b/common/math/Frustum.hpp new file mode 100644 index 000000000..b47af3d47 --- /dev/null +++ b/common/math/Frustum.hpp @@ -0,0 +1,154 @@ +//// Frustum.hpp ////////////////////////////////////////////////////////////// +// +// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz +// zzzzzzz zzz zzzz zzzz zzzz zzzz +// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz +// zzz zzz zzz z zzzz zzzz zzzz zzzz +// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz +// +// Author: Mehdy MORVAN +// Date: 15/02/2026 +// Description: Header file for frustum culling utilities +// +/////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include +#include +#include + +namespace nexo::math { + + struct Plane { + glm::vec3 normal{}; + float distance = 0.0f; + + void normalize() + { + const float len = glm::length(normal); + normal /= len; + distance /= len; + } + + [[nodiscard]] float distanceTo(const glm::vec3 &point) const + { + return glm::dot(normal, point) + distance; + } + }; + + class Frustum { + public: + explicit Frustum(const glm::mat4 &vp) + { + extractPlanes(vp); + } + + /** + * @brief Tests whether an axis-aligned bounding box intersects the frustum. + * + * Uses the p-vertex optimization: for each frustum plane, finds the AABB vertex + * most in the direction of the plane normal. If that vertex is behind the plane, + * the entire AABB is outside the frustum. + * + * @param aabbMin The minimum corner of the AABB in world space. + * @param aabbMax The maximum corner of the AABB in world space. + * @return true if the AABB is at least partially inside the frustum. + */ + [[nodiscard]] bool intersectsAABB(const glm::vec3 &aabbMin, const glm::vec3 &aabbMax) const + { + for (const auto &plane : m_planes) + { + // P-vertex: the corner most in the direction of the plane normal + const glm::vec3 pVertex( + plane.normal.x >= 0.0f ? aabbMax.x : aabbMin.x, + plane.normal.y >= 0.0f ? aabbMax.y : aabbMin.y, + plane.normal.z >= 0.0f ? aabbMax.z : aabbMin.z + ); + + if (plane.distanceTo(pVertex) < 0.0f) + return false; + } + return true; + } + + private: + std::array m_planes; + + /** + * @brief Extracts frustum planes from a view-projection matrix. + * + * Uses the Gribb/Hartmann method to extract and normalize the six frustum planes + * directly from the combined view-projection matrix. + */ + void extractPlanes(const glm::mat4 &vp) + { + // Left: Row3 + Row0 + m_planes[0].normal.x = vp[0][3] + vp[0][0]; + m_planes[0].normal.y = vp[1][3] + vp[1][0]; + m_planes[0].normal.z = vp[2][3] + vp[2][0]; + m_planes[0].distance = vp[3][3] + vp[3][0]; + + // Right: Row3 - Row0 + m_planes[1].normal.x = vp[0][3] - vp[0][0]; + m_planes[1].normal.y = vp[1][3] - vp[1][0]; + m_planes[1].normal.z = vp[2][3] - vp[2][0]; + m_planes[1].distance = vp[3][3] - vp[3][0]; + + // Bottom: Row3 + Row1 + m_planes[2].normal.x = vp[0][3] + vp[0][1]; + m_planes[2].normal.y = vp[1][3] + vp[1][1]; + m_planes[2].normal.z = vp[2][3] + vp[2][1]; + m_planes[2].distance = vp[3][3] + vp[3][1]; + + // Top: Row3 - Row1 + m_planes[3].normal.x = vp[0][3] - vp[0][1]; + m_planes[3].normal.y = vp[1][3] - vp[1][1]; + m_planes[3].normal.z = vp[2][3] - vp[2][1]; + m_planes[3].distance = vp[3][3] - vp[3][1]; + + // Near: Row3 + Row2 + m_planes[4].normal.x = vp[0][3] + vp[0][2]; + m_planes[4].normal.y = vp[1][3] + vp[1][2]; + m_planes[4].normal.z = vp[2][3] + vp[2][2]; + m_planes[4].distance = vp[3][3] + vp[3][2]; + + // Far: Row3 - Row2 + m_planes[5].normal.x = vp[0][3] - vp[0][2]; + m_planes[5].normal.y = vp[1][3] - vp[1][2]; + m_planes[5].normal.z = vp[2][3] - vp[2][2]; + m_planes[5].distance = vp[3][3] - vp[3][2]; + + for (auto &plane : m_planes) + plane.normalize(); + } + }; + + /** + * @brief Transforms a local-space AABB by a 4x4 matrix to produce a world-space AABB. + * + * Uses Arvo's method for efficient AABB-matrix transformation, producing the tightest + * axis-aligned bounding box that contains the transformed original box. + */ + inline void transformAABB(const glm::vec3 &localMin, const glm::vec3 &localMax, + const glm::mat4 &transform, + glm::vec3 &worldMin, glm::vec3 &worldMax) + { + // Start with the translation component + const glm::vec3 translation(transform[3]); + worldMin = translation; + worldMax = translation; + + // Apply rotation/scale contribution using Arvo's method + for (int i = 0; i < 3; ++i) + { + for (int j = 0; j < 3; ++j) + { + const float a = transform[j][i] * localMin[j]; + const float b = transform[j][i] * localMax[j]; + worldMin[i] += std::min(a, b); + worldMax[i] += std::max(a, b); + } + } + } + +} // namespace nexo::math diff --git a/engine/src/EntityFactory3D.cpp b/engine/src/EntityFactory3D.cpp index 045e02749..1d95f1e2e 100644 --- a/engine/src/EntityFactory3D.cpp +++ b/engine/src/EntityFactory3D.cpp @@ -45,6 +45,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getCubeVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-0.5f); + mesh.localMax = glm::vec3(0.5f); auto material = std::make_unique(); material->albedoColor = color; @@ -80,6 +83,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getCubeVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-0.5f); + mesh.localMax = glm::vec3(0.5f); const auto materialRef = assets::AssetCatalog::getInstance().createAsset( assets::AssetLocation("_internal::CubeMat@_internal"), @@ -174,6 +180,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getTetrahedronVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); auto material = std::make_unique(); material->albedoColor = color; @@ -205,6 +214,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getTetrahedronVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); const auto materialRef = assets::AssetCatalog::getInstance().createAsset( assets::AssetLocation("_internal::TetrahedronMat@_internal"), @@ -232,6 +244,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getPyramidVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); auto material = std::make_unique(); material->albedoColor = color; @@ -262,6 +277,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getPyramidVAO(); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); const auto materialRef = assets::AssetCatalog::getInstance().createAsset( assets::AssetLocation("_internal::PyramidMat@_internal"), @@ -290,6 +308,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getCylinderVAO(nbSegment); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); auto material = std::make_unique(); material->albedoColor = color; @@ -320,6 +341,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getCylinderVAO(nbSegment); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); const auto materialRef = assets::AssetCatalog::getInstance().createAsset( assets::AssetLocation("_internal::CylinderMat@_internal"), @@ -348,6 +372,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getSphereVAO(nbSubdivision); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); auto material = std::make_unique(); material->albedoColor = color; @@ -378,6 +405,9 @@ namespace nexo components::StaticMeshComponent mesh; mesh.vao = renderer::NxRenderer3D::getSphereVAO(nbSubdivision); + mesh.hasBounds = true; + mesh.localMin = glm::vec3(-1.0f); + mesh.localMax = glm::vec3(1.0f); const auto materialRef = assets::AssetCatalog::getInstance().createAsset( assets::AssetLocation("_internal::SphereMat@_internal"), @@ -489,6 +519,9 @@ namespace nexo components::StaticMeshComponent staticMesh; staticMesh.vao = mesh.vao; + staticMesh.hasBounds = true; + staticMesh.localMin = mesh.localMin; + staticMesh.localMax = mesh.localMax; components::RenderComponent renderComponent; renderComponent.isRendered = true; diff --git a/engine/src/assets/Assets/Model/Model.hpp b/engine/src/assets/Assets/Model/Model.hpp index 6c6367deb..30bad73e3 100644 --- a/engine/src/assets/Assets/Model/Model.hpp +++ b/engine/src/assets/Assets/Model/Model.hpp @@ -26,6 +26,8 @@ namespace nexo::assets { AssetRef material; glm::vec3 localCenter = {0.0f, 0.0f, 0.0f}; + glm::vec3 localMin = {0.0f, 0.0f, 0.0f}; + glm::vec3 localMax = {0.0f, 0.0f, 0.0f}; }; struct MeshNode { diff --git a/engine/src/assets/Assets/Model/ModelImporter.cpp b/engine/src/assets/Assets/Model/ModelImporter.cpp index b55364b08..a1f754cc0 100644 --- a/engine/src/assets/Assets/Model/ModelImporter.cpp +++ b/engine/src/assets/Assets/Model/ModelImporter.cpp @@ -427,7 +427,7 @@ namespace nexo::assets { } LOG(NEXO_INFO, "Loaded mesh {}", mesh->mName.C_Str()); - return {mesh->mName.C_Str(), vao, materialComponent, centerLocal}; + return {mesh->mName.C_Str(), vao, materialComponent, centerLocal, minBB, maxBB}; } glm::mat4 ModelImporter::convertAssimpMatrixToGLM(const aiMatrix4x4& matrix) diff --git a/engine/src/components/StaticMesh.hpp b/engine/src/components/StaticMesh.hpp index db3746186..f66f5cf7a 100644 --- a/engine/src/components/StaticMesh.hpp +++ b/engine/src/components/StaticMesh.hpp @@ -16,6 +16,8 @@ #include "renderer/Attributes.hpp" #include "renderer/VertexArray.hpp" +#include + namespace nexo::components { struct StaticMeshComponent { @@ -23,18 +25,28 @@ namespace nexo::components { renderer::RequiredAttributes meshAttributes; + bool hasBounds = false; + glm::vec3 localMin = {0.0f, 0.0f, 0.0f}; + glm::vec3 localMax = {0.0f, 0.0f, 0.0f}; + struct Memento { std::shared_ptr vao; + bool hasBounds; + glm::vec3 localMin; + glm::vec3 localMax; }; void restore(const Memento &memento) { vao = memento.vao; + hasBounds = memento.hasBounds; + localMin = memento.localMin; + localMax = memento.localMax; } [[nodiscard]] Memento save() const { - return {vao}; + return {vao, hasBounds, localMin, localMax}; } }; diff --git a/engine/src/systems/RenderCommandSystem.cpp b/engine/src/systems/RenderCommandSystem.cpp index a24173b70..e4e9f4b34 100644 --- a/engine/src/systems/RenderCommandSystem.cpp +++ b/engine/src/systems/RenderCommandSystem.cpp @@ -24,6 +24,7 @@ #include "components/StaticMesh.hpp" #include "components/Transform.hpp" #include "core/event/Input.hpp" +#include "math/Frustum.hpp" #include "math/Projection.hpp" #include "math/Vector.hpp" #include "renderPasses/Masks.hpp" @@ -286,36 +287,42 @@ namespace nexo::system { const auto materialSpan = get(); const std::span entitySpan = m_group->entities(); - std::vector drawCommands; - for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { - const ecs::Entity entity = entitySpan[i]; - if (coord->entityHasComponent(entity) && sceneType != SceneType::EDITOR) - continue; - const auto &transform = transformSpan[i]; - const auto &materialAsset = materialSpan[i].material.lock(); - std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; - const auto &mesh = meshSpan[i]; - auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr); - if (!shader) - continue; - drawCommands.push_back(createDrawCommand( - entity, - shader, - mesh, - materialAsset, - transform) - ); + for (auto &camera : renderContext.cameras) { + const math::Frustum frustum(camera.viewProjectionMatrix); + std::vector drawCommands; + + for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) { + const ecs::Entity entity = entitySpan[i]; + if (coord->entityHasComponent(entity) && sceneType != SceneType::EDITOR) + continue; + const auto &transform = transformSpan[i]; + const auto &mesh = meshSpan[i]; + + // Frustum culling: skip entities whose AABB is entirely outside the camera frustum + if (mesh.hasBounds) + { + glm::vec3 worldMin, worldMax; + math::transformAABB(mesh.localMin, mesh.localMax, transform.worldMatrix, worldMin, worldMax); + if (!frustum.intersectsAABB(worldMin, worldMax)) + continue; + } - if (coord->entityHasComponent(entity)) - drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform)); - } + const auto &materialAsset = materialSpan[i].material.lock(); + std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : ""; + auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr); + if (!shader) + continue; - for (auto &camera : renderContext.cameras) { - for (auto &cmd : drawCommands) { + auto cmd = createDrawCommand(entity, shader, mesh, materialAsset, transform); cmd.uniforms["uViewProjection"] = camera.viewProjectionMatrix; cmd.uniforms["uCamPos"] = camera.cameraPosition; setupLights(cmd, renderContext.sceneLights); + drawCommands.push_back(std::move(cmd)); + + if (coord->entityHasComponent(entity)) + drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform)); } + camera.pipeline.addDrawCommands(drawCommands); if (sceneType == SceneType::EDITOR && renderContext.gridParams.enabled) camera.pipeline.addDrawCommand(createGridDrawCommand(camera, renderContext));