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
Binary file not shown.
Binary file not shown.
@@ -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"));
@@ -54,6 +54,7 @@ public:
API_FIELD(Attributes="EditorOrder(20), EditorDisplay(\"General\", \"Use V-Sync\")")
bool UseVSync = false;
public:
/// <summary>
/// Anti Aliasing quality setting.
/// </summary>
@@ -96,6 +97,26 @@ public:
API_FIELD(Attributes="EditorOrder(1320), EditorDisplay(\"Quality\", \"Allow CSM Blending\")")
bool AllowCSMBlending = false;
public:
/// <summary>
/// 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.
/// </summary>
API_FIELD(Attributes="EditorOrder(1450), EditorDisplay(\"Culling\"), TypeReference(typeof(IOcclusionCulling)), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.TypeNameEditor\")")
StringAnsi OcclusionCulling;
/// <summary>
/// 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.
/// </summary>
API_FIELD(Attributes="EditorOrder(1460), EditorDisplay(\"Culling\"), Limit(1, 4)")
int32 OcclusionBufferedFrames = 2;
/// <summary>
/// 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.
/// </summary>
API_FIELD(Attributes="EditorOrder(1461), EditorDisplay(\"Culling\"), Limit(1, 1.5f)")
float OcclusionBoundsScale = 1.1f;
public:
/// <summary>
/// Default probes cubemap resolution (use for Environment Probes, can be overriden per-actor).
/// </summary>
+10 -2
View File
@@ -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)
+1
View File
@@ -172,6 +172,7 @@ private:
Float3 LodViewPosition;
int32 MinLOD, MaxLOD;
BoundingFrustum CullingFrustum;
SceneRendering* Scene;
FORCE_INLINE int32 ClampLODIndex(int32 index) const
{
+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
+10 -2
View File
@@ -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();
@@ -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);
}
}
+19 -3
View File
@@ -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)
+9 -3
View File
@@ -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);
}
}
+10 -2
View File
@@ -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();
+7 -1
View File
@@ -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);
+3 -3
View File
@@ -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);
+3 -1
View File
@@ -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:
/// </summary>
/// <param name="renderContextBatch">The rendering context.</param>
/// <param name="effect">The owning actor.</param>
static void DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect);
/// <param name="drawModes">The drawing modes.</param>
static void DrawParticles(RenderContextBatch& renderContextBatch, ParticleEffect* effect, DrawPass drawModes);
#if USE_EDITOR
/// <summary>
@@ -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<Shader>(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<GPUTextureView*>(), Span<GPUDrawPassAction>());
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<HardwareOcclusionCulling, &HardwareOcclusionCulling::OnShaderReloading>(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<GPUBuffer*>(&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
@@ -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;
/// <summary>
/// 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).
/// </summary>
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<Item> _items;
Array<uint32> _freeItems;
GPUPipelineState* _pso = nullptr;
DynamicVertexBuffer _vertexBuffer;
GPUBuffer* _indexBuffer = nullptr;
Vector3 _origin = Vector3::Zero;
Vector3 _viewPos = Vector3::Zero;
bool _forceUpdateBounds = true;
AssetReference<class Shader> _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
};
@@ -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;
/// <summary>
/// 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.
/// </summary>
/// <remarks>Can be implemented only in the native code (C++) but also used in scripting for custom objects culling.</remarks>
API_INTERFACE() class FLAXENGINE_API IOcclusionCulling
{
DECLARE_SCRIPTING_TYPE_MINIMAL(IOcclusionCulling);
virtual ~IOcclusionCulling() = default;
/// <summary>
/// Checks if the culling system is supported on the current platform and hardware.
/// </summary>
virtual bool IsSupported() { return true; }
/// <summary>
/// Frame begin event. Called before the drawing to prepare the culling system for the new frame.
/// </summary>
virtual void BeginFrame(const RenderContext& renderContext) {}
/// <summary>
/// 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.
/// </summary>
virtual void Submit(const RenderContext& renderContext) {}
/// <summary>
/// Frame end event. Called after the drawing to prepare the culling system for the new frame.
/// </summary>
virtual void EndFrame(const RenderContext& renderContext) {}
/// <summary>
/// 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.
/// </summary>
/// <param name="bounds">The bounds of the object to check.</param>
/// <param name="cullingId">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.</param>
/// <returns>True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false.</returns>
API_FUNCTION(Sealed) virtual bool IsVisible(const BoundingBox& bounds, API_PARAM(Ref) uint32& cullingId) { return true; }
/// <summary>
/// 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.
/// </summary>
/// <param name="bounds">The bounds of the object to check.</param>
/// <param name="drawState">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.</param>
/// <returns>True if object can be rendered (is visible or visibility will be calculated on GPU), otherwise false.</returns>
virtual bool IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState) { return true; }
};
+5
View File
@@ -334,6 +334,11 @@ struct GeometryDrawState
/// </summary>
uint64 PrevFrame = 0;
/// <summary>
/// Unique identifier of the object culling state. Assigned and managed by the occlusion culling system (IOcclusionCulling).
/// </summary>
uint32 CullingId = 0;
/// <summary>
/// The previous frame model LOD index used. It's locked during LOD transition to cache the transition start LOD.
/// </summary>
@@ -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);
+5
View File
@@ -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());
+4 -4
View File
@@ -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);
+20 -9
View File
@@ -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<TerrainChunk*, RendererAllocation> 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<TerrainChunk*, RendererAllocation>& drawnChunks)
void Terrain::DrawImpl(RenderContext& renderContext, DrawPass drawModes, HashSet<TerrainChunk*, RendererAllocation>& 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<TerrainChunk*, RendererAllocation> 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, HashSet<TerrainChunk*, Rend
// Frustum vs Box culling for chunks
for (int32 chunkIndex = 0; chunkIndex < Terrain::ChunksCount; chunkIndex++)
{
auto chunk = &patch->Chunks[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, HashSet<TerrainChunk*, Rend
// Draw all visible chunks
for (int32 i = 0; i < drawChunks.Count(); i++)
{
drawChunks.Get()[i]->Draw(renderContext);
drawChunks.Get()[i]->Draw(renderContext, drawModes);
}
}
+1 -1
View File
@@ -457,7 +457,7 @@ public:
private:
ImplementPhysicsDebug;
bool DrawSetup(RenderContext& renderContext);
void DrawImpl(RenderContext& renderContext, HashSet<TerrainChunk*, class RendererAllocation>& drawnChunks);
void DrawImpl(RenderContext& renderContext, DrawPass drawModes, HashSet<TerrainChunk*, class RendererAllocation>& drawnChunks, bool isMain);
public:
// [PhysicsColliderActor]
+2 -2
View File
@@ -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);
}
+2 -1
View File
@@ -128,7 +128,8 @@ public:
/// Draws the chunk (adds the draw call). Must be called after PrepareDraw.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
void Draw(const RenderContext& renderContext) const;
/// <param name="drawModes">The drawing modes.</param>
void Draw(const RenderContext& renderContext, DrawPass drawModes =DrawPass::Default) const;
/// <summary>
/// Draws the terrain chunk.
+9 -1
View File
@@ -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();
+14
View File
@@ -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);
}