Add **Hardware Occlusion Culling** to models, foliage, particles, terrain and local light shadows

This commit is contained in:
2026-08-28 19:53:10 +02:00
parent 48b1931546
commit 97186f1970
31 changed files with 793 additions and 50 deletions
+8 -3
View File
@@ -1193,15 +1193,20 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64
/// </summary>
Particles = 1 << 28,
/// <summary>
/// Shows/hides occlusion culling.
/// </summary>
OcclusionCulling = 1 << 29,
/// <summary>
/// Default flags for Game.
/// </summary>
DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky | Particles,
DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky | Particles | OcclusionCulling,
/// <summary>
/// Default flags for Editor.
/// </summary>
DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky | Particles,
DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky | Particles | OcclusionCulling,
/// <summary>
/// Default flags for materials/models previews generating.
@@ -1211,7 +1216,7 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64
/// <summary>
/// All flags enabled.
/// </summary>
All = None | DebugDraw | EditorSprites | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | Decals | DepthOfField | PhysicsDebug | Fog | MotionBlur | ContactShadows | GlobalSDF | Sky | LightsDebug | Particles,
All = None | DebugDraw | EditorSprites | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | Decals | DepthOfField | PhysicsDebug | Fog | MotionBlur | ContactShadows | GlobalSDF | Sky | LightsDebug | Particles | OcclusionCulling,
};
DECLARE_ENUM_OPERATORS(ViewFlags);
+111 -1
View File
@@ -1,16 +1,20 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "RenderBuffers.h"
#include "Engine/Core/Config/GraphicsSettings.h"
#include "RenderContext.h"
#include "RenderTools.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPULimits.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Renderer/Utils/MultiScaler.h"
#include "Engine/Renderer/Culling/IOcclusionCulling.h"
#include "Engine/Core/Config/GraphicsSettings.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Scripting/Scripting.h"
// How many frames keep cached buffers for temporal or optional effects?
#define LAZY_FRAMES_COUNT 4
bool UnsupportedOcclusionCulling = false;
RenderBuffers::RenderBuffers(const SpawnParams& params)
: ScriptingObject(params)
@@ -275,6 +279,10 @@ void RenderBuffers::Release()
for (int32 i = 0; i < _resources.Count(); i++)
_resources[i]->ReleaseGPU();
if (auto* culling = FromInterface(OcclusionCulling))
Delete(culling);
OcclusionCulling = nullptr;
RenderTargetPool::Release(VolumetricFog);
VolumetricFog = nullptr;
RenderTargetPool::Release(VolumetricFogHistory);
@@ -305,6 +313,60 @@ RenderBuffers::ReadOnlyDepthBuffer RenderBuffers::GetReadOnlyDepthBuffer() const
return { depthBufferRTV, depthBufferSRV };
}
void RenderBuffers::OnRendering(const RenderContext& renderContext)
{
// Initialize occlusion culling
if (UnsupportedOcclusionCulling)
return;
bool enableCulling = EnumHasAllFlags(renderContext.View.Flags, ViewFlags::OcclusionCulling) && !renderContext.View.IsCullingDisabled && !renderContext.View.IsSingleFrame;
const StringAnsi& occlusionCullingTypeName = GraphicsSettings::Get()->OcclusionCulling;
if (auto* culling = FromInterface(OcclusionCulling))
{
// Check if type still matches and effect is active
if (culling->GetType().Fullname != occlusionCullingTypeName || !enableCulling)
{
Delete(culling);
OcclusionCulling = nullptr;
}
}
if (!OcclusionCulling && occlusionCullingTypeName.HasChars() && enableCulling)
{
const ScriptingTypeHandle occlusionCullingType = Scripting::FindScriptingType(occlusionCullingTypeName);
if (occlusionCullingType && occlusionCullingType.GetType().GetInterface(IOcclusionCulling::TypeInitializer))
{
OcclusionCulling = ToInterface<IOcclusionCulling>(NewObject(occlusionCullingType));
if (!OcclusionCulling->IsSupported())
{
UnsupportedOcclusionCulling = true;
LOG(Error, "Occlusion Culling system '{}' is unsupported", occlusionCullingTypeName.ToString());
return;
}
if (_usedCulling)
{
// Reset existing state to use a fresh CullingIds
_cullingLocker.Lock();
for (auto& e : Scenes)
{
for (auto& q : e.Value.Geo)
{
for (auto& geo : q)
{
geo.CullingId = 0;
}
}
e.Value.CullingIds.Clear();
}
_cullingLocker.Unlock();
}
else
_usedCulling = true;
}
}
if (OcclusionCulling)
OcclusionCulling->BeginFrame(renderContext);
}
void RenderBuffers::OnSceneRendering(SceneRendering* scene)
{
if (!Scenes.ContainsKey(scene))
@@ -342,6 +404,41 @@ GeometryDrawState* RenderBuffers::GetGeometryDrawState(SceneRendering* scene, in
return nullptr;
}
bool RenderBuffers::TestOcclusionCulling(const Actor* actor, uint32& cullingId) const
{
return TestOcclusionCulling(actor->GetSceneRendering(), actor, actor->GetBox(), cullingId);
}
bool RenderBuffers::TestOcclusionCulling(SceneRendering* scene, const Actor* actor, const BoundingBox& objectBounds, uint32& cullingId, const void* object) const
{
cullingId = 0;
if (!OcclusionCulling)
return true;
bool result = true;
if (auto* sceneData = Scenes.TryGet(scene))
{
// Get stable CullingId
const Pair<const Actor*, const void*> key(actor, object);
_cullingLocker.Lock();
_cullingIdsOwnerTypes.Add(actor->GetTypeHandle());
sceneData->CullingIds.TryGet(key, cullingId);
_cullingLocker.Unlock();
// Cull
uint32 cullingIdPrev = cullingId;
result = OcclusionCulling->IsVisible(objectBounds, cullingId);
// Update CullingId if got changed
if (cullingIdPrev != cullingId)
{
_cullingLocker.Lock();
sceneData->CullingIds[key] = cullingId;
_cullingLocker.Unlock();
}
}
return result;
}
void RenderBuffers::OnSceneRenderingAddActor(SceneRendering* scene, int32 key, Actor* a)
{
// Init geo state of that object
@@ -361,6 +458,19 @@ void RenderBuffers::OnSceneRenderingUpdateActor(SceneRendering* scene, int32 key
void RenderBuffers::OnSceneRenderingRemoveActor(SceneRendering* scene, int32 key, Actor* a)
{
// Skip actors that don't have nested sub-objects
if (!_cullingIdsOwnerTypes.Contains(a->GetTypeHandle()))
return;
if (auto* sceneData = Scenes.TryGet(scene))
{
for (auto it = sceneData->CullingIds.Begin(); it.IsNotEnd(); ++it)
{
if (it->Key.First == a)
{
sceneData->CullingIds.Remove(it);
}
}
}
}
void RenderBuffers::OnSceneRenderingClear(SceneRendering* scene)
+41 -8
View File
@@ -4,6 +4,7 @@
#include "Engine/Core/Math/Viewport.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Scripting/ScriptingObject.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
@@ -29,6 +30,7 @@
class Actor;
class SceneRendering;
class IOcclusionCulling;
/// <summary>
/// The scene rendering buffers container.
@@ -64,14 +66,27 @@ private:
uint64 LastFrameHalfResDepth = 0;
uint64 LastFrameHiZ = 0;
// Scene drawing cache with the per-object state (eg. LOD transitions, motion-vectors movement)
struct SceneData
{
// Per-object drawing state (eg. LOD transition). Indexing matches actor/object key of object registered in SceneRendering.
Array<GeometryDrawState> Geo[SceneRendering::DrawCategory::MAX];
// Scene culling cache with per-object (pair of actor and subobject) CullingId used by the IOcclusionCulling. Allows for stable visibility testing of custom non-actor objects (eg. terrain chunks or foliage patches).
Dictionary<Pair<const Actor*, const void*>, uint32> CullingIds;
};
Dictionary<SceneRendering*, SceneData> Scenes;
protected:
int32 _width = 0;
int32 _height = 0;
float _aspectRatio = 0.0f;
bool _useAlpha = false;
bool _useNull = false;
bool _usedCulling = false;
Viewport _viewport;
Array<GPUTexture*, FixedAllocation<32>> _resources;
CriticalSection _cullingLocker;
mutable HashSet<ScriptingTypeHandle> _cullingIdsOwnerTypes;
public:
union
@@ -120,14 +135,6 @@ public:
// Maps the custom buffer type into the object that holds the state.
Array<CustomBuffer*, HeapAllocation> CustomBuffers;
// Scene drawing cache with the per-object state (eg. LOD transitions, motion-vectors movement)
struct SceneData
{
// Per-object drawing state (eg. LOD transition). Indexing matches actor/object key of object registered in SceneRendering.
Array<GeometryDrawState> Geo[SceneRendering::DrawCategory::MAX];
};
Dictionary<SceneRendering*, SceneData> Scenes;
public:
/// <summary>
/// Finalizes an instance of the <see cref="RenderBuffers"/> class.
@@ -264,6 +271,11 @@ public:
/// </summary>
API_FIELD() RenderBuffers* LinkedCustomBuffers = nullptr;
/// <summary>
/// Occlusion culling implementation (optional). Can skip drawing occluded objects. Maintains a state synchronized with scene rendering with container RenderBuffers.
/// </summary>
API_FIELD(ReadOnly) IOcclusionCulling* OcclusionCulling = nullptr;
public:
/// <summary>
/// Allocates the buffers.
@@ -283,6 +295,8 @@ public:
/// </summary>
ReadOnlyDepthBuffer GetReadOnlyDepthBuffer() const;
// Internal event called by Renderer to initiate drawing.
void OnRendering(const RenderContext& renderContext);
// Internal event called by SceneRendering to initiate drawing.
void OnSceneRendering(SceneRendering* scene);
@@ -291,6 +305,25 @@ public:
/// </summary>
GeometryDrawState* GetGeometryDrawState(SceneRendering* scene, int32 key, const Actor* actor) const;
/// <summary>
/// Performs the occlusion culling test for a specific sub-object of the actor (eg. terrain chunk or foliage patch) and returns the assigned CullingId (for draw call).
/// </summary>
/// <param name="actor">The owning actor.</param>
/// <param name="cullingId">Result CullingId to use for drawing this actor.</param>
/// <returns>True if actor can be rendered (is visible or visibility will be calculated on GPU), otherwise false.</returns>
bool TestOcclusionCulling(const Actor* actor, uint32& cullingId) const;
/// <summary>
/// Performs the occlusion culling test for a specific sub-object of the actor (eg. terrain chunk or foliage patch) and returns the assigned CullingId (for draw call).
/// </summary>
/// <param name="scene">The scene owning this actor.</param>
/// <param name="actor">The owning actor.</param>
/// <param name="objectBounds">The world-space bounds of the actor (or sub-object).</param>
/// <param name="cullingId">Result CullingId to use for drawing this actor (or sub-object).</param>
/// <param name="object">The pointer to the sub-actor object. Null for actor-only culling.</param>
/// <returns>True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false.</returns>
bool TestOcclusionCulling(SceneRendering* scene, const Actor* actor, const BoundingBox& objectBounds, uint32& cullingId, const void* object = nullptr) const;
public:
// [ISceneRenderingListener]
void OnSceneRenderingAddActor(SceneRendering* scene, int32 key, Actor* a) override;
+5
View File
@@ -17,6 +17,7 @@
#include "Engine/Engine/Engine.h"
#include "Engine/Profiler/Profiler.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Renderer/Culling/IOcclusionCulling.h"
#include "Engine/Threading/JobSystem.h"
#include "Engine/Threading/Threading.h"
#if USE_EDITOR
@@ -345,7 +346,11 @@ void SceneRenderTask::OnPostRender(GPUContext* context, RenderContext& renderCon
PostRender(context, renderContext);
if (Buffers)
{
if (Buffers->OcclusionCulling)
Buffers->OcclusionCulling->EndFrame(renderContext);
Buffers->ReleaseUnusedMemory();
}
}
Viewport SceneRenderTask::GetViewport() const