Optimize Foliage with quad-tree clustering per foliage type

This commit is contained in:
Wojtek Figat
2021-07-01 23:08:47 +02:00
parent c93c811375
commit fe2e17e353
4 changed files with 276 additions and 183 deletions

View File

@@ -10,6 +10,9 @@ struct FoliageInstance;
// Enable/disable foliage editing and changing at runtime. If your game need to use procedural foliage then enable this option.
#define FOLIAGE_EDITING (USE_EDITOR)
// Enables using single quad-tree acceleration structure per foliage actor, otherwise will use quad-tree per foliage type to optimize drawing performance at a cost of higher memory usage.
#define FOLIAGE_USE_SINGLE_QUAD_TREE 0
// Size of the instance allocation chunks (number of instances per allocated page)
#define FOLIAGE_INSTANCE_CHUNKS_SIZE (4096*4)

View File

@@ -16,62 +16,9 @@ Foliage::Foliage(const SpawnParams& params)
: Actor(params)
{
_disableFoliageTypeEvents = false;
Root = nullptr;
}
void Foliage::EnsureRoot()
{
// Skip if root is already here or there is no instances at all
if (Root || Instances.IsEmpty())
return;
ASSERT(Clusters.IsEmpty());
PROFILE_CPU();
// Calculate total bounds of valid instances
BoundingBox totalBounds;
{
bool anyValid = false;
// TODO: inline code and use SIMD
BoundingBox box;
for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
if (!FoliageTypes[i->Type].IsReady())
continue;
BoundingBox::FromSphere(i->Bounds, box);
ASSERT(!i->Bounds.Center.IsNanOrInfinity());
if (anyValid)
{
BoundingBox::Merge(totalBounds, box, totalBounds);
}
else
{
totalBounds = box;
anyValid = true;
}
}
// Skip if nothing is valid
if (!anyValid)
return;
}
ASSERT(!totalBounds.Minimum.IsNanOrInfinity() && !totalBounds.Maximum.IsNanOrInfinity());
// Setup first and topmost cluster
Clusters.Resize(1);
Root = &Clusters[0];
Root->Init(totalBounds);
// Cache bounds
_box = Root->Bounds;
BoundingSphere::FromBox(_box, _sphere);
if (_sceneRenderingKey != -1)
GetSceneRendering()->UpdateGeometry(this, _sceneRenderingKey);
}
void Foliage::AddToCluster(FoliageCluster* cluster, FoliageInstance& instance)
void Foliage::AddToCluster(ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_SIZE>& clusters, FoliageCluster* cluster, FoliageInstance& instance)
{
ASSERT(instance.Bounds.Radius > ZeroTolerance);
ASSERT(cluster->Bounds.Intersects(instance.Bounds));
@@ -101,12 +48,12 @@ void Foliage::AddToCluster(FoliageCluster* cluster, FoliageInstance& instance)
else
{
// Subdivide cluster
const int32 count = Clusters.Count();
Clusters.Resize(count + 4);
cluster->Children[0] = &Clusters[count + 0];
cluster->Children[1] = &Clusters[count + 1];
cluster->Children[2] = &Clusters[count + 2];
cluster->Children[3] = &Clusters[count + 3];
const int32 count = clusters.Count();
clusters.Resize(count + 4);
cluster->Children[0] = &clusters[count + 0];
cluster->Children[1] = &clusters[count + 1];
cluster->Children[2] = &clusters[count + 2];
cluster->Children[3] = &clusters[count + 3];
// Setup children
const Vector3 min = cluster->Bounds.Minimum;
@@ -120,10 +67,69 @@ void Foliage::AddToCluster(FoliageCluster* cluster, FoliageInstance& instance)
// Move instances to a proper cells
for (int32 i = 0; i < cluster->Instances.Count(); i++)
{
AddToCluster(cluster, *cluster->Instances[i]);
AddToCluster(clusters, cluster, *cluster->Instances[i]);
}
cluster->Instances.Clear();
AddToCluster(cluster, instance);
AddToCluster(clusters, cluster, instance);
}
}
void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, Mesh::DrawInfo& draw)
{
// Skip clusters that around too far from view
if (Vector3::Distance(renderContext.View.Position, cluster->TotalBoundsSphere.Center) - cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance)
return;
//DebugDraw::DrawBox(cluster->Bounds, Color::Red);
// Draw visible children
if (cluster->Children[0])
{
// Don't store instances in non-leaf nodes
ASSERT_LOW_LAYER(cluster->Instances.IsEmpty());
#define DRAW_CLUSTER(idx) \
if (renderContext.View.CullingFrustum.Intersects(cluster->Children[idx]->TotalBounds)) \
DrawCluster(renderContext, cluster->Children[idx], draw)
DRAW_CLUSTER(0);
DRAW_CLUSTER(1);
DRAW_CLUSTER(2);
DRAW_CLUSTER(3);
#undef DRAW_CLUSTER
}
else
{
// Draw visible instances
const auto frame = Engine::FrameCount;
for (int32 i = 0; i < cluster->Instances.Count(); i++)
{
auto& instance = *cluster->Instances[i];
auto& type = FoliageTypes[instance.Type];
// Check if can draw this instance
if (type._canDraw &&
Vector3::Distance(renderContext.View.Position, instance.Bounds.Center) - instance.Bounds.Radius < instance.CullDistance &&
renderContext.View.CullingFrustum.Intersects(instance.Bounds))
{
// Disable motion blur
instance.DrawState.PrevWorld = instance.World;
// Draw model
draw.Lightmap = GetScene()->LightmapsData.GetReadyLightmap(instance.Lightmap.TextureIndex);
draw.LightmapUVs = &instance.Lightmap.UVsArea;
draw.Buffer = &type.Entries;
draw.World = &instance.World;
draw.DrawState = &instance.DrawState;
draw.Bounds = instance.Bounds;
draw.PerInstanceRandom = instance.Random;
draw.DrawModes = type._drawModes;
type.Model->Draw(renderContext, draw);
//DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen);
instance.DrawState.PrevFrame = frame;
}
}
}
}
@@ -364,16 +370,121 @@ void Foliage::RebuildClusters()
{
PROFILE_CPU();
// Remove previous clusters data
Root = nullptr;
Clusters.Clear();
// Faster path if foliage is empty or no types is ready
bool anyTypeValid = false;
for (auto& type : FoliageTypes)
anyTypeValid |= type.IsReady();
if (!anyTypeValid || Instances.IsEmpty())
{
#if FOLIAGE_USE_SINGLE_QUAD_TREE
Root = nullptr;
Clusters.Clear();
#else
for (auto& type : FoliageTypes)
{
type.Root = nullptr;
type.Clusters.Clear();
}
#endif
_box = BoundingBox(_transform.Translation, _transform.Translation);
_sphere = BoundingSphere(_transform.Translation, 0.0f);
if (_sceneRenderingKey != -1)
GetSceneRendering()->UpdateGeometry(this, _sceneRenderingKey);
return;
}
EnsureRoot();
// Clear clusters and initialize root
{
PROFILE_CPU_NAMED("Init Root");
BoundingBox totalBounds, box;
#if FOLIAGE_USE_SINGLE_QUAD_TREE
{
// Calculate total bounds of all instances
auto i = Instances.Begin();
for (; i.IsNotEnd(); ++i)
{
if (!FoliageTypes[i->Type].IsReady())
continue;
BoundingBox::FromSphere(i->Bounds, box);
totalBounds = box;
break;
}
++i;
// TODO: inline code and use SIMD
for (; i.IsNotEnd(); ++i)
{
if (!FoliageTypes[i->Type].IsReady())
continue;
BoundingBox::FromSphere(i->Bounds, box);
BoundingBox::Merge(totalBounds, box, totalBounds);
}
}
// Setup first and topmost cluster
Clusters.Resize(1);
Root = &Clusters[0];
Root->Init(totalBounds);
#else
bool hasTotalBounds = false;
for (auto& type : FoliageTypes)
{
if (!type.IsReady())
{
type.Root = nullptr;
type.Clusters.Clear();
continue;
}
// Calculate total bounds of all instances of this type
BoundingBox totalBoundsType;
auto i = Instances.Begin();
for (; i.IsNotEnd(); ++i)
{
if (i->Type == type.Index)
{
BoundingBox::FromSphere(i->Bounds, box);
totalBoundsType = box;
break;
}
}
++i;
// TODO: inline code and use SIMD
for (; i.IsNotEnd(); ++i)
{
if (i->Type == type.Index)
{
BoundingBox::FromSphere(i->Bounds, box);
BoundingBox::Merge(totalBoundsType, box, totalBoundsType);
}
}
// Setup first and topmost cluster
type.Clusters.Resize(1);
type.Root = &type.Clusters[0];
type.Root->Init(totalBoundsType);
if (hasTotalBounds)
{
BoundingBox::Merge(totalBounds, totalBoundsType, totalBounds);
}
else
{
totalBounds = totalBoundsType;
hasTotalBounds = true;
}
}
ASSERT(hasTotalBounds);
#endif
ASSERT(!totalBounds.Minimum.IsNanOrInfinity() && !totalBounds.Maximum.IsNanOrInfinity());
_box = totalBounds;
BoundingSphere::FromBox(_box, _sphere);
if (_sceneRenderingKey != -1)
GetSceneRendering()->UpdateGeometry(this, _sceneRenderingKey);
}
// Insert all instances to the clusters
{
PROFILE_CPU_NAMED("Create Clusters");
const float globalDensityScale = GetGlobalDensityScale();
for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
@@ -383,17 +494,31 @@ void Foliage::RebuildClusters()
if (type.IsReady() && instance.Random < densityScale)
{
AddToCluster(Root, instance);
#if FOLIAGE_USE_SINGLE_QUAD_TREE
AddToCluster(Clusters, Root, instance);
#else
AddToCluster(type.Clusters, type.Root, instance);
#endif
}
}
}
#if FOLIAGE_USE_SINGLE_QUAD_TREE
if (Root)
{
PROFILE_CPU_NAMED("Update Cache");
Root->UpdateTotalBoundsAndCullDistance();
}
#else
for (auto& type : FoliageTypes)
{
if (type.Root)
{
PROFILE_CPU_NAMED("Update Cache");
type.Root->UpdateTotalBoundsAndCullDistance();
}
}
#endif
}
void Foliage::UpdateCullDistance()
@@ -402,7 +527,6 @@ void Foliage::UpdateCullDistance()
{
PROFILE_CPU_NAMED("Instances");
for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
auto& instance = *i;
@@ -411,12 +535,22 @@ void Foliage::UpdateCullDistance()
}
}
#if FOLIAGE_USE_SINGLE_QUAD_TREE
if (Root)
{
PROFILE_CPU_NAMED("Clusters");
Root->UpdateCullDistance();
}
#else
for (auto& type : FoliageTypes)
{
if (type.Root)
{
PROFILE_CPU_NAMED("Clusters");
type.Root->UpdateCullDistance();
}
}
#endif
}
static float GlobalDensityScale = 1.0f;
@@ -455,105 +589,54 @@ bool Foliage::Intersects(const Ray& ray, float& distance, Vector3& normal, int32
PROFILE_CPU();
instanceIndex = -1;
if (Root)
{
FoliageInstance* instance;
if (Root->Intersects(this, ray, distance, normal, instance))
{
int32 j = 0;
for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
if (&*i == instance)
{
instanceIndex = j;
break;
}
j++;
}
return true;
}
}
distance = MAX_float;
normal = Vector3::Up;
return false;
}
distance = MAX_float;
void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, Mesh::DrawInfo& draw)
{
// Skip clusters that around too far from view
if (Vector3::Distance(renderContext.View.Position, cluster->TotalBoundsSphere.Center) - cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance)
return;
//DebugDraw::DrawBox(cluster->Bounds, Color::Red);
// Draw visible children
if (cluster->Children[0])
FoliageInstance* instance = nullptr;
#if FOLIAGE_USE_SINGLE_QUAD_TREE
if (Root)
Root->Intersects(this, ray, distance, normal, instance);
#else
float tmpDistance;
Vector3 tmpNormal;
FoliageInstance* tmpInstance;
for (auto& type : FoliageTypes)
{
// Don't store instances in non-leaf nodes
ASSERT_LOW_LAYER(cluster->Instances.IsEmpty());
#define DRAW_CLUSTER(idx) \
if (renderContext.View.CullingFrustum.Intersects(cluster->Children[idx]->TotalBounds)) \
DrawCluster(renderContext, cluster->Children[idx], draw)
DRAW_CLUSTER(0);
DRAW_CLUSTER(1);
DRAW_CLUSTER(2);
DRAW_CLUSTER(3);
#undef DRAW_CLUSTER
}
else
{
// Draw visible instances
const auto frame = Engine::FrameCount;
for (int32 i = 0; i < cluster->Instances.Count(); i++)
if (type.Root && type.Root->Intersects(this, ray, tmpDistance, tmpNormal, tmpInstance) && tmpDistance < distance)
{
auto& instance = *cluster->Instances[i];
auto& type = FoliageTypes[instance.Type];
// Check if can draw this instance
if (type._canDraw &&
Vector3::Distance(renderContext.View.Position, instance.Bounds.Center) - instance.Bounds.Radius < instance.CullDistance &&
renderContext.View.CullingFrustum.Intersects(instance.Bounds))
{
// Disable motion blur
instance.DrawState.PrevWorld = instance.World;
// Draw model
draw.Lightmap = GetScene()->LightmapsData.GetReadyLightmap(instance.Lightmap.TextureIndex);
draw.LightmapUVs = &instance.Lightmap.UVsArea;
draw.Buffer = &type.Entries;
draw.World = &instance.World;
draw.DrawState = &instance.DrawState;
draw.Bounds = instance.Bounds;
draw.PerInstanceRandom = instance.Random;
draw.DrawModes = type._drawModes;
type.Model->Draw(renderContext, draw);
//DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen);
instance.DrawState.PrevFrame = frame;
}
distance = tmpDistance;
normal = tmpNormal;
instance = tmpInstance;
}
}
#endif
if (instance != nullptr)
{
int32 j = 0;
for (auto i = Instances.Begin(); i.IsNotEnd(); ++i)
{
if (&*i == instance)
{
instanceIndex = j;
return true;
}
j++;
}
}
return false;
}
void Foliage::Draw(RenderContext& renderContext)
{
// Skip if no instances spawned
if (Instances.IsEmpty() || !Root)
if (Instances.IsEmpty())
return;
auto& view = renderContext.View;
PROFILE_CPU();
// Cache data per foliage instance type
for (int32 i = 0; i < FoliageTypes.Count(); i++)
for (auto& type : FoliageTypes)
{
auto& type = FoliageTypes[i];
const auto drawModes = static_cast<DrawPass>(type.DrawModes & view.Pass & (int32)view.GetShadowsDrawPassMask(type.ShadowsMode));
type._canDraw = type.IsReady() && drawModes != DrawPass::None;
type._drawModes = drawModes;
@@ -577,7 +660,17 @@ void Foliage::Draw(RenderContext& renderContext)
draw.LODBias = 0;
draw.ForcedLOD = -1;
draw.VertexColors = nullptr;
DrawCluster(renderContext, Root, draw);
#if FOLIAGE_USE_SINGLE_QUAD_TREE
if (Root)
DrawCluster(renderContext, Root, draw);
#else
for (auto& type : FoliageTypes)
{
if (!type.Root)
continue;
DrawCluster(renderContext, type.Root, draw);
}
#endif
}
void Foliage::DrawGeneric(RenderContext& renderContext)
@@ -639,10 +732,10 @@ void Foliage::Serialize(SerializeStream& stream, const void* otherObj)
stream.JKEY("Foliage");
stream.StartArray();
for (int32 i = 0; i < FoliageTypes.Count(); i++)
for (auto& type : FoliageTypes)
{
stream.StartObject();
FoliageTypes[i].Serialize(stream, nullptr);
type.Serialize(stream, nullptr);
stream.EndObject();
}
stream.EndArray();
@@ -675,9 +768,11 @@ void Foliage::Deserialize(DeserializeStream& stream, ISerializeModifier* modifie
PROFILE_CPU();
// Clear
#if FOLIAGE_USE_SINGLE_QUAD_TREE
Root = nullptr;
Instances.Release();
Clusters.Release();
#endif
Instances.Release();
FoliageTypes.Resize(0, false);
// Deserialize foliage types

View File

@@ -7,7 +7,6 @@
#include "FoliageCluster.h"
#include "FoliageType.h"
#include "Engine/Level/Actor.h"
#include "Engine/Core/Collections/ChunkedArray.h"
/// <summary>
/// Represents a foliage actor that contains a set of instanced meshes.
@@ -23,20 +22,22 @@ private:
public:
/// <summary>
/// The root cluster. Contains all the instances and it's the starting point of the quad-tree hierarchy. Null if no foliage added. It's read-only.
/// </summary>
FoliageCluster* Root;
/// <summary>
/// The allocated foliage instances. It's read-only.
/// </summary>
ChunkedArray<FoliageInstance, FOLIAGE_INSTANCE_CHUNKS_SIZE> Instances;
#if FOLIAGE_USE_SINGLE_QUAD_TREE
/// <summary>
/// The root cluster. Contains all the instances and it's the starting point of the quad-tree hierarchy. Null if no foliage added. It's read-only.
/// </summary>
FoliageCluster* Root = nullptr;
/// <summary>
/// The allocated foliage clusters. It's read-only.
/// </summary>
ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_SIZE> Clusters;
#endif
/// <summary>
/// The foliage instances types used by the current foliage actor. It's read-only.
@@ -141,35 +142,16 @@ public:
/// <summary>
/// Gets the global density scale for all foliage instances. The default value is 1. Use values from range 0-1. Lower values decrease amount of foliage instances in-game. Use it to tweak game performance for slower devices.
/// </summary>
/// <returns>The value.</returns>
API_PROPERTY() static float GetGlobalDensityScale();
/// <summary>
/// Sets the global density scale for all foliage instances. The default value is 1. Use values from range 0-1. Lower values decrease amount of foliage instances in-game. Use it to tweak game performance for slower devices.
/// </summary>
/// <param name="value">The value.</param>
API_PROPERTY() static void SetGlobalDensityScale(float value);
private:
/// <summary>
/// Ensures that the root node of the foliage was added.
/// </summary>
void EnsureRoot();
/// <summary>
/// Adds the given foliage instance to the cluster.
/// </summary>
/// <param name="cluster">The root cluster.</param>
/// <param name="instance">The instance.</param>
void AddToCluster(FoliageCluster* cluster, FoliageInstance& instance);
/// <summary>
/// Draws the cluster.
/// </summary>
/// <param name="renderContext">The rendering context.</param>
/// <param name="cluster">The cluster.</param>
/// <param name="draw">The draw data.</param>
void AddToCluster(ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_SIZE>& clusters, FoliageCluster* cluster, FoliageInstance& instance);
void DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, Mesh::DrawInfo& draw);
public:

View File

@@ -3,6 +3,7 @@
#pragma once
#include "Config.h"
#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Serialization/ISerializable.h"
@@ -86,6 +87,18 @@ public:
/// The shared model instance entries.
/// </summary>
ModelInstanceEntries Entries;
#if !FOLIAGE_USE_SINGLE_QUAD_TREE
/// <summary>
/// The root cluster. Contains all the instances and it's the starting point of the quad-tree hierarchy. Null if no foliage added. It's read-only.
/// </summary>
FoliageCluster* Root = nullptr;
/// <summary>
/// The allocated foliage clusters. It's read-only.
/// </summary>
ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_SIZE> Clusters;
#endif
public: