// Copyright (c) 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 { enum { Value = true }; }; struct GeometryLookup { BoundingSphere Brush; Array Triangles; Array 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(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(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(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 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 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 treeWalkFunction(GeometryLookup::Search); SceneQuery::TreeExecute(treeWalkFunction, geometry); } // For each selected foliage instance type try to place something Array 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 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(); }