// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "MeshUtilities.h" #include "MeshUtilitiesPrivate.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Materials/Material.h" #include "RawMesh.h" #include "StaticMeshResources.h" #include "DistanceFieldAtlas.h" //@todo - implement required vector intrinsics for other implementations #if PLATFORM_ENABLE_VECTORINTRINSICS #include "kDOP.h" #endif #if USE_EMBREE #include #include #else typedef void* RTCDevice; typedef void* RTCScene; #endif //@todo - implement required vector intrinsics for other implementations #if PLATFORM_ENABLE_VECTORINTRINSICS class FMeshBuildDataProvider { public: /** Initialization constructor. */ FMeshBuildDataProvider( const TkDOPTree& InkDopTree) : kDopTree(InkDopTree) {} // kDOP data provider interface. FORCEINLINE const TkDOPTree& GetkDOPTree(void) const { return kDopTree; } FORCEINLINE const FMatrix& GetLocalToWorld(void) const { return FMatrix::Identity; } FORCEINLINE const FMatrix& GetWorldToLocal(void) const { return FMatrix::Identity; } FORCEINLINE FMatrix GetLocalToWorldTransposeAdjoint(void) const { return FMatrix::Identity; } FORCEINLINE float GetDeterminant(void) const { return 1.0f; } private: const TkDOPTree& kDopTree; }; /** Generates unit length, stratified and uniformly distributed direction samples in a hemisphere. */ void GenerateStratifiedUniformHemisphereSamples(int32 NumThetaSteps, int32 NumPhiSteps, FRandomStream& RandomStream, TArray& Samples) { Samples.Empty(NumThetaSteps * NumPhiSteps); for (int32 ThetaIndex = 0; ThetaIndex < NumThetaSteps; ThetaIndex++) { for (int32 PhiIndex = 0; PhiIndex < NumPhiSteps; PhiIndex++) { const float U1 = RandomStream.GetFraction(); const float U2 = RandomStream.GetFraction(); const float Fraction1 = (ThetaIndex + U1) / (float)NumThetaSteps; const float Fraction2 = (PhiIndex + U2) / (float)NumPhiSteps; const float R = FMath::Sqrt(1.0f - Fraction1 * Fraction1); const float Phi = 2.0f * (float)PI * Fraction2; // Convert to Cartesian Samples.Add(FVector4(FMath::Cos(Phi) * R, FMath::Sin(Phi) * R, Fraction1)); } } } struct FEmbreeTriangleDesc { int16 ElementIndex; }; // Mapping between Embree Geometry Id and engine Mesh/LOD Id struct FEmbreeGeometry { TArray TriangleDescs; // The material ID of each triangle. }; #if USE_EMBREE struct FEmbreeRay : public RTCRay { FEmbreeRay() : ElementIndex(-1) { u = v = 0; time = 0; mask = 0xFFFFFFFF; geomID = -1; instID = -1; primID = -1; } // Additional Outputs. int32 ElementIndex; // Material Index }; void EmbreeFilterFunc(void* UserPtr, RTCRay& InRay) { FEmbreeGeometry* EmbreeGeometry = (FEmbreeGeometry*)UserPtr; FEmbreeRay& EmbreeRay = (FEmbreeRay&)InRay; FEmbreeTriangleDesc Desc = EmbreeGeometry->TriangleDescs[InRay.primID]; EmbreeRay.ElementIndex = Desc.ElementIndex; } #endif class FMeshDistanceFieldAsyncTask : public FNonAbandonableTask { public: FMeshDistanceFieldAsyncTask( TkDOPTree* InkDopTree, bool bInUseEmbree, RTCScene InEmbreeScene, const TArray* InSampleDirections, FBox InVolumeBounds, FIntVector InVolumeDimensions, float InVolumeMaxDistance, int32 InZIndex, TArray* DistanceFieldVolume) : kDopTree(InkDopTree), bUseEmbree(bInUseEmbree), EmbreeScene(InEmbreeScene), SampleDirections(InSampleDirections), VolumeBounds(InVolumeBounds), VolumeDimensions(InVolumeDimensions), VolumeMaxDistance(InVolumeMaxDistance), ZIndex(InZIndex), OutDistanceFieldVolume(DistanceFieldVolume), bNegativeAtBorder(false) {} void DoWork(); FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FMeshDistanceFieldAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } bool WasNegativeAtBorder() const { return bNegativeAtBorder; } private: // Readonly inputs TkDOPTree* kDopTree; bool bUseEmbree; RTCScene EmbreeScene; const TArray* SampleDirections; FBox VolumeBounds; FIntVector VolumeDimensions; float VolumeMaxDistance; int32 ZIndex; // Output TArray* OutDistanceFieldVolume; bool bNegativeAtBorder; }; void FMeshDistanceFieldAsyncTask::DoWork() { FMeshBuildDataProvider kDOPDataProvider(*kDopTree); const FVector DistanceFieldVoxelSize(VolumeBounds.GetSize() / FVector(VolumeDimensions.X, VolumeDimensions.Y, VolumeDimensions.Z)); const float VoxelDiameterSqr = DistanceFieldVoxelSize.SizeSquared(); for (int32 YIndex = 0; YIndex < VolumeDimensions.Y; YIndex++) { for (int32 XIndex = 0; XIndex < VolumeDimensions.X; XIndex++) { const FVector VoxelPosition = FVector(XIndex + .5f, YIndex + .5f, ZIndex + .5f) * DistanceFieldVoxelSize + VolumeBounds.Min; const int32 Index = (ZIndex * VolumeDimensions.Y * VolumeDimensions.X + YIndex * VolumeDimensions.X + XIndex); float MinDistance = VolumeMaxDistance; int32 Hit = 0; int32 HitBack = 0; for (int32 SampleIndex = 0; SampleIndex < SampleDirections->Num(); SampleIndex++) { const FVector UnitRayDirection = (*SampleDirections)[SampleIndex]; const FVector EndPosition = VoxelPosition + UnitRayDirection * VolumeMaxDistance; if (FMath::LineBoxIntersection(VolumeBounds, VoxelPosition, EndPosition, UnitRayDirection)) { #if USE_EMBREE if (bUseEmbree) { FEmbreeRay EmbreeRay; FVector RayDirection = EndPosition - VoxelPosition; EmbreeRay.org[0] = VoxelPosition.X; EmbreeRay.org[1] = VoxelPosition.Y; EmbreeRay.org[2] = VoxelPosition.Z; EmbreeRay.dir[0] = RayDirection.X; EmbreeRay.dir[1] = RayDirection.Y; EmbreeRay.dir[2] = RayDirection.Z; EmbreeRay.tnear = 0; EmbreeRay.tfar = 1.0f; rtcIntersect(EmbreeScene, EmbreeRay); if (EmbreeRay.geomID != -1 && EmbreeRay.primID != -1) { Hit++; const FVector HitNormal = FVector(EmbreeRay.Ng[0], EmbreeRay.Ng[1], EmbreeRay.Ng[2]).GetSafeNormal(); if (FVector::DotProduct(UnitRayDirection, HitNormal) > 0 // MaterialIndex on the build triangles was set to 1 if two-sided, or 0 if one-sided && EmbreeRay.ElementIndex == 0) { HitBack++; } const float CurrentDistance = VolumeMaxDistance * EmbreeRay.tfar; if (CurrentDistance < MinDistance) { MinDistance = CurrentDistance; } } } else #endif { FkHitResult Result; TkDOPLineCollisionCheck kDOPCheck( VoxelPosition, EndPosition, true, kDOPDataProvider, &Result); bool bHit = kDopTree->LineCheck(kDOPCheck); if (bHit) { Hit++; const FVector HitNormal = kDOPCheck.GetHitNormal(); if (FVector::DotProduct(UnitRayDirection, HitNormal) > 0 // MaterialIndex on the build triangles was set to 1 if two-sided, or 0 if one-sided && kDOPCheck.Result->Item == 0) { HitBack++; } const float CurrentDistance = VolumeMaxDistance * Result.Time; if (CurrentDistance < MinDistance) { MinDistance = CurrentDistance; } } } } } const float UnsignedDistance = MinDistance; // Consider this voxel 'inside' an object if more than 50% of the rays hit back faces MinDistance *= (Hit == 0 || HitBack < SampleDirections->Num() * .5f) ? 1 : -1; // If we are very close to a surface and nearly all of our rays hit backfaces, treat as inside // This is important for one sided planes if (FMath::Square(UnsignedDistance) < VoxelDiameterSqr && HitBack > .95f * Hit) { MinDistance = -UnsignedDistance; } MinDistance = FMath::Min(MinDistance, VolumeMaxDistance); const float VolumeSpaceDistance = MinDistance / VolumeBounds.GetExtent().GetMax(); if (MinDistance < 0 && (XIndex == 0 || XIndex == VolumeDimensions.X - 1 || YIndex == 0 || YIndex == VolumeDimensions.Y - 1 || ZIndex == 0 || ZIndex == VolumeDimensions.Z - 1)) { bNegativeAtBorder = true; } (*OutDistanceFieldVolume)[Index] = VolumeSpaceDistance; } } } void FMeshUtilities::GenerateSignedDistanceFieldVolumeData( FString MeshName, const FStaticMeshLODResources& LODModel, class FQueuedThreadPool& ThreadPool, const TArray& MaterialBlendModes, const FBoxSphereBounds& Bounds, float DistanceFieldResolutionScale, bool bGenerateAsIfTwoSided, FDistanceFieldVolumeData& OutData) { if (DistanceFieldResolutionScale > 0) { const double StartTime = FPlatformTime::Seconds(); const FPositionVertexBuffer& PositionVertexBuffer = LODModel.VertexBuffers.PositionVertexBuffer; FIndexArrayView Indices = LODModel.IndexBuffer.GetArrayView(); TArray > BuildTriangles; RTCDevice EmbreeDevice = NULL; RTCScene EmbreeScene = NULL; bool bUseEmbree = false; #if USE_EMBREE static const auto CVarEmbree = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFieldBuild.UseEmbree")); bUseEmbree = CVarEmbree->GetValueOnAnyThread() != 0; if (bUseEmbree) { EmbreeDevice = rtcNewDevice(NULL); RTCError ReturnErrorNewDevice = rtcDeviceGetError(EmbreeDevice); if (ReturnErrorNewDevice != RTC_NO_ERROR) { UE_LOG(LogMeshUtilities, Warning, TEXT("GenerateSignedDistanceFieldVolumeData failed for %s. Embree rtcNewDevice failed. Code: %d"), *MeshName, (int32)ReturnErrorNewDevice); return; } EmbreeScene = rtcDeviceNewScene(EmbreeDevice, RTC_SCENE_STATIC, RTC_INTERSECT1); RTCError ReturnErrorNewScene = rtcDeviceGetError(EmbreeDevice); if (ReturnErrorNewScene != RTC_NO_ERROR) { UE_LOG(LogMeshUtilities, Warning, TEXT("GenerateSignedDistanceFieldVolumeData failed for %s. Embree rtcDeviceNewScene failed. Code: %d"), *MeshName, (int32)ReturnErrorNewScene); rtcDeleteDevice(EmbreeDevice); return; } } #endif FVector BoundsSize = Bounds.GetBox().GetExtent() * 2; float MaxDimension = FMath::Max(FMath::Max(BoundsSize.X, BoundsSize.Y), BoundsSize.Z); // Consider the mesh a plane if it is very flat const bool bMeshWasPlane = BoundsSize.Z * 100 < MaxDimension // And it lies mostly on the origin && Bounds.Origin.Z - Bounds.BoxExtent.Z < KINDA_SMALL_NUMBER && Bounds.Origin.Z + Bounds.BoxExtent.Z > -KINDA_SMALL_NUMBER; TArray FilteredTriangles; FilteredTriangles.Empty(Indices.Num() / 3); for (int32 i = 0; i < Indices.Num(); i += 3) { FVector V0 = PositionVertexBuffer.VertexPosition(Indices[i + 0]); FVector V1 = PositionVertexBuffer.VertexPosition(Indices[i + 1]); FVector V2 = PositionVertexBuffer.VertexPosition(Indices[i + 2]); if (bMeshWasPlane) { // Flatten out the mesh into an actual plane, this will allow us to manipulate the component's Z scale at runtime without artifacts V0.Z = 0; V1.Z = 0; V2.Z = 0; } const FVector LocalNormal = ((V1 - V2) ^ (V0 - V2)).GetSafeNormal(); // No degenerates if (LocalNormal.IsUnit()) { bool bTriangleIsOpaqueOrMasked = false; for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++) { const FStaticMeshSection& Section = LODModel.Sections[SectionIndex]; if ((uint32)i >= Section.FirstIndex && (uint32)i < Section.FirstIndex + Section.NumTriangles * 3) { if (MaterialBlendModes.IsValidIndex(Section.MaterialIndex)) { bTriangleIsOpaqueOrMasked = !IsTranslucentBlendMode(MaterialBlendModes[Section.MaterialIndex]); } break; } } if (bTriangleIsOpaqueOrMasked) { FilteredTriangles.Add(i / 3); } } } FVector4* EmbreeVertices = NULL; int32* EmbreeIndices = NULL; uint32 GeomID = 0; FEmbreeGeometry Geometry; #if USE_EMBREE if (bUseEmbree) { GeomID = rtcNewTriangleMesh(EmbreeScene, RTC_GEOMETRY_STATIC, FilteredTriangles.Num(), PositionVertexBuffer.GetNumVertices()); rtcSetIntersectionFilterFunction(EmbreeScene, GeomID, EmbreeFilterFunc); rtcSetOcclusionFilterFunction(EmbreeScene, GeomID, EmbreeFilterFunc); rtcSetUserData(EmbreeScene, GeomID, &Geometry); EmbreeVertices = (FVector4*)rtcMapBuffer(EmbreeScene, GeomID, RTC_VERTEX_BUFFER); EmbreeIndices = (int32*)rtcMapBuffer(EmbreeScene, GeomID, RTC_INDEX_BUFFER); Geometry.TriangleDescs.Empty(FilteredTriangles.Num()); } #endif for (int32 TriangleIndex = 0; TriangleIndex < FilteredTriangles.Num(); TriangleIndex++) { int32 I0 = Indices[TriangleIndex * 3 + 0]; int32 I1 = Indices[TriangleIndex * 3 + 1]; int32 I2 = Indices[TriangleIndex * 3 + 2]; FVector V0 = PositionVertexBuffer.VertexPosition(I0); FVector V1 = PositionVertexBuffer.VertexPosition(I1); FVector V2 = PositionVertexBuffer.VertexPosition(I2); if (bMeshWasPlane) { // Flatten out the mesh into an actual plane, this will allow us to manipulate the component's Z scale at runtime without artifacts V0.Z = 0; V1.Z = 0; V2.Z = 0; } if (bUseEmbree) { EmbreeIndices[TriangleIndex * 3 + 0] = I0; EmbreeIndices[TriangleIndex * 3 + 1] = I1; EmbreeIndices[TriangleIndex * 3 + 2] = I2; EmbreeVertices[I0] = FVector4(V0, 0); EmbreeVertices[I1] = FVector4(V1, 0); EmbreeVertices[I2] = FVector4(V2, 0); FEmbreeTriangleDesc Desc; // Store bGenerateAsIfTwoSided in material index Desc.ElementIndex = bGenerateAsIfTwoSided ? 1 : 0; Geometry.TriangleDescs.Add(Desc); } else { BuildTriangles.Add(FkDOPBuildCollisionTriangle( // Store bGenerateAsIfTwoSided in material index bGenerateAsIfTwoSided, V0, V1, V2)); } } #if USE_EMBREE if (bUseEmbree) { rtcUnmapBuffer(EmbreeScene, GeomID, RTC_VERTEX_BUFFER); rtcUnmapBuffer(EmbreeScene, GeomID, RTC_INDEX_BUFFER); RTCError ReturnError = rtcDeviceGetError(EmbreeDevice); if (ReturnError != RTC_NO_ERROR) { UE_LOG(LogMeshUtilities, Warning, TEXT("GenerateSignedDistanceFieldVolumeData failed for %s. Embree rtcUnmapBuffer failed. Code: %d"), *MeshName, (int32)ReturnError); rtcDeleteScene(EmbreeScene); rtcDeleteDevice(EmbreeDevice); return; } } #endif TkDOPTree kDopTree; #if USE_EMBREE if (bUseEmbree) { rtcCommit(EmbreeScene); RTCError ReturnError = rtcDeviceGetError(EmbreeDevice); if (ReturnError != RTC_NO_ERROR) { UE_LOG(LogMeshUtilities, Warning, TEXT("GenerateSignedDistanceFieldVolumeData failed for %s. Embree rtcCommit failed. Code: %d"), *MeshName, (int32)ReturnError); rtcDeleteScene(EmbreeScene); rtcDeleteDevice(EmbreeDevice); return; } } else #endif { kDopTree.Build(BuildTriangles); } //@todo - project setting const int32 NumVoxelDistanceSamples = 1200; TArray SampleDirections; const int32 NumThetaSteps = FMath::TruncToInt(FMath::Sqrt(NumVoxelDistanceSamples / (2.0f * (float)PI))); const int32 NumPhiSteps = FMath::TruncToInt(NumThetaSteps * (float)PI); FRandomStream RandomStream(0); GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, SampleDirections); TArray OtherHemisphereSamples; GenerateStratifiedUniformHemisphereSamples(NumThetaSteps, NumPhiSteps, RandomStream, OtherHemisphereSamples); for (int32 i = 0; i < OtherHemisphereSamples.Num(); i++) { FVector4 Sample = OtherHemisphereSamples[i]; Sample.Z *= -1; SampleDirections.Add(Sample); } static const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFields.MaxPerMeshResolution")); const int32 PerMeshMax = CVar->GetValueOnAnyThread(); // Meshes with explicit artist-specified scale can go higher const int32 MaxNumVoxelsOneDim = DistanceFieldResolutionScale <= 1 ? PerMeshMax / 2 : PerMeshMax; const int32 MinNumVoxelsOneDim = 8; static const auto CVarDensity = IConsoleManager::Get().FindTConsoleVariableDataFloat(TEXT("r.DistanceFields.DefaultVoxelDensity")); const float VoxelDensity = CVarDensity->GetValueOnAnyThread(); const float NumVoxelsPerLocalSpaceUnit = VoxelDensity * DistanceFieldResolutionScale; FBox MeshBounds(Bounds.GetBox()); static const auto CVarEightBit = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFieldBuild.EightBit")); const bool bEightBitFixedPoint = CVarEightBit->GetValueOnAnyThread() != 0; const int32 FormatSize = GPixelFormats[bEightBitFixedPoint ? PF_G8 : PF_R16F].BlockBytes; { const float MaxOriginalExtent = MeshBounds.GetExtent().GetMax(); // Expand so that the edges of the volume are guaranteed to be outside of the mesh // Any samples outside the bounds will be clamped to the border, so they must be outside const FVector NewExtent(MeshBounds.GetExtent() + FVector(.2f * MaxOriginalExtent).ComponentMax(4 * MeshBounds.GetExtent() / MinNumVoxelsOneDim)); FBox DistanceFieldVolumeBounds = FBox(MeshBounds.GetCenter() - NewExtent, MeshBounds.GetCenter() + NewExtent); const float DistanceFieldVolumeMaxDistance = DistanceFieldVolumeBounds.GetExtent().Size(); const FVector DesiredDimensions(DistanceFieldVolumeBounds.GetSize() * FVector(NumVoxelsPerLocalSpaceUnit)); const FIntVector VolumeDimensions( FMath::Clamp(FMath::TruncToInt(DesiredDimensions.X), MinNumVoxelsOneDim, MaxNumVoxelsOneDim), FMath::Clamp(FMath::TruncToInt(DesiredDimensions.Y), MinNumVoxelsOneDim, MaxNumVoxelsOneDim), FMath::Clamp(FMath::TruncToInt(DesiredDimensions.Z), MinNumVoxelsOneDim, MaxNumVoxelsOneDim)); TArray DistanceFieldVolume; DistanceFieldVolume.Empty(VolumeDimensions.X * VolumeDimensions.Y * VolumeDimensions.Z); DistanceFieldVolume.AddZeroed(VolumeDimensions.X * VolumeDimensions.Y * VolumeDimensions.Z); TIndirectArray> AsyncTasks; for (int32 ZIndex = 0; ZIndex < VolumeDimensions.Z; ZIndex++) { FAsyncTask* Task = new FAsyncTask( &kDopTree, bUseEmbree, EmbreeScene, &SampleDirections, DistanceFieldVolumeBounds, VolumeDimensions, DistanceFieldVolumeMaxDistance, ZIndex, &DistanceFieldVolume); //Task->StartSynchronousTask(); Task->StartBackgroundTask(&ThreadPool); AsyncTasks.Add(Task); } bool bNegativeAtBorder = false; for (int32 TaskIndex = 0; TaskIndex < AsyncTasks.Num(); TaskIndex++) { FAsyncTask& Task = AsyncTasks[TaskIndex]; Task.EnsureCompletion(false); bNegativeAtBorder = bNegativeAtBorder || Task.GetTask().WasNegativeAtBorder(); } TArray QuantizedDistanceFieldVolume; QuantizedDistanceFieldVolume.Empty(VolumeDimensions.X * VolumeDimensions.Y * VolumeDimensions.Z * FormatSize); QuantizedDistanceFieldVolume.AddZeroed(VolumeDimensions.X * VolumeDimensions.Y * VolumeDimensions.Z * FormatSize); float MinVolumeDistance = 1.0f; float MaxVolumeDistance = -1.0f; for (int32 Index = 0; Index < DistanceFieldVolume.Num(); Index++) { const float VolumeSpaceDistance = DistanceFieldVolume[Index]; MinVolumeDistance = FMath::Min(MinVolumeDistance, VolumeSpaceDistance); MaxVolumeDistance = FMath::Max(MaxVolumeDistance, VolumeSpaceDistance); } MinVolumeDistance = FMath::Max(MinVolumeDistance, -1.0f); MaxVolumeDistance = FMath::Min(MaxVolumeDistance, 1.0f); const float InvDistanceRange = 1.0f / (MaxVolumeDistance - MinVolumeDistance); for (int32 Index = 0; Index < DistanceFieldVolume.Num(); Index++) { const float VolumeSpaceDistance = DistanceFieldVolume[Index]; if (bEightBitFixedPoint) { check(FormatSize == sizeof(uint8)); // [MinVolumeDistance, MaxVolumeDistance] -> [0, 1] const float RescaledDistance = (VolumeSpaceDistance - MinVolumeDistance) * InvDistanceRange; // Encoding based on D3D format conversion rules for float -> UNORM const int32 QuantizedDistance = FMath::FloorToInt(RescaledDistance * 255.0f + .5f); QuantizedDistanceFieldVolume[Index * FormatSize] = (uint8)FMath::Clamp(QuantizedDistance, 0, 255); } else { check(FormatSize == sizeof(FFloat16)); FFloat16* OutputPointer = (FFloat16*)&(QuantizedDistanceFieldVolume[Index * FormatSize]); *OutputPointer = FFloat16(VolumeSpaceDistance); } } DistanceFieldVolume.Empty(); OutData.bMeshWasClosed = !bNegativeAtBorder; OutData.bBuiltAsIfTwoSided = bGenerateAsIfTwoSided; OutData.bMeshWasPlane = bMeshWasPlane; OutData.Size = VolumeDimensions; OutData.LocalBoundingBox = DistanceFieldVolumeBounds; OutData.DistanceMinMax = FVector2D(MinVolumeDistance, MaxVolumeDistance); // Toss distance field if mesh was not closed if (bNegativeAtBorder) { OutData.Size = FIntVector(0, 0, 0); QuantizedDistanceFieldVolume.Empty(); UE_LOG(LogMeshUtilities, Log, TEXT("Discarded distance field for %s as mesh was not closed! Assign a two-sided material to fix."), *MeshName); } if (QuantizedDistanceFieldVolume.Num() > 0) { static const auto CVarCompress = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFieldBuild.Compress")); const bool bCompress = CVarCompress->GetValueOnAnyThread() != 0; if (bCompress) { const int32 UncompressedSize = QuantizedDistanceFieldVolume.Num() * QuantizedDistanceFieldVolume.GetTypeSize(); TArray TempCompressedMemory; // Compressed can be slightly larger than uncompressed TempCompressedMemory.Empty(UncompressedSize * 4 / 3); TempCompressedMemory.AddUninitialized(UncompressedSize * 4 / 3); int32 CompressedSize = TempCompressedMemory.Num() * TempCompressedMemory.GetTypeSize(); verify(FCompression::CompressMemory( NAME_Zlib, TempCompressedMemory.GetData(), CompressedSize, QuantizedDistanceFieldVolume.GetData(), UncompressedSize, COMPRESS_BiasMemory)); OutData.CompressedDistanceFieldVolume.Empty(CompressedSize); OutData.CompressedDistanceFieldVolume.AddUninitialized(CompressedSize); FPlatformMemory::Memcpy(OutData.CompressedDistanceFieldVolume.GetData(), TempCompressedMemory.GetData(), CompressedSize); } else { int32 CompressedSize = QuantizedDistanceFieldVolume.Num() * QuantizedDistanceFieldVolume.GetTypeSize(); OutData.CompressedDistanceFieldVolume.Empty(CompressedSize); OutData.CompressedDistanceFieldVolume.AddUninitialized(CompressedSize); FPlatformMemory::Memcpy(OutData.CompressedDistanceFieldVolume.GetData(), QuantizedDistanceFieldVolume.GetData(), CompressedSize); } } else { OutData.CompressedDistanceFieldVolume.Empty(); } UE_LOG(LogMeshUtilities, Log, TEXT("Finished distance field build in %.1fs - %ux%ux%u distance field, %u triangles, Range [%.1f, %.1f], %s"), (float)(FPlatformTime::Seconds() - StartTime), VolumeDimensions.X, VolumeDimensions.Y, VolumeDimensions.Z, Indices.Num() / 3, MinVolumeDistance, MaxVolumeDistance, *MeshName); } #if USE_EMBREE if (bUseEmbree) { rtcDeleteScene(EmbreeScene); rtcDeleteDevice(EmbreeDevice); } #endif } } #else void FMeshUtilities::GenerateSignedDistanceFieldVolumeData( FString MeshName, const FStaticMeshLODResources& LODModel, class FQueuedThreadPool& ThreadPool, const TArray& MaterialBlendModes, const FBoxSphereBounds& Bounds, float DistanceFieldResolutionScale, bool bGenerateAsIfTwoSided, FDistanceFieldVolumeData& OutData) { if (DistanceFieldResolutionScale > 0) { UE_LOG(LogMeshUtilities, Error, TEXT("Couldn't generate distance field for mesh, platform is missing required Vector intrinsics.")); } } #endif // PLATFORM_ENABLE_VECTORINTRINSICS void FMeshUtilities::DownSampleDistanceFieldVolumeData(FDistanceFieldVolumeData& DistanceFieldData, float Divider) { static const auto CVarCompress = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFieldBuild.Compress")); const bool bDataIsCompressed = CVarCompress->GetValueOnAnyThread() != 0; static const auto CVarEightBit = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DistanceFieldBuild.EightBit")); const bool bDataIsEightBit = CVarEightBit->GetValueOnAnyThread() != 0; if (!bDataIsEightBit) { return; } TArray UncompressedData; const TArray* SourceData = &DistanceFieldData.CompressedDistanceFieldVolume; if (DistanceFieldData.Size.X <= 4 || DistanceFieldData.Size.Y <= 4 || DistanceFieldData.Size.Z <= 4) { return; } int32 UncompressedSize = DistanceFieldData.Size.X * DistanceFieldData.Size.Y * DistanceFieldData.Size.Z; if (bDataIsCompressed) { UncompressedData.AddUninitialized(UncompressedSize); verify(FCompression::UncompressMemory(NAME_Zlib, UncompressedData.GetData(), UncompressedSize, DistanceFieldData.CompressedDistanceFieldVolume.GetData(), DistanceFieldData.CompressedDistanceFieldVolume.Num())); SourceData = &UncompressedData; } const FIntVector SrcSize = DistanceFieldData.Size; FVector DstSizeFloat = FVector(SrcSize); DstSizeFloat /= Divider; FIntVector DstSize; DstSize.X = FMath::TruncToInt(DstSizeFloat.X); DstSize.Y = FMath::TruncToInt(DstSizeFloat.Y); DstSize.Z = FMath::TruncToInt(DstSizeFloat.Z); FIntVector IntVectorOne = FIntVector(1); FIntVector SrcSizeMinusOne = SrcSize - IntVectorOne; FIntVector DstSizeMinusOne = DstSize - IntVectorOne; FVector Divider3 = FVector(SrcSizeMinusOne) / FVector(DstSizeMinusOne); TArray DownSampledTexture; DownSampledTexture.SetNum(DstSize.X * DstSize.Y * DstSize.Z); int32 SrcPitchX = SrcSize.X; int32 SrcPitchY = SrcSize.X * SrcSize.Y; int32 DstPitchX = DstSize.X; int32 DstPitchY = DstSize.X * DstSize.Y; for (int32 DstPosZ = 0; DstPosZ < DstSize.Z; ++DstPosZ) { int32 SrcPosZ0 = FMath::TruncToInt((float(DstPosZ) + 0.0f) * Divider3.Z); int32 SrcPosZ1 = FMath::TruncToInt((float(DstPosZ) + 1.0f) * Divider3.Z); SrcPosZ1 = FMath::Min(SrcPosZ1, SrcSizeMinusOne.Z); int32 SrcOffsetZ0 = SrcPosZ0 * SrcPitchY; int32 SrcOffsetZ1 = SrcPosZ1 * SrcPitchY; int32 DstOffsetZ = DstPosZ * DstPitchY; for (int32 DstPosY = 0; DstPosY < DstSize.Y; ++DstPosY) { int32 SrcPosY0 = FMath::TruncToInt(float(DstPosY) + 0.0f) * Divider3.Y; int32 SrcPosY1 = FMath::TruncToInt(float(DstPosY) + 1.0f) * Divider3.Y; SrcPosY1 = FMath::Min(SrcPosY1, SrcSizeMinusOne.Y); int32 SrcOffsetZ0Y0 = SrcPosY0 * SrcPitchX + SrcOffsetZ0; int32 SrcOffsetZ0Y1 = SrcPosY1 * SrcPitchX + SrcOffsetZ0; int32 SrcOffsetZ1Y0 = SrcPosY0 * SrcPitchX + SrcOffsetZ1; int32 SrcOffsetZ1Y1 = SrcPosY1 * SrcPitchX + SrcOffsetZ1; int32 DstOffsetZY = DstPosY * DstPitchX + DstOffsetZ; for (int32 DstPosX = 0; DstPosX < DstSize.X; ++DstPosX) { int32 SrcPosX0 = FMath::TruncToInt(float(DstPosX) + 0.0f) * Divider3.X; int32 SrcPosX1 = FMath::TruncToInt(float(DstPosX) + 1.0f) * Divider3.X; SrcPosX1 = FMath::Min(SrcPosX1, SrcSizeMinusOne.X - 1); uint32 a = uint32((*SourceData)[SrcPosX0 + SrcOffsetZ0Y0]); uint32 b = uint32((*SourceData)[SrcPosX1 + SrcOffsetZ0Y0]); uint32 c = uint32((*SourceData)[SrcPosX0 + SrcOffsetZ0Y1]); uint32 d = uint32((*SourceData)[SrcPosX1 + SrcOffsetZ0Y1]); uint32 e = uint32((*SourceData)[SrcPosX0 + SrcOffsetZ1Y0]); uint32 f = uint32((*SourceData)[SrcPosX1 + SrcOffsetZ1Y0]); uint32 g = uint32((*SourceData)[SrcPosX0 + SrcOffsetZ1Y1]); uint32 h = uint32((*SourceData)[SrcPosX1 + SrcOffsetZ1Y1]); a = a + b + c + d + e + f + g + h; a /= 8; DownSampledTexture[DstOffsetZY + DstPosX] = uint8(a); } } } DistanceFieldData.Size = DstSize; UncompressedSize = DownSampledTexture.Num(); if (bDataIsCompressed) { TArray TempCompressedMemory; // Compressed can be slightly larger than uncompressed TempCompressedMemory.Empty(UncompressedSize * 4 / 3); TempCompressedMemory.AddUninitialized(UncompressedSize * 4 / 3); int32 CompressedSize = TempCompressedMemory.Num() * TempCompressedMemory.GetTypeSize(); verify(FCompression::CompressMemory( NAME_Zlib, TempCompressedMemory.GetData(), CompressedSize, DownSampledTexture.GetData(), UncompressedSize, COMPRESS_BiasMemory)); DistanceFieldData.CompressedDistanceFieldVolume.Empty(CompressedSize); DistanceFieldData.CompressedDistanceFieldVolume.AddUninitialized(CompressedSize); FPlatformMemory::Memcpy(DistanceFieldData.CompressedDistanceFieldVolume.GetData(), TempCompressedMemory.GetData(), CompressedSize); } else { DistanceFieldData.CompressedDistanceFieldVolume.Empty(UncompressedSize); DistanceFieldData.CompressedDistanceFieldVolume.AddUninitialized(UncompressedSize); FPlatformMemory::Memcpy(DistanceFieldData.CompressedDistanceFieldVolume.GetData(), DownSampledTexture.GetData(), UncompressedSize); } }