Files
UnrealEngine/Engine/Source/Developer/MeshUtilities/Private/MeshCardRepresentationUtilities.cpp
2025-05-18 13:04:45 +08:00

1128 lines
38 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MeshUtilities.h"
#include "MeshUtilitiesPrivate.h"
#include "Async/ParallelFor.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Materials/Material.h"
#include "RawMesh.h"
#include "StaticMeshResources.h"
#include "MeshCardBuild.h"
#include "MeshCardRepresentation.h"
#include "DistanceFieldAtlas.h"
#include "MeshRepresentationCommon.h"
#include "Containers/BinaryHeap.h"
#include <cmath>
static TAutoConsoleVariable<int32> CVarCardRepresentationParallelBuild(
TEXT("r.MeshCardRepresentation.ParallelBuild"),
1,
TEXT("Whether to use task for mesh card building."),
ECVF_Scalability);
namespace MeshCardGen
{
int32 constexpr NumAxisAlignedDirections = 6;
int32 constexpr MaxCardsPerMesh = 32;
};
class FGenerateCardMeshContext
{
public:
const FString& MeshName;
const FEmbreeScene& EmbreeScene;
FCardRepresentationData& OutData;
FGenerateCardMeshContext(const FString& InMeshName, const FEmbreeScene& InEmbreeScene, FCardRepresentationData& InOutData) :
MeshName(InMeshName),
EmbreeScene(InEmbreeScene),
OutData(InOutData)
{}
};
struct FIntBox
{
FIntBox()
: Min(INT32_MAX)
, Max(-INT32_MAX)
{}
FIntBox(const FIntVector& InMin, const FIntVector& InMax)
: Min(InMin)
, Max(InMax)
{}
void Init()
{
Min = FIntVector(INT32_MAX);
Max = FIntVector(-INT32_MAX);
}
void Add(const FIntVector& Point)
{
Min = FIntVector(FMath::Min(Min.X, Point.X), FMath::Min(Min.Y, Point.Y), FMath::Min(Min.Z, Point.Z));
Max = FIntVector(FMath::Max(Max.X, Point.X), FMath::Max(Max.Y, Point.Y), FMath::Max(Max.Z, Point.Z));
}
FIntVector2 GetFaceXY() const
{
return FIntVector2(Max.X + 1 - Min.X, Max.Y + 1 - Min.Y);
}
int32 GetFaceArea() const
{
return (Max.X + 1 - Min.X) * (Max.Y + 1 - Min.Y);
}
bool Contains(const FIntBox& Other) const
{
if (Other.Min.X >= Min.X && Other.Max.X <= Max.X
&& Other.Min.Y >= Min.Y && Other.Max.Y <= Max.Y
&& Other.Min.Z >= Min.Z && Other.Max.Z <= Max.Z)
{
return true;
}
return false;
}
FIntVector GetAxisDistanceFromBox(const FIntBox& Box)
{
const FIntVector CenterDelta2 = (Max - Min) - (Box.Max - Box.Min);
const FIntVector ExtentSum2 = (Max + Min) + (Box.Max + Box.Min);
FIntVector AxisDistance;
AxisDistance.X = FMath::Max(FMath::Abs(CenterDelta2.X) - ExtentSum2.X, 0) / 2;
AxisDistance.Y = FMath::Max(FMath::Abs(CenterDelta2.Y) - ExtentSum2.Y, 0) / 2;
AxisDistance.Z = FMath::Max(FMath::Abs(CenterDelta2.Z) - ExtentSum2.Z, 0) / 2;
return AxisDistance;
}
FIntVector GetAxisDistanceFromPoint(const FIntVector& Point)
{
FIntVector AxisDistance;
AxisDistance.X = FMath::Max(FMath::Max(Min.X - Point.X, Point.X - Max.X), 0);
AxisDistance.Y = FMath::Max(FMath::Max(Min.Y - Point.Y, Point.Y - Max.Y), 0);
AxisDistance.Z = FMath::Max(FMath::Max(Min.Z - Point.Z, Point.Z - Max.Z), 0);
return AxisDistance;
}
FIntVector Min;
FIntVector Max;
};
#if USE_EMBREE
typedef uint16 FSurfelIndex;
constexpr FSurfelIndex INVALID_SURFEL_INDEX = UINT16_MAX;
struct FSurfel
{
FIntVector Coord;
// Card's min near plane distance from this surfel
int32 MinRayZ;
// Percentage of rays which hit something in this cell
float Coverage;
// Coverage weighted by the visibility of this surfel from outside the mesh, decides how important is to cover this surfel
float WeightedCoverage;
};
struct FSurfelScenePerDirection
{
TArray<FSurfel> Surfels;
FLumenCardBuildDebugData DebugData;
void Init()
{
DebugData.Init();
Surfels.Reset();
}
};
struct FSurfelScene
{
FSurfelScenePerDirection Directions[MeshCardGen::NumAxisAlignedDirections];
int32 NumSurfels = 0;
};
struct FAxisAlignedDirectionBasis
{
FMatrix44f LocalToWorldRotation;
FVector3f LocalToWorldOffset;
FIntVector VolumeSize;
float VoxelSize;
FVector3f TransformSurfel(FIntVector SurfelCoord) const
{
return LocalToWorldRotation.TransformPosition(FVector3f(SurfelCoord.X + 0.5f, SurfelCoord.Y + 0.5f, SurfelCoord.Z)) * VoxelSize + LocalToWorldOffset;
}
};
struct FClusteringParams
{
float VoxelSize = 0.0f;
float MinClusterCoverage = 0.0f;
float MinOuterClusterCoverage = 0.0f;
float MinDensityPerCluster = 0.0f;
int32 MaxLumenMeshCards = 0;
bool bDebug = false;
bool bSingleThreadedBuild = true;
FAxisAlignedDirectionBasis ClusterBasis[MeshCardGen::NumAxisAlignedDirections];
};
FVector3f AxisAlignedDirectionIndexToNormal(int32 AxisAlignedDirectionIndex)
{
const int32 AxisIndex = AxisAlignedDirectionIndex / 2;
FVector3f Normal(0.0f, 0.0f, 0.0f);
Normal[AxisIndex] = AxisAlignedDirectionIndex & 1 ? 1.0f : -1.0f;
return Normal;
}
class FSurfelCluster
{
public:
FIntBox Bounds;
TArray<FSurfelIndex> SurfelIndices;
int32 NearPlane = 0;
float Coverage = 0.0f;
// Coverage weighted by visibility
float WeightedCoverage = 0.0f;
// Best surfels to add to this cluster
FSurfelIndex BestSurfelIndex = INVALID_SURFEL_INDEX;
float BestSurfelDistance = FLT_MAX;
void Init(int32 InNearPlane)
{
Bounds.Init();
SurfelIndices.Reset();
NearPlane = InNearPlane;
Coverage = 0.0f;
WeightedCoverage = 0.0f;
BestSurfelIndex = INVALID_SURFEL_INDEX;
BestSurfelDistance = FLT_MAX;
}
bool IsValid(const FClusteringParams& ClusteringParams) const
{
return WeightedCoverage >= (NearPlane == 0 ? ClusteringParams.MinOuterClusterCoverage : ClusteringParams.MinClusterCoverage)
&& GetDensity() > ClusteringParams.MinDensityPerCluster;
}
float GetDensity() const
{
const float Density = Coverage / (float)Bounds.GetFaceArea();
return Density;
}
void AddSurfel(const FSurfelScenePerDirection& SurfelScene, FSurfelIndex SurfelToAddIndex);
};
void FSurfelCluster::AddSurfel(const FSurfelScenePerDirection& SurfelScene, FSurfelIndex SurfelIndex)
{
const FSurfel& Surfel = SurfelScene.Surfels[SurfelIndex];
if (Surfel.Coord.Z >= NearPlane && Surfel.MinRayZ <= NearPlane)
{
SurfelIndices.Add(SurfelIndex);
Bounds.Add(Surfel.Coord);
Coverage += Surfel.Coverage;
WeightedCoverage += Surfel.WeightedCoverage;
// Check if all surfels are visible after add
check(NearPlane <= Bounds.Min.Z);
}
}
struct FSurfelSample
{
FVector3f Position;
FVector3f Normal;
int32 MinRayZ;
int32 CellZ;
};
struct FSurfelVisibility
{
float Visibility;
bool bValid;
};
// Trace rays over the hemisphere and discard surfels which mostly hit back faces
FSurfelVisibility ComputeSurfelVisibility(
const FGenerateCardMeshContext& Context,
const TArray<FSurfelSample>& SurfelSamples,
uint32 SurfelSamplesOffset,
uint32 SurfelSamplesNum,
const TArray<FVector3f>& RayDirectionsOverHemisphere,
FLumenCardBuildDebugData& DebugData)
{
uint32 SurfelSampleIndex = 0;
uint32 NumHits = 0;
uint32 NumBackFaceHits = 0;
const float SurfaceRayBias = 0.1f;
float VisibilitySum = 0.0f;
for (int32 RayIndex = 0; RayIndex < RayDirectionsOverHemisphere.Num(); ++RayIndex)
{
const FSurfelSample& SurfelSample = SurfelSamples[SurfelSampleIndex + SurfelSamplesOffset];
const FMatrix44f SurfaceBasis = MeshRepresentation::GetTangentBasisFrisvad(SurfelSample.Normal);
const FVector3f RayOrigin = SurfelSample.Position;
const FVector3f RayDirection = SurfaceBasis.TransformVector(RayDirectionsOverHemisphere[RayIndex]);
FEmbreeRay EmbreeRay;
EmbreeRay.ray.org_x = RayOrigin.X;
EmbreeRay.ray.org_y = RayOrigin.Y;
EmbreeRay.ray.org_z = RayOrigin.Z;
EmbreeRay.ray.dir_x = RayDirection.X;
EmbreeRay.ray.dir_y = RayDirection.Y;
EmbreeRay.ray.dir_z = RayDirection.Z;
EmbreeRay.ray.tnear = SurfaceRayBias;
EmbreeRay.ray.tfar = FLT_MAX;
EmbreeRay.ray.mask = 0xFFFFFFFF;
#if USE_EMBREE_MAJOR_VERSION >= 4
FEmbreeRayQueryContext EmbreeContext;
rtcInitRayQueryContext(&EmbreeContext);
RTCIntersectArguments args;
rtcInitIntersectArguments(&args);
args.context = &EmbreeContext;
rtcIntersect1(Context.EmbreeScene.Scene, &EmbreeRay, &args);
#else
FEmbreeIntersectionContext EmbreeContext;
rtcInitIntersectContext(&EmbreeContext);
rtcIntersect1(Context.EmbreeScene.Scene, &EmbreeContext, &EmbreeRay);
#endif
if (EmbreeRay.hit.geomID != RTC_INVALID_GEOMETRY_ID && EmbreeRay.hit.primID != RTC_INVALID_GEOMETRY_ID)
{
++NumHits;
if (FVector::DotProduct((FVector)RayDirection, (FVector)EmbreeRay.GetHitNormal()) > 0.0f && !EmbreeContext.IsHitTwoSided())
{
++NumBackFaceHits;
}
else
{
VisibilitySum += 0.0f;
}
}
else
{
VisibilitySum += 1.0f;
}
#if 0
FLumenCardBuildDebugData::FRay& SurfelRay = DebugData.SurfelRays.AddDefaulted_GetRef();
SurfelRay.RayStart = RayOrigin;
SurfelRay.RayEnd = RayOrigin + RayDirection * (EmbreeRay.ray.tfar < FLT_MAX ? EmbreeRay.ray.tfar : 200.0f);
SurfelRay.bHit = EmbreeRay.ray.tfar < FLT_MAX;
#endif
SurfelSampleIndex = (SurfelSampleIndex + 1) % SurfelSamplesNum;
}
const bool bInsideGeometry =
NumHits > 0.8f * RayDirectionsOverHemisphere.Num()
&& NumBackFaceHits > 0.2f * RayDirectionsOverHemisphere.Num();
FSurfelVisibility SurfelVisibility;
SurfelVisibility.Visibility = VisibilitySum / RayDirectionsOverHemisphere.Num();
SurfelVisibility.bValid = !bInsideGeometry;
return SurfelVisibility;
}
/**
* Voxelize mesh by casting multiple rays per cell
*/
void GenerateSurfelsForDirection(
const FGenerateCardMeshContext& Context,
const FAxisAlignedDirectionBasis& ClusterBasis,
const TArray<FVector3f>& RayDirectionsOverHemisphere,
const FClusteringParams& ClusteringParams,
FSurfelScenePerDirection& SurfelScenePerDirection)
{
const float NormalWeightTreshold = MeshCardRepresentation::GetNormalTreshold();
const FVector3f RayDirection = ClusterBasis.LocalToWorldRotation.GetScaledAxis(EAxis::Type::Z);
const uint32 NumSurfelSamples = 32;
const uint32 MinSurfelSamples = 1;
TArray<FSurfelSample> SurfelSamples;
TArray<uint32> NumSurfelSamplesPerCell;
TArray<uint32> SurfelSamplesOffsetPerCell;
for (int32 CoordY = 0; CoordY < ClusterBasis.VolumeSize.Y; ++CoordY)
{
for (int32 CoordX = 0; CoordX < ClusterBasis.VolumeSize.X; ++CoordX)
{
SurfelSamples.Reset();
NumSurfelSamplesPerCell.SetNum(ClusterBasis.VolumeSize.Z);
SurfelSamplesOffsetPerCell.SetNum(ClusterBasis.VolumeSize.Z);
for (int32 CoordZ = 0; CoordZ < ClusterBasis.VolumeSize.Z; ++CoordZ)
{
NumSurfelSamplesPerCell[CoordZ] = 0;
SurfelSamplesOffsetPerCell[CoordZ] = 0;
}
// Trace multiple rays per cell and mark cells which need to spawn a surfel
for (uint32 SampleIndex = 0; SampleIndex < NumSurfelSamples; ++SampleIndex)
{
FVector3f Jitter;
Jitter.X = (SampleIndex + 0.5f) / NumSurfelSamples;
Jitter.Y = (double)ReverseBits(SampleIndex) / (double)0x100000000LL;
FVector3f RayOrigin = ClusterBasis.LocalToWorldRotation.TransformPosition(FVector3f(CoordX + Jitter.X, CoordY + Jitter.Y, 0.0f)) * ClusteringParams.VoxelSize + ClusterBasis.LocalToWorldOffset;
// Need to pullback to make sure that ray will start outside of geometry, as voxels may be smaller than mesh
// due to voxel size rounding or they may start exactly at edge of geometry
const float NearPlaneOffset = 2.0f * ClusteringParams.VoxelSize;
RayOrigin -= RayDirection * NearPlaneOffset;
// Cell index where any geometry was last found
int32 LastHitCoordZ = -2;
int32 SkipPrimId = RTC_INVALID_GEOMETRY_ID;
float RayTNear = 0.0f;
while (LastHitCoordZ + 1 < ClusterBasis.VolumeSize.Z)
{
FEmbreeRay EmbreeRay;
EmbreeRay.ray.org_x = RayOrigin.X;
EmbreeRay.ray.org_y = RayOrigin.Y;
EmbreeRay.ray.org_z = RayOrigin.Z;
EmbreeRay.ray.dir_x = RayDirection.X;
EmbreeRay.ray.dir_y = RayDirection.Y;
EmbreeRay.ray.dir_z = RayDirection.Z;
EmbreeRay.ray.tnear = RayTNear;
EmbreeRay.ray.tfar = FLT_MAX;
EmbreeRay.ray.mask = 0xFFFFFFFF;
#if USE_EMBREE_MAJOR_VERSION >= 4
FEmbreeRayQueryContext EmbreeContext;
rtcInitRayQueryContext(&EmbreeContext);
EmbreeContext.SkipPrimId = SkipPrimId;
RTCIntersectArguments args;
rtcInitIntersectArguments(&args);
args.context = &EmbreeContext;
rtcIntersect1(Context.EmbreeScene.Scene, &EmbreeRay, &args);
#else
FEmbreeIntersectionContext EmbreeContext;
rtcInitIntersectContext(&EmbreeContext);
EmbreeContext.SkipPrimId = SkipPrimId;
rtcIntersect1(Context.EmbreeScene.Scene, &EmbreeContext, &EmbreeRay);
#endif
if (EmbreeRay.hit.geomID != RTC_INVALID_GEOMETRY_ID && EmbreeRay.hit.primID != RTC_INVALID_GEOMETRY_ID)
{
const int32 HitCoordZ = FMath::Clamp((EmbreeRay.ray.tfar - NearPlaneOffset) / ClusteringParams.VoxelSize, 0, ClusterBasis.VolumeSize.Z - 1);
FVector SurfaceNormal = (FVector)EmbreeRay.GetHitNormal();
float NdotD = FVector::DotProduct((FVector)-RayDirection, SurfaceNormal);
// Handle two sided hits
if (NdotD < 0.0f && EmbreeContext.IsHitTwoSided())
{
NdotD = -NdotD;
SurfaceNormal = -SurfaceNormal;
}
const bool bPassProjectionTest = NdotD >= NormalWeightTreshold;
if (bPassProjectionTest && HitCoordZ >= 0 && HitCoordZ > LastHitCoordZ + 1 && HitCoordZ < ClusterBasis.VolumeSize.Z)
{
FSurfelSample& SurfelSample = SurfelSamples.AddDefaulted_GetRef();
SurfelSample.Position = RayOrigin + RayDirection * EmbreeRay.ray.tfar;
SurfelSample.Normal = (FVector3f)SurfaceNormal;
SurfelSample.CellZ = HitCoordZ;
SurfelSample.MinRayZ = 0;
if (LastHitCoordZ >= 0)
{
SurfelSample.MinRayZ = FMath::Max(SurfelSample.MinRayZ, LastHitCoordZ + 1);
}
}
// Move ray to the next intersection
LastHitCoordZ = HitCoordZ;
// Sometimes EmbreeRay.ray.tnear was further than EmbreeRay.ray.tfar causing an infinite loop
const float SafeTFar = FMath::Max(EmbreeRay.ray.tfar, EmbreeRay.ray.tnear);
RayTNear = std::nextafter(FMath::Max(NearPlaneOffset + (LastHitCoordZ + 1) * ClusteringParams.VoxelSize, SafeTFar), std::numeric_limits<float>::infinity());
SkipPrimId = EmbreeRay.hit.primID;
}
else
{
break;
}
}
}
// Sort surfel candidates and compact arrays
{
struct FSortByZ
{
FORCEINLINE bool operator()(const FSurfelSample& A, const FSurfelSample& B) const
{
if (A.CellZ != B.CellZ)
{
return A.CellZ < B.CellZ;
}
return A.MinRayZ > B.MinRayZ;
}
};
SurfelSamples.Sort(FSortByZ());
for (int32 SampleIndex = 0; SampleIndex < SurfelSamples.Num(); ++SampleIndex)
{
const FSurfelSample& SurfelSample = SurfelSamples[SampleIndex];
++NumSurfelSamplesPerCell[SurfelSample.CellZ];
}
for (int32 CoordZ = 1; CoordZ < ClusterBasis.VolumeSize.Z; ++CoordZ)
{
SurfelSamplesOffsetPerCell[CoordZ] = SurfelSamplesOffsetPerCell[CoordZ - 1] + NumSurfelSamplesPerCell[CoordZ - 1];
}
}
// Convert surfel candidates into actual surfels
for (int32 CoordZ = 0; CoordZ < ClusterBasis.VolumeSize.Z; ++CoordZ)
{
const int32 CellNumSurfelSamples = NumSurfelSamplesPerCell[CoordZ];
const int32 CellSurfelSamplesOffset = SurfelSamplesOffsetPerCell[CoordZ];
int32 SurfelSampleSpanBegin = 0;
int32 SurfelSampleSpanSize = 0;
bool bAnySurfelAdded = false;
while (SurfelSampleSpanBegin + 1 < CellNumSurfelSamples)
{
// Find continuous spans of equal MinRayZ
// Every such span will spawn one surfel
SurfelSampleSpanSize = 0;
for (int32 SampleIndex = SurfelSampleSpanBegin; SampleIndex < CellNumSurfelSamples; ++SampleIndex)
{
if (SurfelSamples[SampleIndex].MinRayZ == SurfelSamples[SurfelSampleSpanBegin].MinRayZ)
{
++SurfelSampleSpanSize;
}
else
{
break;
}
}
if (SurfelSampleSpanSize >= MinSurfelSamples)
{
FSurfelVisibility SurfelVisibility = ComputeSurfelVisibility(
Context,
SurfelSamples,
CellSurfelSamplesOffset + SurfelSampleSpanBegin,
SurfelSampleSpanSize,
RayDirectionsOverHemisphere,
SurfelScenePerDirection.DebugData);
const float Coverage = SurfelSampleSpanSize / float(NumSurfelSamples);
if (SurfelVisibility.bValid)
{
const int32 MedianMinRayZ = SurfelSamples[CellSurfelSamplesOffset + SurfelSampleSpanBegin].MinRayZ;
FSurfel& Surfel = SurfelScenePerDirection.Surfels.AddDefaulted_GetRef();
Surfel.Coord = FIntVector(CoordX, CoordY, CoordZ);
Surfel.MinRayZ = MedianMinRayZ;
Surfel.Coverage = Coverage;
Surfel.WeightedCoverage = Coverage * (SurfelVisibility.Visibility + 1.0f);
check(Surfel.Coord.Z > Surfel.MinRayZ || Surfel.MinRayZ == 0);
}
if (ClusteringParams.bDebug)
{
FLumenCardBuildDebugData::FSurfel& DebugSurfel = SurfelScenePerDirection.DebugData.Surfels.AddDefaulted_GetRef();
DebugSurfel.Position = ClusterBasis.TransformSurfel(FIntVector(CoordX, CoordY, CoordZ));
DebugSurfel.Normal = -RayDirection;
DebugSurfel.Coverage = Coverage;
DebugSurfel.Visibility = SurfelVisibility.Visibility;
DebugSurfel.SourceSurfelIndex = SurfelScenePerDirection.Surfels.Num() - 1;
DebugSurfel.Type = SurfelVisibility.bValid ? FLumenCardBuildDebugData::ESurfelType::Valid : FLumenCardBuildDebugData::ESurfelType::Invalid;
bAnySurfelAdded = true;
}
}
SurfelSampleSpanBegin += SurfelSampleSpanSize;
}
#define DEBUG_ADD_ALL_SURFELS 0
#if DEBUG_ADD_ALL_SURFELS
if (ClusteringParams.bDebug && !bAnySurfelAdded)
{
FLumenCardBuildDebugData::FSurfel& DebugSurfel = SurfelScenePerDirection.DebugData.Surfels.AddDefaulted_GetRef();
DebugSurfel.Position = ClusterBasis.TransformSurfel(FIntVector(CoordX, CoordY, CoordZ));
DebugSurfel.Normal = -RayDirection;
DebugSurfel.Coverage = 1.0f;
DebugSurfel.Visibility = 1.0f;
DebugSurfel.SourceSurfelIndex = SurfelScenePerDirection.Surfels.Num() - 1;
DebugSurfel.Type = FLumenCardBuildDebugData::ESurfelType::Invalid;
}
#endif
}
}
}
}
void InitClusteringParams(FClusteringParams& ClusteringParams, const FBox& MeshCardsBounds, int32 MaxVoxels, int32 MaxLumenMeshCards)
{
const float TargetVoxelSize = 10.0f;
const FVector3f MeshCardsBoundsSize = 2.0f * (FVector3f)MeshCardsBounds.GetExtent();
const float MaxMeshCardsBounds = MeshCardsBoundsSize.GetMax();
// Target object space detail size
const float MaxSizeInVoxels = FMath::Clamp(MaxMeshCardsBounds / TargetVoxelSize + 0.5f, 1, MaxVoxels);
const float VoxelSize = FMath::Max(TargetVoxelSize, MaxMeshCardsBounds / MaxSizeInVoxels);
FIntVector SizeInVoxels;
SizeInVoxels.X = FMath::Clamp(FMath::RoundToFloat(MeshCardsBoundsSize.X / VoxelSize), 1, MaxVoxels);
SizeInVoxels.Y = FMath::Clamp(FMath::RoundToFloat(MeshCardsBoundsSize.Y / VoxelSize), 1, MaxVoxels);
SizeInVoxels.Z = FMath::Clamp(FMath::RoundToFloat(MeshCardsBoundsSize.Z / VoxelSize), 1, MaxVoxels);
const FVector3f VoxelBoundsCenter = (FVector3f)MeshCardsBounds.GetCenter();
const FVector3f VoxelBoundsExtent = FVector3f(SizeInVoxels) * VoxelSize * 0.5f;
const FVector3f VoxelBoundsMin = VoxelBoundsCenter - VoxelBoundsExtent;
const FVector3f VoxelBoundsMax = VoxelBoundsCenter + VoxelBoundsExtent;
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
FAxisAlignedDirectionBasis& ClusterBasis = ClusteringParams.ClusterBasis[AxisAlignedDirectionIndex];
ClusterBasis.VoxelSize = VoxelSize;
FVector3f XAxis = FVector3f(1.0f, 0.0f, 0.0f);
FVector3f YAxis = FVector3f(0.0f, 1.0f, 0.0f);
switch (AxisAlignedDirectionIndex / 2)
{
case 0:
XAxis = FVector3f(0.0f, 1.0f, 0.0f);
YAxis = FVector3f(0.0f, 0.0f, 1.0f);
break;
case 1:
XAxis = FVector3f(1.0f, 0.0f, 0.0f);
YAxis = FVector3f(0.0f, 0.0f, 1.0f);
break;
case 2:
XAxis = FVector3f(1.0f, 0.0f, 0.0f);
YAxis = FVector3f(0.0f, 1.0f, 0.0f);
break;
}
FVector3f ZAxis = AxisAlignedDirectionIndexToNormal(AxisAlignedDirectionIndex);
ClusterBasis.LocalToWorldRotation = FMatrix44f(XAxis, YAxis, -ZAxis, FVector3f::ZeroVector);
ClusterBasis.LocalToWorldOffset = VoxelBoundsMin;
if (AxisAlignedDirectionIndex & 1)
{
ClusterBasis.LocalToWorldOffset[AxisAlignedDirectionIndex / 2] = VoxelBoundsMax[AxisAlignedDirectionIndex / 2];
}
switch (AxisAlignedDirectionIndex / 2)
{
case 0:
ClusterBasis.VolumeSize.X = SizeInVoxels.Y;
ClusterBasis.VolumeSize.Y = SizeInVoxels.Z;
ClusterBasis.VolumeSize.Z = SizeInVoxels.X;
break;
case 1:
ClusterBasis.VolumeSize.X = SizeInVoxels.X;
ClusterBasis.VolumeSize.Y = SizeInVoxels.Z;
ClusterBasis.VolumeSize.Z = SizeInVoxels.Y;
break;
case 2:
ClusterBasis.VolumeSize.X = SizeInVoxels.X;
ClusterBasis.VolumeSize.Y = SizeInVoxels.Y;
ClusterBasis.VolumeSize.Z = SizeInVoxels.Z;
break;
}
}
const float AverageFaceArea = 2.0f * (SizeInVoxels.X * SizeInVoxels.Y + SizeInVoxels.X * SizeInVoxels.Z + SizeInVoxels.Y * SizeInVoxels.Z) / 6.0f;
ClusteringParams.VoxelSize = VoxelSize;
ClusteringParams.MinDensityPerCluster = MeshCardRepresentation::GetMinDensity();
ClusteringParams.MinDensityPerCluster = MeshCardRepresentation::GetMinDensity() / 3.0f;
ClusteringParams.MinClusterCoverage = 15.0f;
ClusteringParams.MinOuterClusterCoverage = FMath::Min(ClusteringParams.MinClusterCoverage, 0.5f * AverageFaceArea);
ClusteringParams.MaxLumenMeshCards = MaxLumenMeshCards;
ClusteringParams.bDebug = MeshCardRepresentation::IsDebugMode();
ClusteringParams.bSingleThreadedBuild = CVarCardRepresentationParallelBuild.GetValueOnAnyThread() == 0;
}
void InitSurfelScene(
const FGenerateCardMeshContext& Context,
const FBox& MeshCardsBounds,
int32 MaxLumenMeshCards,
FSurfelScene& SurfelScene,
FClusteringParams& ClusteringParams)
{
TRACE_CPUPROFILER_EVENT_SCOPE(GenerateSurfels);
if (Context.EmbreeScene.NumTrianglesTotal == 0)
{
return;
}
// Generate random ray directions over a hemisphere
constexpr uint32 NumRayDirectionsOverHemisphere = 32;
TArray<FVector3f> RayDirectionsOverHemisphere;
{
FRandomStream RandomStream(0);
MeshUtilities::GenerateStratifiedUniformHemisphereSamples(NumRayDirectionsOverHemisphere, RandomStream, RayDirectionsOverHemisphere);
}
const int32 DebugSurfelDirection = MeshCardRepresentation::GetDebugSurfelDirection();
// Limit max number of surfels to prevent generation time from exploding, as dense two sided meshes can generate many more surfels than simple walls
int32 TargetNumSufels = 10000;
float MaxVoxels = 64;
do
{
InitClusteringParams(ClusteringParams, MeshCardsBounds, MaxVoxels, MaxLumenMeshCards);
ParallelFor(TEXT("InitSurfelScene.PF"), MeshCardGen::NumAxisAlignedDirections, 1,
[&](int32 AxisAlignedDirectionIndex)
{
if (DebugSurfelDirection < 0 || DebugSurfelDirection == AxisAlignedDirectionIndex)
{
FSurfelScenePerDirection& SurfelScenePerDirection = SurfelScene.Directions[AxisAlignedDirectionIndex];
SurfelScenePerDirection.Init();
GenerateSurfelsForDirection(
Context,
ClusteringParams.ClusterBasis[AxisAlignedDirectionIndex],
RayDirectionsOverHemisphere,
ClusteringParams,
SurfelScenePerDirection
);
}
}, ClusteringParams.bSingleThreadedBuild ? EParallelForFlags::ForceSingleThread : EParallelForFlags::None);
SurfelScene.NumSurfels = 0;
for (const FSurfelScenePerDirection& SurfelScenePerDirection : SurfelScene.Directions)
{
SurfelScene.NumSurfels += SurfelScenePerDirection.Surfels.Num();
}
MaxVoxels = MaxVoxels / 2;
} while (SurfelScene.NumSurfels > TargetNumSufels && MaxVoxels > 1);
if (ClusteringParams.bDebug)
{
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
FLumenCardBuildDebugData& MergedDebugData = Context.OutData.MeshCardsBuildData.DebugData;
FLumenCardBuildDebugData& DirectionDebugData = SurfelScene.Directions[AxisAlignedDirectionIndex].DebugData;
const int32 SurfelOffset = MergedDebugData.Surfels.Num();
MergedDebugData.Surfels.Append(DirectionDebugData.Surfels);
MergedDebugData.SurfelRays.Append(DirectionDebugData.SurfelRays);
for (FSurfelIndex SurfelIndex = SurfelOffset; SurfelIndex < MergedDebugData.Surfels.Num(); ++SurfelIndex)
{
MergedDebugData.Surfels[SurfelIndex].SourceSurfelIndex += SurfelOffset;
}
}
}
}
struct FMeshCardsPerDirection
{
TArray<FSurfelCluster> Clusters;
};
struct FMeshCards
{
FMeshCardsPerDirection Directions[MeshCardGen::NumAxisAlignedDirections];
float WeightedSurfaceCoverage = 0.0f;
float SurfaceArea = 0.0f;
int32 NumSurfels = 0;
int32 NumClusters = 0;
};
void UpdateMeshCardsCoverage(const FSurfelScene& SurfelScene, const FClusteringParams& ClusteringParams, FMeshCards& MeshCards)
{
MeshCards.WeightedSurfaceCoverage = 0.0f;
MeshCards.SurfaceArea = 0.0f;
MeshCards.NumSurfels = 0;
MeshCards.NumClusters = 0;
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
TArray<FSurfelCluster>& Clusters = MeshCards.Directions[AxisAlignedDirectionIndex].Clusters;
const TArray<FSurfel>& Surfels = SurfelScene.Directions[AxisAlignedDirectionIndex].Surfels;
for (FSurfelCluster& Cluster : Clusters)
{
MeshCards.WeightedSurfaceCoverage += Cluster.WeightedCoverage;
MeshCards.SurfaceArea += Cluster.Bounds.GetFaceArea();
}
MeshCards.NumSurfels += Surfels.Num();
MeshCards.NumClusters += Clusters.Num();
}
}
/**
* Assign surfels to a single cluster
*/
void BuildCluster(
int32 NearPlane,
const FSurfelScenePerDirection& SurfelScene,
TBitArray<>& SurfelAssignedToAnyCluster,
FSurfelCluster& Cluster)
{
Cluster.Init(NearPlane);
for (int32 SurfelIndex = 0; SurfelIndex < SurfelScene.Surfels.Num(); ++SurfelIndex)
{
if (!SurfelAssignedToAnyCluster[SurfelIndex])
{
Cluster.AddSurfel(SurfelScene, SurfelIndex);
}
}
}
/**
* Add cluster to the cluster list
*/
void CommitCluster(TArray<FSurfelCluster>& Clusters, TBitArray<>& SurfelAssignedToAnyCluster, FSurfelCluster const& Cluster)
{
for (int32 SurfelIndex : Cluster.SurfelIndices)
{
SurfelAssignedToAnyCluster[SurfelIndex] = true;
}
Clusters.Add(Cluster);
}
/**
* Sort clusters by importance and limit number of clusters based on the set target
*/
void LimitClusters(const FClusteringParams& ClusteringParams, const FSurfelScene& SurfelScene, FMeshCards& MeshCards)
{
int32 NumClusters = 0;
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
TArray<FSurfelCluster>& Clusters = MeshCards.Directions[AxisAlignedDirectionIndex].Clusters;
const TArray<FSurfel>& Surfels = SurfelScene.Directions[AxisAlignedDirectionIndex].Surfels;
struct FSortByClusterWeightedCoverage
{
FORCEINLINE bool operator()(const FSurfelCluster& A, const FSurfelCluster& B) const
{
return A.WeightedCoverage > B.WeightedCoverage;
}
};
Clusters.Sort(FSortByClusterWeightedCoverage());
NumClusters += Clusters.Num();
}
while (NumClusters > ClusteringParams.MaxLumenMeshCards)
{
float SmallestClusterWeightedCoverage = FLT_MAX;
int32 SmallestClusterDirectionIndex = 0;
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
const TArray<FSurfelCluster>& Clusters = MeshCards.Directions[AxisAlignedDirectionIndex].Clusters;
if (Clusters.Num() > 0)
{
const FSurfelCluster& Cluster = Clusters.Last();
if (Cluster.WeightedCoverage < SmallestClusterWeightedCoverage)
{
SmallestClusterDirectionIndex = AxisAlignedDirectionIndex;
SmallestClusterWeightedCoverage = Cluster.WeightedCoverage;
}
}
}
FMeshCardsPerDirection& MeshCardsPerDirection = MeshCards.Directions[SmallestClusterDirectionIndex];
MeshCardsPerDirection.Clusters.Pop();
--NumClusters;
}
}
/**
* Cover mesh using a set of clusters(cards)
*/
void BuildSurfelClusters(const FBox& MeshBounds, const FGenerateCardMeshContext& Context, const FSurfelScene& SurfelScene, const FClusteringParams& ClusteringParams, FMeshCards& MeshCards)
{
TBitArray<> SurfelAssignedToAnyClusterArray[MeshCardGen::NumAxisAlignedDirections];
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
SurfelAssignedToAnyClusterArray[AxisAlignedDirectionIndex].Init(false, SurfelScene.Directions[AxisAlignedDirectionIndex].Surfels.Num());
}
ParallelFor(TEXT("BuildMeshCards.PF"), MeshCardGen::NumAxisAlignedDirections, 1,
[&](int32 AxisAlignedDirectionIndex)
{
const FSurfelScenePerDirection& SurfelScenePerDirection = SurfelScene.Directions[AxisAlignedDirectionIndex];
TArray<FSurfelCluster>& Clusters = MeshCards.Directions[AxisAlignedDirectionIndex].Clusters;
TBitArray<>& SurfelAssignedToAnyCluster = SurfelAssignedToAnyClusterArray[AxisAlignedDirectionIndex];
const FAxisAlignedDirectionBasis& ClusterBasis = ClusteringParams.ClusterBasis[AxisAlignedDirectionIndex];
FSurfelCluster TempCluster;
BuildCluster(/*NearPlane*/ 0, SurfelScenePerDirection, SurfelAssignedToAnyCluster, TempCluster);
if (TempCluster.IsValid(ClusteringParams))
{
CommitCluster(Clusters, SurfelAssignedToAnyCluster, TempCluster);
}
// Assume that two sided is foliage and revert to a simpler box projection
if (!Context.EmbreeScene.bMostlyTwoSided)
{
FSurfelCluster BestCluster;
bool bCanAddCluster = true;
while (bCanAddCluster)
{
BestCluster.Init(-1);
for (int32 NearPlane = 1; NearPlane < ClusterBasis.VolumeSize.Z; ++NearPlane)
{
BuildCluster(NearPlane, SurfelScenePerDirection, SurfelAssignedToAnyCluster, TempCluster);
if (TempCluster.IsValid(ClusteringParams) && TempCluster.WeightedCoverage > BestCluster.WeightedCoverage)
{
BestCluster = TempCluster;
}
}
bCanAddCluster = BestCluster.IsValid(ClusteringParams);
if (bCanAddCluster)
{
CommitCluster(Clusters, SurfelAssignedToAnyCluster, BestCluster);
}
}
}
}, ClusteringParams.bSingleThreadedBuild ? EParallelForFlags::ForceSingleThread : EParallelForFlags::None);
LimitClusters(ClusteringParams, SurfelScene, MeshCards);
}
void SerializeLOD(
const FGenerateCardMeshContext& Context,
const FClusteringParams& ClusteringParams,
const FSurfelScene& SurfelScene,
FMeshCards const& MeshCards,
const FBox& MeshCardsBounds,
FMeshCardsBuildData& MeshCardsBuildData)
{
int32 SourceSurfelOffset = 0;
for (int32 AxisAlignedDirectionIndex = 0; AxisAlignedDirectionIndex < MeshCardGen::NumAxisAlignedDirections; ++AxisAlignedDirectionIndex)
{
const FAxisAlignedDirectionBasis& ClusterBasis = ClusteringParams.ClusterBasis[AxisAlignedDirectionIndex];
const FSurfelScenePerDirection& SurfelScenePerDirection = SurfelScene.Directions[AxisAlignedDirectionIndex];
const TArray<FSurfel>& Surfels = SurfelScenePerDirection.Surfels;
const TArray<FSurfelCluster>& Clusters = MeshCards.Directions[AxisAlignedDirectionIndex].Clusters;
TBitArray<> DebugSurfelInCluster;
TBitArray<> DebugSurfelInAnyCluster(false, Surfels.Num());
const FBox3f LocalMeshCardsBounds = FBox3f(MeshCardsBounds.ShiftBy((FVector)-ClusterBasis.LocalToWorldOffset).TransformBy(FMatrix(ClusterBasis.LocalToWorldRotation.GetTransposed())));
for (const FSurfelCluster& Cluster : Clusters)
{
// Set card to cover voxels, with a 0.5 voxel margin for the near/far plane
FVector3f ClusterBoundsMin = (FVector3f(Cluster.Bounds.Min) - FVector3f(0.0f, 0.0f, 0.5f)) * ClusteringParams.VoxelSize;
FVector3f ClusterBoundsMax = (FVector3f(Cluster.Bounds.Max) + FVector3f(1.0f, 1.0f, 1.5f)) * ClusteringParams.VoxelSize;
// Clamp to mesh bounds
// Leave small margin for Z as LOD/displacement may move it outside of bounds
static float MarginZ = 10.0f;
ClusterBoundsMin.X = FMath::Max(ClusterBoundsMin.X, LocalMeshCardsBounds.Min.X);
ClusterBoundsMin.Y = FMath::Max(ClusterBoundsMin.Y, LocalMeshCardsBounds.Min.Y);
ClusterBoundsMin.Z = FMath::Max(ClusterBoundsMin.Z, LocalMeshCardsBounds.Min.Z - MarginZ);
ClusterBoundsMax.X = FMath::Min(ClusterBoundsMax.X, LocalMeshCardsBounds.Max.X);
ClusterBoundsMax.Y = FMath::Min(ClusterBoundsMax.Y, LocalMeshCardsBounds.Max.Y);
ClusterBoundsMax.Z = FMath::Min(ClusterBoundsMax.Z, LocalMeshCardsBounds.Max.Z + MarginZ);
const FVector3f ClusterBoundsOrigin = (ClusterBoundsMax + ClusterBoundsMin) * 0.5f;
const FVector3f ClusterBoundsExtent = (ClusterBoundsMax - ClusterBoundsMin) * 0.5f;
const FVector3f MeshClusterBoundsOrigin = ClusterBasis.LocalToWorldRotation.TransformPosition(ClusterBoundsOrigin) + ClusterBasis.LocalToWorldOffset;
FLumenCardBuildData BuiltData;
BuiltData.OBB.Origin = MeshClusterBoundsOrigin;
BuiltData.OBB.Extent = ClusterBoundsExtent;
BuiltData.OBB.AxisX = ClusterBasis.LocalToWorldRotation.GetScaledAxis(EAxis::X);
BuiltData.OBB.AxisY = ClusterBasis.LocalToWorldRotation.GetScaledAxis(EAxis::Y);
BuiltData.OBB.AxisZ = -ClusterBasis.LocalToWorldRotation.GetScaledAxis(EAxis::Z);
BuiltData.AxisAlignedDirectionIndex = AxisAlignedDirectionIndex;
MeshCardsBuildData.CardBuildData.Add(BuiltData);
if (ClusteringParams.bDebug)
{
DebugSurfelInCluster.Reset();
DebugSurfelInCluster.Add(false, Surfels.Num());
FLumenCardBuildDebugData::FSurfelCluster& DebugCluster = MeshCardsBuildData.DebugData.Clusters.AddDefaulted_GetRef();
DebugCluster.Surfels.Reserve(DebugCluster.Surfels.Num() + Surfels.Num());
for (FSurfelIndex SurfelIndex : Cluster.SurfelIndices)
{
FLumenCardBuildDebugData::FSurfel DebugSurfel;
DebugSurfel.Position = ClusterBasis.TransformSurfel(Surfels[SurfelIndex].Coord);
DebugSurfel.Normal = AxisAlignedDirectionIndexToNormal(AxisAlignedDirectionIndex);
DebugSurfel.SourceSurfelIndex = SourceSurfelOffset + SurfelIndex;
DebugSurfel.Type = FLumenCardBuildDebugData::ESurfelType::Cluster;
DebugCluster.Surfels.Add(DebugSurfel);
const int32 SurfelMinRayZ = Surfels[SurfelIndex].MinRayZ;
if (SurfelMinRayZ > 0)
{
FIntVector MinRayZCoord = Surfels[SurfelIndex].Coord;
MinRayZCoord.Z = SurfelMinRayZ;
FLumenCardBuildDebugData::FRay DebugRay;
DebugRay.RayStart = DebugSurfel.Position;
DebugRay.RayEnd = ClusterBasis.TransformSurfel(MinRayZCoord);
DebugRay.bHit = false;
DebugCluster.Rays.Add(DebugRay);
}
DebugSurfelInAnyCluster[SurfelIndex] = true;
DebugSurfelInCluster[SurfelIndex] = true;
}
for (FSurfelIndex SurfelIndex = 0; SurfelIndex < Surfels.Num(); ++SurfelIndex)
{
if (!DebugSurfelInCluster[SurfelIndex])
{
FLumenCardBuildDebugData::FSurfel DebugSurfel;
DebugSurfel.Position = ClusterBasis.TransformSurfel(Surfels[SurfelIndex].Coord);
DebugSurfel.Normal = AxisAlignedDirectionIndexToNormal(AxisAlignedDirectionIndex);
DebugSurfel.SourceSurfelIndex = SourceSurfelOffset + SurfelIndex;
DebugSurfel.Type = DebugSurfelInAnyCluster[SurfelIndex] ? FLumenCardBuildDebugData::ESurfelType::Used : FLumenCardBuildDebugData::ESurfelType::Idle;
DebugCluster.Surfels.Add(DebugSurfel);
}
}
}
}
SourceSurfelOffset += Surfels.Num();
}
if (ClusteringParams.bDebug)
{
UE_LOG(LogMeshUtilities, Log, TEXT("CardGen Mesh:%s Surfels:%d Clusters:%d WeightedSurfaceCoverage:%f ClusterArea:%f"),
*Context.MeshName,
MeshCards.NumSurfels,
MeshCards.NumClusters,
MeshCards.WeightedSurfaceCoverage,
MeshCards.SurfaceArea);
}
}
void BuildMeshCards(const FBox& MeshBounds, const FGenerateCardMeshContext& Context, int32 MaxLumenMeshCards, FCardRepresentationData& OutData)
{
// Make sure BBox isn't empty and we can generate card representation for it. This handles e.g. infinitely thin planes.
const FVector MeshCardsBoundsCenter = MeshBounds.GetCenter();
const FVector MeshCardsBoundsExtent = FVector::Max(MeshBounds.GetExtent() + 1.0f, FVector(1.0f));
const FBox MeshCardsBounds(MeshCardsBoundsCenter - MeshCardsBoundsExtent, MeshCardsBoundsCenter + MeshCardsBoundsExtent);
// Prepare a list of surfels for cluster fitting
FSurfelScene SurfelScene;
FClusteringParams ClusteringParams;
InitSurfelScene(Context, MeshCardsBounds, MaxLumenMeshCards, SurfelScene, ClusteringParams);
FMeshCards MeshCards;
BuildSurfelClusters(MeshBounds, Context, SurfelScene, ClusteringParams, MeshCards);
OutData.MeshCardsBuildData.Bounds = MeshCardsBounds;
OutData.MeshCardsBuildData.bMostlyTwoSided = Context.EmbreeScene.bMostlyTwoSided;
OutData.MeshCardsBuildData.CardBuildData.Reset();
SerializeLOD(Context, ClusteringParams, SurfelScene, MeshCards, MeshCardsBounds, OutData.MeshCardsBuildData);
OutData.MeshCardsBuildData.DebugData.NumSurfels = 0;
for (const FSurfelScenePerDirection& SurfelScenePerDirection : SurfelScene.Directions)
{
OutData.MeshCardsBuildData.DebugData.NumSurfels += SurfelScenePerDirection.Surfels.Num();
}
}
#endif // #if USE_EMBREE
bool FMeshUtilities::GenerateCardRepresentationData(
FString MeshName,
const FMeshDataForDerivedDataTask& MeshData,
const FDistanceFieldVolumeData* DistanceFieldVolumeData,
int32 MaxLumenMeshCards,
bool bGenerateAsIfTwoSided,
FCardRepresentationData& OutData)
{
#if USE_EMBREE
TRACE_CPUPROFILER_EVENT_SCOPE(FMeshUtilities::GenerateCardRepresentationData);
if (MaxLumenMeshCards > 0)
{
const double StartTime = FPlatformTime::Seconds();
// We include translucent triangles to get card representation for them in case translucent mesh tracing is used
// in combination with hardware ray tracing for hit lighting.
const bool bIncludeTranslucentTriangles = true;
FEmbreeScene EmbreeScene;
MeshRepresentation::SetupEmbreeScene(MeshName, bGenerateAsIfTwoSided, EmbreeScene);
if (!EmbreeScene.Scene)
{
return false;
}
if (!MeshRepresentation::AddMeshDataToEmbreeScene(EmbreeScene, MeshData, bIncludeTranslucentTriangles))
{
return false;
}
EmbreeScene.Commit();
FGenerateCardMeshContext Context(MeshName, EmbreeScene, OutData);
// Note: must operate on the SDF bounds when available, because SDF generation can expand the mesh's bounds
const FBox3f BuildCardsBounds = DistanceFieldVolumeData && DistanceFieldVolumeData->LocalSpaceMeshBounds.IsValid ? DistanceFieldVolumeData->LocalSpaceMeshBounds : MeshData.Bounds.GetBox();
BuildMeshCards(FBox(BuildCardsBounds), Context, MaxLumenMeshCards, OutData);
MeshRepresentation::DeleteEmbreeScene(EmbreeScene);
const float TimeElapsed = (float)(FPlatformTime::Seconds() - StartTime);
if (TimeElapsed > 1.0f)
{
UE_LOG(LogMeshUtilities, Log, TEXT("Finished mesh card build in %.1fs %s tris:%d surfels:%d"),
TimeElapsed,
*MeshName,
EmbreeScene.NumTrianglesTotal,
OutData.MeshCardsBuildData.DebugData.NumSurfels);
}
}
return true;
#else
UE_LOG(LogMeshUtilities, Warning, TEXT("Platform did not set USE_EMBREE, GenerateCardRepresentationData failed."));
return false;
#endif
}