Files
UnrealEngine/Engine/Plugins/MetaHuman/MetaHumanCharacter/Source/MetaHumanCharacterEditor/Private/MetaHumanGeometryRemoval.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

785 lines
30 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MetaHumanGeometryRemoval.h"
#include "MetaHumanCharacterEditorLog.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/Texture2D.h"
#include "ImageCore.h"
#include "ImageCoreUtils.h"
#include "Logging/StructuredLog.h"
#include "Rendering/SkeletalMeshLODImporterData.h"
#include "Rendering/SkeletalMeshLODModel.h"
#include "Rendering/SkeletalMeshModel.h"
#include "SkeletalMeshTypes.h"
#define LOCTEXT_NAMESPACE "MetaHumanGeometryRemoval"
namespace UE::MetaHuman::GeometryRemoval
{
bool TryCombineHiddenFaceMaps(TConstArrayView<FHiddenFaceMapImage> SourceMaps, FHiddenFaceMapImage& OutDestinationMap, FText& OutFailureReason)
{
if (SourceMaps.Num() == 0)
{
OutFailureReason = LOCTEXT("NoSourceMaps", "No source maps provided");
return false;
}
// For simplicity, MaxCullValue and MinKeepValue are left on their defaults, rather than trying
// to minimise scaling artifacts.
OutDestinationMap.Settings.MaxShrinkDistance = 0.0f;
OutDestinationMap.DebugName = TEXT("Combined map");
int64 MaxWidth = 0;
int64 MaxHeight = 0;
for (const FHiddenFaceMapImage& SourceMap : SourceMaps)
{
if (SourceMap.Image.GetNumPixels() == 0)
{
OutFailureReason = FText::Format(LOCTEXT("NoSourceData", "Source map {0} has no source data and can't be used"), FText::FromString(SourceMap.DebugName));
return false;
}
if (SourceMap.Settings.MaxCullValue < 0.0f
|| SourceMap.Settings.MaxCullValue > SourceMap.Settings.MinKeepValue
|| SourceMap.Settings.MinKeepValue > 1.0f)
{
OutFailureReason = FText::Format(LOCTEXT("InvalidSourceSettings", "Source map {0} has invalid values for MinKeepValue ({1}) and MaxCullValue ({2}) and can't be used"),
FText::FromString(SourceMap.DebugName), SourceMap.Settings.MinKeepValue, SourceMap.Settings.MaxCullValue);
return false;
}
MaxWidth = FMath::Max(MaxWidth, SourceMap.Image.GetWidth());
MaxHeight = FMath::Max(MaxHeight, SourceMap.Image.GetHeight());
OutDestinationMap.Settings.MaxShrinkDistance = FMath::Max(OutDestinationMap.Settings.MaxShrinkDistance, SourceMap.Settings.MaxShrinkDistance);
}
// The checks above guarantee this
check(MaxWidth > 0 && MaxHeight > 0);
OutDestinationMap.Image = FImage(MaxWidth, MaxHeight, ERawImageFormat::G8, EGammaSpace::Linear);
// G8 should have 1 byte per pixel
check(OutDestinationMap.Image.GetBytesPerPixel() == 1);
TArrayView64<uint8> DestinationPixels = OutDestinationMap.Image.AsG8();
// Stores information about the source map needed in the loop below, and handles locking it for reading.
struct FSourceHiddenFaceMapData
{
FSourceHiddenFaceMapData(const FHiddenFaceMapImage& InSourceMap)
: ResX(InSourceMap.Image.GetWidth())
, ResY(InSourceMap.Image.GetHeight())
, ResXAsDouble(static_cast<double>(ResX))
, ResYAsDouble(static_cast<double>(ResY))
, SourceMap(InSourceMap)
{
}
void EvaluatePixel(int64 PosX, int64 PosY, bool& bOutKeepGeometry, float& OutShrinkDistance) const
{
check(PosX >= 0 && PosX < ResX);
check(PosY >= 0 && PosY < ResY);
const FLinearColor Color = SourceMap.Image.GetOnePixelLinear(PosX, PosY);
// For backwards compatibility with the previous implementation, all 3 channels have to
// be under the threshold in order to cull the geometry.
const float SelectedValue = FMath::Max3(Color.R, Color.G, Color.B);
bOutKeepGeometry = SelectedValue >= SourceMap.Settings.MaxCullValue;
OutShrinkDistance = 0.0f;
if (SourceMap.Settings.MaxShrinkDistance > 0.0f
&& SourceMap.Settings.MinKeepValue > SourceMap.Settings.MaxCullValue
&& bOutKeepGeometry
&& SelectedValue < SourceMap.Settings.MinKeepValue)
{
const float ShrinkFactor = 1.0f - (SelectedValue - SourceMap.Settings.MaxCullValue) / (SourceMap.Settings.MinKeepValue - SourceMap.Settings.MaxCullValue);
// This should be guaranteed by the logic above
check(ShrinkFactor >= 0.0f && ShrinkFactor <= 1.0f);
OutShrinkDistance = ShrinkFactor * SourceMap.Settings.MaxShrinkDistance;
}
}
const int64 ResX;
const int64 ResY;
// Store as a double as well, to avoid converting from int64 in inner loop
const double ResXAsDouble;
const double ResYAsDouble;
private:
const FHiddenFaceMapImage& SourceMap;
};
TArray<FSourceHiddenFaceMapData> SourceMapData;
for (const FHiddenFaceMapImage& SourceMap : SourceMaps)
{
SourceMapData.Emplace(SourceMap);
}
// For every pixel in the destination map, read from the source maps to determine the result
const double DestResX = (double)MaxWidth;
const double DestResY = (double)MaxHeight;
for (int64 DestPosY = 0; DestPosY < MaxHeight; DestPosY++)
{
const double V = ((double)DestPosY + 0.5) / DestResY;
for (int64 DestPosX = 0; DestPosX < MaxWidth; DestPosX++)
{
float AccumulatedPixelValue = 1.0f;
for (const FSourceHiddenFaceMapData& SourceMap : SourceMapData)
{
const double U = ((double)DestPosX + 0.5) / DestResX;
const int64 SourcePosX = FMath::FloorToInt64(U * SourceMap.ResXAsDouble);
const int64 SourcePosY = FMath::FloorToInt64(V * SourceMap.ResYAsDouble);
check(SourcePosX >= 0 && SourcePosX < SourceMap.ResX);
check(SourcePosY >= 0 && SourcePosY < SourceMap.ResY);
bool bKeepGeometry = false;
float ShrinkDistance = 0.0f;
SourceMap.EvaluatePixel(SourcePosX, SourcePosY, bKeepGeometry, ShrinkDistance);
float NewPixelValue;
if (bKeepGeometry)
{
if (ShrinkDistance == 0.0f)
{
// Geometry should be unmodified
NewPixelValue = 1.0f;
}
else
{
const float ShrinkFactor = ShrinkDistance / OutDestinationMap.Settings.MaxShrinkDistance;
NewPixelValue = FMath::Lerp(OutDestinationMap.Settings.MinKeepValue, OutDestinationMap.Settings.MaxCullValue, ShrinkFactor);
}
}
else
{
// Cull geometry
NewPixelValue = 0.0f;
}
// If any of the source maps wants to hide this pixel, it will be hidden.
//
// If any of them want to shrink it, it will be shrunk by the highest amount requested.
AccumulatedPixelValue = FMath::Min(AccumulatedPixelValue, NewPixelValue);
}
// No need to multiply by format data size here, as it's guaranteed to be 1
const int64 DestArrayPos = DestPosX + (DestPosY * DestResX);
DestinationPixels[DestArrayPos] = static_cast<uint8>(FMath::Clamp(AccumulatedPixelValue * MAX_uint8, 0.0f, static_cast<float>(MAX_uint8)));
}
}
return true;
}
bool TryConvertHiddenFaceMapTexturesToImages(TConstArrayView<FHiddenFaceMapTexture> SourceMaps, TArray<FHiddenFaceMapImage>& OutImages, FText& OutFailureReason)
{
OutImages.Reset(SourceMaps.Num());
for (const FHiddenFaceMapTexture& SourceMap : SourceMaps)
{
if (!SourceMap.Texture)
{
continue;
}
FHiddenFaceMapImage& DestinationMap = OutImages.AddDefaulted_GetRef();
if (!SourceMap.Texture->Source.GetMipImage(DestinationMap.Image, 0)
|| DestinationMap.Image.GetNumPixels() == 0)
{
OutFailureReason = FText::Format(LOCTEXT("FailedToGetMipData", "Failed to get mip data from hidden face map {0}"), FText::FromString(SourceMap.Texture->GetPathName()));
return false;
}
DestinationMap.DebugName = SourceMap.Texture->GetPathName();
DestinationMap.Settings = SourceMap.Settings;
}
return true;
}
void UpdateHiddenFaceMapTextureFromImage(const FImage& Image, TNotNull<UTexture2D*> Texture)
{
Texture->PreEditChange(nullptr);
Texture->Source.Init(Image);
// Hidden face maps are linear
Texture->SRGB = false;
Texture->PostEditChange();
}
// The following code has been adapted from FLODUtilities::StripLODGeometry
namespace TriangleStripHelper
{
struct FTriangle2D
{
FVector2D Vertices[3];
};
bool IntersectTriangleAndAABB(const FTriangle2D& Triangle, const FBox2D& Box)
{
FBox2D TriangleBox(Triangle.Vertices[0], Triangle.Vertices[0]);
TriangleBox += Triangle.Vertices[1];
TriangleBox += Triangle.Vertices[2];
auto IntersectBoxes = [&TriangleBox, &Box]()-> bool
{
if ((FMath::RoundToInt(TriangleBox.Min.X) >= FMath::RoundToInt(Box.Max.X)) || (FMath::RoundToInt(Box.Min.X) >= FMath::RoundToInt(TriangleBox.Max.X)))
{
return false;
}
if ((FMath::RoundToInt(TriangleBox.Min.Y) >= FMath::RoundToInt(Box.Max.Y)) || (FMath::RoundToInt(Box.Min.Y) >= FMath::RoundToInt(TriangleBox.Max.Y)))
{
return false;
}
return true;
};
//If the triangle box do not intersect, return false
if (!IntersectBoxes())
{
return false;
}
auto IsInsideBox = [&Box](const FVector2D& TestPoint)->bool
{
return ((FMath::RoundToInt(TestPoint.X) >= FMath::RoundToInt(Box.Min.X)) &&
(FMath::RoundToInt(TestPoint.X) <= FMath::RoundToInt(Box.Max.X)) &&
(FMath::RoundToInt(TestPoint.Y) >= FMath::RoundToInt(Box.Min.Y)) &&
(FMath::RoundToInt(TestPoint.Y) <= FMath::RoundToInt(Box.Max.Y)) );
};
if( IsInsideBox(Triangle.Vertices[0]) ||
IsInsideBox(Triangle.Vertices[1]) ||
IsInsideBox(Triangle.Vertices[2]) )
{
return true;
}
auto SegmentIntersection2D = [](const FVector2D & SegmentStartA, const FVector2D & SegmentEndA, const FVector2D & SegmentStartB, const FVector2D & SegmentEndB)
{
const FVector2D VectorA = SegmentEndA - SegmentStartA;
const FVector2D VectorB = SegmentEndB - SegmentStartB;
const double S = (-VectorA.Y * (SegmentStartA.X - SegmentStartB.X) + VectorA.X * (SegmentStartA.Y - SegmentStartB.Y)) / (-VectorB.X * VectorA.Y + VectorA.X * VectorB.Y);
const double T = (VectorB.X * (SegmentStartA.Y - SegmentStartB.Y) - VectorB.Y * (SegmentStartA.X - SegmentStartB.X)) / (-VectorB.X * VectorA.Y + VectorA.X * VectorB.Y);
return (S >= 0 && S <= 1 && T >= 0 && T <= 1);
};
auto IsInsideTriangle = [&Triangle, &SegmentIntersection2D, &Box, &TriangleBox](const FVector2D& TestPoint)->bool
{
double Extent = (2.0 * Box.GetSize().Size()) + (2.0 * TriangleBox.GetSize().Size());
FVector2D TestPointExtend(Extent, Extent);
int32 IntersectionCount = SegmentIntersection2D(Triangle.Vertices[0], Triangle.Vertices[1], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
IntersectionCount += SegmentIntersection2D(Triangle.Vertices[1], Triangle.Vertices[2], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
IntersectionCount += SegmentIntersection2D(Triangle.Vertices[2], Triangle.Vertices[0], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
return (IntersectionCount == 1);
};
if (IsInsideTriangle(Box.Min) ||
IsInsideTriangle(Box.Max) ||
IsInsideTriangle(FVector2D(Box.Min.X, Box.Max.Y)) ||
IsInsideTriangle(FVector2D(Box.Max.X, Box.Min.Y)))
{
return true;
}
auto IsTriangleEdgeIntersectBoxEdges = [&SegmentIntersection2D, &Box]( const FVector2D& EdgeStart, const FVector2D& EdgeEnd)->bool
{
//Triangle Edges 0-1 intersection with box
if( SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Min, FVector2D(Box.Min.X, Box.Max.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Max, FVector2D(Box.Min.X, Box.Max.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Max, FVector2D(Box.Max.X, Box.Min.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Min, FVector2D(Box.Max.X, Box.Min.Y)) )
{
return true;
}
return false;
};
if( IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[0], Triangle.Vertices[1]) ||
IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[1], Triangle.Vertices[2]) ||
IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[2], Triangle.Vertices[0]))
{
return true;
}
return false;
}
} //End namespace TriangleStripHelper
bool RemoveAndShrinkGeometry(
TNotNull<USkeletalMesh*> SkeletalMesh,
const int32 LODIndex,
const FHiddenFaceMapImage& HiddenFaceMap,
TConstArrayView<FName> MaterialSlotsToProcess)
{
if (LODIndex < 0 || LODIndex >= SkeletalMesh->GetLODNum() || !SkeletalMesh->GetImportedModel() || !SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex))
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Error, "Cannot strip triangle for skeletal mesh {Mesh} LOD {LOD}", SkeletalMesh->GetPathName(), LODIndex);
return false;
}
// Aliases to reduce code verbosity
const float MaxCullValue = HiddenFaceMap.Settings.MaxCullValue;
const float MinKeepValue = HiddenFaceMap.Settings.MinKeepValue;
const float MaxShrinkDistance = HiddenFaceMap.Settings.MaxShrinkDistance;
if (MaxCullValue < 0.0f
|| MaxCullValue > MinKeepValue
|| MinKeepValue > 1.0f)
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Error, "Cannot strip triangle for skeletal mesh {Mesh} LOD {LOD}. MaxCullValue and MinKeepValue must be in the range 0..1, and MinKeepValue must be greater than or equal to MaxCullValue.",
SkeletalMesh->GetPathName(), LODIndex);
return false;
}
// Grab the reference data
FSkeletalMeshLODModel& LODModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
const FSkeletalMeshLODInfo& LODInfo = *(SkeletalMesh->GetLODInfo(LODIndex));
const bool bIsReductionActive = SkeletalMesh->IsReductionActive(LODIndex);
if (bIsReductionActive && LODInfo.ReductionSettings.BaseLOD < LODIndex)
{
// No need to strip if the LOD is reduce using another LOD as the source data
UE_LOGFMT(LogMetaHumanCharacterEditor, Warning, "Cannot strip triangle for skeletal mesh {Mesh} LOD {LOD}, because this LOD is generated. Strip the source instead.",
SkeletalMesh->GetPathName(), LODIndex);
return false;
}
// Check that the image is valid
const float ResX = static_cast<float>(HiddenFaceMap.Image.SizeX);
const float ResY = static_cast<float>(HiddenFaceMap.Image.SizeY);
if (HiddenFaceMap.Image.GetNumPixels() == 0)
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Warning, "Cannot strip triangle for skeletal mesh {Mesh} LOD {LOD}, because the image size is 0.", SkeletalMesh->GetPathName(), LODIndex);
return false;
}
const ERawImageFormat::Type RawFormat = HiddenFaceMap.Image.Format;
if (RawFormat >= ERawImageFormat::MAX)
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Warning, "Cannot strip triangle for skeletal mesh {Mesh} LOD {LOD}, because the image format is invalid.", SkeletalMesh->GetPathName(), LODIndex);
return false;
}
// Post edit change scope
{
FScopedSkeletalMeshPostEditChange ScopePostEditChange(SkeletalMesh);
// This is like a re-import, we must force to use a new DDC key
SkeletalMesh->InvalidateDeriveDataCacheGUID();
const bool bBuildAvailable = SkeletalMesh->HasMeshDescription(LODIndex);
FSkeletalMeshImportData ImportedData;
// Get the imported data if available
if (bBuildAvailable)
{
if (const FMeshDescription* MeshDescription = SkeletalMesh->GetMeshDescription(LODIndex))
{
if (!MeshDescription->IsEmpty())
{
ImportedData = FSkeletalMeshImportData::CreateFromMeshDescription(*MeshDescription);
}
}
}
else
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Warning, "Cannot strip triangle for skeletalmesh {Mesh} LOD {LOD}, because it has no mesh description", SkeletalMesh->GetPathName(), LODIndex);
return false;
}
SkeletalMesh->Modify();
auto GetPixelValue = [&HiddenFaceMap](int32 PosX, int32 PosY) -> float
{
const FLinearColor Color = HiddenFaceMap.Image.GetOnePixelLinear(PosX, PosY);
// Note that the alpha value is not used
return Color.R;
};
auto ShouldStripTriangle = [&](const FVector2f& UvA, const FVector2f& UvB, const FVector2f& UvC) -> bool
{
// Note that the vertex UVs are wrapped to the 0-1 range here.
//
// Triangles that cross an integer boundary will be handled incorrectly, e.g. if two
// neighboring verts have U values 0.9 and 1.1, they will be wrapped to 0.9 and 0.1, so
// the region of the hidden face map covered by that triangle will be calculated
// incorrectly.
//
// This is a known issue that doesn't affect MetaHuman faces or bodies, which don't
// contain any triangles that cross integer UV boundaries. It could be addressed in
// future, but it's not a priority for now.
const FIntVector2 PixelUvA(FMath::FloorToInt32(FMath::Wrap(UvA.X * ResX, 0.0f, ResX - 1.0f)), FMath::FloorToInt32(FMath::Wrap(UvA.Y * ResY, 0.0f, ResY - 1.0f)));
const FIntVector2 PixelUvB(FMath::FloorToInt32(FMath::Wrap(UvB.X * ResX, 0.0f, ResX - 1.0f)), FMath::FloorToInt32(FMath::Wrap(UvB.Y * ResY, 0.0f, ResY - 1.0f)));
const FIntVector2 PixelUvC(FMath::FloorToInt32(FMath::Wrap(UvC.X * ResX, 0.0f, ResX - 1.0f)), FMath::FloorToInt32(FMath::Wrap(UvC.Y * ResY, 0.0f, ResY - 1.0f)));
const int32 MinU = FMath::Min3<int32>(PixelUvA.X, PixelUvB.X, PixelUvC.X);
const int32 MinV = FMath::Min3<int32>(PixelUvA.Y, PixelUvB.Y, PixelUvC.Y);
const int32 MaxU = FMath::Max3<int32>(PixelUvA.X, PixelUvB.X, PixelUvC.X);
const int32 MaxV = FMath::Max3<int32>(PixelUvA.Y, PixelUvB.Y, PixelUvC.Y);
// Triangle smaller or equal to one pixel. Just need to test the pixel color value
if (MinU == MaxU || MinV == MaxV)
{
return GetPixelValue(MinU, MinV) <= MaxCullValue;
}
for (int32 PosY = MinV; PosY <= MaxV; ++PosY)
{
for (int32 PosX = MinU; PosX <= MaxU; ++PosX)
{
const bool bStripPixel = GetPixelValue(PosX, PosY) <= MaxCullValue;
// If any non-zeroed pixel intersects the triangle, prevent stripping of this triangle
if (!bStripPixel)
{
FVector2D StartPixel(PosX, PosY);
FVector2D EndPixel(PosX + 1, PosY + 1);
FBox2D Box2D(StartPixel, EndPixel);
// Test if the triangle UV touches this pixel
TriangleStripHelper::FTriangle2D Triangle;
Triangle.Vertices[0] = FVector2D(PixelUvA.X, PixelUvA.Y);
Triangle.Vertices[1] = FVector2D(PixelUvB.X, PixelUvB.Y);
Triangle.Vertices[2] = FVector2D(PixelUvC.X, PixelUvC.Y);
if(TriangleStripHelper::IntersectTriangleAndAABB(Triangle, Box2D))
{
return false;
}
}
}
}
return true;
};
const TArray<uint32>& SoftVertexIndexToImportDataPointIndex = LODModel.GetRawPointIndices();
TMap<FInt32Vector3, int32> OptimizedFaceFinder;
for (int32 FaceIndex = 0; FaceIndex < ImportedData.Faces.Num(); ++FaceIndex)
{
const SkeletalMeshImportData::FTriangle& Face = ImportedData.Faces[FaceIndex];
int32 FaceVertexA = ImportedData.Wedges[Face.WedgeIndex[0]].VertexIndex;
int32 FaceVertexB = ImportedData.Wedges[Face.WedgeIndex[1]].VertexIndex;
int32 FaceVertexC = ImportedData.Wedges[Face.WedgeIndex[2]].VertexIndex;
OptimizedFaceFinder.Add(FInt32Vector3(FaceVertexA, FaceVertexB, FaceVertexC), FaceIndex);
}
TArray<bool> ShouldStripSection;
ShouldStripSection.Reserve(LODModel.Sections.Num());
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
bool bShouldStripThisSection;
if (MaterialSlotsToProcess.Num() == 0)
{
// Apply to all sections if no material slots were specified
bShouldStripThisSection = true;
}
else
{
const TArray<FSkeletalMaterial>& Materials = SkeletalMesh->GetMaterials();
if (Materials.IsValidIndex(LODModel.Sections[SectionIndex].MaterialIndex))
{
bShouldStripThisSection = MaterialSlotsToProcess.Contains(Materials[LODModel.Sections[SectionIndex].MaterialIndex].MaterialSlotName);
}
else
{
bShouldStripThisSection = false;
}
}
ShouldStripSection.Add(bShouldStripThisSection);
}
int32 RemovedFaceCount = 0;
TBitArray<> FaceToRemove;
FaceToRemove.Init(false, ImportedData.Faces.Num());
int32 NumTriangleIndex = LODModel.IndexBuffer.Num();
for (int32 TriangleIndex = NumTriangleIndex - 1; TriangleIndex >= 0; TriangleIndex -= 3)
{
int32 VertexIndexA = LODModel.IndexBuffer[TriangleIndex - 2];
int32 VertexIndexB = LODModel.IndexBuffer[TriangleIndex - 1];
int32 VertexIndexC = LODModel.IndexBuffer[TriangleIndex];
int32 SectionIndex;
int32 SectionVertexIndexA;
int32 SectionVertexIndexB;
int32 SectionVertexIndexC;
LODModel.GetSectionFromVertexIndex(VertexIndexA, SectionIndex, SectionVertexIndexA);
LODModel.GetSectionFromVertexIndex(VertexIndexB, SectionIndex, SectionVertexIndexB);
LODModel.GetSectionFromVertexIndex(VertexIndexC, SectionIndex, SectionVertexIndexC);
if (!ShouldStripSection[SectionIndex])
{
continue;
}
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
//Get the UV triangle, add the small number that will act like threshold when converting the UV into pixel coordinate.
const FVector2f UvA = Section.SoftVertices[SectionVertexIndexA].UVs[0] + UE_KINDA_SMALL_NUMBER;
const FVector2f UvB = Section.SoftVertices[SectionVertexIndexB].UVs[0] + UE_KINDA_SMALL_NUMBER;
const FVector2f UvC = Section.SoftVertices[SectionVertexIndexC].UVs[0] + UE_KINDA_SMALL_NUMBER;
if (ShouldStripTriangle(UvA, UvB, UvC))
{
// Find the face in the imported data
int32 ImportedPointIndexA = SoftVertexIndexToImportDataPointIndex[VertexIndexA];
int32 ImportedPointIndexB = SoftVertexIndexToImportDataPointIndex[VertexIndexB];
int32 ImportedPointIndexC = SoftVertexIndexToImportDataPointIndex[VertexIndexC];
int32 FaceIndex = OptimizedFaceFinder.FindChecked(FInt32Vector3(ImportedPointIndexA, ImportedPointIndexB, ImportedPointIndexC));
if (!FaceToRemove[FaceIndex])
{
FaceToRemove[FaceIndex] = true;
RemovedFaceCount++;
}
}
}
// Stores data about a point in the unmodified ImportedData
struct FPointData
{
// The delta from the original point to the shrunk point
FVector3f ShrinkDelta = FVector3f::ZeroVector;
// The number of times the point has been shrunk
int32 ShrinkCount = 0;
// The index of the shrunk version of this point in StrippedImportedData
int32 StrippedVertexIndex_Shrunk = INDEX_NONE;
// The index of the unmodified version of this point in StrippedImportedData
int32 StrippedVertexIndex_Unshrunk = INDEX_NONE;
// True if this point's value in the hidden face map would cause it to be removed.
//
// Note that the point will only actually be removed if all triangles using it are
// removed. If there's a triangle that uses this point, and other points on that
// triangle are not removed, this point will be kept as well.
bool bShouldBeCulled = false;
};
bool bAppliedAnyShrinking = false;
// Tracks info about how points in ImportedData are being modified. Indices match ImportedData.Points
TArray<FPointData> PointData;
PointData.Reserve(ImportedData.Points.Num());
PointData.AddDefaulted(ImportedData.Points.Num());
if (MaxShrinkDistance > 0.0f && MinKeepValue > MaxCullValue)
{
check(LODModel.NumVertices <= (uint32)MAX_int32);
for (int32 VertexIndex = 0; VertexIndex < (int32)LODModel.NumVertices; VertexIndex++)
{
int32 SectionIndex = 0;
int32 SectionVertexIndex = 0;
LODModel.GetSectionFromVertexIndex(VertexIndex, SectionIndex, SectionVertexIndex);
if (!ShouldStripSection[SectionIndex])
{
continue;
}
const FSoftSkinVertex& Vertex = LODModel.Sections[SectionIndex].SoftVertices[SectionVertexIndex];
// Get the UV triangle, add the small number that will act like threshold when converting the UV into pixel coordinate.
const FVector2f UV = Vertex.UVs[0] + KINDA_SMALL_NUMBER;
const FIntVector2 PixelUV(FMath::FloorToInt32(FMath::Wrap(UV.X * ResX, 0.0f, ResX - 1.0f)), FMath::FloorToInt32(FMath::Wrap(UV.Y * ResY, 0.0f, ResY - 1.0f)));
const float PixelValue = GetPixelValue(PixelUV.X, PixelUV.Y);
// A value at or below the MaxCullValue, should translate to a ShrinkWeight of 1.
//
// A value at or above the MinKeepValue, should give a ShrinkWeight of 0.
//
// Values in between the thresholds should linearly interpolate from 0 to 1.
const float ShrinkWeight = 1.0f - FMath::Clamp((PixelValue - MaxCullValue) / (MinKeepValue - MaxCullValue), 0.0f, 1.0f);
if (ShrinkWeight > 0.0f)
{
FPointData& Point = PointData[SoftVertexIndexToImportDataPointIndex[VertexIndex]];
Point.ShrinkDelta -= Vertex.TangentZ * (MaxShrinkDistance * ShrinkWeight);
Point.ShrinkCount++;
// This assumes that different verts referencing the same point (e.g. verts on
// a hard edge) have the same UV. This isn't guaranteed to be true for all
// meshes, but should be true for the meshes this function is intended to be
// used on.
Point.bShouldBeCulled = PixelValue <= MaxCullValue;
bAppliedAnyShrinking = true;
}
}
}
if (RemovedFaceCount > 0 || bAppliedAnyShrinking)
{
// Recreate a new imported data with only the remaining faces
FSkeletalMeshImportData StrippedImportedData;
StrippedImportedData = ImportedData;
StrippedImportedData.Faces.Reset();
StrippedImportedData.Wedges.Reset();
StrippedImportedData.Points.Reset();
StrippedImportedData.PointToRawMap.Reset();
StrippedImportedData.Influences.Reset();
for (int32 MorphTargetIndex = 0; MorphTargetIndex < ImportedData.MorphTargets.Num(); MorphTargetIndex++)
{
StrippedImportedData.MorphTargets[MorphTargetIndex].Empty();
StrippedImportedData.MorphTargets[MorphTargetIndex].Points.Reserve(ImportedData.MorphTargets[MorphTargetIndex].Points.Num());
StrippedImportedData.MorphTargetModifiedPoints[MorphTargetIndex].Reset();
}
StrippedImportedData.Faces.AddDefaulted(ImportedData.Faces.Num() - RemovedFaceCount);
StrippedImportedData.Wedges.AddDefaulted(StrippedImportedData.Faces.Num() * 3);
int32 NewFaceIndex = 0;
int32 NewWedgeIndex = 0;
for (int32 FaceIndex = 0; FaceIndex < ImportedData.Faces.Num(); ++FaceIndex)
{
// Skip removed faces
if (FaceToRemove[FaceIndex])
{
continue;
}
bool bHasAnyUnculledShrunkPoints = false;
for (int32 FaceWedgeIndex = 0; FaceWedgeIndex < 3; ++FaceWedgeIndex)
{
const FPointData& Point = PointData[ImportedData.Wedges[ImportedData.Faces[FaceIndex].WedgeIndex[FaceWedgeIndex]].VertexIndex];
if (!Point.bShouldBeCulled
&& Point.ShrinkCount > 0)
{
bHasAnyUnculledShrunkPoints = true;
break;
}
}
SkeletalMeshImportData::FTriangle& NewFace = StrippedImportedData.Faces[NewFaceIndex++];
NewFace = ImportedData.Faces[FaceIndex];
for (int32 FaceWedgeIndex = 0; FaceWedgeIndex < 3; ++FaceWedgeIndex)
{
SkeletalMeshImportData::FVertex& NewWedge = StrippedImportedData.Wedges[NewWedgeIndex];
NewWedge = ImportedData.Wedges[NewFace.WedgeIndex[FaceWedgeIndex]];
NewFace.WedgeIndex[FaceWedgeIndex] = NewWedgeIndex;
const int32 VertexIndex = NewWedge.VertexIndex;
FPointData& Point = PointData[VertexIndex];
// Importantly, a culled point should only be shrunk if any of the visible
// points it's connected to have also been shrunk.
//
// This is because there are two cases that need to be handled differently:
//
// 1. On a hard border between unmodified geometry and completely culled
// geometry, we don't want geometry on the edge to be unintentionally
// shrunk, as the hidden face map has not requested any shrinking.
//
// This is particularly an issue on meshes that recompute normals at
// runtime, since the unintentionally shrunk edge points will affect
// the normals of neighbouring visible points, changing their appearance.
//
// 2. On a border between shrunk geometry and culled geometry, we want the
// geometry on the edge to be shrunk as well.
//
// If we forced all culled points not to be shrunk in order to satisfy
// case 1, then on triangles that span shrunk and culled points (and are
// not themselves culled, because the shrunk points are visible), the
// culled points would be in their original positions and therefore might
// intersect with geometry in front of them.
//
// This logic ensures that each case is handled correctly. Sometimes this can
// mean that a single point in the original mesh needs both shrunk and unshrunk
// versions in the new mesh, so in some cases the new mesh can end up with more
// points than the original.
const bool bShouldApplyShrink = Point.ShrinkCount > 0 && bHasAnyUnculledShrunkPoints;
int32& NewVertexIndex = bShouldApplyShrink ? Point.StrippedVertexIndex_Shrunk : Point.StrippedVertexIndex_Unshrunk;
if (NewVertexIndex == INDEX_NONE)
{
FVector3f NewPointPosition = ImportedData.Points[VertexIndex];
// Apply accumulated shrink if any
if (bShouldApplyShrink)
{
NewPointPosition += Point.ShrinkDelta / Point.ShrinkCount;
}
StrippedImportedData.PointToRawMap.Add(ImportedData.PointToRawMap[VertexIndex]);
NewWedge.VertexIndex = StrippedImportedData.Points.Add(NewPointPosition);
NewVertexIndex = NewWedge.VertexIndex;
}
else
{
NewWedge.VertexIndex = NewVertexIndex;
}
NewWedgeIndex++;
}
}
// Fix the influences with the RemapVertexIndex
for (int32 InfluenceIndex = 0; InfluenceIndex < ImportedData.Influences.Num(); ++InfluenceIndex)
{
auto AddInfluence = [&ImportedData, &StrippedImportedData, InfluenceIndex](int32 RemappedVertexIndex)
{
if (RemappedVertexIndex != INDEX_NONE)
{
SkeletalMeshImportData::FRawBoneInfluence& Influence = StrippedImportedData.Influences.Add_GetRef(ImportedData.Influences[InfluenceIndex]);
Influence.VertexIndex = RemappedVertexIndex;
}
};
const FPointData& Point = PointData[ImportedData.Influences[InfluenceIndex].VertexIndex];
AddInfluence(Point.StrippedVertexIndex_Shrunk);
AddInfluence(Point.StrippedVertexIndex_Unshrunk);
}
for (int32 MorphTargetIndex = ImportedData.MorphTargets.Num() - 1; MorphTargetIndex >= 0; MorphTargetIndex--)
{
int32 MorphTargetPointIndex = 0;
for (const uint32 ModifiedPointIndex : ImportedData.MorphTargetModifiedPoints[MorphTargetIndex])
{
// Any shrunk points are assumed to be hidden under other geometry and are
// therefore excluded from morph targets.
if (PointData[ModifiedPointIndex].StrippedVertexIndex_Unshrunk != INDEX_NONE
&& PointData[ModifiedPointIndex].ShrinkCount == 0)
{
// Copy the point over from the original morph target
StrippedImportedData.MorphTargetModifiedPoints[MorphTargetIndex].Add(PointData[ModifiedPointIndex].StrippedVertexIndex_Unshrunk);
StrippedImportedData.MorphTargets[MorphTargetIndex].Points.Add(ImportedData.MorphTargets[MorphTargetIndex].Points[MorphTargetPointIndex]);
}
MorphTargetPointIndex++;
}
if (StrippedImportedData.MorphTargetModifiedPoints[MorphTargetIndex].Num() == 0)
{
StrippedImportedData.MorphTargets.RemoveAtSwap(MorphTargetIndex);
StrippedImportedData.MorphTargetModifiedPoints.RemoveAtSwap(MorphTargetIndex);
StrippedImportedData.MorphTargetNames.RemoveAtSwap(MorphTargetIndex);
}
}
FMeshDescription MeshDescription;
if (StrippedImportedData.GetMeshDescription(nullptr, &SkeletalMesh->GetLODInfo(LODIndex)->BuildSettings, MeshDescription))
{
SkeletalMesh->CreateMeshDescription(LODIndex, MoveTemp(MeshDescription));
SkeletalMesh->CommitMeshDescription(LODIndex);
}
}
}
return true;
}
} // namespace UE::MetaHuman::GeometryRemoval
#undef LOCTEXT_NAMESPACE