From 514b914068794247739c115cfed8d5d545a45d28 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Feb 2026 21:15:24 -0800 Subject: [PATCH] Add shadow frustum culling to terrain and M2 depth passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both passes were rendering the entire loaded scene (17×17 tile radius) into a shadow map that only covers 360×360 world units — submitting 10-50× more geometry than the shadow frustum can actually use. - TerrainRenderer::renderShadow: skip chunks whose bounding sphere doesn't overlap the shadow frustum AABB in XY. Reduces terrain draw calls from O(all loaded chunks) to O(chunks within ~180 units). - M2Renderer::renderShadow: skip instances whose world AABB doesn't overlap the shadow frustum in XY. Reduces M2 draw calls similarly. - Both functions now take shadowCenter + halfExtent parameters. --- include/rendering/m2_renderer.hpp | 2 +- include/rendering/terrain_renderer.hpp | 2 +- src/rendering/m2_renderer.cpp | 8 +++++++- src/rendering/renderer.cpp | 10 ++++++---- src/rendering/terrain_renderer.cpp | 9 ++++++++- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 9368c2dc..ef9b5e86 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -236,7 +236,7 @@ public: /** * Render depth-only pass for shadow casting */ - void renderShadow(GLuint shadowShaderProgram); + void renderShadow(GLuint shadowShaderProgram, const glm::vec3& shadowCenter, float halfExtent); /** * Render smoke particles (call after render()) diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 49335bd5..3fb285b1 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -135,7 +135,7 @@ public: /** * Render terrain geometry into shadow depth map */ - void renderShadow(GLuint shaderProgram); + void renderShadow(GLuint shaderProgram, const glm::vec3& shadowCenter, float halfExtent); /** * Set shadow map for receiving shadows diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 7828d911..28cca90a 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2151,7 +2151,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: } } -void M2Renderer::renderShadow(GLuint shadowShaderProgram) { +void M2Renderer::renderShadow(GLuint shadowShaderProgram, const glm::vec3& shadowCenter, float halfExtent) { if (instances.empty() || shadowShaderProgram == 0) { return; } @@ -2172,6 +2172,12 @@ void M2Renderer::renderShadow(GLuint shadowShaderProgram) { glActiveTexture(GL_TEXTURE0); for (const auto& instance : instances) { + // Cull instances whose AABB doesn't overlap the shadow frustum (XY plane) + glm::vec3 instCenter = (instance.worldBoundsMin + instance.worldBoundsMax) * 0.5f; + glm::vec3 instHalf = (instance.worldBoundsMax - instance.worldBoundsMin) * 0.5f; + if (std::abs(instCenter.x - shadowCenter.x) > halfExtent + instHalf.x) continue; + if (std::abs(instCenter.y - shadowCenter.y) > halfExtent + instHalf.y) continue; + auto it = models.find(instance.modelId); if (it == models.end()) continue; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 9aedb0a6..1b5fdcfb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3444,9 +3444,10 @@ void Renderer::renderShadowPass() { if (useBonesLoc >= 0) glUniform1i(useBonesLoc, 0); if (texLoc >= 0) glUniform1i(texLoc, 0); - // Render terrain into shadow map + // Render terrain into shadow map (only chunks within shadow frustum) if (terrainRenderer) { - terrainRenderer->renderShadow(shadowShaderProgram); + glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition; + terrainRenderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent); } // Render WMO into shadow map @@ -3476,9 +3477,10 @@ void Renderer::renderShadowPass() { shadowShaderWrapper.releaseProgram(); // Don't let wrapper delete our program } - // Render M2 doodads into shadow map + // Render M2 doodads into shadow map (only instances within shadow frustum) if (m2Renderer) { - m2Renderer->renderShadow(shadowShaderProgram); + glm::vec3 shadowCtr = shadowCenterInitialized ? shadowCenter : characterPosition; + m2Renderer->renderShadow(shadowShaderProgram, shadowCtr, kShadowHalfExtent); } // Render characters into shadow map diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index bed8e138..a15f2c01 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -369,7 +369,7 @@ GLuint TerrainRenderer::createAlphaTexture(const std::vector& alphaData return textureID; } -void TerrainRenderer::renderShadow(GLuint shaderProgram) { +void TerrainRenderer::renderShadow(GLuint shaderProgram, const glm::vec3& shadowCenter, float halfExtent) { if (chunks.empty()) return; GLint modelLoc = glGetUniformLocation(shaderProgram, "uModel"); @@ -378,6 +378,13 @@ void TerrainRenderer::renderShadow(GLuint shaderProgram) { for (const auto& chunk : chunks) { if (!chunk.isValid()) continue; + + // Cull chunks whose bounding sphere doesn't overlap the shadow frustum (XY plane) + float maxDist = halfExtent + chunk.boundingSphereRadius; + float dx = chunk.boundingSphereCenter.x - shadowCenter.x; + float dy = chunk.boundingSphereCenter.y - shadowCenter.y; + if (dx * dx + dy * dy > maxDist * maxDist) continue; + glBindVertexArray(chunk.vao); glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0); glBindVertexArray(0);