Add Global SDF Overdraw debug mode for content optimizations

This commit is contained in:
2026-05-20 16:20:45 +02:00
parent 8d8d69b847
commit 92a0a40d16
8 changed files with 201 additions and 11 deletions
Binary file not shown.
+1
View File
@@ -2112,6 +2112,7 @@ namespace FlaxEditor.Viewport
new ViewModeOptions(ViewMode.LODPreview, "LOD Preview", Editor.Instance.Options.Options.Input.LODPreview),
new ViewModeOptions(ViewMode.MaterialComplexity, "Material Complexity", Editor.Instance.Options.Options.Input.MaterialComplexity),
new ViewModeOptions(ViewMode.QuadOverdraw, "Quad Overdraw", Editor.Instance.Options.Options.Input.QuadOverdraw),
new ViewModeOptions(ViewMode.GlobalSDFOverdraw, "Global SDF Overdraw"),
}),
new ViewModeOptions(ViewMode.GlobalSDF, "Global SDF", Editor.Instance.Options.Options.Input.GloablSDF),
new ViewModeOptions(ViewMode.GlobalSurfaceAtlas, "Global Surface Atlas", Editor.Instance.Options.Options.Input.GlobalSurfaceAtlas),
+5
View File
@@ -1005,6 +1005,11 @@ API_ENUM() enum class ViewMode
/// Draw Global Illumination debug preview (eg. irradiance probes).
/// </summary>
GlobalIllumination = 26,
/// <summary>
/// Draw Global Sign Distant Field (SDF) overdraw to visualize performance of SDF tracing.
/// </summary>
GlobalSDFOverdraw = 27,
};
/// <summary>
@@ -923,6 +923,7 @@ void GlobalSurfaceAtlasPass::OnCollectDrawCalls(RenderContextBatch& renderContex
return;
if (GBufferPass::IsDebugView(renderContext.View.Mode) ||
renderContext.View.Mode == ViewMode::GlobalSDF ||
renderContext.View.Mode == ViewMode::GlobalSDFOverdraw ||
renderContext.View.Mode == ViewMode::QuadOverdraw ||
renderContext.View.Mode == ViewMode::MaterialComplexity)
return;
@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "GlobalSignDistanceFieldPass.h"
#include "GBufferPass.h"
#include "RenderList.h"
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Matrix3x4.h"
@@ -81,6 +82,15 @@ GPU_CB_STRUCT(ModelsRasterizeData {
uint32 MipMipOffsetX;
});
GPU_CB_STRUCT(OverdrawData {
ShaderGBufferData GBuffer;
Float3 CascadeBoundsMin;
float CascadeBoundsResolution;
Float3 CascadeBoundsMax;
float Padding30;
uint32 ChunksBuffer[512]; // (MaxVolumeRes/ChunkRes)^3 = (256/32)^3
});
struct RasterizeChunk
{
uint16 ModelsCount;
@@ -108,6 +118,7 @@ struct RasterizeObject
};
constexpr int32 RasterizeChunkKeyHashResolution = GLOBAL_SDF_RASTERIZE_CHUNK_SIZE;
#define KEY_GET_HASH(key) key.Coord.Z * (RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution) + key.Coord.Y * RasterizeChunkKeyHashResolution + key.Coord.X
struct RasterizeChunkKey
{
@@ -115,6 +126,7 @@ struct RasterizeChunkKey
uint32 Layer;
Int3 Coord;
// Moves to the next layer and permutates the hash
FORCE_INLINE void NextLayer()
{
Layer++;
@@ -152,6 +164,7 @@ struct CascadeData
// Cache
Dictionary<RasterizeChunkKey, RasterizeChunk> Chunks;
Dictionary<RasterizeChunkKey, RasterizeChunk> DebugOverdrawChunks;
Array<RasterizeObject> RasterizeObjects;
Array<byte> ObjectsData;
Array<GPUTextureView*> ObjectsTextures;
@@ -182,7 +195,7 @@ struct CascadeData
{
for (key.Coord.X = objectChunkMin.X; key.Coord.X <= objectChunkMax.X; key.Coord.X++)
{
key.Hash = key.Coord.Z * (RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution) + key.Coord.Y * RasterizeChunkKeyHashResolution + key.Coord.X;
key.Hash = KEY_GET_HASH(key);
StaticChunks.Remove(key);
}
}
@@ -195,6 +208,7 @@ class GlobalSignDistanceFieldCustomBuffer : public RenderBuffers::CustomBuffer,
public:
int32 FrameIndex = 0;
int32 Resolution = 0;
bool DebugOverdraw = false;
GPUTexture* Texture = nullptr;
GPUTexture* TextureMip = nullptr;
Vector3 Origin = Vector3::Zero;
@@ -308,6 +322,7 @@ public:
FrameIndex = 0;
AsyncRenderContext = renderContext;
AsyncRenderContext.View.Pass = DrawPass::GlobalSDF;
DebugOverdraw = renderContext.View.Mode == ViewMode::GlobalSDFOverdraw;
const bool useCache = !reset && !GLOBAL_SDF_DEBUG_FORCE_REDRAW;
static_assert(GLOBAL_SDF_RASTERIZE_CHUNK_SIZE % GLOBAL_SDF_RASTERIZE_GROUP_SIZE == 0, "Invalid chunk size for Global SDF rasterization group size.");
const int32 rasterizeChunks = Math::CeilToInt((float)resolution / (float)GLOBAL_SDF_RASTERIZE_CHUNK_SIZE);
@@ -472,6 +487,8 @@ void GlobalSignDistanceFieldCustomBuffer::UpdateCascadeChunks(CascadeData& casca
PROFILE_CPU();
// Update static chunks
if (DebugOverdraw)
cascade.DebugOverdrawChunks = cascade.Chunks;
for (auto it = cascade.Chunks.Begin(); it.IsNotEnd(); ++it)
{
auto& e = *it;
@@ -642,6 +659,7 @@ bool GlobalSignDistanceFieldPass::setupResources()
_objectsBuffer = New<DynamicStructuredBuffer>(0, (uint32)sizeof(ObjectRasterizeData), false, TEXT("GlobalSDF.ObjectsBuffer"));
// Create pipeline state
// TODO: don't compile those shaders in Release builds (and skip PSOs)
GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
if (!_psDebug)
{
@@ -650,6 +668,14 @@ bool GlobalSignDistanceFieldPass::setupResources()
if (_psDebug->Init(psDesc))
return true;
}
if (!_psOverdraw)
{
_psOverdraw = device->CreatePipelineState();
psDesc.PS = shader->GetPS("PS_Overdraw");
psDesc.BlendMode = BlendingMode::AlphaBlend;
if (_psOverdraw->Init(psDesc))
return true;
}
return false;
}
@@ -659,6 +685,7 @@ bool GlobalSignDistanceFieldPass::setupResources()
void GlobalSignDistanceFieldPass::OnShaderReloading(Asset* obj)
{
SAFE_DELETE_GPU_RESOURCE(_psDebug);
SAFE_DELETE_GPU_RESOURCE(_psOverdraw);
_csRasterizeModel0 = nullptr;
_csRasterizeModel1 = nullptr;
_csRasterizeHeightfield = nullptr;
@@ -1094,12 +1121,80 @@ void GlobalSignDistanceFieldPass::RenderDebug(RenderContext& renderContext, GPUC
context->UpdateCB(_cb0, &data);
context->BindCB(0, _cb0);
}
context->BindSR(0, bindingData.Texture ? bindingData.Texture->ViewVolume() : nullptr);
context->BindSR(1, bindingData.TextureMip ? bindingData.TextureMip->ViewVolume() : nullptr);
context->SetState(_psDebug);
context->SetRenderTarget(output->View());
context->SetViewportAndScissors(outputSize.X, outputSize.Y);
context->DrawFullscreenTriangle();
if (renderContext.View.Mode == ViewMode::GlobalSDFOverdraw)
{
auto& sdfData = *renderContext.Buffers->GetCustomBuffer<GlobalSignDistanceFieldCustomBuffer>(TEXT("GlobalSignDistanceField"));
int32 chunkRes = sdfData.Resolution / GLOBAL_SDF_RASTERIZE_CHUNK_SIZE;
OverdrawData data;
GBufferPass::SetInputs(renderContext.View, data.GBuffer);
GPUConstantBuffer* cb2 = _shader->GetShader()->GetCB(2);
context->BindCB(2, cb2);
context->BindSR(0, renderContext.Buffers->DepthBuffer);
context->BindSR(1, renderContext.Buffers->GBuffer0);
context->SetState(_psOverdraw);
context->Clear(output->View(), Color::Transparent);
// Rasterize all cascades to show overdraw
for (int32 cascadeIndex = sdfData.Cascades.Count() - 1; cascadeIndex >= 0; cascadeIndex--)
{
auto& cascade = sdfData.Cascades[cascadeIndex];
// Scale complexity down for far cascades as they are updated less often
float cascadeScale = Math::Max(1.0f - (float)cascadeIndex / (float)sdfData.Cascades.Count(), 0.3f);
// Provide per-chunk overdraw based on objects count and complexity
RasterizeChunkKey key;
for (key.Coord.Z = 0; key.Coord.Z < chunkRes; key.Coord.Z++)
{
for (key.Coord.Y = 0; key.Coord.Y < chunkRes; key.Coord.Y++)
{
for (key.Coord.X = 0; key.Coord.X < chunkRes; key.Coord.X++)
{
int32 models = 0, heightfields = 0, dynamic = 0;
key.Layer = 0;
key.Hash = KEY_GET_HASH(key);
while (auto* chunk = cascade.DebugOverdrawChunks.TryGet(key))
{
models += chunk->ModelsCount;
heightfields += chunk->HeightfieldsCount;
dynamic += chunk->Dynamic ? 1 : 0;
key.NextLayer();
}
// Calculate complexity of the chunk that is related to amount of compute shader dispatches and data amount to process
uint32 complexity = 0;
complexity = (key.Layer - 1) * 30;
complexity += (models + heightfields) * 5;
complexity += dynamic * 50;
if (heightfields > 1)
complexity += 20;
if (models > 1)
complexity += 20;
complexity = (uint32)(cascadeScale * complexity);
int32 i = key.Coord.Z * chunkRes * chunkRes + key.Coord.Y * chunkRes + key.Coord.X;
data.ChunksBuffer[i] = Math::Min<uint32>(complexity, MAX_uint8);
}
}
}
data.CascadeBoundsMin = (Float3)cascade.Bounds.Minimum;
data.CascadeBoundsMax = (Float3)cascade.Bounds.Maximum;
data.CascadeBoundsResolution = (float)sdfData.Resolution / GLOBAL_SDF_RASTERIZE_CHUNK_SIZE;
context->UpdateCB(cb2, &data);
context->DrawFullscreenTriangle();
}
context->UnBindCB(2);
}
else
{
context->BindSR(0, bindingData.Texture ? bindingData.Texture->ViewVolume() : nullptr);
context->BindSR(1, bindingData.TextureMip ? bindingData.TextureMip->ViewVolume() : nullptr);
context->SetState(_psDebug);
context->DrawFullscreenTriangle();
}
}
void GlobalSignDistanceFieldPass::GetCullingData(BoundingBox& bounds) const
@@ -1144,7 +1239,7 @@ void GlobalSignDistanceFieldPass::RasterizeModelSDF(Actor* actor, const ModelBas
for (key.Coord.X = objectChunkMin.X; key.Coord.X <= objectChunkMax.X; key.Coord.X++)
{
key.Layer = 0;
key.Hash = key.Coord.Z * (RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution) + key.Coord.Y * RasterizeChunkKeyHashResolution + key.Coord.X;
key.Hash = KEY_GET_HASH(key);
RasterizeChunk* chunk = &chunks[key];
chunk->Dynamic |= dynamic;
@@ -1205,7 +1300,7 @@ void GlobalSignDistanceFieldPass::RasterizeHeightfield(Actor* actor, GPUTexture*
for (key.Coord.X = objectChunkMin.X; key.Coord.X <= objectChunkMax.X; key.Coord.X++)
{
key.Layer = 0;
key.Hash = key.Coord.Z * (RasterizeChunkKeyHashResolution * RasterizeChunkKeyHashResolution) + key.Coord.Y * RasterizeChunkKeyHashResolution + key.Coord.X;
key.Hash = KEY_GET_HASH(key);
RasterizeChunk* chunk = &chunks[key];
chunk->Dynamic |= dynamic;
@@ -34,6 +34,7 @@ private:
bool _supported = false;
AssetReference<Shader> _shader;
GPUPipelineState* _psDebug = nullptr;
GPUPipelineState* _psOverdraw = nullptr;
GPUShaderProgramCS* _csRasterizeModel0 = nullptr;
GPUShaderProgramCS* _csRasterizeModel1 = nullptr;
GPUShaderProgramCS* _csRasterizeHeightfield = nullptr;
+6 -2
View File
@@ -404,6 +404,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
(EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI);
setup.UseGlobalSDF = (graphicsSettings->EnableGlobalSDF && EnumHasAnyFlags(view.Flags, ViewFlags::GlobalSDF)) ||
renderContext.View.Mode == ViewMode::GlobalSDF ||
renderContext.View.Mode == ViewMode::GlobalSDFOverdraw ||
setup.UseGlobalSurfaceAtlas;
setup.UseVolumetricFog = (view.Flags & ViewFlags::Fog) != ViewFlags::None;
@@ -424,6 +425,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
case ViewMode::ShadingModel:
case ViewMode::Reflections:
case ViewMode::GlobalSDF:
case ViewMode::GlobalSDFOverdraw:
case ViewMode::GlobalSurfaceAtlas:
case ViewMode::LightmapUVsDensity:
case ViewMode::MaterialComplexity:
@@ -460,6 +462,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
case ViewMode::LightmapUVsDensity:
case ViewMode::GlobalSurfaceAtlas:
case ViewMode::GlobalSDF:
case ViewMode::GlobalSDFOverdraw:
case ViewMode::MaterialComplexity:
case ViewMode::VertexColors:
drawShadows = false;
@@ -596,7 +599,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
GBufferPass::Instance()->Fill(renderContext, lightBuffer);
// Debug drawing
if (renderContext.View.Mode == ViewMode::GlobalSDF)
if (renderContext.View.Mode == ViewMode::GlobalSDF || renderContext.View.Mode == ViewMode::GlobalSDFOverdraw)
GlobalSignDistanceFieldPass::Instance()->RenderDebug(renderContext, context, lightBuffer);
else if (renderContext.View.Mode == ViewMode::GlobalSurfaceAtlas)
GlobalSurfaceAtlasPass::Instance()->RenderDebug(renderContext, context, lightBuffer);
@@ -604,7 +607,8 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
renderContext.View.Mode == ViewMode::VertexColors ||
renderContext.View.Mode == ViewMode::LightmapUVsDensity ||
renderContext.View.Mode == ViewMode::GlobalSurfaceAtlas ||
renderContext.View.Mode == ViewMode::GlobalSDF)
renderContext.View.Mode == ViewMode::GlobalSDF ||
renderContext.View.Mode == ViewMode::GlobalSDFOverdraw)
{
context->ResetRenderTarget();
context->SetRenderTarget(task->GetOutputView());
@@ -352,3 +352,86 @@ float4 PS_Debug(Quad_VS2PS input) : SV_Target
}
#endif
#ifdef _PS_Overdraw
#define NO_GBUFFER_SAMPLING
#include "./Flax/GBuffer.hlsl"
#include "./Flax/Noise.hlsl"
META_CB_BEGIN(2, OverdrawData)
GBufferData GBuffer;
float3 CascadeBoundsMin;
float CascadeBoundsResolution;
float3 CascadeBoundsMax;
float Padding30;
uint4 ChunksBuffer[512/4]; // (MaxVolumeRes/ChunkRes)^3 = (256/32)^3
META_CB_END
Texture2D Depth : register(t0);
Texture2D GBuffer0 : register(t1);
float GteOutline(float baseDepth, float2 uv, float2 offset, float2 depthSizeInv)
{
float neighborDepth = SAMPLE_RT_DEPTH(Depth, uv + offset * depthSizeInv);
return DEPTH_DIFF(baseDepth, neighborDepth) * 1000.0f;
}
float4 GetHeatmap(float value)
{
const float4 colors[4] = {
float4(0, 0.97f, 0.12f, 1),
float4(0.2f, 0.2f, 0.7f, 1),
float4(1, 0.57f, 0, 1),
float4(1, 0, 0, 1)
};
const float weights[4] = { 0.0f, 0.5f, 0.8f, 1.0f };
float4 color;
if (value < weights[1])
color = lerp(colors[0], colors[1], value / weights[1]);
else if (value < weights[2])
color = lerp(colors[1], colors[2], (value - weights[1]) / (weights[2] - weights[1]));
else
color = lerp(colors[2], colors[3], (value - weights[2]) / (weights[3] - weights[2]));
return color;
}
// Pixel shader for Global SDF overdraw drawing
META_PS(true, FEATURE_LEVEL_SM5)
float4 PS_Overdraw(Quad_VS2PS input) : SV_Target
{
// Use over-exposed diffuse as a base for scene identification
float3 diffuse = SAMPLE_RT(GBuffer0, input.TexCoord).rgb;
diffuse = saturate(Luminance(diffuse.rgb).xxx * 0.5f + 0.6f);
// Make depth-based outlines
float baseDepth = SAMPLE_RT_DEPTH(Depth, input.TexCoord);
float2 depthSize;
Depth.GetDimensions(depthSize.x, depthSize.y);
float2 depthSizeInv = 1.0f / depthSize;
float outline = GteOutline(baseDepth, input.TexCoord, float2(1, 0), depthSizeInv);
outline += GteOutline(baseDepth, input.TexCoord, float2(0, 1), depthSizeInv);
outline += GteOutline(baseDepth, input.TexCoord, float2(-1, 0), depthSizeInv);
outline += GteOutline(baseDepth, input.TexCoord, float2(0, -1), depthSizeInv);
outline = 1 - saturate(outline);
// Get position inside chunk used by the pixel position
float3 worldPos = GetWorldPos(GBuffer, input.TexCoord, baseDepth);
worldPos += rand3dTo3d(float3(input.TexCoord, diffuse.x)) * 0.1f; // Noise a bit to reduce z-fighting at chunk borders
float3 cascadeBoundsSize = CascadeBoundsMax - CascadeBoundsMin;
float3 cascadePos = (worldPos - CascadeBoundsMin) / cascadeBoundsSize;
cascadePos = floor(cascadePos * CascadeBoundsResolution) / CascadeBoundsResolution;
if (any(cascadePos < 0) || any(cascadePos >= 1)) return float4(0, 0, 0, 0); // Skip pixels outside the current cascade
//return float4(cascadePos, 1);
// Read complexity of the Global SDF rasterization at the pixel position
cascadePos *= CascadeBoundsResolution;
uint i = (uint)(cascadePos.z * CascadeBoundsResolution * CascadeBoundsResolution + cascadePos.y * CascadeBoundsResolution + cascadePos.x);
float complexity = (float)ChunksBuffer[i / 4][i % 4] / 255.0f;
// Mix complexity with depth/diffuse to improve scene readability
return float4(diffuse * outline, 1) * GetHeatmap(complexity);
}
#endif