// 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 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 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(ResX)) , ResYAsDouble(static_cast(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 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(FMath::Clamp(AccumulatedPixelValue * MAX_uint8, 0.0f, static_cast(MAX_uint8))); } } return true; } bool TryConvertHiddenFaceMapTexturesToImages(TConstArrayView SourceMaps, TArray& 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 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 SkeletalMesh, const int32 LODIndex, const FHiddenFaceMapImage& HiddenFaceMap, TConstArrayView 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(HiddenFaceMap.Image.SizeX); const float ResY = static_cast(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(PixelUvA.X, PixelUvB.X, PixelUvC.X); const int32 MinV = FMath::Min3(PixelUvA.Y, PixelUvB.Y, PixelUvC.Y); const int32 MaxU = FMath::Max3(PixelUvA.X, PixelUvB.X, PixelUvC.X); const int32 MaxV = FMath::Max3(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& SoftVertexIndexToImportDataPointIndex = LODModel.GetRawPointIndices(); TMap 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 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& 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 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