// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Async/ParallelFor.h" #include "Components/BaseDynamicMeshComponent.h" #include "Components/DynamicMeshComponent.h" #include "DynamicMeshBuilder.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "LocalVertexFactory.h" #include "Rendering/StaticMeshVertexBuffer.h" #include "Rendering/PositionVertexBuffer.h" #include "Rendering/ColorVertexBuffer.h" #include "RHICommandList.h" #include "RayTracingGeometry.h" #include "VertexFactory.h" using UE::Geometry::FDynamicMesh3; using UE::Geometry::FDynamicMeshAttributeSet; using UE::Geometry::FDynamicMeshUVOverlay; using UE::Geometry::FDynamicMeshNormalOverlay; using UE::Geometry::FDynamicMeshColorOverlay; using UE::Geometry::FDynamicMeshMaterialAttribute; class UMaterialInterface; /** * FMeshRenderBufferSet stores a set of RenderBuffers for a mesh */ class FMeshRenderBufferSet { public: /** Number of triangles in this renderbuffer set. Note that triangles may be split between IndexBuffer and SecondaryIndexBuffer. */ int TriangleCount = 0; /** The buffer containing vertex data. */ FStaticMeshVertexBuffer StaticMeshVertexBuffer; /** The buffer containing the position vertex data. */ FPositionVertexBuffer PositionVertexBuffer; /** The buffer containing the vertex color data. */ FColorVertexBuffer ColorVertexBuffer; /** triangle indices */ FDynamicMeshIndexBuffer32 IndexBuffer; /** vertex factory */ FLocalVertexFactory VertexFactory; /** Material to draw this mesh with */ UMaterialInterface* Material = nullptr; /** * Optional list of triangles stored in this buffer. Storing this allows us * to rebuild the buffers if vertex data changes. */ TOptional> Triangles; /** * If secondary index buffer is enabled, we populate this index buffer with additional triangles indexing into the same vertex buffers */ bool bEnableSecondaryIndexBuffer = false; /** * partition or subset of IndexBuffer that indexes into same vertex buffers */ FDynamicMeshIndexBuffer32 SecondaryIndexBuffer; /** * configure whether raytracing should be enabled for this RenderBufferSet */ bool bEnableRaytracing = false; #if RHI_RAYTRACING /** * Raytracing buffers */ FRayTracingGeometry PrimaryRayTracingGeometry; FRayTracingGeometry SecondaryRayTracingGeometry; bool bIsRayTracingDataValid = false; #endif /** * In situations where we want to *update* the existing Vertex or Index buffers, we need to synchronize * access between the Game and Render threads. We use this lock to do that. */ FCriticalSection BuffersLock; FMeshRenderBufferSet(ERHIFeatureLevel::Type FeatureLevelType) : VertexFactory(FeatureLevelType, "FMeshRenderBufferSet") { StaticMeshVertexBuffer.SetUseFullPrecisionUVs(true); StaticMeshVertexBuffer.SetUseHighPrecisionTangentBasis(true); #if RHI_RAYTRACING EnumAddFlags(IndexBuffer.UsageFlags, EBufferUsageFlags::ShaderResource); EnumAddFlags(SecondaryIndexBuffer.UsageFlags, EBufferUsageFlags::ShaderResource); #endif // RHI_RAYTRACING } virtual ~FMeshRenderBufferSet() { if (TriangleCount > 0) { PositionVertexBuffer.ReleaseResource(); StaticMeshVertexBuffer.ReleaseResource(); ColorVertexBuffer.ReleaseResource(); VertexFactory.ReleaseResource(); if (IndexBuffer.IsInitialized()) { IndexBuffer.ReleaseResource(); } if (SecondaryIndexBuffer.IsInitialized()) { SecondaryIndexBuffer.ReleaseResource(); } #if RHI_RAYTRACING if (bEnableRaytracing) { PrimaryRayTracingGeometry.ReleaseResource(); SecondaryRayTracingGeometry.ReleaseResource(); } #endif } } /** * Upload initialized mesh buffers. * @warning This can only be called on the Rendering Thread. */ GEOMETRYFRAMEWORK_API void Upload(); /** * Fast path to only update the primary and secondary index buffers. This can be used * when (eg) the secondary index buffer is being used to highlight/hide a subset of triangles. * @warning This can only be called on the Rendering Thread. */ void UploadIndexBufferUpdate() { // todo: can this be done with RHI locking and memcpy, like in TransferVertexUpdateToGPU? FRHICommandListBase& RHICmdList = FRHICommandListImmediate::Get(); if (IndexBuffer.Indices.Num() > 0) { InitOrUpdateResource(RHICmdList, &IndexBuffer); } if (bEnableSecondaryIndexBuffer && SecondaryIndexBuffer.Indices.Num() > 0) { InitOrUpdateResource(RHICmdList, &SecondaryIndexBuffer); } InvalidateRayTracingData(); ValidateRayTracingData(); // currently we are immediately validating. This may be revisited in future. } /** * Fast path to only update vertex buffers. This path rebuilds all the * resources and reconfigures the vertex factory, so the counts/etc could be modified. * @warning This can only be called on the Rendering Thread. */ GEOMETRYFRAMEWORK_API void UploadVertexUpdate(bool bPositions, bool bMeshAttribs, bool bColors); /** * Fast path to update various vertex buffers. This path does not support changing the * size/counts of any of the sub-buffers, a direct memcopy from the CPU-side buffer to the RHI buffer is used. * @warning This can only be called on the Rendering Thread. */ GEOMETRYFRAMEWORK_API void TransferVertexUpdateToGPU(FRHICommandListBase& RHICmdList, bool bPositions, bool bNormals, bool bTexCoords, bool bColors); void InvalidateRayTracingData() { #if RHI_RAYTRACING bIsRayTracingDataValid = false; #endif } // Verify that valid raytracing data is available. This will cause a rebuild of the // raytracing data if any of our buffers have been modified. Currently this is called // by GetDynamicRayTracingInstances to ensure the RT data is available when needed. void ValidateRayTracingData() { #if RHI_RAYTRACING if (bIsRayTracingDataValid == false && IsRayTracingEnabled() && bEnableRaytracing) { UpdateRaytracingGeometryIfEnabled(); bIsRayTracingDataValid = true; } #endif } protected: // rebuild raytracing data for current buffers void UpdateRaytracingGeometryIfEnabled() { #if RHI_RAYTRACING // do we always want to do this? PrimaryRayTracingGeometry.ReleaseResource(); SecondaryRayTracingGeometry.ReleaseResource(); FRHICommandListBase& RHICmdList = FRHICommandListImmediate::Get(); for (int32 k = 0; k < 2; ++k) { FDynamicMeshIndexBuffer32& UseIndexBuffer = (k == 0) ? IndexBuffer : SecondaryIndexBuffer; if (UseIndexBuffer.Indices.Num() == 0) { continue; } FRayTracingGeometry& RayTracingGeometry = (k == 0) ? PrimaryRayTracingGeometry : SecondaryRayTracingGeometry; FRayTracingGeometryInitializer Initializer; Initializer.IndexBuffer = UseIndexBuffer.IndexBufferRHI; Initializer.TotalPrimitiveCount = UseIndexBuffer.Indices.Num() / 3; Initializer.GeometryType = RTGT_Triangles; Initializer.bFastBuild = true; Initializer.bAllowUpdate = false; FRayTracingGeometrySegment Segment; Segment.VertexBuffer = PositionVertexBuffer.VertexBufferRHI; Segment.NumPrimitives = Initializer.TotalPrimitiveCount; Segment.MaxVertices = PositionVertexBuffer.GetNumVertices(); Initializer.Segments.Add(Segment); RayTracingGeometry.SetInitializer(MoveTemp(Initializer)); RayTracingGeometry.InitResource(RHICmdList); } #endif } /** * Initializes a render resource, or update it if already initialized. * @warning This function can only be called on the Render Thread */ void InitOrUpdateResource(FRHICommandListBase& RHICmdList, FRenderResource* Resource) { if (!Resource->IsInitialized()) { Resource->InitResource(RHICmdList); } else { Resource->UpdateRHI(RHICmdList); } } public: /** * Enqueue a command on the Render Thread to destroy the passed in buffer set. * At this point the buffer set should be considered invalid. */ static void DestroyRenderBufferSet(FMeshRenderBufferSet* BufferSet) { //@todo, remove this check? based on our usage it could produce a memory leak - see BaseDynamicMeshSceneProxy.cpp if (BufferSet->TriangleCount == 0) { return; } delete BufferSet; } }; /** * Parameters and accompanying functions used to build and update FMeshRenderBuffers from a dynamic mesh. * For usage, see FBaseDynamicMeshSceneProxy or the simple converter below FDynamicMeshComponentToMeshRenderBufferSet * * Note: This is low-level and methods assume, but do not check, all input data is consistent (e.g. that all overlays, the triangle count and enumerator are all consistent with the provided mesh. ) */ struct FMeshRenderBufferSetConversionUtil { /** * Constant color assigned to vertices if no other vertex color is specified */ FColor ConstantVertexColor = FColor::White; /** * If true, vertex colors on the FDynamicMesh will be ignored */ bool bIgnoreVertexColors = false; /** * If true, a per-triangle color is used to set vertex colors */ bool bUsePerTriangleColor = false; /** * Per-triangle color function. Only called if bUsePerTriangleColor=true */ TFunction PerTriangleColorFunc = nullptr; /** * If true, VertexColorRemappingFunc is called on Vertex Colors provided from Mesh to remap them to a different color */ bool bApplyVertexColorRemapping = false; /** * Vertex color remapping function. Only called if bApplyVertexColorRemapping == true, for mesh vertex colors */ TUniqueFunction VertexColorRemappingFunc = nullptr; /** * Color Space Transform/Conversion applied to Vertex Colors provided from Mesh Color Overlay Attribute * Color Space Conversion is applied after any Vertex Color Remapping. */ EDynamicMeshVertexColorTransformMode ColorSpaceTransformMode = EDynamicMeshVertexColorTransformMode::NoTransform; /** * If true, a facet normals are used instead of mesh normals */ bool bUsePerTriangleNormals = false; /** * If true, populate secondary buffers using SecondaryTriFilterFunc */ bool bUseSecondaryTriBuffers = false; /** * Filter predicate for secondary triangle index buffer. Only called if bUseSecondaryTriBuffers=true */ TUniqueFunction SecondaryTriFilterFunc = nullptr; /** * Initialize rendering buffers from given attribute overlays. * Creates three vertices per triangle, IE no shared vertices in buffers. */ template void InitializeBuffersFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int NumTriangles, const TriangleEnumerable& Enumerable, const FDynamicMeshUVOverlay* UVOverlay, const FDynamicMeshNormalOverlay* NormalOverlay, const FDynamicMeshColorOverlay* ColorOverlay, TFunctionRef TangentsFunc, bool bTrackTriangles = false, bool bParallel = false) { TArray UVOverlays; UVOverlays.Add(UVOverlay); InitializeBuffersFromOverlays(RenderBuffers, Mesh, NumTriangles, Enumerable, UVOverlays, NormalOverlay, ColorOverlay, TangentsFunc, bTrackTriangles, bParallel); } /** * Initialize rendering buffers from given attribute overlays. * Creates three vertices per triangle, IE no shared vertices in buffers. */ template void InitializeBuffersFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int NumTriangles, const TriangleEnumerable& Enumerable, const TArray& UVOverlays, const FDynamicMeshNormalOverlay* NormalOverlay, const FDynamicMeshColorOverlay* ColorOverlay, TFunctionRef TangentsFunc, bool bTrackTriangles = false, bool bParallel = false); /** * Filter the triangles in a FMeshRenderBufferSet into the SecondaryIndexBuffer. * Requires that RenderBuffers->Triangles has been initialized. * @param bDuplicate if set, then primary IndexBuffer is unmodified and SecondaryIndexBuffer contains duplicates. Otherwise triangles are sorted via predicate into either primary or secondary. */ void GEOMETRYFRAMEWORK_API UpdateSecondaryTriangleBuffer( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, bool bDuplicate); /** * RecomputeRenderBufferTriangleIndexSets re-sorts the existing set of triangles in a FMeshRenderBufferSet * into primary and secondary index buffers. Note that UploadIndexBufferUpdate() must be called * after this function! */ void GEOMETRYFRAMEWORK_API RecomputeRenderBufferTriangleIndexSets( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh); /** * Update vertex positions/normals/colors of an existing set of render buffers. * Assumes that buffers were created with unshared vertices, ie three vertices per triangle, eg by InitializeBuffersFromOverlays() */ template void UpdateVertexBuffersFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int NumTriangles, const TriangleEnumerable& Enumerable, const FDynamicMeshNormalOverlay* NormalOverlay, const FDynamicMeshColorOverlay* ColorOverlay, TFunctionRef TangentsFunc, bool bUpdatePositions = true, bool bUpdateNormals = false, bool bUpdateColors = false); /** * Update vertex uvs of an existing set of render buffers. * Assumes that buffers were created with unshared vertices, ie three vertices per triangle, eg by InitializeBuffersFromOverlays() */ template void UpdateVertexUVBufferFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int32 NumTriangles, const TriangleEnumerable& Enumerable, const TArray& UVOverlays); /** * Get the overlay color the FColor, respecting the ColorSpaceTransformMode utilizing the VertexColorRemappingFunc if requested. */ FColor GetOverlayColorAsFColor( const FDynamicMeshColorOverlay* ColorOverlay, int32 ElementID) { checkSlow(ColorOverlay); FVector4f UseColor = ColorOverlay->GetElement(ElementID); if (bApplyVertexColorRemapping) { VertexColorRemappingFunc(UseColor); } if (ColorSpaceTransformMode == EDynamicMeshVertexColorTransformMode::SRGBToLinear) { // is there a better way to do this? FColor QuantizedSRGBColor = ((FLinearColor)UseColor).ToFColor(false); return FLinearColor(QuantizedSRGBColor).ToFColor(false); } else { bool bConvertToSRGB = (ColorSpaceTransformMode == EDynamicMeshVertexColorTransformMode::LinearToSRGB); return ((FLinearColor)UseColor).ToFColor(bConvertToSRGB); } } private: using FIndex2i = UE::Geometry::FIndex2i; using FIndex3i = UE::Geometry::FIndex3i; }; // Simple tool to initialize a single set of mesh buffers for the entire mesh. // for examples of more complicated conversions, see DynamicMeshSceneProxy class FDynamicMeshComponentToMeshRenderBufferSet { public: FMeshRenderBufferSetConversionUtil MeshRenderBufferSetConverter; // note, this conversion may recompute the tangents on the DynamicMeshComponent // since a dynamic mesh component with "autocalculated" tangents will compute them on first request // @param DynamicMeshComponent - the mesh to be converted // @param MeshRenderBufferSet - the buffer set to be initialized // @param bUseComponentSettings - if true, the Component settings will override (and update) MeshRederBufferSetBuilder.{ColorspaceTransformMode, bUsePerTriangleNormals}. GEOMETRYFRAMEWORK_API void Convert(class UDynamicMeshComponent& DynamicMeshComponent, FMeshRenderBufferSet& MeshRenderBufferSet, bool bUseComponentSettings = true); protected: TUniqueFunction MakeTangentsFunc(class UDynamicMeshComponent& DynamicMeshCompnent, bool bSkipAutoCompute = false); }; // -- template implementations -- // template void FMeshRenderBufferSetConversionUtil::InitializeBuffersFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int NumTriangles, const TriangleEnumerable& Enumerable, const TArray& UVOverlays, const FDynamicMeshNormalOverlay* NormalOverlay, const FDynamicMeshColorOverlay* ColorOverlay, TFunctionRef TangentsFunc, bool bTrackTriangles, bool bParallel) { RenderBuffers->TriangleCount = NumTriangles; if (NumTriangles == 0) { return; } bool bHaveColors = (ColorOverlay != nullptr) && (bIgnoreVertexColors == false); const int NumVertices = NumTriangles * 3; const int NumUVOverlays = UVOverlays.Num(); const int NumTexCoords = FMath::Max(1, NumUVOverlays); // must have at least one tex coord { RenderBuffers->PositionVertexBuffer.Init(NumVertices); RenderBuffers->StaticMeshVertexBuffer.Init(NumVertices, NumTexCoords); RenderBuffers->ColorVertexBuffer.Init(NumVertices); RenderBuffers->IndexBuffer.Indices.AddUninitialized(NumTriangles * 3); } // build triangle list if requested, or if we are using secondary buffers in which case we need it to filter later const bool bBuildTriangleList = bTrackTriangles || bUseSecondaryTriBuffers; // populate the triangle array. we use this for parallelism. TArray TriangleArray; { TriangleArray.Reserve(NumTriangles); for (int TriangleID : Enumerable) { TriangleArray.Add(TriangleID); } } TArray& IndexBuffer = RenderBuffers->IndexBuffer.Indices; { // populate the index buffer. for (int idx = 0, IdxMax = 3 * NumTriangles; idx < IdxMax; ++idx) { IndexBuffer[idx] = idx; } } ParallelFor(TriangleArray.Num(), [&](int idx) { int32 TriangleID = TriangleArray[idx]; FIndex3i Tri = Mesh->GetTriangle(TriangleID); FIndex3i TriNormal = (NormalOverlay != nullptr) ? NormalOverlay->GetTriangle(TriangleID) : FIndex3i::Zero(); FIndex3i TriColor = (ColorOverlay != nullptr) ? ColorOverlay->GetTriangle(TriangleID) : FIndex3i::Zero(); FColor UniformTriColor = ConstantVertexColor; if (bUsePerTriangleColor && PerTriangleColorFunc != nullptr) { UniformTriColor = PerTriangleColorFunc(Mesh, TriangleID); bHaveColors = false; } int32 VertIdx = idx * 3; for (int j = 0; j < 3; ++j) { RenderBuffers->PositionVertexBuffer.VertexPosition(VertIdx) = (FVector3f)Mesh->GetVertex(Tri[j]); FVector3f Normal; if (bUsePerTriangleNormals) { Normal = (FVector3f)Mesh->GetTriNormal(TriangleID); } else { Normal = (NormalOverlay != nullptr && TriNormal[j] != FDynamicMesh3::InvalidID) ? NormalOverlay->GetElement(TriNormal[j]) : Mesh->GetVertexNormal(Tri[j]); } // get tangents FVector3f TangentX, TangentY; TangentsFunc(Tri[j], TriangleID, j, Normal, TangentX, TangentY); RenderBuffers->StaticMeshVertexBuffer.SetVertexTangents(VertIdx, TangentX, TangentY, Normal); FColor VertexFColor = (bHaveColors && TriColor[j] != FDynamicMesh3::InvalidID) ? GetOverlayColorAsFColor(ColorOverlay, TriColor[j]) : UniformTriColor; RenderBuffers->ColorVertexBuffer.VertexColor(VertIdx) = VertexFColor; VertIdx++; } for (int32 k = 0; k < NumTexCoords; ++k) { VertIdx = idx * 3; FIndex3i UVTriangle = (k < NumUVOverlays && UVOverlays[k] != nullptr) ? UVOverlays[k]->GetTriangle(TriangleID) : FIndex3i::Invalid(); for (int j = 0; j < 3; ++j) { FVector2f UV = (UVTriangle[j] != FDynamicMesh3::InvalidID) ? UVOverlays[k]->GetElement(UVTriangle[j]) : FVector2f::Zero(); RenderBuffers->StaticMeshVertexBuffer.SetVertexUV(VertIdx, k, UV); VertIdx++; } } }, !bParallel); if (bBuildTriangleList) { RenderBuffers->Triangles = MoveTemp(TriangleArray); } // split triangles into secondary buffer (at bit redundant since we just built IndexBuffer, but we may optionally duplicate triangles in the future) if (bUseSecondaryTriBuffers) { RenderBuffers->bEnableSecondaryIndexBuffer = true; UpdateSecondaryTriangleBuffer(RenderBuffers, Mesh, false); } } /** * Update vertex positions/normals/colors of an existing set of render buffers. * Assumes that buffers were created with unshared vertices, ie three vertices per triangle, eg by InitializeBuffersFromOverlays() */ template void FMeshRenderBufferSetConversionUtil::UpdateVertexBuffersFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int NumTriangles, const TriangleEnumerable& Enumerable, const FDynamicMeshNormalOverlay* NormalOverlay, const FDynamicMeshColorOverlay* ColorOverlay, TFunctionRef TangentsFunc, bool bUpdatePositions, bool bUpdateNormals, bool bUpdateColors) { if (RenderBuffers->TriangleCount == 0) { return; } bool bHaveColors = (ColorOverlay != nullptr) && (bIgnoreVertexColors == false); int NumVertices = NumTriangles * 3; if ((bUpdatePositions && ensure(RenderBuffers->PositionVertexBuffer.GetNumVertices() == NumVertices) == false) || (bUpdateNormals && ensure(RenderBuffers->StaticMeshVertexBuffer.GetNumVertices() == NumVertices) == false) || (bUpdateColors && ensure(RenderBuffers->ColorVertexBuffer.GetNumVertices() == NumVertices) == false)) { return; } int VertIdx = 0; FVector3f TangentX, TangentY; for (int TriangleID : Enumerable) { FIndex3i Tri = Mesh->GetTriangle(TriangleID); FIndex3i TriNormal = (bUpdateNormals && NormalOverlay != nullptr) ? NormalOverlay->GetTriangle(TriangleID) : FIndex3i::Zero(); FIndex3i TriColor = (bUpdateColors && ColorOverlay != nullptr) ? ColorOverlay->GetTriangle(TriangleID) : FIndex3i::Zero(); FColor UniformTriColor = ConstantVertexColor; if (bUpdateColors && bUsePerTriangleColor && PerTriangleColorFunc != nullptr) { UniformTriColor = PerTriangleColorFunc(Mesh, TriangleID); bHaveColors = false; } for (int j = 0; j < 3; ++j) { if (bUpdatePositions) { RenderBuffers->PositionVertexBuffer.VertexPosition(VertIdx) = (FVector3f)Mesh->GetVertex(Tri[j]); } if (bUpdateNormals) { // get normal and tangent FVector3f Normal; if (bUsePerTriangleNormals) { Normal = (FVector3f)Mesh->GetTriNormal(TriangleID); } else { Normal = (NormalOverlay != nullptr && TriNormal[j] != FDynamicMesh3::InvalidID) ? NormalOverlay->GetElement(TriNormal[j]) : Mesh->GetVertexNormal(Tri[j]); } TangentsFunc(Tri[j], TriangleID, j, Normal, TangentX, TangentY); RenderBuffers->StaticMeshVertexBuffer.SetVertexTangents(VertIdx, (FVector3f)TangentX, (FVector3f)TangentY, (FVector3f)Normal); } if (bUpdateColors) { FColor VertexFColor = (bHaveColors && TriColor[j] != FDynamicMesh3::InvalidID) ? GetOverlayColorAsFColor(ColorOverlay, TriColor[j]) : UniformTriColor; RenderBuffers->ColorVertexBuffer.VertexColor(VertIdx) = VertexFColor; } VertIdx++; } } } template void FMeshRenderBufferSetConversionUtil::UpdateVertexUVBufferFromOverlays( FMeshRenderBufferSet* RenderBuffers, const FDynamicMesh3* Mesh, int32 NumTriangles, const TriangleEnumerable& Enumerable, const TArray& UVOverlays) { // We align the update to the way we set UV's in InitializeBuffersFromOverlays. if (RenderBuffers->TriangleCount == 0) { return; } int NumVertices = NumTriangles * 3; if (ensure(RenderBuffers->StaticMeshVertexBuffer.GetNumVertices() == NumVertices) == false) { return; } int NumUVOverlays = UVOverlays.Num(); int NumTexCoords = RenderBuffers->StaticMeshVertexBuffer.GetNumTexCoords(); if (!ensure(NumUVOverlays <= NumTexCoords)) { return; } // Temporarily stores the UV element indices for all UV channels of a single triangle TArray> UVTriangles; UVTriangles.SetNum(NumTexCoords); int VertIdx = 0; for (int TriangleID : Enumerable) { for (int32 k = 0; k < NumTexCoords; ++k) { UVTriangles[k] = (k < NumUVOverlays && UVOverlays[k] != nullptr) ? UVOverlays[k]->GetTriangle(TriangleID) : FIndex3i::Invalid(); } for (int j = 0; j < 3; ++j) { for (int32 k = 0; k < NumTexCoords; ++k) { FVector2f UV = (UVTriangles[k][j] != FDynamicMesh3::InvalidID) ? UVOverlays[k]->GetElement(UVTriangles[k][j]) : FVector2f::Zero(); RenderBuffers->StaticMeshVertexBuffer.SetVertexUV(VertIdx, k, UV); } ++VertIdx; } } }