diff --git a/assets/manifest.yaml b/assets/manifest.yaml index 8bab0597..c7cfedc4 100644 --- a/assets/manifest.yaml +++ b/assets/manifest.yaml @@ -82,4 +82,6 @@ shaders: cube.vert: shaders/slang/cube.slang:vertMain depthprepass.vert: shaders/slang/depthprepass.slang:vertMain depthprepass.frag: shaders/slang/depthprepass.slang:fragMain + occlusionbox.vert: shaders/slang/occlusionbox.slang:vertMain + occlusionbox.frag: shaders/slang/occlusionbox.slang:fragMain cluster_assign.comp: shaders/slang/cluster_assign.slang:csMain diff --git a/assets/shaders/slang/occlusionbox.slang b/assets/shaders/slang/occlusionbox.slang new file mode 100644 index 00000000..f3a7c023 --- /dev/null +++ b/assets/shaders/slang/occlusionbox.slang @@ -0,0 +1,27 @@ +// occlusionbox.slang: depth-only AABB proxy for hardware occlusion queries. +// Colour writes are masked off by the caller — the fragment output is never +// read, it only needs to exist for the query's depth test to run. +uniform float4x4 mvp; + +struct VSInput { + float3 position : POSITION; +}; +struct VSOutput { + float4 pos : SV_Position; +}; + +// clang-format off +[shader("vertex")] +VSOutput vertMain(VSInput input) { + // clang-format on + VSOutput o; + o.pos = mul(mvp, float4(input.position, 1.0)); + return o; +}; + +// clang-format off +[shader("fragment")] +float4 fragMain(VSOutput input) : SV_Target { + // clang-format on + return float4(0.0); +}; diff --git a/game/src/layer/imgui/imguilayer.cpp b/game/src/layer/imgui/imguilayer.cpp index b9438e56..768c362c 100644 --- a/game/src/layer/imgui/imguilayer.cpp +++ b/game/src/layer/imgui/imguilayer.cpp @@ -57,6 +57,9 @@ bool ImGuiLayer::hasLogMenu = true; std::vector ImGuiLayer::levelNames; std::vector ImGuiLayer::categoryNames; +std::array ImGuiLayer::meshVisibleHistory{}; +int ImGuiLayer::historyOffset = 0; + using sponge::layer::Layer; using sponge::layer::LayerStack; using sponge::platform::opengl::renderer::AssetManager; @@ -132,19 +135,34 @@ void ImGuiLayer::showInfoSection() { ImGui::Text("%s", resolution.c_str()); const auto mazeLayer = Maze::get().getMazeLayer(); + + const auto meshVisible = mazeLayer->getVisibleMeshCount(); + const auto meshTotal = mazeLayer->getTotalMeshCount(); + + // Ring buffer: write the current value, then advance so + // historyOffset points at the oldest sample — PlotLines' own + // values_offset scrolls the plot from there. + meshVisibleHistory[static_cast(historyOffset)] = + static_cast(meshVisible); + historyOffset = (historyOffset + 1) % historyLength; + ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Meshes"); ImGui::TableNextColumn(); - ImGui::Text("%u / %u visible", mazeLayer->getVisibleMeshCount(), - mazeLayer->getTotalMeshCount()); + const auto meshOverlay = fmt::format("{} / {}", meshVisible, meshTotal); + ImGui::PlotLines("##MeshesGraph", meshVisibleHistory.data(), + historyLength, historyOffset, meshOverlay.c_str(), 0.F, + static_cast(std::max(meshTotal, 1U)), + ImVec2(0, 30)); ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::Text("Cull / submit"); + ImGui::Text("Objects"); ImGui::TableNextColumn(); - ImGui::Text("%u us / %u us", mazeLayer->getCullMicros(), - mazeLayer->getSubmitMicros()); + ImGui::Text("%u / %u occlusion-visible", + mazeLayer->getOcclusionVisibleCount(), + mazeLayer->getOcclusionTotalCount()); ImGui::EndTable(); } diff --git a/game/src/layer/imgui/imguilayer.hpp b/game/src/layer/imgui/imguilayer.hpp index 2658d257..641ece93 100644 --- a/game/src/layer/imgui/imguilayer.hpp +++ b/game/src/layer/imgui/imguilayer.hpp @@ -5,6 +5,7 @@ #include "layer/layerstack.hpp" #include "logging/log.hpp" +#include #include #include #include @@ -29,6 +30,11 @@ class ImGuiLayer final : public sponge::layer::Layer { static std::vector levelNames; static std::vector categoryNames; + // Rolling per-frame history for the Info section's Meshes graph. + static constexpr int historyLength = 100; + static std::array meshVisibleHistory; + static int historyOffset; + // Main sections static void showInfoSection(); static void showAppInfoWindow(float width); diff --git a/game/src/layer/mazelayer.cpp b/game/src/layer/mazelayer.cpp index 83ea4c56..34f39f71 100644 --- a/game/src/layer/mazelayer.cpp +++ b/game/src/layer/mazelayer.cpp @@ -60,6 +60,7 @@ using sponge::platform::opengl::scene::FXAA; using sponge::platform::opengl::scene::Mesh; using sponge::platform::opengl::scene::Model; using sponge::platform::opengl::scene::ModelCreateInfo; +using sponge::platform::opengl::scene::OcclusionCuller; using sponge::platform::opengl::scene::SceneTarget; using sponge::platform::opengl::scene::ShadowMap; using sponge::platform::opengl::scene::TAA; @@ -107,20 +108,26 @@ void MazeLayer::finishLoading(std::vector> builtModels) { std::vector meshBounds; meshBounds.reserve(model->getMeshCount()); + sponge::scene::AABB objectBounds{ + glm::vec3(std::numeric_limits::max()), + glm::vec3(std::numeric_limits::lowest()) + }; for (size_t m = 0; m < model->getMeshCount(); m++) { - meshBounds.push_back( - sponge::scene::transform(model->getMeshBounds(m), modelMatrix)); + const auto worldBounds = + sponge::scene::transform(model->getMeshBounds(m), modelMatrix); + objectBounds.min = glm::min(objectBounds.min, worldBounds.min); + objectBounds.max = glm::max(objectBounds.max, worldBounds.max); + meshBounds.push_back(worldBounds); } objectMeshWorldBounds.push_back(std::move(meshBounds)); + objectWorldBounds.push_back(objectBounds); } sceneBounds = { glm::vec3(std::numeric_limits::max()), glm::vec3(std::numeric_limits::lowest()) }; - for (const auto& bounds : objectMeshWorldBounds) { - for (const auto& box : bounds) { - sceneBounds.min = glm::min(sceneBounds.min, box.min); - sceneBounds.max = glm::max(sceneBounds.max, box.max); - } + for (const auto& box : objectWorldBounds) { + sceneBounds.min = glm::min(sceneBounds.min, box.min); + sceneBounds.max = glm::max(sceneBounds.max, box.max); } const auto gameCameraCreateInfo = @@ -163,6 +170,9 @@ void MazeLayer::finishLoading(std::vector> builtModels) { shadowMap = std::make_unique(directionalLight.shadowMapRes); cube = std::make_unique(); + occlusionCuller = std::make_unique(objectModels.size()); + shadowOcclusionCuller = + std::make_unique(objectModels.size()); fxaa = std::make_unique(Maze::get().getWindow()->getWidth(), Maze::get().getWindow()->getHeight()); @@ -485,6 +495,15 @@ void MazeLayer::onRender() { const auto& frame = renderFrames[renderReadIndex.load(std::memory_order_acquire)]; + // Pick up whichever occlusion queries resolved since last frame, before + // any pass decides what's visible. + if (occlusionCuller) { + occlusionCuller->pollResults(); + } + if (shadowOcclusionCuller) { + shadowOcclusionCuller->pollResults(); + } + // Phase 1: shadow map if (frame.shadowEnabled && frame.shadowCastShadow) { renderSceneToDepthMap(frame); @@ -493,6 +512,10 @@ void MazeLayer::onRender() { // Phase 2: depth prepass renderDepthPrepass(frame); + // Phase 2.5: occlusion queries against the depth just rasterized — + // results feed next frame's occlusion skip, not this one's. + renderOcclusionQueries(frame); + // Phase 3: light culling if (clusteredLights && frame.numLights > 0) { clusteredLights->update(frame.lightPositions.data(), @@ -794,8 +817,14 @@ void MazeLayer::renderGameObjects(const thread::MazeRenderFrame& frame) const { shadowMap->activateAndBindShadowTexture(1); } - const auto submitStart = std::chrono::steady_clock::now(); + const auto submitStart = std::chrono::steady_clock::now(); + uint32_t occlusionVisible = 0; for (size_t i = 0; i < frame.objectModels.size(); i++) { + if (occlusionCuller && !occlusionCuller->isVisible(i)) { + continue; + } + occlusionVisible++; + const auto& modelMatrix = frame.objectModelMatrices[i]; shader->setMat4("mvp", frame.cameraMVP * modelMatrix); @@ -812,6 +841,9 @@ void MazeLayer::renderGameObjects(const thread::MazeRenderFrame& frame) const { .count(); submitMicros.store(static_cast(submitUs), std::memory_order_relaxed); + occlusionVisibleCount.store(occlusionVisible, std::memory_order_relaxed); + occlusionTotalCount.store(static_cast(frame.objectModels.size()), + std::memory_order_relaxed); shader->unbind(); } @@ -882,6 +914,10 @@ void MazeLayer::renderDepthPrepass(const thread::MazeRenderFrame& frame) const { depthPrepassShader->bind(); for (size_t i = 0; i < frame.objectModels.size(); ++i) { + if (occlusionCuller && !occlusionCuller->isVisible(i)) { + continue; + } + // Rasterization uses the jittered matrix so prepass depth lines up with // the scene pass; motion is measured unjittered, or the jitter itself // would read as movement. @@ -921,6 +957,34 @@ void MazeLayer::renderDepthPrepass(const thread::MazeRenderFrame& frame) const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } +void MazeLayer::renderOcclusionQueries( + const thread::MazeRenderFrame& frame) const { + if (!occlusionCuller) { + return; + } + + // Test every object's AABB against the depth prepass just rasterized. + // Depth-test only: no colour, no depth write, so this can't perturb the + // buffer the opaque pass is about to blit and depth-test against. + glBindFramebuffer(GL_FRAMEBUFFER, depthPrepassFbo); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glDepthFunc(GL_LEQUAL); + // Global back-face culling (RendererAPI) would cull every face of a box + // the camera is inside — true for any large object's AABB, e.g. sponza's + // — leaving the query with nothing to rasterize and the object stuck + // permanently "occluded". The proxy has no back faces to hide anyway. + glDisable(GL_CULL_FACE); + + occlusionCuller->query(objectWorldBounds, frame.cameraMVP, frame.cameraPos); + + glEnable(GL_CULL_FACE); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + void MazeLayer::blitDepthToCurrentFbo(const int w, const int h) const { GLint drawFbo = 0; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo); @@ -959,12 +1023,33 @@ void MazeLayer::renderSceneToDepthMap( shader->setMat4("lightSpaceMatrix", frame.lightSpaceMatrix); for (size_t i = 0; i < frame.objectModels.size(); i++) { + if (shadowOcclusionCuller && !shadowOcclusionCuller->isVisible(i)) { + continue; + } shader->setMat4("model", frame.objectModelMatrices[i]); frame.objectModels[i]->render(shader, frame.objectMeshVisibleLight[i]); } shader->unbind(); + // Test every object's AABB against the shadow depth just rasterized, + // same technique as renderOcclusionQueries() but against the light's own + // depth instead of the camera's — still bound, so no FBO switch needed. + if (shadowOcclusionCuller) { + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glDepthFunc(GL_LEQUAL); + glDisable(GL_CULL_FACE); + + shadowOcclusionCuller->query(objectWorldBounds, frame.lightSpaceMatrix, + shadowMap->getEyePosition()); + + glEnable(GL_CULL_FACE); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } + shadowMap->unbind(); } diff --git a/game/src/layer/mazelayer.hpp b/game/src/layer/mazelayer.hpp index ba71588c..037e67d3 100644 --- a/game/src/layer/mazelayer.hpp +++ b/game/src/layer/mazelayer.hpp @@ -11,6 +11,7 @@ #include "platform/opengl/scene/cube.hpp" #include "platform/opengl/scene/fxaa.hpp" #include "platform/opengl/scene/model.hpp" +#include "platform/opengl/scene/occlusionculler.hpp" #include "platform/opengl/scene/scenetarget.hpp" #include "platform/opengl/scene/shadowmap.hpp" #include "platform/opengl/scene/taa.hpp" @@ -123,6 +124,16 @@ class MazeLayer final : public sponge::layer::Layer { return submitMicros.load(std::memory_order_relaxed); } + // This frame's occlusion-culling stats, for the debug UI. Written from + // renderGameObjects() (render thread), read from the render thread too — + // atomic only so the type matches the frustum counters above. + uint32_t getOcclusionVisibleCount() const { + return occlusionVisibleCount.load(std::memory_order_relaxed); + } + uint32_t getOcclusionTotalCount() const { + return occlusionTotalCount.load(std::memory_order_relaxed); + } + // True once finishLoading() has run; LoadingLayer skips reloading if set. bool isLoaded() const { return resourcesReady.load(std::memory_order_acquire); @@ -154,9 +165,25 @@ class MazeLayer final : public sponge::layer::Layer { // Objects never move after finishLoading(), so this is computed once // there rather than every frame like the visibility test that reads it. std::vector> objectMeshWorldBounds; + // Per-object union of objectMeshWorldBounds, index-locked with + // objectModels — the box occlusionCuller and shadowOcclusionCuller test + // each object against. Same one-time computation, same reason: objects + // never move. + std::vector objectWorldBounds; // Union of objectMeshWorldBounds, for fitting the shadow frustum to the // scene. Same one-time computation as above, same reason. sponge::scene::AABB sceneBounds; + // Hardware occlusion queries against the depth prepass, gating the + // camera-view passes (depth prepass, opaque). One frame of latency; see + // occlusionculler.hpp. + std::unique_ptr + occlusionCuller; + // Same technique against the shadow map's own depth: an object can be + // light-occluded independently of camera-occluded, so this is a separate + // result from occlusionCuller, alongside the light-frustum mask + // (objectMeshVisibleLight). + std::unique_ptr + shadowOcclusionCuller; std::unique_ptr clusteredLights; std::shared_ptr @@ -231,6 +258,8 @@ class MazeLayer final : public sponge::layer::Layer { // Set from renderGameObjects(), which is const (render-thread methods // are const throughout this class). mutable std::atomic submitMicros{ 0 }; + mutable std::atomic occlusionVisibleCount{ 0 }; + mutable std::atomic occlusionTotalCount{ 0 }; float ambientStrength = .25F; float ao = .25F; int32_t attenuationIndex = 4; @@ -264,6 +293,8 @@ class MazeLayer final : public sponge::layer::Layer { void renderDepthPrepass(const thread::MazeRenderFrame& frame) const; + void renderOcclusionQueries(const thread::MazeRenderFrame& frame) const; + void blitDepthToCurrentFbo(int w, int h) const; void renderGameObjects(const thread::MazeRenderFrame& frame) const; diff --git a/sponge/src/platform/opengl/scene/cube.cpp b/sponge/src/platform/opengl/scene/cube.cpp index 6972ac6a..7fc33f53 100644 --- a/sponge/src/platform/opengl/scene/cube.cpp +++ b/sponge/src/platform/opengl/scene/cube.cpp @@ -2,36 +2,12 @@ #include "logging/log.hpp" #include "platform/opengl/renderer/assetmanager.hpp" +#include "platform/opengl/scene/unitcube.hpp" #include -#include #include #include -namespace { -constexpr std::array vertices = { - glm::vec3{ -0.5, 0.5, -0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, - glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, - glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, - glm::vec3{ -0.5, 0.5, -0.5 }, glm::vec3{ -0.5, -0.5, -0.5 }, - glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, - glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, - glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ 0.5, -0.5, 0.5 }, - glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, 0.5 }, - glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, - glm::vec3{ 0.5, 0.5, -0.5 }, glm::vec3{ 0.5, -0.5, -0.5 }, - glm::vec3{ -0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, - glm::vec3{ -0.5, -0.5, -0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, - glm::vec3{ -0.5, 0.5, 0.5 }, glm::vec3{ -0.5, -0.5, 0.5 }, - glm::vec3{ 0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, - glm::vec3{ 0.5, -0.5, 0.5 }, glm::vec3{ 0.5, 0.5, 0.5 }, - glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, -0.5, -0.5 }, - glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ -0.5, -0.5, 0.5 }, - glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, -0.5, 0.5 }, -}; -constexpr uint32_t vertexCount = 36; -} // namespace - namespace sponge::platform::opengl::scene { using renderer::AssetManager; @@ -47,8 +23,8 @@ Cube::Cube() { vao = std::make_unique(); vao->bind(); - vbo = std::make_unique(vertices.data(), - sizeof(vertices)); + vbo = std::make_unique(unitCubeVertices.data(), + sizeof(unitCubeVertices)); vbo->bind(); constexpr uint32_t positionLoc = 0; @@ -62,7 +38,7 @@ Cube::Cube() { void Cube::render() const { vao->bind(); - glDrawArrays(GL_TRIANGLES, 0, vertexCount); + glDrawArrays(GL_TRIANGLES, 0, unitCubeVertexCount); vao->unbind(); } diff --git a/sponge/src/platform/opengl/scene/occlusionculler.cpp b/sponge/src/platform/opengl/scene/occlusionculler.cpp new file mode 100644 index 00000000..df63b00a --- /dev/null +++ b/sponge/src/platform/opengl/scene/occlusionculler.cpp @@ -0,0 +1,102 @@ +#include "platform/opengl/scene/occlusionculler.hpp" + +#include "platform/opengl/renderer/assetmanager.hpp" +#include "platform/opengl/renderer/gl.hpp" +#include "platform/opengl/scene/unitcube.hpp" + +#include + +namespace sponge::platform::opengl::scene { +using renderer::AssetManager; + +OcclusionCuller::OcclusionCuller(const size_t objectCount) : + queries(objectCount, 0), visible(objectCount, 1), issued(objectCount, 0) { + if (objectCount == 0) { + return; + } + + glGenQueries(static_cast(objectCount), queries.data()); + + shader = AssetManager::createShader({ + .name = "occlusionbox", + .vertexShader = "occlusionbox.vert", + .fragmentShader = "occlusionbox.frag", + }); + shader->bind(); + + vao = std::make_unique(); + vao->bind(); + + vbo = std::make_unique(unitCubeVertices.data(), + sizeof(unitCubeVertices)); + vbo->bind(); + + constexpr uint32_t positionLoc = 0; + glEnableVertexAttribArray(positionLoc); + glVertexAttribPointer(positionLoc, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), + reinterpret_cast(0)); + + shader->unbind(); + vao->unbind(); +} + +OcclusionCuller::~OcclusionCuller() { + if (!queries.empty()) { + glDeleteQueries(static_cast(queries.size()), queries.data()); + } +} + +void OcclusionCuller::query(const std::vector& worldBounds, + const glm::mat4& viewProj, + const glm::vec3& eyePos) { + shader->bind(); + vao->bind(); + + for (size_t i = 0; i < worldBounds.size(); i++) { + const auto& box = worldBounds[i]; + + const bool eyeInside = eyePos.x >= box.min.x && eyePos.x <= box.max.x && + eyePos.y >= box.min.y && eyePos.y <= box.max.y && + eyePos.z >= box.min.z && eyePos.z <= box.max.z; + if (eyeInside) { + visible[i] = 1; + // Leave unissued: pollResults() then skips it until a query + // fired after the eye leaves the box gives a real answer. + issued[i] = 0; + continue; + } + + const auto center = (box.min + box.max) * 0.5F; + const auto size = box.max - box.min; + const auto boxModel = glm::translate(glm::mat4(1.F), center) * + glm::scale(glm::mat4(1.F), size); + shader->setMat4("mvp", viewProj * boxModel); + + glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[i]); + glDrawArrays(GL_TRIANGLES, 0, unitCubeVertexCount); + glEndQuery(GL_ANY_SAMPLES_PASSED); + issued[i] = 1; + } + + vao->unbind(); + shader->unbind(); +} + +void OcclusionCuller::pollResults() { + for (size_t i = 0; i < queries.size(); i++) { + if (!issued[i]) { + continue; + } + + GLuint available = 0; + glGetQueryObjectuiv(queries[i], GL_QUERY_RESULT_AVAILABLE, &available); + if (available == GL_FALSE) { + continue; + } + GLuint anyPassed = 0; + glGetQueryObjectuiv(queries[i], GL_QUERY_RESULT, &anyPassed); + visible[i] = anyPassed != 0U ? 1 : 0; + } +} + +} // namespace sponge::platform::opengl::scene diff --git a/sponge/src/platform/opengl/scene/occlusionculler.hpp b/sponge/src/platform/opengl/scene/occlusionculler.hpp new file mode 100644 index 00000000..559a25a0 --- /dev/null +++ b/sponge/src/platform/opengl/scene/occlusionculler.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "platform/opengl/renderer/shader.hpp" +#include "platform/opengl/renderer/vertexarray.hpp" +#include "platform/opengl/renderer/vertexbuffer.hpp" +#include "scene/frustum.hpp" + +#include + +#include +#include +#include +#include + +namespace sponge::platform::opengl::scene { + +// Hardware occlusion culling: one GL_ANY_SAMPLES_PASSED query per object, +// its world AABB drawn depth-test-only against whatever is already in the +// bound depth buffer. Results lag one frame: call pollResults() at the +// start of a frame to pick up last frame's answers before deciding what to +// draw, then query() once this frame's real depth is rasterized, so the +// result is ready for next frame. An object stays visible until its first +// result arrives, so nothing is hidden before there's data to hide it with. +class OcclusionCuller { +public: + explicit OcclusionCuller(size_t objectCount); + ~OcclusionCuller(); + + OcclusionCuller(const OcclusionCuller&) = delete; + OcclusionCuller& operator=(const OcclusionCuller&) = delete; + + // Caller must already have the target depth buffer bound, with depth + // test on (GL_LEQUAL) and colour/depth writes off — this only reads + // depth. worldBounds must be objectCount long, index-locked with the + // caller's own object list. viewProj/eyePos are whatever's being tested + // against — a perspective camera or an orthographic light. + // + // An object whose box contains eyePos is skipped and left visible. This + // matters for a perspective eye inside a convex box: only the far face + // rasterizes (the near face is behind the eye, clipped away), and + // that's always farther than the object's own nearby geometry already + // in the depth buffer, so the query would always "fail" against itself + // — true for any object large enough to enclose the eye (e.g. the whole + // level). An orthographic eye (e.g. a directional light) doesn't clip + // this way, so there the guard is just a harmless no-op. + void query(const std::vector& worldBounds, + const glm::mat4& viewProj, const glm::vec3& eyePos); + + // Non-blocking: pulls in whichever queries already have a result. + // Objects with no result yet keep their last known visibility. + void pollResults(); + + bool isVisible(size_t index) const { + return visible[index] != 0; + } + +private: + std::shared_ptr shader; + std::unique_ptr vao; + std::unique_ptr vbo; + std::vector queries; + std::vector visible; + // A name from glGenQueries isn't a valid query object until it's been + // through one glBeginQuery/glEndQuery pair — polling it before that is a + // GL_INVALID_OPERATION ("query object not found"), so pollResults() + // skips any index query() hasn't issued yet. + std::vector issued; +}; + +} // namespace sponge::platform::opengl::scene diff --git a/sponge/src/platform/opengl/scene/shadowmap.cpp b/sponge/src/platform/opengl/scene/shadowmap.cpp index 1c4bc924..5ab80238 100644 --- a/sponge/src/platform/opengl/scene/shadowmap.cpp +++ b/sponge/src/platform/opengl/scene/shadowmap.cpp @@ -213,6 +213,7 @@ void ShadowMap::updateLightSpaceMatrix(const glm::vec3& lightDirection, glm::vec3(0.F, 1.F, 0.F); const auto eye = center - lightDirection * radius * 2.F; const auto lightView = glm::lookAt(eye, center, up); + eyePosition = eye; // Fit the ortho box to the scene's bounds as seen from the light, so the // frustum always exactly covers the static scene, at whatever size it diff --git a/sponge/src/platform/opengl/scene/shadowmap.hpp b/sponge/src/platform/opengl/scene/shadowmap.hpp index b29ee0e9..3968eade 100644 --- a/sponge/src/platform/opengl/scene/shadowmap.hpp +++ b/sponge/src/platform/opengl/scene/shadowmap.hpp @@ -36,6 +36,12 @@ class ShadowMap { const glm::mat4& getLightSpaceMatrix() const; + // World-space eye used to build the light's view matrix, for occlusion + // queries run against this map's own depth (see OcclusionCuller). + const glm::vec3& getEyePosition() const { + return eyePosition; + } + // Fits the ortho box and near/far to sceneBounds (world space) each call, // so the frustum always exactly covers the static scene regardless of // its size, instead of a fixed box sized for whatever scene existed when @@ -66,6 +72,7 @@ class ShadowMap { uint32_t shadowWidth; glm::mat4 lightSpaceMatrix{ 1.0f }; + glm::vec3 eyePosition{ 0.0f }; void initialize(); void applyBlur() const; diff --git a/sponge/src/platform/opengl/scene/unitcube.hpp b/sponge/src/platform/opengl/scene/unitcube.hpp new file mode 100644 index 00000000..66ff66ce --- /dev/null +++ b/sponge/src/platform/opengl/scene/unitcube.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include + +namespace sponge::platform::opengl::scene { + +// Unit cube centred at the origin, extent [-0.5, 0.5] per axis. Shared by +// anything that just needs a cheap box proxy: light-position debug cubes, +// occlusion-query AABB proxies. +constexpr std::array unitCubeVertices = { + glm::vec3{ -0.5, 0.5, -0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, + glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, + glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, + glm::vec3{ -0.5, 0.5, -0.5 }, glm::vec3{ -0.5, -0.5, -0.5 }, + glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, + glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, + glm::vec3{ 0.5, 0.5, 0.5 }, glm::vec3{ 0.5, -0.5, 0.5 }, + glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, 0.5 }, + glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, + glm::vec3{ 0.5, 0.5, -0.5 }, glm::vec3{ 0.5, -0.5, -0.5 }, + glm::vec3{ -0.5, -0.5, -0.5 }, glm::vec3{ 0.5, 0.5, -0.5 }, + glm::vec3{ -0.5, -0.5, -0.5 }, glm::vec3{ -0.5, 0.5, -0.5 }, + glm::vec3{ -0.5, 0.5, 0.5 }, glm::vec3{ -0.5, -0.5, 0.5 }, + glm::vec3{ 0.5, -0.5, 0.5 }, glm::vec3{ -0.5, 0.5, 0.5 }, + glm::vec3{ 0.5, -0.5, 0.5 }, glm::vec3{ 0.5, 0.5, 0.5 }, + glm::vec3{ -0.5, -0.5, 0.5 }, glm::vec3{ -0.5, -0.5, -0.5 }, + glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ -0.5, -0.5, 0.5 }, + glm::vec3{ 0.5, -0.5, -0.5 }, glm::vec3{ 0.5, -0.5, 0.5 }, +}; +constexpr uint32_t unitCubeVertexCount = 36; + +} // namespace sponge::platform::opengl::scene