Files
FlaxEngine/Source/Editor/Tools/Foliage/FoliageTools.cpp

390 lines
15 KiB
C++

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "FoliageTools.h"
#include "Engine/Core/Math/BoundingSphere.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/SceneQuery.h"
#include "Engine/Terrain/Terrain.h"
#include "Engine/Terrain/TerrainPatch.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Foliage/Foliage.h"
#include "Engine/UI/TextRender.h"
#include "Engine/Core/Random.h"
struct GeometryTriangle
{
Vector3 Vertex;
Vector3 Vector1;
Vector3 Vector2;
Vector3 Normal;
Real Area;
GeometryTriangle(const bool isDeterminantPositive, const Vector3& v0, const Vector3& v1, const Vector3& v2)
{
Vertex = v0;
Vector1 = v1 - Vertex;
Vector2 = v2 - Vertex;
Normal = isDeterminantPositive ? Vector1 ^ Vector2 : Vector2 ^ Vector1;
const Real normalLength = Normal.Length();
Area = normalLength * 0.5f;
if (normalLength > ZeroTolerance)
{
Normal /= normalLength;
}
}
void GetRandomPoint(Vector3& result) const
{
float x = Random::Rand();
float y = Random::Rand();
if (x + y > 1.0f)
{
x = 1.0f - x;
y = 1.0f - y;
}
result = Vertex + x * Vector1 + y * Vector2;
}
};
template<>
struct TIsPODType<GeometryTriangle>
{
enum { Value = true };
};
struct GeometryLookup
{
BoundingSphere Brush;
Array<GeometryTriangle> Triangles;
Array<Vector3> TerrainCache;
GeometryLookup(const Vector3& brushPosition, float brushRadius)
: Brush(brushPosition, brushRadius)
{
}
static bool Search(Actor* actor, GeometryLookup& lookup)
{
// Early out if object is not intersecting with the foliage brush bounds
if (!actor->GetIsActive() || !actor->GetBox().Intersects(lookup.Brush))
return true;
const auto brush = lookup.Brush;
if (const auto* staticModel = dynamic_cast<StaticModel*>(actor))
{
// Skip if model is not loaded
if (staticModel->Model == nullptr || staticModel->Model->WaitForLoaded() || staticModel->Model->GetLoadedLODs() == 0)
return true;
PROFILE_CPU_NAMED("StaticModel");
// Check model meshes
Transform transform = staticModel->GetTransform();
Matrix worldMatrix;
transform.GetWorld(worldMatrix);
const bool isDeterminantPositive = transform.GetDeterminant() >= 0.0f;
auto& lod = staticModel->Model->LODs[0];
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
auto& mesh = lod.Meshes[meshIndex];
auto& proxy = mesh.GetCollisionProxy();
// Check every triangle
for (int32 triangleIndex = 0; triangleIndex < proxy.Triangles.Count(); triangleIndex++)
{
auto t = proxy.Triangles[triangleIndex];
// Transform triangle vertices from mesh space to world space
Vector3 t0, t1, t2;
Vector3::Transform(t.V0, worldMatrix, t0);
Vector3::Transform(t.V1, worldMatrix, t1);
Vector3::Transform(t.V2, worldMatrix, t2);
// Check if triangles intersects with the brush
if (CollisionsHelper::SphereIntersectsTriangle(brush, t0, t1, t2))
{
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
}
else if (const auto* terrain = dynamic_cast<Terrain*>(actor))
{
const bool isDeterminantPositive = terrain->GetTransform().GetDeterminant() >= 0.0f;
PROFILE_CPU_NAMED("Terrain");
// Check every patch
for (int32 patchIndex = 0; patchIndex < terrain->GetPatchesCount(); patchIndex++)
{
auto patch = terrain->GetPatch(patchIndex);
auto& triangles = lookup.TerrainCache;
patch->GetCollisionTriangles(brush, triangles);
for (int32 vertexIndex = 0; vertexIndex < triangles.Count();)
{
const Vector3 t0 = triangles[vertexIndex++];
const Vector3 t1 = triangles[vertexIndex++];
const Vector3 t2 = triangles[vertexIndex++];
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
else if (const auto* textRender = dynamic_cast<TextRender*>(actor))
{
PROFILE_CPU_NAMED("TextRender");
// Skip if text is not ready
if (textRender->GetCollisionProxy().Triangles.IsEmpty())
return true;
auto& proxy = textRender->GetCollisionProxy();
// Check model meshes
Matrix world;
textRender->GetLocalToWorldMatrix(world);
const bool isDeterminantPositive = textRender->GetTransform().GetDeterminant() >= 0.0f;
// Check every triangle
for (int32 triangleIndex = 0; triangleIndex < proxy.Triangles.Count(); triangleIndex++)
{
auto t = proxy.Triangles[triangleIndex];
// Transform triangle vertices from mesh space to world space
Vector3 t0, t1, t2;
Vector3::Transform(t.V0, world, t0);
Vector3::Transform(t.V1, world, t1);
Vector3::Transform(t.V2, world, t2);
// Check if triangles intersects with the brush
if (CollisionsHelper::SphereIntersectsTriangle(brush, t0, t1, t2))
{
lookup.Triangles.Add(GeometryTriangle(isDeterminantPositive, t0, t1, t2));
}
}
}
return true;
}
};
struct FoliagePlacement
{
int32 FoliageTypeIndex;
Vector3 Location; // In world space
Vector3 Normal; // In world space
};
void FoliageTools::Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, bool additive, float densityScale)
{
if (additive)
Paint(foliage, foliageTypesIndices, brushPosition, brushRadius, densityScale);
else
Remove(foliage, foliageTypesIndices, brushPosition, brushRadius);
}
void FoliageTools::Paint(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius, float densityScale)
{
if (foliageTypesIndices.Length() <= 0)
return;
PROFILE_CPU();
// Prepare
GeometryLookup geometry(brushPosition, brushRadius);
// Find geometry actors to place foliage on top of them
{
PROFILE_CPU_NAMED("Search Geometry");
Function<bool(Actor*, GeometryLookup&)> treeWalkFunction(GeometryLookup::Search);
SceneQuery::TreeExecute<GeometryLookup&>(treeWalkFunction, geometry);
}
// For each selected foliage instance type try to place something
Array<FoliagePlacement> placements;
{
PROFILE_CPU_NAMED("Find Placements");
for (int32 i1 = 0; i1 < foliageTypesIndices.Length(); i1++)
{
const int32 foliageTypeIndex = foliageTypesIndices[i1];
ASSERT(foliageTypeIndex >= 0 && foliageTypeIndex < foliage->FoliageTypes.Count());
const FoliageType& foliageType = foliage->FoliageTypes[foliageTypeIndex];
// Prepare
const float minNormalAngle = Math::Cos(foliageType.PaintGroundSlopeAngleMin * DegreesToRadians);
const float maxNormalAngle = Math::Cos(foliageType.PaintGroundSlopeAngleMax * DegreesToRadians);
const bool usePaintRadius = foliageType.PaintRadius > 0.0f;
const float paintRadiusSqr = foliageType.PaintRadius * foliageType.PaintRadius;
// Check every area
for (int32 triangleIndex = 0; triangleIndex < geometry.Triangles.Count(); triangleIndex++)
{
const auto& triangle = geometry.Triangles[triangleIndex];
// Check if can reject triangle based on its normal
if ((maxNormalAngle > (triangle.Normal.Y + ZeroTolerance) || minNormalAngle < (triangle.Normal.Y - ZeroTolerance)))
{
continue;
}
// Calculate amount of foliage instances to place
const float targetInstanceCountEst = (float)(triangle.Area * foliageType.PaintDensity * densityScale / (1000.0f * 1000.0f));
const int32 targetInstanceCount = targetInstanceCountEst > 1.0f ? Math::RoundToInt(targetInstanceCountEst) : Random::Rand() < targetInstanceCountEst ? 1 : 0;
// Try to add new instances
FoliagePlacement placement;
for (int32 j = 0; j < targetInstanceCount; j++)
{
triangle.GetRandomPoint(placement.Location);
// Reject locations outside the brush
if (CollisionsHelper::SphereContainsPoint(geometry.Brush, placement.Location) == ContainmentType::Disjoint)
continue;
// Check if it's too close to any other instances
if (usePaintRadius)
{
// Skip if any places instance is close that placement location
bool isInvalid = false;
const Transform foliageTransform = foliage->GetTransform();
// TODO: use quad tree to boost this logic
for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
{
const auto& instance = *i;
const Vector3 instancePosition = foliageTransform.LocalToWorld(instance.Transform.Translation);
if (Vector3::DistanceSquared(instancePosition, placement.Location) <= paintRadiusSqr)
{
isInvalid = true;
break;
}
}
if (isInvalid)
continue;
// Skip if any places instance is close that placement location
isInvalid = false;
for (int32 i = 0; i < placements.Count(); i++)
{
if (Vector3::DistanceSquared(placements[i].Location, placement.Location) <= paintRadiusSqr)
{
isInvalid = true;
break;
}
}
if (isInvalid)
continue;
}
placement.FoliageTypeIndex = foliageTypeIndex;
placement.Normal = triangle.Normal;
placements.Add(placement);
}
}
}
}
// Place foliage instances
if (placements.HasItems())
{
PROFILE_CPU_NAMED("Place Instances");
FoliageInstance instance;
Quaternion tmp;
Matrix world;
foliage->GetLocalToWorldMatrix(world);
for (int32 i = 0; i < placements.Count(); i++)
{
const auto& placement = placements[i];
const FoliageType& foliageType = foliage->FoliageTypes[placement.FoliageTypeIndex];
const Vector3 normal = foliageType.PlacementAlignToNormal ? placement.Normal : Vector3::Up;
if (normal == Vector3::Down)
instance.Transform.Orientation = Quaternion(0.0f, 0.0f, Math::Sin(PI_OVER_2), Math::Cos(PI_OVER_2));
else
instance.Transform.Orientation = Quaternion::LookRotation(Vector3::Cross(Vector3::Cross(normal, Vector3::Forward), normal), normal);
if (foliageType.PlacementRandomYaw)
{
Quaternion::RotationAxis(Vector3::UnitY, Random::Rand() * TWO_PI, tmp);
instance.Transform.Orientation *= tmp;
}
if (!Math::IsZero(foliageType.PlacementRandomRollAngle))
{
Quaternion::RotationAxis(Vector3::UnitZ, Random::Rand() * DegreesToRadians * foliageType.PlacementRandomRollAngle, tmp);
instance.Transform.Orientation *= tmp;
}
if (!Math::IsZero(foliageType.PlacementRandomPitchAngle))
{
Quaternion::RotationAxis(Vector3::UnitX, Random::Rand() * DegreesToRadians * foliageType.PlacementRandomPitchAngle, tmp);
instance.Transform.Orientation *= tmp;
}
instance.Type = placement.FoliageTypeIndex;
instance.Random = Random::Rand();
instance.Transform.Translation = placement.Location;
if (!foliageType.PlacementOffsetY.IsZero())
{
float offsetY = Math::Lerp(foliageType.PlacementOffsetY.X, foliageType.PlacementOffsetY.Y, Random::Rand());
instance.Transform.Translation += (instance.Transform.Orientation * Vector3::Up) * offsetY;
}
instance.Transform.Scale = foliageType.GetRandomScale();
instance.Transform.Orientation.Normalize();
// Convert instance transformation into the local-space of the foliage actor
foliage->GetTransform().WorldToLocal(instance.Transform, instance.Transform);
// Add foliage instance
foliage->AddInstance(instance);
}
foliage->RebuildClusters();
}
}
void FoliageTools::Remove(Foliage* foliage, Span<int32> foliageTypesIndices, const Vector3& brushPosition, float brushRadius)
{
if (foliageTypesIndices.Length() <= 0)
return;
PROFILE_CPU();
// For each selected foliage instance type try to remove something
const BoundingSphere brush(brushPosition, brushRadius);
const Transform foliageTransform = foliage->GetTransform();
for (auto i = foliage->Instances.Begin(); i.IsNotEnd(); ++i)
{
auto& instance = *i;
// Skip instances outside the brush
const Vector3 instancePosition = foliageTransform.LocalToWorld(instance.Transform.Translation);
if (CollisionsHelper::SphereContainsPoint(brush, instancePosition) == ContainmentType::Disjoint)
continue;
// Skip instances not existing in a filter
bool skip = true;
for (int32 i1 = 0; i1 < foliageTypesIndices.Length(); i1++)
{
if (foliageTypesIndices[i1] == instance.Type)
{
skip = false;
break;
}
}
if (skip)
continue;
// Remove instance
foliage->RemoveInstance(i);
--i;
}
foliage->RebuildClusters();
}