// Copyright Epic Games, Inc. All Rights Reserved. #include "ProxyLODMeshParameterization.h" #include "CoreMinimal.h" THIRD_PARTY_INCLUDES_START #include #include THIRD_PARTY_INCLUDES_END #include "ProxyLODThreadedWrappers.h" #include "ProxyLODMeshUtilities.h" bool ProxyLOD::GenerateUVs(const FTextureAtlasDesc& TextureAtlasDesc, const TArray& VertexBuffer, const TArray& IndexBuffer, const TArray& AdjacencyBuffer, TFunction& Callback, TArray& UVVertexBuffer, TArray& UVIndexBuffer, TArray& VertexRemapArray, float& MaxStretch, int32& NumCharts) { const int32 NumVerts = VertexBuffer.Num(); const int32 NumFaces = IndexBuffer.Num() / 3; // Copy data into DirectX library format TArray PosArray; PosArray.Empty(NumVerts); for (int32 i = 0; i < NumVerts; ++i) { const FVector3f& Vertex = VertexBuffer[i]; PosArray.Emplace(DirectX::XMFLOAT3(Vertex.X, Vertex.Y, Vertex.Z)); } TArray Indices; Indices.Empty(3 * NumFaces); for (int32 i = 0, I = NumFaces * 3; i < I; ++i) { Indices.Add(IndexBuffer[i]); } TArray AdjacencyArray; AdjacencyArray.Empty(3 * NumFaces); for (int32 i = 0, I = NumFaces * 3; i < I; ++i) { AdjacencyArray.Add( static_cast(AdjacencyBuffer[i]) ); } // Verify the mesh is valid { HRESULT ValidateHR = DirectX::Validate(Indices.GetData(), NumFaces, NumVerts, AdjacencyArray.GetData(), DirectX::VALIDATE_DEFAULT, NULL); if (FAILED(ValidateHR)) { return false; } } // Capture the callback and convert result type auto StatusCallBack = [&Callback](float percentComplete)->HRESULT { //return S_OK; return (Callback(percentComplete)) ? S_OK : S_FALSE; }; // Translate controls into corret types const size_t NumChartsIn = (size_t)NumCharts; const float MaxStretchIn = MaxStretch; const size_t Width = TextureAtlasDesc.Size.X; const size_t Height = TextureAtlasDesc.Size.Y; const float Gutter = TextureAtlasDesc.Gutter; // Symmetric Identity matrix used for the signal float * pIMTArray = new float[NumFaces * 3]; { for (int32 f = 0; f < NumFaces; ++f) { int32 offset = 3 * f; { pIMTArray[offset + 0] = 1.f; pIMTArray[offset + 1] = 0.f; pIMTArray[offset + 2] = 1.f; } } } size_t NumChartsOut = 0; float MaxStretchOut = 0.f; // info to capture std::vector VB; std::vector IB; std::vector RemapArray; std::vector FacePartitioning; // Generate UVs HRESULT hr = DirectX::UVAtlasCreate(PosArray.GetData(), NumVerts, Indices.GetData(), DXGI_FORMAT_R32_UINT, NumFaces, NumChartsIn, MaxStretchIn, Width, Height, Gutter, AdjacencyArray.GetData(), NULL /*false adj*/, pIMTArray /*IMTArray*/, StatusCallBack, DirectX::UVATLAS_DEFAULT_CALLBACK_FREQUENCY, DirectX::UVATLAS_DEFAULT, VB, IB, &FacePartitioning, &RemapArray, &MaxStretchOut, &NumChartsOut); // Translate results to output form. if (hr == S_OK) { const int32 NumUVverts = (int32)VB.size(); const int32 NumUVindices = (int32)IB.size(); const int32 NumRemapArray = (int32)RemapArray.size(); // empty and resize UVVertexBuffer.Empty(NumUVverts); UVIndexBuffer.Empty(NumUVindices); VertexRemapArray.Empty(NumRemapArray); for (const DirectX::UVAtlasVertex& UVvertex : VB) { const auto& UV = UVvertex.uv; UVVertexBuffer.Emplace(FVector2D(UV.x, UV.y)); } // This part is weird { std::vector indices; indices.resize(3 * NumFaces); std::memcpy(indices.data(), IB.data(), sizeof(uint32) * 3 * NumFaces); for (uint32 i : indices) { UVIndexBuffer.Add(i); } } for (const uint32 i : RemapArray) { int32 r = i; VertexRemapArray.Add(r); } MaxStretch = MaxStretchOut; NumCharts = (int32)NumChartsOut; } return (hr == S_OK) ? true : false; } bool ProxyLOD::GenerateUVs(FVertexDataMesh& InOutMesh, const FTextureAtlasDesc& TextureAtlasDesc, const bool VertexColorParts) { TRACE_CPUPROFILER_EVENT_SCOPE(ProxyLOD::GenerateUVs) // desired parameters for ISO-Chart method // MaxChartNum = 0 will allow any number of charts to be generated. const size_t MaxChartNumber = 0; // Let the polys in the partitions stretch some.. 1.f will let it stretch freely const float MaxStretch = 0.125f; // Question. Does this override the pIMT? // Note: I tried constructing this from the normals, but that resulted in some large planar regions being really compressed in the // UV chart. const bool bComputeIMTFromVertexNormal = false; // No Op auto NoOpCallBack = [](float percent)->HRESULT {return S_OK; }; return GenerateUVs(InOutMesh, TextureAtlasDesc, VertexColorParts, MaxStretch, MaxChartNumber, bComputeIMTFromVertexNormal, NoOpCallBack); } bool ProxyLOD::GenerateUVs(FVertexDataMesh& InOutMesh, const FTextureAtlasDesc& TextureAtlasDesc, const bool VertexColorParts, const float MaxStretch, const size_t MaxChartNumber, const bool bComputeIMTFromVertexNormal, std::function StatusCallBack, float* MaxStretchOut, size_t* NumChartsOut) { TRACE_CPUPROFILER_EVENT_SCOPE(ProxyLOD::GenerateUVs) std::vector DirextXAdjacency; const bool bValidMesh = GenerateAdjacenyAndCleanMesh(InOutMesh, DirextXAdjacency); if (!bValidMesh) return false; // Data from the existing mesh const DirectX::XMFLOAT3* Pos = (DirectX::XMFLOAT3*) (InOutMesh.Points.GetData()); const size_t NumVerts = InOutMesh.Points.Num(); const size_t NumFaces = InOutMesh.Indices.Num() / 3; uint32* indices = InOutMesh.Indices.GetData(); // The mesh adjacency const uint32* adjacency = DirextXAdjacency.data(); // Size of the texture atlas const size_t width = TextureAtlasDesc.Size.X; const size_t height = TextureAtlasDesc.Size.Y; const float gutter = TextureAtlasDesc.Gutter; // Partition and mesh info to capture std::vector vb; std::vector ib; std::vector vertexRemapArray; std::vector facePartitioning; // Capture stats about the result. float maxStretchUsed = 0.f; size_t numChartsUsed = 0; TArray IMTArray; IMTArray.SetNumUninitialized(NumFaces * 3); float* pIMTArray = IMTArray.GetData(); if (!bComputeIMTFromVertexNormal) { for (int32 f = 0; f < NumFaces; ++f) { int32 offset = 3 * f; { pIMTArray[offset] = 1.f; pIMTArray[offset + 1] = 0.f; pIMTArray[offset + 2] = 1.f; } } } else { // per-triangle IMT from per-vertex data(normal). const TArray& Normals = InOutMesh.Normal; const float* PerVertSignal = (float*)Normals.GetData(); size_t SignalStride = 3 * sizeof(float); HRESULT IMTResult = UVAtlasComputeIMTFromPerVertexSignal(Pos, NumVerts, indices, DXGI_FORMAT_R32_UINT, NumFaces, PerVertSignal, 3, SignalStride, StatusCallBack, pIMTArray); if (FAILED(IMTResult)) { return false; } } std::vector vPartitionResultAdjacency; HRESULT hr; { TRACE_CPUPROFILER_EVENT_SCOPE(DirectX::UVAtlasPartition) hr = DirectX::UVAtlasPartition(Pos, NumVerts, indices, DXGI_FORMAT_R32_UINT, NumFaces, MaxChartNumber, MaxStretch, adjacency, NULL /*false adj*/, pIMTArray, StatusCallBack, DirectX::UVATLAS_DEFAULT_CALLBACK_FREQUENCY, DirectX::UVATLAS_DEFAULT, vb, ib, &facePartitioning, &vertexRemapArray, vPartitionResultAdjacency, &maxStretchUsed, &numChartsUsed); } if (SUCCEEDED(hr)) { TRACE_CPUPROFILER_EVENT_SCOPE(DirectX::UVAtlasPack) hr = DirectX::UVAtlasPack( vb, ib, DXGI_FORMAT_R32_UINT, width, height, gutter, vPartitionResultAdjacency, StatusCallBack, DirectX::UVATLAS_DEFAULT_CALLBACK_FREQUENCY); } if (MaxStretchOut) { *MaxStretchOut = maxStretchUsed; } if (NumChartsOut) { *NumChartsOut = numChartsUsed; } if (FAILED(hr)) { return false; } // testing check(ib.size() / sizeof(uint32) == NumFaces * 3); check(facePartitioning.size() == NumFaces); check(vertexRemapArray.size() == vb.size()); // The mesh partitioning may split vertices, and this needs to be reflected in the mesh. // Update Faces { std::memcpy(indices, ib.data(), sizeof(uint32) * 3 * NumFaces); } // Add UVs { const size_t NumNewVerts = vb.size(); ResizeArray(InOutMesh.UVs, NumNewVerts); FVector2f* UVCoords = InOutMesh.UVs.GetData(); size_t j = 0; for (auto it = vb.cbegin(); it != vb.cend() && j < NumNewVerts; ++it, ++j) { const auto& UV = it->uv; UVCoords[j] = FVector2f(UV.x, UV.y); } } ProxyLOD::FTaskGroup TaskGroup; TaskGroup.Run([&]() { const size_t NumNewVerts = vb.size(); bool bReducedVertCount = NumNewVerts < NumVerts; check(!bReducedVertCount); // Copy the New Verts into a TArray so we can do a swap. TArray NewVertArray; ResizeArray(NewVertArray, NumNewVerts); // re-order the verts DirectX::UVAtlasApplyRemap(InOutMesh.Points.GetData(), sizeof(FVector3f), NumVerts, NumNewVerts, vertexRemapArray.data(), NewVertArray.GetData()); // swap the data into the raw mesh Swap(NewVertArray, InOutMesh.Points); }); // Update the normals TaskGroup.Run([&]() { const size_t NumNewVerts = vb.size(); TArray NewNormalsArray; ResizeArray(NewNormalsArray, NumNewVerts); // re-order the verts DirectX::UVAtlasApplyRemap(InOutMesh.Normal.GetData(), sizeof(FVector3f), NumVerts, NumNewVerts, vertexRemapArray.data(), NewNormalsArray.GetData()); // swap the data into the raw mesh Swap(NewNormalsArray, InOutMesh.Normal); }); // Update the transfer normals TaskGroup.Run([&]() { const size_t NumNewVerts = vb.size(); TArray NewTransferNormalsArray; ResizeArray(NewTransferNormalsArray, NumNewVerts); // re-order the verts DirectX::UVAtlasApplyRemap(InOutMesh.TransferNormal.GetData(), sizeof(FVector3f), NumVerts, NumNewVerts, vertexRemapArray.data(), NewTransferNormalsArray.GetData()); // swap the data into the raw mesh Swap(NewTransferNormalsArray, InOutMesh.TransferNormal); }); // Update the tangents TaskGroup.Run([&]() { const size_t NumNewVerts = vb.size(); TArray NewTangentArray; ResizeArray(NewTangentArray, NumNewVerts); // re-order the verts DirectX::UVAtlasApplyRemap(InOutMesh.Tangent.GetData(), sizeof(FVector3f), NumVerts, NumNewVerts, vertexRemapArray.data(), NewTangentArray.GetData()); // swap the data into the raw mesh Swap(NewTangentArray, InOutMesh.Tangent); }); TaskGroup.Run([&]() { const size_t NumNewVerts = vb.size(); TArray NewBiTangentArray; ResizeArray(NewBiTangentArray, NumNewVerts); // re-order the verts DirectX::UVAtlasApplyRemap(InOutMesh.BiTangent.GetData(), sizeof(FVector3f), NumVerts, NumNewVerts, vertexRemapArray.data(), NewBiTangentArray.GetData()); // swap the data into the raw mesh Swap(NewBiTangentArray, InOutMesh.BiTangent); }); TaskGroup.Wait(); ResizeArray(InOutMesh.FacePartition, NumFaces); ProxyLOD::Parallel_For(ProxyLOD::FIntRange(0, NumFaces), [&InOutMesh, &facePartitioning](const ProxyLOD::FIntRange& Range) { auto& FacePartition = InOutMesh.FacePartition; for (int32 f = Range.begin(), F = Range.end(); f < F; ++f) { FacePartition[f] = facePartitioning[f]; } }); if (VertexColorParts) { // Color the verts by partition for debuging. ColorPartitions(InOutMesh, facePartitioning); } return true; } void ProxyLOD::GenerateAdjacency(const FAOSMesh& AOSMesh, std::vector& AdjacencyArray) { const uint32 NumTris = AOSMesh.GetNumIndexes() / 3; const uint32 AdjacencySize = AOSMesh.GetNumIndexes(); // = 3 for each face. // Get the positions as a single array. std::vector PosArray; AOSMesh.GetPosArray(PosArray); // Allocate adjacency AdjacencyArray.resize(AdjacencySize); // position comparison epsilon const float Eps = 0.f; HRESULT hr = DirectX::GenerateAdjacencyAndPointReps(AOSMesh.Indexes, NumTris, (DirectX::XMFLOAT3*)PosArray.data(), PosArray.size(), Eps, NULL /* optional point rep pointer*/, AdjacencyArray.data()); } void ProxyLOD::GenerateAdjacency(const FVertexDataMesh& Mesh, std::vector& AdjacencyArray) { const uint32 NumTris = Mesh.Indices.Num() / 3; const uint32 AdjacencySize = Mesh.Indices.Num(); // = 3 for each face. // Get the positions as a single array. const FVector3f* PosArray = Mesh.Points.GetData(); const uint32 NumPos = Mesh.Points.Num(); // Allocate adjacency AdjacencyArray.resize(AdjacencySize); // position comparison epsilon const float Eps = 0.f; HRESULT hr = DirectX::GenerateAdjacencyAndPointReps(Mesh.Indices.GetData(), NumTris, (const DirectX::XMFLOAT3*)PosArray, NumPos, Eps, NULL /* optional point rep pointer*/, AdjacencyArray.data()); } void ProxyLOD::GenerateAdjacency(const FMeshDescription& RawMesh, std::vector& AdjacencyArray) { uint32 NumTris = RawMesh.Triangles().Num(); const uint32 NumVerts = RawMesh.Vertices().Num(); const uint32 AdjacencySize = RawMesh.VertexInstances().Num(); // 3 for each face // Allocate adjacency AdjacencyArray.resize(AdjacencySize); // @todo: possibility to pass the vertex position raw array directly into the below function. // Can it be assumed that it is not sparse? TArrayView VertexPositionsAttribute = RawMesh.GetVertexPositions().GetRawArray(); TArray VertexPositions; VertexPositions.AddZeroed(NumVerts); for (const FVertexID VertexID : RawMesh.Vertices().GetElementIDs()) { VertexPositions[VertexID.GetValue()] = VertexPositionsAttribute[VertexID]; } TArray Indices; Indices.AddZeroed(AdjacencySize); for (const FVertexInstanceID VertexInstanceID : RawMesh.VertexInstances().GetElementIDs()) { Indices[VertexInstanceID.GetValue()] = RawMesh.GetVertexInstanceVertex(VertexInstanceID).GetValue(); } // position comparison epsilon const float Eps = 0.f; HRESULT hr = DirectX::GenerateAdjacencyAndPointReps(Indices.GetData(), NumTris, (DirectX::XMFLOAT3*)VertexPositions.GetData(), NumVerts, Eps, NULL /* optional point rep pointer*/, AdjacencyArray.data()); } bool ProxyLOD::GenerateAdjacenyAndCleanMesh(FVertexDataMesh& InOutMesh, std::vector& Adjacency) { std::vector dupVerts; uint32 CleanCount = 0; while (CleanCount == 0 || (dupVerts.size() != 0 && CleanCount < 5)) { CleanCount++; // Rebuild the adjacency. Adjacency.clear(); GenerateAdjacency(InOutMesh, Adjacency); dupVerts.clear(); HRESULT hr = DirectX::Clean(InOutMesh.Indices.GetData(), InOutMesh.Indices.Num() / 3, InOutMesh.Points.Num(), Adjacency.data(), NULL, dupVerts, true /*break bowties*/); SplitVertices(InOutMesh, dupVerts); uint32 Offset = InOutMesh.Points.Num(); // spatially separate bowties for (int32 f = 0; f < InOutMesh.Indices.Num() / 3; ++f) { for (int32 v = 0; v < 3; ++v) { const uint32 Idx = InOutMesh.Indices[3 * f + v]; // This is a duplicate vert, find it's triangle and push it towards the center. if (Idx > Offset - 1) { const uint32 TriIds[3] = { InOutMesh.Indices[3 * f], InOutMesh.Indices[3 * f + 1], InOutMesh.Indices[3 * f + 2] }; // compute center of this face FVector3f CenterOfFace = InOutMesh.Points[TriIds[0]] + InOutMesh.Points[TriIds[1]] + InOutMesh.Points[TriIds[2]]; CenterOfFace /= 3.f; // Vector to center FVector3f PointToCenter = (InOutMesh.Points[Idx] - CenterOfFace); PointToCenter.Normalize(); // move vert towards center InOutMesh.Points[Idx] = InOutMesh.Points[Idx] - 0.0001f * PointToCenter; } } } } return (dupVerts.size() == 0); }