From 97186f19700ce16255f705207ae2d3ac8ed5da74 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 28 Aug 2026 19:53:10 +0200 Subject: [PATCH] Add **Hardware Occlusion Culling** to models, foliage, particles, terrain and local light shadows --- Content/Shaders/Quad.flax | 4 +- Content/Shaders/Utils/Culling.flax | 3 + Source/Editor/Cooker/Steps/DeployDataStep.cpp | 1 + Source/Engine/Core/Config/GraphicsSettings.h | 21 ++ Source/Engine/Foliage/Foliage.cpp | 12 +- Source/Engine/Foliage/Foliage.h | 1 + Source/Engine/Graphics/Enums.h | 11 +- Source/Engine/Graphics/RenderBuffers.cpp | 112 +++++- Source/Engine/Graphics/RenderBuffers.h | 49 ++- Source/Engine/Graphics/RenderTask.cpp | 5 + Source/Engine/Level/Actors/AnimatedModel.cpp | 12 +- Source/Engine/Level/Actors/PointLight.cpp | 7 + Source/Engine/Level/Actors/SplineModel.cpp | 22 +- Source/Engine/Level/Actors/SpotLight.cpp | 12 +- Source/Engine/Level/Actors/StaticModel.cpp | 12 +- Source/Engine/Particles/ParticleEffect.cpp | 8 +- Source/Engine/Particles/Particles.cpp | 6 +- Source/Engine/Particles/Particles.h | 4 +- .../Culling/HardwareOcclusionCulling.cpp | 328 ++++++++++++++++++ .../Culling/HardwareOcclusionCulling.h | 65 ++++ .../Renderer/Culling/IOcclusionCulling.h | 62 ++++ Source/Engine/Renderer/DrawCall.h | 5 + .../Renderer/Editor/QuadOverdrawPass.cpp | 6 +- Source/Engine/Renderer/GBufferPass.cpp | 5 + Source/Engine/Renderer/Renderer.cpp | 8 +- Source/Engine/Terrain/Terrain.cpp | 29 +- Source/Engine/Terrain/Terrain.h | 2 +- Source/Engine/Terrain/TerrainChunk.cpp | 4 +- Source/Engine/Terrain/TerrainChunk.h | 3 +- Source/Engine/UI/TextRender.cpp | 10 +- Source/Shaders/Utils/Culling.shader | 14 + 31 files changed, 793 insertions(+), 50 deletions(-) create mode 100644 Content/Shaders/Utils/Culling.flax create mode 100644 Source/Engine/Renderer/Culling/HardwareOcclusionCulling.cpp create mode 100644 Source/Engine/Renderer/Culling/HardwareOcclusionCulling.h create mode 100644 Source/Engine/Renderer/Culling/IOcclusionCulling.h create mode 100644 Source/Shaders/Utils/Culling.shader diff --git a/Content/Shaders/Quad.flax b/Content/Shaders/Quad.flax index 117671fdf..93abc1f70 100644 --- a/Content/Shaders/Quad.flax +++ b/Content/Shaders/Quad.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67c6fe843ae15a94b005b526ecb77df178550b723687eea0aa09ee09cab29dd3 -size 4577 +oid sha256:2703fc03f3e90b68e852a8ed2f4c2fbee83b47f304becc38a8261728a52bad1f +size 4594 diff --git a/Content/Shaders/Utils/Culling.flax b/Content/Shaders/Utils/Culling.flax new file mode 100644 index 000000000..b24424f3c --- /dev/null +++ b/Content/Shaders/Utils/Culling.flax @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ed1450e1f11317da6235e802c0c19e9e6128251dc6545a60b730fcf128722db +size 883 diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index ff4f2fb91..2313d7317 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -406,6 +406,7 @@ bool DeployDataStep::Perform(CookingData& data) data.AddRootEngineAsset(TEXT("Shaders/Reflections/SSR")); data.AddRootEngineAsset(TEXT("Shaders/Shadows/Shadows")); data.AddRootEngineAsset(TEXT("Shaders/Utils/BitonicSort")); + data.AddRootEngineAsset(TEXT("Shaders/Utils/Culling")); //data.AddRootEngineAsset(TEXT("Shaders/Utils/DebugDraw")); // TODO: debug draw in dev-builds data.AddRootEngineAsset(TEXT("Shaders/Utils/GlobalSignDistanceField")); data.AddRootEngineAsset(TEXT("Shaders/Utils/GPUParticlesSorting")); diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h index cf79413d4..12441c639 100644 --- a/Source/Engine/Core/Config/GraphicsSettings.h +++ b/Source/Engine/Core/Config/GraphicsSettings.h @@ -54,6 +54,7 @@ public: API_FIELD(Attributes="EditorOrder(20), EditorDisplay(\"General\", \"Use V-Sync\")") bool UseVSync = false; +public: /// /// Anti Aliasing quality setting. /// @@ -96,6 +97,26 @@ public: API_FIELD(Attributes="EditorOrder(1320), EditorDisplay(\"Quality\", \"Allow CSM Blending\")") bool AllowCSMBlending = false; +public: + /// + /// The type of the occlusion culling (implements IOcclusionCulling) that will be used to test visibility of the objects and skip rendering occluded ones. Can be left empty to use frustum-culling only. + /// + API_FIELD(Attributes="EditorOrder(1450), EditorDisplay(\"Culling\"), TypeReference(typeof(IOcclusionCulling)), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")") + StringAnsi OcclusionCulling; + + /// + /// The number of buffered frames for the visibility query results readback from GPU (to avoid stalls). The higher value the more latency but less CPU stalls (to wait for GPU results). Higher values increase object popping artifacts. + /// + API_FIELD(Attributes="EditorOrder(1460), EditorDisplay(\"Culling\"), Limit(1, 4)") + int32 OcclusionBufferedFrames = 2; + + /// + /// The object bounds scale for occlusion culling to reduce popping artifacts caused by latency of visibility readback from GPU to CPU. Higher values inflate bounds which improves visual stability but lowers occlusion culling efficiency. + /// + API_FIELD(Attributes="EditorOrder(1461), EditorDisplay(\"Culling\"), Limit(1, 1.5f)") + float OcclusionBoundsScale = 1.1f; + +public: /// /// Default probes cubemap resolution (use for Environment Probes, can be overriden per-actor). /// diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index e797d77db..9558a3d0e 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -10,6 +10,8 @@ #include "Engine/Engine/Engine.h" #include "Engine/Graphics/Graphics.h" #include "Engine/Graphics/RenderTools.h" +#include "Engine/Graphics/RenderContext.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Content/Deprecated.h" #if !FOLIAGE_USE_SINGLE_QUAD_TREE @@ -19,8 +21,7 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Renderer/RenderList.h" -#include "Engine/Graphics/RenderContext.h" -#include "Engine/Renderer/GlobalSignDistanceFieldPass.h" +#include "Engine/Renderer/Utils/GlobalSignDistanceFieldPass.h" #include "Engine/Renderer/GI/GlobalSurfaceAtlasPass.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Serialization/MemoryReadStream.h" @@ -174,6 +175,12 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCal return; //DebugDraw::DrawBox(cluster->Bounds, Color::Red); + // Main-view occlusion culling + uint32 cullingId = 0; + bool isMain = context.RenderContext.View.Pass != DrawPass::Depth; + if (isMain && !context.RenderContext.Buffers->TestOcclusionCulling(context.Scene, this, cluster->TotalBounds, cullingId, cluster)) + return; + // Draw visible children if (cluster->Children[0]) { @@ -487,6 +494,7 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Me type.Model->HighestResidentLODIndex(), type.Model->GetLODsCount() - 1, renderContext.View.CullingFrustum, + GetSceneRendering(), }; _cachedDrawWorldOrigin = renderContext.View.Origin; if (context.RenderContext.View.Pass != DrawPass::Depth) diff --git a/Source/Engine/Foliage/Foliage.h b/Source/Engine/Foliage/Foliage.h index 801e51198..a9056bf62 100644 --- a/Source/Engine/Foliage/Foliage.h +++ b/Source/Engine/Foliage/Foliage.h @@ -172,6 +172,7 @@ private: Float3 LodViewPosition; int32 MinLOD, MaxLOD; BoundingFrustum CullingFrustum; + SceneRendering* Scene; FORCE_INLINE int32 ClampLODIndex(int32 index) const { diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 28200a895..25888c3eb 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -1193,15 +1193,20 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// Particles = 1 << 28, + /// + /// Shows/hides occlusion culling. + /// + OcclusionCulling = 1 << 29, + /// /// Default flags for Game. /// - 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, /// /// Default flags for Editor. /// - 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, /// /// Default flags for materials/models previews generating. @@ -1211,7 +1216,7 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// /// All flags enabled. /// - 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); diff --git a/Source/Engine/Graphics/RenderBuffers.cpp b/Source/Engine/Graphics/RenderBuffers.cpp index 3e0422d24..b5fb094a8 100644 --- a/Source/Engine/Graphics/RenderBuffers.cpp +++ b/Source/Engine/Graphics/RenderBuffers.cpp @@ -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(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 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) diff --git a/Source/Engine/Graphics/RenderBuffers.h b/Source/Engine/Graphics/RenderBuffers.h index 2b4cd9bb3..6cb168ac8 100644 --- a/Source/Engine/Graphics/RenderBuffers.h +++ b/Source/Engine/Graphics/RenderBuffers.h @@ -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; /// /// 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 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, uint32> CullingIds; + }; + Dictionary Scenes; + protected: int32 _width = 0; int32 _height = 0; float _aspectRatio = 0.0f; bool _useAlpha = false; bool _useNull = false; + bool _usedCulling = false; Viewport _viewport; Array> _resources; + CriticalSection _cullingLocker; + mutable HashSet _cullingIdsOwnerTypes; public: union @@ -120,14 +135,6 @@ public: // Maps the custom buffer type into the object that holds the state. Array 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 Geo[SceneRendering::DrawCategory::MAX]; - }; - Dictionary Scenes; - public: /// /// Finalizes an instance of the class. @@ -264,6 +271,11 @@ public: /// API_FIELD() RenderBuffers* LinkedCustomBuffers = nullptr; + /// + /// Occlusion culling implementation (optional). Can skip drawing occluded objects. Maintains a state synchronized with scene rendering with container RenderBuffers. + /// + API_FIELD(ReadOnly) IOcclusionCulling* OcclusionCulling = nullptr; + public: /// /// Allocates the buffers. @@ -283,6 +295,8 @@ public: /// 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: /// GeometryDrawState* GetGeometryDrawState(SceneRendering* scene, int32 key, const Actor* actor) const; + /// + /// 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). + /// + /// The owning actor. + /// Result CullingId to use for drawing this actor. + /// True if actor can be rendered (is visible or visibility will be calculated on GPU), otherwise false. + bool TestOcclusionCulling(const Actor* actor, uint32& cullingId) const; + + /// + /// 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). + /// + /// The scene owning this actor. + /// The owning actor. + /// The world-space bounds of the actor (or sub-object). + /// Result CullingId to use for drawing this actor (or sub-object). + /// The pointer to the sub-actor object. Null for actor-only culling. + /// True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false. + 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; diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp index 0f6c23a30..12b4ffc6e 100644 --- a/Source/Engine/Graphics/RenderTask.cpp +++ b/Source/Engine/Graphics/RenderTask.cpp @@ -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 diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 4f45504b8..faa169ad3 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -21,6 +21,7 @@ #include "Engine/Graphics/Models/MeshDeformation.h" #include "Engine/Renderer/DrawCall.h" #include "Engine/Renderer/RenderList.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Level/SceneObjectsFactory.h" #include "Engine/Profiler/Profiler.h" @@ -1262,6 +1263,15 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) const Float3 translation = _transform.Translation - renderContext.View.Origin; Matrix::Transformation(_transform.Scale, _transform.Orientation, translation, world); auto drawState = renderContext.Buffers->GetGeometryDrawState(&GetScene()->Rendering, _sceneRenderingKey, this); + SkinnedMesh::DrawInfo draw; + draw.DrawModes = DrawModes; + if (drawState && renderContextBatch.Buffers->OcclusionCulling && !renderContextBatch.Buffers->OcclusionCulling->IsVisible(_box, *drawState)) + { + // Draw shadows-only (or cull) + draw.DrawModes &= DrawPass::Depth; + if (renderContextBatch.Contexts.Count() == 1 || draw.DrawModes == DrawPass::None) + return; + } GEOMETRY_DRAW_STATE_EVENT_BEGIN(drawState, world); _lastMinDstSqr = Math::Min(_lastMinDstSqr, Vector3::DistanceSquared(_transform.Translation, renderContext.View.WorldPosition)); @@ -1271,7 +1281,6 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) if (_bones.IsDirty) _bones.Flush(); - SkinnedMesh::DrawInfo draw; draw.Buffer = &Entries; draw.SkinningBones = RenderListExtension.GlobalBuffer; draw.SkinningBonesOffset = _bones.GlobalBufferOffset / sizeof(Matrix3x4); @@ -1289,7 +1298,6 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) draw.World = &world; draw.DrawState = drawState; draw.Deformation = _deformation; - draw.DrawModes = DrawModes; draw.Bounds = _sphere; draw.Bounds.Center -= renderContext.View.Origin; draw.PerInstanceRandom = GetPerInstanceRandom(); diff --git a/Source/Engine/Level/Actors/PointLight.cpp b/Source/Engine/Level/Actors/PointLight.cpp index 89ab2763b..3dc5c6aaf 100644 --- a/Source/Engine/Level/Actors/PointLight.cpp +++ b/Source/Engine/Level/Actors/PointLight.cpp @@ -2,6 +2,7 @@ #include "PointLight.h" #include "Engine/Content/Deprecated.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderContext.h" @@ -123,6 +124,12 @@ void PointLight::Draw(RenderContextBatch& renderContextBatch) data.StaticFlags = GetStaticFlags(); data.ID = GetID(); data.ScreenSize = Math::Min(1.0f, Math::Sqrt(RenderTools::ComputeBoundsScreenRadiusSquared(position, (float)_sphere.Radius, renderContext.View))); + uint32 cullingId = 0; + if (data.CanRenderShadow(renderContext.View) && !renderContext.Buffers->TestOcclusionCulling(this, cullingId)) + { + // Occlusion cull dynamic shadow + data.ShadowsMode = ShadowsCastingMode::None; + } renderContext.List->PointLights.Add(data); } } diff --git a/Source/Engine/Level/Actors/SplineModel.cpp b/Source/Engine/Level/Actors/SplineModel.cpp index 1f702d59e..b179a6a1f 100644 --- a/Source/Engine/Level/Actors/SplineModel.cpp +++ b/Source/Engine/Level/Actors/SplineModel.cpp @@ -11,12 +11,15 @@ #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUBuffer.h" #include "Engine/Graphics/GPUContext.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderContext.h" #include "Engine/Graphics/RenderTools.h" +#include "Engine/Level/Scene/Scene.h" #include "Engine/Level/Scene/SceneRendering.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Renderer/DrawCall.h" #include "Engine/Renderer/RenderList.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" #if USE_EDITOR #include "Editor/Editor.h" #endif @@ -392,6 +395,17 @@ void SplineModel::Draw(RenderContextBatch& renderContextBatch) return; // TODO: Spline Model rendering to Global SDF if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas) return; // TODO: Spline Model rendering to Global Surface Atlas + auto drawState = renderContext.Buffers->GetGeometryDrawState(&GetScene()->Rendering, _sceneRenderingKey, this); + ACTOR_GET_WORLD_MATRIX(this, view, world); + DrawPass drawModes = DrawModes; + if (drawState && renderContextBatch.Buffers->OcclusionCulling && !renderContextBatch.Buffers->OcclusionCulling->IsVisible(_box, *drawState)) + { + // Draw shadows-only (or cull) + drawModes &= DrawPass::Depth; + if (renderContextBatch.Contexts.Count() == 1 || drawModes == DrawPass::None) + return; + } + GEOMETRY_DRAW_STATE_EVENT_BEGIN(drawState, world); if (!Entries.IsValidFor(model)) Entries.Setup(model); @@ -466,16 +480,18 @@ void SplineModel::Draw(RenderContextBatch& renderContextBatch) // Check if skip rendering const auto shadowsMode = entry.ShadowsMode & slot.ShadowsMode; - const auto drawModes = DrawModes & material->GetDrawModes(); - if (drawModes == DrawPass::None) + const auto meshDrawModes = drawModes & material->GetDrawModes(); + if (meshDrawModes == DrawPass::None) continue; // Submit draw call mesh->GetDrawCallGeometry(drawCall); drawCall.Material = material; - renderContext.List->AddDrawCall(renderContextBatch, drawModes, _staticFlags, shadowsMode, instanceSphere, drawCall, entry.ReceiveDecals); + renderContext.List->AddDrawCall(renderContextBatch, meshDrawModes, _staticFlags, shadowsMode, instanceSphere, drawCall, entry.ReceiveDecals); } } + + GEOMETRY_DRAW_STATE_EVENT_END(drawState, world); } bool SplineModel::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) diff --git a/Source/Engine/Level/Actors/SpotLight.cpp b/Source/Engine/Level/Actors/SpotLight.cpp index 164b5680d..1a23e1958 100644 --- a/Source/Engine/Level/Actors/SpotLight.cpp +++ b/Source/Engine/Level/Actors/SpotLight.cpp @@ -1,13 +1,13 @@ // Copyright (c) Wojciech Figat. All rights reserved. #include "SpotLight.h" - #include "Engine/Content/Deprecated.h" -#include "Engine/Graphics/RenderView.h" -#include "Engine/Renderer/RenderList.h" #include "Engine/Content/Assets/IESProfile.h" +#include "Engine/Graphics/RenderView.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderContext.h" #include "Engine/Graphics/RenderTools.h" +#include "Engine/Renderer/RenderList.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Level/Scene/SceneRendering.h" @@ -175,6 +175,12 @@ void SpotLight::Draw(RenderContextBatch& renderContextBatch) data.StaticFlags = GetStaticFlags(); data.ID = GetID(); data.ScreenSize = Math::Min(1.0f, Math::Sqrt(RenderTools::ComputeBoundsScreenRadiusSquared(position, (float)_sphere.Radius, renderContext.View))); + uint32 cullingId = 0; + if (data.CanRenderShadow(renderContext.View) && !renderContext.Buffers->TestOcclusionCulling(this, cullingId)) + { + // Occlusion cull dynamic shadow + data.ShadowsMode = ShadowsCastingMode::None; + } renderContext.List->SpotLights.Add(data); } } diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index 5fe92ace8..c8cb1fbee 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -17,6 +17,7 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Renderer/DrawCall.h" #include "Engine/Renderer/Utils/GlobalSignDistanceFieldPass.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" #include "Engine/Renderer/GI/GlobalSurfaceAtlasPass.h" #include "Engine/Utilities/Encryption.h" #if USE_EDITOR @@ -386,11 +387,19 @@ void StaticModel::Draw(RenderContextBatch& renderContextBatch) } auto drawState = renderContext.Buffers->GetGeometryDrawState(&GetScene()->Rendering, _sceneRenderingKey, this); ACTOR_GET_WORLD_MATRIX(this, view, world); + Mesh::DrawInfo draw; + draw.DrawModes = _drawModes; + if (drawState && renderContextBatch.Buffers->OcclusionCulling && !renderContextBatch.Buffers->OcclusionCulling->IsVisible(_box, *drawState)) + { + // Draw shadows-only (or cull) + draw.DrawModes &= DrawPass::Depth; + if (renderContextBatch.Contexts.Count() == 1 || draw.DrawModes == DrawPass::None) + return; + } GEOMETRY_DRAW_STATE_EVENT_BEGIN(drawState, world); if (_vertexColorsDirty) FlushVertexColors(); - Mesh::DrawInfo draw; draw.Buffer = &Entries; draw.World = &world; draw.DrawState = drawState; @@ -398,7 +407,6 @@ void StaticModel::Draw(RenderContextBatch& renderContextBatch) draw.Lightmap = _scene && Lightmap.TextureIndex != -1 ? _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex) : nullptr; draw.LightmapUVs = &Lightmap.UVsArea; draw.Flags = _staticFlags; - draw.DrawModes = _drawModes; draw.Bounds = _sphere; draw.Bounds.Center -= renderContext.View.Origin; draw.PerInstanceRandom = GetPerInstanceRandom(); diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index d703e3c26..c6e9c9483 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -10,7 +10,9 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Engine.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderContext.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" #if USE_EDITOR #include "Editor/Editor.h" #include "Editor/Managed/ManagedEditor.h" @@ -613,7 +615,11 @@ void ParticleEffect::Draw(RenderContextBatch& renderContextBatch) mainView.Pass == DrawPass::GlobalSurfaceAtlas || EnumHasNoneFlags(mainView.Flags, ViewFlags::Particles)) return; - Particles::DrawParticles(renderContextBatch, this); + auto drawState = renderContext.Buffers->GetGeometryDrawState(&GetScene()->Rendering, _sceneRenderingKey, this); + DrawPass drawModes = DrawModes; + if (drawState && renderContextBatch.Buffers->OcclusionCulling && !renderContextBatch.Buffers->OcclusionCulling->IsVisible(_box, *drawState)) + drawModes &= DrawPass::Depth; + Particles::DrawParticles(renderContextBatch, this, drawModes); // Cull again against the main context (if using multiple ones) to skip caching draw distance from shadow projections const BoundingSphere bounds(_sphere.Center - mainView.Origin, _sphere.Radius); diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index a9d3bcf0f..415dd91da 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1137,7 +1137,7 @@ void DrawEmitterGPU(RenderContextBatch& renderContextBatch, ParticleBuffer* buff #endif -void Particles::DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect) +void Particles::DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect, DrawPass drawModes) { PROFILE_CPU(); PROFILE_MEM(Particles); @@ -1153,7 +1153,7 @@ void Particles::DrawParticles(RenderContextBatch& renderContextBatch, ParticleEf for (int32 i = 0; i < renderContextBatch.Contexts.Count(); i++) { const RenderView& view = renderContextBatch.Contexts.Get()[i].View; - const bool visible = (view.Pass & effect->DrawModes) != DrawPass::None && (view.IsCullingDisabled || view.CullingFrustum.Intersects(bounds)); + const bool visible = (view.Pass & drawModes) != DrawPass::None && (view.IsCullingDisabled || view.CullingFrustum.Intersects(bounds)); if (visible) { drawAnyView = true; @@ -1163,7 +1163,7 @@ void Particles::DrawParticles(RenderContextBatch& renderContextBatch, ParticleEf } if (drawAnyView == false) return; - viewsDrawModes &= effect->DrawModes; + viewsDrawModes &= drawModes; // Setup ScopeReadLock systemScope(SystemLocker); diff --git a/Source/Engine/Particles/Particles.h b/Source/Engine/Particles/Particles.h index 2007a3d8d..288e144cd 100644 --- a/Source/Engine/Particles/Particles.h +++ b/Source/Engine/Particles/Particles.h @@ -3,6 +3,7 @@ #pragma once #include "Engine/Scripting/ScriptingType.h" +#include "Engine/Graphics/Enums.h" class TaskGraphSystem; struct RenderContextBatch; @@ -49,7 +50,8 @@ public: /// /// The rendering context. /// The owning actor. - static void DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect); + /// The drawing modes. + static void DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect, DrawPass drawModes); #if USE_EDITOR /// diff --git a/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.cpp b/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.cpp new file mode 100644 index 000000000..4ac40f6f7 --- /dev/null +++ b/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.cpp @@ -0,0 +1,328 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#include "HardwareOcclusionCulling.h" +#include "Engine/Renderer/DrawCall.h" +#include "Engine/Content/Content.h" +#include "Engine/Content/Assets/Shader.h" +#include "Engine/Graphics/GPUContext.h" +#include "Engine/Graphics/GPUDevice.h" +#include "Engine/Graphics/GPUPass.h" +#include "Engine/Graphics/GPUPipelineState.h" +#include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/RenderBuffers.h" +#include "Engine/Graphics/RenderContext.h" +#include "Engine/Graphics/Shaders/GPUShader.h" +#include "Engine/Graphics/Shaders/GPUVertexLayout.h" +#include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerGPU.h" +#include "Engine/Core/Config/GraphicsSettings.h" +#include "Engine/Engine/Engine.h" + +HardwareOcclusionCulling::HardwareOcclusionCulling(const SpawnParams& params) + : ScriptingObject(params) + , _vertexBuffer(0, sizeof(Float3), TEXT("HardwareOcclusionCulling.VB"), GPUVertexLayout::Get({ { VertexElement::Types::Position, 0, 0, 0, PixelFormat::R32G32B32_Float } })) + , _shader(Content::LoadAsyncInternal(TEXT("Shaders/Utils/Culling"))) +{ +} + +HardwareOcclusionCulling::~HardwareOcclusionCulling() +{ + SAFE_DELETE_GPU_RESOURCE(_indexBuffer); +} + +void HardwareOcclusionCulling::BeginFrame(const RenderContext& renderContext) +{ + PROFILE_CPU(); + + // Read settings + auto settings = GraphicsSettings::Get(); + auto framesCount = Math::Clamp(settings->OcclusionBufferedFrames, 1, MaxFrames); + if (_framesCount != framesCount) + { + // Reset state (graphics backend recycles stale queries) + _framesCount = framesCount; + for (auto& e : _items) + { + Platform::MemoryClear(e.Queries, sizeof(e.Queries) + sizeof(e.Frames)); + } + } + _boundsScale = Math::Max(settings->OcclusionBoundsScale, 1.01f); + + // Handle origin-relative rendering + _forceUpdateBounds = renderContext.View.Origin != _origin; + _origin = renderContext.View.Origin; + _viewPos = renderContext.View.WorldPosition; + + // Skip reading occlusion results on camera cuts (but issue queries for the next frame) + bool forceVisible = renderContext.Task->IsCameraCut; + + // Skip reading when view was not rendered for some time (queries might expire) + uint64 engineFrame = Engine::FrameCount; + forceVisible |= (int32)(engineFrame - _lastEngineFrameUsed) >= framesCount; + _lastEngineFrameUsed = engineFrame; + + if (forceVisible) + { + // Reset visibility + for (auto& item : _items) + { + item.Occluded = false; + Platform::MemoryClear(item.Queries, sizeof(item.Queries) + sizeof(item.Frames)); + } + } + else if (_frameCounter > 0) + { + // Resolve the last buffered frame results (with wait) + PROFILE_CPU_NAMED("Wait for Occlusion Queries"); + ZoneColor(TracyWaitZoneColor); + int32 frame = _frameCounter, bufferedFrame = _frameCounter % framesCount, itemsUsed = 0; + auto device = GPUDevice::Instance; + for (auto& item : _items) + { + uint64 query = item.Queries[bufferedFrame]; + int32 lag = frame - item.Frames[bufferedFrame]; + if (query && lag <= framesCount) + { + // Clear query + item.Queries[bufferedFrame] = 0; + + // Read result (occluded object didn't pass any depth test, assume visible if query failed) + uint64 result = 1; + device->GetQueryResult(query, result, true); + item.Occluded = result == 0; + itemsUsed++; + } + else + { + // Maintain state if no new query has been issued (eg. object goes outside frustum or gets hidden) + } + } + ZoneValue(itemsUsed); + } + + // Remove used free items + _freeItems.Resize(Math::Max((int32)_freeItemsCount, 0)); + +#if 0 // TODO: find a different way as there might be some invisible object with CullingId assigned and drawing it later will overlap with reused IDs + // Trim history + constexpr int32 frameTTL = 20; + if (_frameCounter % 10 == 0 && _frameCounter > frameTTL) + { + const int32 lastFrame = _frameCounter - frameTTL; + for (int32 i = 0; i < _items.Count(); i++) + { + auto& item = _items.Get()[i]; + if (item.LastUsedFrame && item.LastUsedFrame < lastFrame) + { + Platform::MemoryClear(&item, sizeof(item)); + _freeItems.Add(i); + } + } + } +#endif + + // Allocate new items (as requested during the previous frame) + if (_newItemsCount > 0) + { + int32 itemsStart = _items.Count(), count = (int32)_newItemsCount, freeStart = _freeItems.Count(); + if (itemsStart == 0) + count++; // 0 is invalid for cullingId + _items.AddZeroed(count); + _freeItems.AddUninitialized(count); + for (int32 i = 0; i < count; i++) + _freeItems.Get()[freeStart + i] = itemsStart + i; + if (itemsStart == 0) + _freeItems.RemoveAt(0); // 0 is invalid for cullingId + _newItemsCount = 0; + } + _freeItemsCount = _freeItems.Count(); + + // Prepare vertex buffer to build geometry bound meshes in async during drawing + _vertexBuffer.Data.Resize(_items.Count() * 8 * sizeof(Float3), true); + _dirtyBounds = 0; +} + +void HardwareOcclusionCulling::EndFrame(const RenderContext& renderContext) +{ + // Move to the next frame + _frameCounter++; +} + +void HardwareOcclusionCulling::Submit(const RenderContext& renderContext) +{ + if (_items.IsEmpty() || !_shader || !_shader->IsLoaded()) + return; + PROFILE_CPU(); + PROFILE_GPU("Occlusion Culling"); + GPUContext* context = GPUDevice::Instance->GetMainContext(); + + // Setup vertex and index buffers + if (!_indexBuffer) + { + const uint16 cubeIndices[12 * 3] = + { + 0, 2, 3, + 0, 3, 1, + 4, 5, 7, + 4, 7, 6, + 0, 1, 5, + 0, 5, 4, + 2, 6, 7, + 2, 7, 3, + 0, 4, 6, + 0, 6, 2, + 1, 3, 7, + 1, 7, 5, + }; + auto desc = GPUBufferDescription::Index(sizeof(uint16), ARRAY_COUNT(cubeIndices), cubeIndices); + _indexBuffer = GPUDevice::Instance->CreateBuffer(TEXT("HardwareOcclusionCulling.IB")); + _indexBuffer->Init(desc); + } + if (_dirtyBounds) + _vertexBuffer.Flush(context); + auto vb = _vertexBuffer.GetBuffer(); + + // Use depth-only for testing visibility + GPUDrawPass pass(context, *renderContext.Buffers->DepthBuffer, GPUDrawPassAction::Load, Span(), Span()); + if (!_pso) + { + _pso = GPUDevice::Instance->CreatePipelineState(); + auto desc = GPUPipelineState::Description::Default; + desc.DepthWriteEnable = false; + desc.DepthClipEnable = false; + desc.DepthFunc = ComparisonFunc::DefaultEqual; + desc.StencilEnable = false; + desc.CullMode = CullMode::Inverted; + desc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::None; + desc.VS = _shader->GPU->GetVS("VS_HardwareOcclusionCulling"); + if (_pso->Init(desc)) + return; +#if COMPILE_WITH_DEV_ENV + _shader->Reloading.Bind(this); +#endif + } + auto cb = _shader->GPU->GetCB(0); + { + Matrix viewProjectionMatrix; + Matrix::Transpose(renderContext.View.ViewProjection(), viewProjectionMatrix); + context->UpdateCB(cb, &viewProjectionMatrix); + } + context->BindCB(0, cb); + context->BindIB(_indexBuffer); + context->BindVB(Span(&vb, 1)); + context->SetState(_pso); +#if COMPILE_WITH_PROFILER + auto stats = RenderStatsData::Counter; +#endif + + // Issue occlusion queries + int32 frame = _frameCounter, bufferedFrame = _frameCounter % _framesCount, itemsCount = _items.Count(), itemsUsed = 0; + auto frustum = renderContext.View.Frustum; + auto* items = _items.Get(); + for (int32 i = 0; i < itemsCount; i++) + { + auto& item = items[i]; + if (item.LastUsedFrame != frame || frustum.Contains(item.Bounds) == ContainmentType::Disjoint) + continue; + itemsUsed++; + + // Begin occlusion query for this object index + uint64 query = context->BeginQuery(GPUQueryType::BinaryOcclusion); + item.Queries[bufferedFrame] = query; + item.Frames[bufferedFrame] = frame; + + // Draw the low-poly bounds of that object + context->DrawIndexed(12 * 3, i * 8); + + // End occlusion query + context->EndQuery(query); + } + ZoneValue(itemsUsed); + +#if COMPILE_WITH_PROFILER + // Cancel-out any draw stats from profiler (hidden draws) + RenderStatsData::Counter = stats; +#endif +} + +bool HardwareOcclusionCulling::IsVisible(const BoundingBox& bounds, uint32& cullingId) +{ + return IsVisible(bounds, cullingId, nullptr); +} + +bool HardwareOcclusionCulling::IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState) +{ + return IsVisible(bounds, drawState.CullingId, &drawState); +} + +bool HardwareOcclusionCulling::IsVisible(BoundingBox bounds, uint32& cullingId, GeometryDrawState* drawState) +{ + // Enlarge bounds to reduce popping + bounds = BoundingBox::MakeScaled(bounds, _boundsScale); + // TODO: use camera motion to enlarge bounds + // TODO: use object motion (from prev frame world matrix) to enlarge bounds in the direction of movement to reduce popping + + // Assume visible when view is right inside the bounds + if (bounds.Contains(_viewPos) == ContainmentType::Contains) + return true; + + // Check if object doesn't have ID assigned yet + if (cullingId == 0 || cullingId >= (uint32)_items.Count()) + { + int64 freeIndex = Platform::InterlockedDecrement(&_freeItemsCount); + if (freeIndex >= 0) + { + // Use the ID from the free list + ASSERT_LOW_LAYER(freeIndex < _freeItems.Count()); + cullingId = _freeItems.Get()[freeIndex]; + } + else + { + // Count space needed to contain all objects (for the next frame) + Platform::InterlockedIncrement(&_newItemsCount); + return true; + } + } + + // Update item bounds + auto& item = _items.Get()[cullingId]; + if (item.Bounds != bounds || _forceUpdateBounds) + { + // Update bounds + item.Bounds = bounds; + + // Write to the vertex buffer + Float3 boxMin = bounds.Minimum - _origin; + Float3 boxMax = bounds.Maximum - _origin; + ASSERT_LOW_LAYER(_vertexBuffer.Data.Count() >= (8 * sizeof(Float3)) * (cullingId + 1)); + Float3* vertices = (Float3*)(_vertexBuffer.Data.Get() + (8 * sizeof(Float3)) * cullingId); + vertices[0] = boxMin; + vertices[1] = Float3(boxMin.X, boxMin.Y, boxMax.Z); + vertices[2] = Float3(boxMin.X, boxMax.Y, boxMin.Z); + vertices[3] = Float3(boxMin.X, boxMax.Y, boxMax.Z); + vertices[4] = Float3(boxMax.X, boxMin.Y, boxMin.Z); + vertices[5] = Float3(boxMax.X, boxMin.Y, boxMax.Z); + vertices[6] = Float3(boxMax.X, boxMax.Y, boxMin.Z); + vertices[7] = boxMax; + Platform::InterlockedIncrement(&_dirtyBounds); + } + + // Force visible when object was not rendered last frame (eg. outside the frustum) + if (_frameCounter - item.LastUsedFrame > 1) + { + item.Occluded = false; + } + item.LastUsedFrame = _frameCounter; + + // Read occlusion result + return !item.Occluded; +} + +#if COMPILE_WITH_DEV_ENV + +void HardwareOcclusionCulling::OnShaderReloading(Asset* obj) +{ + SAFE_DELETE_GPU_RESOURCE(_pso); +} + +#endif diff --git a/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.h b/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.h new file mode 100644 index 000000000..fe0b26ea7 --- /dev/null +++ b/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.h @@ -0,0 +1,65 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "IOcclusionCulling.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Graphics/DynamicBuffer.h" +#include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Content/AssetReference.h" + +class GPUPipelineState; + +/// +/// Occlusion culling system based on hardware occlusion queries. +/// Uses GPU query to determine if the object is visible (not occluded by other geometry) and can be drawn. +/// Results are readback with a few frames latency (objects can pop in fast motion). +/// +API_CLASS(Sealed) class FLAXENGINE_API HardwareOcclusionCulling : public ScriptingObject, public IOcclusionCulling +{ + DECLARE_SCRIPTING_TYPE(HardwareOcclusionCulling); + ~HardwareOcclusionCulling(); + + // Maximum number of frames to delay the visibility results readback from GPU (to avoid stalls). The higher value the more latency but less GPU stalls. + constexpr static int32 MaxFrames = 4; + +private: + struct alignas(sizeof(uint64)) Item + { + BoundingBox Bounds; // Object bounds (world-space) + uint64 Queries[MaxFrames]; // Buffered frames (ring-buffer) + int32 Frames[MaxFrames]; // Frame counter for each query + int32 LastUsedFrame; // Last frame object was drawn (incl. not frustum-culled or hidden) + bool Occluded; // Result from the last frame + }; + + int32 _framesCount = 2, _frameCounter = 0; + volatile int64 _freeItemsCount = 0; + volatile int64 _newItemsCount = 0; + volatile int64 _dirtyBounds = 0; + uint64 _lastEngineFrameUsed = 0; + float _boundsScale = 1.0f; + Array _items; + Array _freeItems; + GPUPipelineState* _pso = nullptr; + DynamicVertexBuffer _vertexBuffer; + GPUBuffer* _indexBuffer = nullptr; + Vector3 _origin = Vector3::Zero; + Vector3 _viewPos = Vector3::Zero; + bool _forceUpdateBounds = true; + AssetReference _shader; + +public: + // [IOcclusionCulling] + void BeginFrame(const RenderContext& renderContext) override; + void EndFrame(const RenderContext& renderContext) override; + void Submit(const RenderContext& renderContext) override; + bool IsVisible(const BoundingBox& bounds, uint32& cullingId) override; + bool IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState) override; + +private: + bool IsVisible(BoundingBox bounds, uint32& cullingId, GeometryDrawState* drawState); +#if COMPILE_WITH_DEV_ENV + void OnShaderReloading(Asset* obj); +#endif +}; diff --git a/Source/Engine/Renderer/Culling/IOcclusionCulling.h b/Source/Engine/Renderer/Culling/IOcclusionCulling.h new file mode 100644 index 000000000..5af28679b --- /dev/null +++ b/Source/Engine/Renderer/Culling/IOcclusionCulling.h @@ -0,0 +1,62 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Math/BoundingBox.h" + +struct GeometryDrawState; +struct RenderContext; +struct RenderContextBatch; + +/// +/// Interface for occlusion querying and culling systems. Performs visibility checks for the scene objects (incl. meshes and lights). +/// Implementations can use hardware occlusion queries, software rasterization, Hi-Z tests, or any other method to determine if an object is visible in the current view frustum and not occluded by other geometry. +/// +/// Can be implemented only in the native code (C++) but also used in scripting for custom objects culling. +API_INTERFACE() class FLAXENGINE_API IOcclusionCulling +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(IOcclusionCulling); + virtual ~IOcclusionCulling() = default; + + /// + /// Checks if the culling system is supported on the current platform and hardware. + /// + virtual bool IsSupported() { return true; } + + /// + /// Frame begin event. Called before the drawing to prepare the culling system for the new frame. + /// + virtual void BeginFrame(const RenderContext& renderContext) {} + + /// + /// Submits occlusion queries or performs the culling operations. Called after occluders depth drawing (depth from GBufferPass or DepthPrePass). Can be used to submit occlusion queries for the scene objects (eg., using GPU occlusion queries) or generate a Hi-Z buffer. + /// + virtual void Submit(const RenderContext& renderContext) {} + + /// + /// Frame end event. Called after the drawing to prepare the culling system for the new frame. + /// + virtual void EndFrame(const RenderContext& renderContext) {} + + /// + /// Object bounds visibility check. Returns true if the object is visible (not occluded by other geometry). + /// Works only for CPU-side culling (or with delayed GPU-readback). + /// GPU-based culling uses indirect draw arguments to handle conditional drawing. + /// Usually called from multiple threads at once (async) when rendering scene. + /// + /// The bounds of the object to check. + /// The unique identifier of the visibility query - stable for the same object. Set to 0 by default (as invalid ID), will be assigned internally by the culling system. + /// True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false. + API_FUNCTION(Sealed) virtual bool IsVisible(const BoundingBox& bounds, API_PARAM(Ref) uint32& cullingId) { return true; } + + /// + /// Object bounds visibility check. Returns true if the object is visible (not occluded by other geometry). + /// Works only for CPU-side culling (or with delayed GPU-readback). + /// GPU-based culling uses indirect draw arguments to handle conditional drawing. + /// Usually called from multiple threads at once (async) when rendering scene. + /// + /// The bounds of the object to check. + /// The geometry drawing state - allocated within RenderBuffers for a single model actor. Contains CullingId field that will be assigned and used internally by the culling system. + /// True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false. + virtual bool IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState) { return true; } +}; diff --git a/Source/Engine/Renderer/DrawCall.h b/Source/Engine/Renderer/DrawCall.h index 1cce61aa0..6e55af877 100644 --- a/Source/Engine/Renderer/DrawCall.h +++ b/Source/Engine/Renderer/DrawCall.h @@ -334,6 +334,11 @@ struct GeometryDrawState /// uint64 PrevFrame = 0; + /// + /// Unique identifier of the object culling state. Assigned and managed by the occlusion culling system (IOcclusionCulling). + /// + uint32 CullingId = 0; + /// /// The previous frame model LOD index used. It's locked during LOD transition to cache the transition start LOD. /// diff --git a/Source/Engine/Renderer/Editor/QuadOverdrawPass.cpp b/Source/Engine/Renderer/Editor/QuadOverdrawPass.cpp index b0c13db5a..f8798d1a8 100644 --- a/Source/Engine/Renderer/Editor/QuadOverdrawPass.cpp +++ b/Source/Engine/Renderer/Editor/QuadOverdrawPass.cpp @@ -15,7 +15,7 @@ #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderTargetPool.h" #include "Engine/Renderer/RenderList.h" -#include "Engine/Graphics/RenderTools.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" void CheckDrawListQuadOverdraw(RenderList* list, DrawCallsListType type) { @@ -127,6 +127,10 @@ void QuadOverdrawPass::Render(RenderContext& renderContext, GPUContext* context, context->ResetUA(); context->ResetSR(); + // Submit occlusion queries + if (renderContext.Buffers->OcclusionCulling) + renderContext.Buffers->OcclusionCulling->Submit(renderContext); + // Convert stats into debug colors context->BindSR(0, overdrawTexture->View()); context->SetRenderTarget(lightBuffer); diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp index 1a1e2929b..ea62a0ac6 100644 --- a/Source/Engine/Renderer/GBufferPass.cpp +++ b/Source/Engine/Renderer/GBufferPass.cpp @@ -2,6 +2,7 @@ #include "GBufferPass.h" #include "Engine/Renderer/RenderList.h" +#include "Culling/IOcclusionCulling.h" #if GPU_ENABLE_DEVELOPMENT #include "Engine/Renderer/Editor/VertexColors.h" #include "Engine/Renderer/Editor/LightmapUVsDensity.h" @@ -221,6 +222,10 @@ void GBufferPass::Fill(RenderContext& renderContext, GPUTexture* lightBuffer) renderContext.List->ExecuteDrawCalls(renderContext, DrawCallsListType::GBuffer); } + // Submit occlusion queries + if (renderContext.Buffers->OcclusionCulling) + renderContext.Buffers->OcclusionCulling->Submit(renderContext); + // Draw decals DrawDecals(renderContext, lightBuffer->View()); diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index 060ccb460..783145bfa 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -476,17 +476,17 @@ PRAGMA_ENABLE_DEPRECATION_WARNINGS e->PreRender(context, renderContext); // Final render view preparations + renderContext.View.Pass = DrawPass::GBuffer | DrawPass::Forward | DrawPass::Distortion; + if (setup.UseMotionVectors) + renderContext.View.Pass |= DrawPass::MotionVectors; renderContext.View.Prepare(renderContext); + renderContext.Buffers->OnRendering(renderContext); } static void CollectDrawCalls(GPUContext* context, RenderContext& renderContext, RenderContextBatch& renderContextBatch) { PROFILE_CPU_NAMED("Collect Draw Calls"); RenderSetup& setup = renderContext.List->Setup; - - renderContext.View.Pass = DrawPass::GBuffer | DrawPass::Forward | DrawPass::Distortion; - if (setup.UseMotionVectors) - renderContext.View.Pass |= DrawPass::MotionVectors; renderContextBatch.GetMainContext() = renderContext; // Sync render context in batch with the current value renderContext.List->PreDraw(context, renderContextBatch); diff --git a/Source/Engine/Terrain/Terrain.cpp b/Source/Engine/Terrain/Terrain.cpp index 17c9b1446..22a7f764e 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -12,6 +12,7 @@ #include "Engine/Physics/PhysicalMaterial.h" #include "Engine/Physics/PhysicsBackend.h" #include "Engine/Content/Deprecated.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/RenderView.h" #include "Engine/Graphics/RenderContext.h" #include "Engine/Graphics/Textures/GPUTexture.h" @@ -529,15 +530,15 @@ void Terrain::RemovePatch(const Int2& patchCoord) void Terrain::Draw(RenderContextBatch& renderContextBatch) { PROFILE_CPU(); - if (DrawSetup(renderContextBatch.GetMainContext())) + RenderContext& mainContext = renderContextBatch.GetMainContext(); + if (DrawSetup(mainContext)) return; HashSet drawnChunks; + bool isMain = true; for (RenderContext& renderContext : renderContextBatch.Contexts) { - const DrawPass drawModes = DrawModes & renderContext.View.Pass; - if (drawModes == DrawPass::None) - continue; - DrawImpl(renderContext, drawnChunks); + DrawImpl(renderContext, DrawModes & renderContext.View.Pass, drawnChunks, isMain); + isMain = false; } } @@ -602,14 +603,18 @@ bool Terrain::DrawSetup(RenderContext& renderContext) return false; } -void Terrain::DrawImpl(RenderContext& renderContext, HashSet& drawnChunks) +void Terrain::DrawImpl(RenderContext& renderContext, DrawPass drawModes, HashSet& drawnChunks, bool isMain) { + if (drawModes == DrawPass::None) + return; + // Collect chunks to render and calculate LOD/material for them (required to be done before to gather NeighborLOD) Array drawChunks; // Frustum vs Box culling for patches const BoundingFrustum frustum = renderContext.View.CullingFrustum; const Vector3 origin = renderContext.View.Origin; + auto scene = _scene ? &_scene->Rendering : nullptr; for (int32 patchIndex = 0; patchIndex < _patches.Count(); patchIndex++) { const auto patch = _patches[patchIndex]; @@ -623,16 +628,22 @@ void Terrain::DrawImpl(RenderContext& renderContext, HashSetChunks[chunkIndex]; + TerrainChunk* chunk = &patch->Chunks[chunkIndex]; bounds = BoundingBox(chunk->_bounds.Minimum - origin, chunk->_bounds.Maximum - origin); if (renderContext.View.IsCullingDisabled || frustum.Intersects(bounds)) { + // Init chunks (once) if (!drawnChunks.Contains(chunk) && !chunk->PrepareDraw(renderContext)) continue; + drawnChunks.Add(chunk); + + // Main-view occlusion culling + uint32 cullingId = 0; + if (isMain && !renderContext.Buffers->TestOcclusionCulling(scene, this, chunk->_bounds, cullingId, chunk)) + continue; // Add chunk for drawing drawChunks.Add(chunk); - drawnChunks.Add(chunk); } } } @@ -641,7 +652,7 @@ void Terrain::DrawImpl(RenderContext& renderContext, HashSetDraw(renderContext); + drawChunks.Get()[i]->Draw(renderContext, drawModes); } } diff --git a/Source/Engine/Terrain/Terrain.h b/Source/Engine/Terrain/Terrain.h index 9c041b961..7da09952f 100644 --- a/Source/Engine/Terrain/Terrain.h +++ b/Source/Engine/Terrain/Terrain.h @@ -457,7 +457,7 @@ public: private: ImplementPhysicsDebug; bool DrawSetup(RenderContext& renderContext); - void DrawImpl(RenderContext& renderContext, HashSet& drawnChunks); + void DrawImpl(RenderContext& renderContext, DrawPass drawModes, HashSet& drawnChunks, bool isMain); public: // [PhysicsColliderActor] diff --git a/Source/Engine/Terrain/TerrainChunk.cpp b/Source/Engine/Terrain/TerrainChunk.cpp index f3b24ebc3..4fa4a4ec4 100644 --- a/Source/Engine/Terrain/TerrainChunk.cpp +++ b/Source/Engine/Terrain/TerrainChunk.cpp @@ -83,7 +83,7 @@ bool TerrainChunk::PrepareDraw(const RenderContext& renderContext) return true; } -void TerrainChunk::Draw(const RenderContext& renderContext) const +void TerrainChunk::Draw(const RenderContext& renderContext, DrawPass drawModes) const { const int32 lod = _cachedDrawLOD; const int32 minLod = Math::Max(lod + 1, 0); @@ -140,7 +140,7 @@ void TerrainChunk::Draw(const RenderContext& renderContext) const //drawCall.TerrainData.HeightmapUVScaleBias.W += halfTexelOffset; // Submit draw call - const DrawPass drawModes = _patch->_terrain->DrawModes & renderContext.View.Pass & drawCall.Material->GetDrawModes(); + drawModes &= renderContext.View.Pass & drawCall.Material->GetDrawModes(); if (drawModes != DrawPass::None) renderContext.List->AddDrawCall(renderContext, drawModes, flags, drawCall, true); } diff --git a/Source/Engine/Terrain/TerrainChunk.h b/Source/Engine/Terrain/TerrainChunk.h index 3c73cee32..576ecc6d8 100644 --- a/Source/Engine/Terrain/TerrainChunk.h +++ b/Source/Engine/Terrain/TerrainChunk.h @@ -128,7 +128,8 @@ public: /// Draws the chunk (adds the draw call). Must be called after PrepareDraw. /// /// The rendering context. - void Draw(const RenderContext& renderContext) const; + /// The drawing modes. + void Draw(const RenderContext& renderContext, DrawPass drawModes =DrawPass::Default) const; /// /// Draws the terrain chunk. diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index b88a7806d..6f906210e 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -15,6 +15,7 @@ #include "Engine/Render2D/FontManager.h" #include "Engine/Render2D/FontTextureAtlas.h" #include "Engine/Renderer/RenderList.h" +#include "Engine/Renderer/Culling/IOcclusionCulling.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Serialization/Serialization.h" @@ -376,6 +377,14 @@ void TextRender::Draw(RenderContextBatch& renderContextBatch) Matrix world; renderContext.View.GetWorldMatrix(_transform, world); auto drawState = renderContext.Buffers->GetGeometryDrawState(&GetScene()->Rendering, _sceneRenderingKey, this); + DrawPass drawModes = DrawModes; + if (drawState && renderContextBatch.Buffers->OcclusionCulling && !renderContextBatch.Buffers->OcclusionCulling->IsVisible(_box, *drawState)) + { + // Draw shadows-only (or cull) + drawModes &= DrawPass::Depth; + if (renderContextBatch.Contexts.Count() == 1 || drawModes == DrawPass::None) + return; + } GEOMETRY_DRAW_STATE_EVENT_BEGIN(drawState, world); // Flush buffers @@ -400,7 +409,6 @@ void TextRender::Draw(RenderContextBatch& renderContextBatch) drawCall.InstanceCount = 1; // Submit draw calls - const DrawPass drawModes = DrawModes & renderContext.View.GetShadowsDrawPassMask(ShadowsMode); for (const auto& e : _drawChunks) { const DrawPass chunkDrawModes = drawModes & e.Material->GetDrawModes(); diff --git a/Source/Shaders/Utils/Culling.shader b/Source/Shaders/Utils/Culling.shader new file mode 100644 index 000000000..082d93bf4 --- /dev/null +++ b/Source/Shaders/Utils/Culling.shader @@ -0,0 +1,14 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#include "./Flax/Common.hlsl" + +META_CB_BEGIN(0, OcclusionCullingData) +float4x4 ViewProjectionMatrix; +META_CB_END + +// Vertex Shader function for Hardware Occlusion Culling queries bounds projection +META_VS(true, FEATURE_LEVEL_ES2) +float4 VS_HardwareOcclusionCulling(float3 Position : POSITION0) : SV_Position +{ + return mul(float4(Position, 1), ViewProjectionMatrix); +}