Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions assets/shaders/slang/occlusionbox.slang
Original file line number Diff line number Diff line change
@@ -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);
};
28 changes: 23 additions & 5 deletions game/src/layer/imgui/imguilayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ bool ImGuiLayer::hasLogMenu = true;
std::vector<const char*> ImGuiLayer::levelNames;
std::vector<const char*> ImGuiLayer::categoryNames;

std::array<float, ImGuiLayer::historyLength> ImGuiLayer::meshVisibleHistory{};
int ImGuiLayer::historyOffset = 0;

using sponge::layer::Layer;
using sponge::layer::LayerStack;
using sponge::platform::opengl::renderer::AssetManager;
Expand Down Expand Up @@ -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<size_t>(historyOffset)] =
static_cast<float>(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<float>(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();
}
Expand Down
6 changes: 6 additions & 0 deletions game/src/layer/imgui/imguilayer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "layer/layerstack.hpp"
#include "logging/log.hpp"

#include <array>
#include <cstdint>
#include <optional>
#include <span>
Expand All @@ -29,6 +30,11 @@ class ImGuiLayer final : public sponge::layer::Layer {
static std::vector<const char*> levelNames;
static std::vector<const char*> categoryNames;

// Rolling per-frame history for the Info section's Meshes graph.
static constexpr int historyLength = 100;
static std::array<float, historyLength> meshVisibleHistory;
static int historyOffset;

// Main sections
static void showInfoSection();
static void showAppInfoWindow(float width);
Expand Down
101 changes: 93 additions & 8 deletions game/src/layer/mazelayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,20 +108,26 @@ void MazeLayer::finishLoading(std::vector<std::shared_ptr<Model>> builtModels) {

std::vector<sponge::scene::AABB> meshBounds;
meshBounds.reserve(model->getMeshCount());
sponge::scene::AABB objectBounds{
glm::vec3(std::numeric_limits<float>::max()),
glm::vec3(std::numeric_limits<float>::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<float>::max()),
glm::vec3(std::numeric_limits<float>::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 =
Expand Down Expand Up @@ -163,6 +170,9 @@ void MazeLayer::finishLoading(std::vector<std::shared_ptr<Model>> builtModels) {

shadowMap = std::make_unique<ShadowMap>(directionalLight.shadowMapRes);
cube = std::make_unique<Cube>();
occlusionCuller = std::make_unique<OcclusionCuller>(objectModels.size());
shadowOcclusionCuller =
std::make_unique<OcclusionCuller>(objectModels.size());

fxaa = std::make_unique<FXAA>(Maze::get().getWindow()->getWidth(),
Maze::get().getWindow()->getHeight());
Expand Down Expand Up @@ -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);
Expand All @@ -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(),
Expand Down Expand Up @@ -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);
Expand All @@ -812,6 +841,9 @@ void MazeLayer::renderGameObjects(const thread::MazeRenderFrame& frame) const {
.count();
submitMicros.store(static_cast<uint32_t>(submitUs),
std::memory_order_relaxed);
occlusionVisibleCount.store(occlusionVisible, std::memory_order_relaxed);
occlusionTotalCount.store(static_cast<uint32_t>(frame.objectModels.size()),
std::memory_order_relaxed);

shader->unbind();
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}

Expand Down
31 changes: 31 additions & 0 deletions game/src/layer/mazelayer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<std::vector<sponge::scene::AABB>> 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<sponge::scene::AABB> 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<sponge::platform::opengl::scene::OcclusionCuller>
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<sponge::platform::opengl::scene::OcclusionCuller>
shadowOcclusionCuller;
std::unique_ptr<sponge::platform::opengl::scene::ClusteredLights>
clusteredLights;
std::shared_ptr<sponge::platform::opengl::renderer::Shader>
Expand Down Expand Up @@ -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<uint32_t> submitMicros{ 0 };
mutable std::atomic<uint32_t> occlusionVisibleCount{ 0 };
mutable std::atomic<uint32_t> occlusionTotalCount{ 0 };
float ambientStrength = .25F;
float ao = .25F;
int32_t attenuationIndex = 4;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading