// Copyright Epic Games, Inc. All Rights Reserved. #include "SparseVolumeTextureFactory.h" #include "SparseVolumeTexture/SparseVolumeTexture.h" #if WITH_EDITOR #include "SparseVolumeTextureOpenVDB.h" #include "SparseVolumeTextureOpenVDBUtility.h" #include "OpenVDBImportOptions.h" #include "Serialization/EditorBulkDataWriter.h" #include "Misc/Paths.h" #include "Misc/ScopedSlowTask.h" #include "Misc/FileHelper.h" #include "Async/Async.h" #include "Async/ParallelFor.h" #include "Editor.h" #include "OpenVDBImportWindow.h" #include "HAL/PlatformApplicationMisc.h" #include "HAL/Event.h" #include "HAL/PlatformProcess.h" #include "Interfaces/IMainFrameModule.h" #include #include #define LOCTEXT_NAMESPACE "USparseVolumeTextureFactory" DEFINE_LOG_CATEGORY_STATIC(LogSparseVolumeTextureFactory, Log, All); static void ComputeDefaultOpenVDBGridAssignment(const TArray>& GridComponentInfo, int32 NumFiles, FOpenVDBImportOptions* ImportOptions) { for (FOpenVDBSparseVolumeAttributesDesc& AttributesDesc : ImportOptions->Attributes) { for (FOpenVDBSparseVolumeComponentMapping& Mapping : AttributesDesc.Mappings) { Mapping.SourceGridIndex = INDEX_NONE; Mapping.SourceComponentIndex = INDEX_NONE; } AttributesDesc.Format = ESparseVolumeAttributesFormat::Float32; AttributesDesc.bRemapInputForUnorm = false; } // Assign the components of the input grids to the components of the output SVT. uint32 DstAttributesIdx = 0; uint32 DstComponentIdx = 0; for (const TSharedPtr& GridComponent : GridComponentInfo) { if (GridComponent->Index == INDEX_NONE) { continue; } ImportOptions->Attributes[DstAttributesIdx].Mappings[DstComponentIdx].SourceGridIndex = GridComponent->Index; ImportOptions->Attributes[DstAttributesIdx].Mappings[DstComponentIdx].SourceComponentIndex = GridComponent->ComponentIndex; ++DstComponentIdx; if (DstComponentIdx == 4) { DstComponentIdx = 0; ++DstAttributesIdx; if (DstAttributesIdx == 2) { break; } } } ImportOptions->bIsSequence = NumFiles > 1; } static TArray FindOpenVDBSequenceFileNames(const FString& Filename) { TArray SequenceFilenames; // The file is potentially a sequence if the character before the `.vdb` is a number. const bool bIsFilePotentiallyPartOfASequence = FChar::IsDigit(Filename[Filename.Len() - 5]); if (!bIsFilePotentiallyPartOfASequence) { SequenceFilenames.Add(Filename); } else { const FString Path = FPaths::GetPath(Filename); const FString CleanFilename = FPaths::GetCleanFilename(Filename); FString CleanFilenameWithoutSuffix; { const FString CleanFilenameWithoutExt = CleanFilename.LeftChop(4); const int32 LastNonDigitIndex = CleanFilenameWithoutExt.FindLastCharByPredicate([](TCHAR Letter) { return !FChar::IsDigit(Letter); }) + 1; const int32 DigitCount = CleanFilenameWithoutExt.Len() - LastNonDigitIndex; CleanFilenameWithoutSuffix = CleanFilenameWithoutExt.LeftChop(CleanFilenameWithoutExt.Len() - LastNonDigitIndex); } // Find all files potentially part of the sequence TArray PotentialSequenceFilenames; IFileManager::Get().FindFiles(PotentialSequenceFilenames, *Path, TEXT("*.vdb")); PotentialSequenceFilenames = PotentialSequenceFilenames.FilterByPredicate([CleanFilenameWithoutSuffix](const FString& Str) { return Str.StartsWith(CleanFilenameWithoutSuffix); }); auto GetFilenameNumberSuffix = [](const FString& Filename) -> int32 { const FString FilenameWithoutExt = Filename.LeftChop(4); const int32 LastNonDigitIndex = FilenameWithoutExt.FindLastCharByPredicate([](TCHAR Letter) { return !FChar::IsDigit(Letter); }) + 1; const FString NumberSuffixStr = FilenameWithoutExt.RightChop(LastNonDigitIndex); int32 Number = INDEX_NONE; if (NumberSuffixStr.IsNumeric()) { TTypeFromString::FromString(Number, *NumberSuffixStr); } return Number; }; // Find range of number suffixes int32 LowestIndex = INT32_MAX; int32 HighestIndex = INT32_MIN; for (FString& ItemFilename : PotentialSequenceFilenames) { const int32 Index = GetFilenameNumberSuffix(ItemFilename); if (Index == INDEX_NONE) { ItemFilename.Empty(); continue; } LowestIndex = FMath::Min(LowestIndex, Index); HighestIndex = FMath::Max(HighestIndex, Index); } check(HighestIndex >= LowestIndex); // Sort the filenames into the result array SequenceFilenames.SetNum(HighestIndex - LowestIndex + 1); for (const FString& ItemFilename : PotentialSequenceFilenames) { const int32 Index = ItemFilename.IsEmpty() ? INDEX_NONE : GetFilenameNumberSuffix(ItemFilename); if (Index == INDEX_NONE) { continue; } SequenceFilenames[Index - LowestIndex] = Path / ItemFilename; } // Chop off any items after finding the first gap for (int32 i = 0; i < SequenceFilenames.Num(); ++i) { if (SequenceFilenames[i].IsEmpty()) { SequenceFilenames.SetNum(i); break; } } } check(!SequenceFilenames.IsEmpty()); return SequenceFilenames; } struct FOpenVDBPreviewData { TArray64 LoadedFile; TArray GridInfo; TArray> GridInfoPtrs; TArray> GridComponentInfoPtrs; TArray SequenceFilenames; FOpenVDBImportOptions DefaultImportOptions; }; static bool LoadOpenVDBPreviewData(const FString& Filename, FOpenVDBPreviewData* OutPreviewData) { FOpenVDBPreviewData& Result = *OutPreviewData; check(Result.LoadedFile.IsEmpty()); check(Result.GridInfo.IsEmpty()); check(Result.GridInfoPtrs.IsEmpty()); check(Result.GridComponentInfoPtrs.IsEmpty()); check(Result.SequenceFilenames.IsEmpty()); if (!FFileHelper::LoadFileToArray(Result.LoadedFile, *Filename)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file could not be loaded: %s"), *Filename); return false; } if (!GetOpenVDBGridInfo(Result.LoadedFile, true /*bCreateStrings*/, &Result.GridInfo)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to read OpenVDB file: %s"), *Filename); return false; } if (Result.GridInfo.IsEmpty()) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file contains no grids: %s"), *Filename); return false; } // We need a option to leave channels empty FOpenVDBGridComponentInfo NoneGridComponentInfo; NoneGridComponentInfo.Index = INDEX_NONE; NoneGridComponentInfo.ComponentIndex = INDEX_NONE; NoneGridComponentInfo.Name = TEXT(""); NoneGridComponentInfo.DisplayString = TEXT(""); Result.GridComponentInfoPtrs.Add(MakeShared(NoneGridComponentInfo)); // Create individual entries for each component of all valid source grids. // This is an array of TSharedPtr because SComboBox requires its input to be wrapped in TSharedPtr. bool bFoundSupportedGridType = false; for (const FOpenVDBGridInfo& Grid : Result.GridInfo) { // Append all grids, even if we don't actually support them Result.GridInfoPtrs.Add(MakeShared(Grid)); if (Grid.Type == EOpenVDBGridType::Unknown || !IsOpenVDBGridValid(Grid, Filename)) { continue; } bFoundSupportedGridType = true; // Create one entry per component for (uint32 ComponentIdx = 0; ComponentIdx < Grid.NumComponents; ++ComponentIdx) { FOpenVDBGridComponentInfo ComponentInfo; ComponentInfo.Index = Grid.Index; ComponentInfo.ComponentIndex = ComponentIdx; ComponentInfo.Name = Grid.Name; const TCHAR* ComponentNames[] = { TEXT(".X"), TEXT(".Y"),TEXT(".Z"),TEXT(".W") }; FStringFormatOrderedArguments FormatArgs; FormatArgs.Add(ComponentInfo.Index); FormatArgs.Add(ComponentInfo.Name); FormatArgs.Add(Grid.NumComponents == 1 ? TEXT("") : ComponentNames[ComponentIdx]); ComponentInfo.DisplayString = FString::Format(TEXT("{0}. {1}{2}"), FormatArgs); Result.GridComponentInfoPtrs.Add(MakeShared(MoveTemp(ComponentInfo))); } } if (!bFoundSupportedGridType) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file contains no grids of supported type: %s"), *Filename); return false; } Result.SequenceFilenames = FindOpenVDBSequenceFileNames(Filename); ComputeDefaultOpenVDBGridAssignment(Result.GridComponentInfoPtrs, Result.SequenceFilenames.Num(), &Result.DefaultImportOptions); return true; } static bool ShowOpenVDBImportWindow(const FString& Filename, const FOpenVDBPreviewData& PreviewData, FOpenVDBImportOptions* OutImportOptions) { TSharedPtr ParentWindow; if (FModuleManager::Get().IsModuleLoaded("MainFrame")) { IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); ParentWindow = MainFrame.GetParentWindow(); } // Compute centered window position based on max window size, which include when all categories are expanded const float ImportWindowWidth = 450.0f; const float ImportWindowHeight = 750.0f; FVector2D ImportWindowSize = FVector2D(ImportWindowWidth, ImportWindowHeight); // Max window size it can get based on current slate FSlateRect WorkAreaRect = FSlateApplicationBase::Get().GetPreferredWorkArea(); FVector2D DisplayTopLeft(WorkAreaRect.Left, WorkAreaRect.Top); FVector2D DisplaySize(WorkAreaRect.Right - WorkAreaRect.Left, WorkAreaRect.Bottom - WorkAreaRect.Top); float ScaleFactor = FPlatformApplicationMisc::GetDPIScaleFactorAtPoint(DisplayTopLeft.X, DisplayTopLeft.Y); ImportWindowSize *= ScaleFactor; FVector2D WindowPosition = (DisplayTopLeft + (DisplaySize - ImportWindowSize) / 2.0f) / ScaleFactor; TSharedRef Window = SNew(SWindow) .Title(NSLOCTEXT("UnrealEd", "OpenVDBImportOptionsTitle", "OpenVDB Import Options")) .SizingRule(ESizingRule::Autosized) .AutoCenter(EAutoCenter::None) .ClientSize(ImportWindowSize) .ScreenPosition(WindowPosition); TArray> SupportedFormats = { MakeShared(ESparseVolumeAttributesFormat::Float32), MakeShared(ESparseVolumeAttributesFormat::Float16), MakeShared(ESparseVolumeAttributesFormat::Unorm8) }; TSharedPtr OpenVDBOptionWindow; Window->SetContent ( SAssignNew(OpenVDBOptionWindow, SOpenVDBImportWindow) .ImportOptions(OutImportOptions) .DefaultImportOptions(&PreviewData.DefaultImportOptions) .NumFoundFiles(PreviewData.SequenceFilenames.Num()) .OpenVDBGridInfo(&PreviewData.GridInfoPtrs) .OpenVDBGridComponentInfo(&PreviewData.GridComponentInfoPtrs) .OpenVDBSupportedTargetFormats(&SupportedFormats) .WidgetWindow(Window) .FullPath(FText::FromString(Filename)) .MaxWindowHeight(ImportWindowHeight) .MaxWindowWidth(ImportWindowWidth) ); FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false); OutImportOptions->bIsSequence = OpenVDBOptionWindow->ShouldImportAsSequence(); return OpenVDBOptionWindow->ShouldImport(); } static bool ValidateImportOptions(const FOpenVDBImportOptions& ImportOptions, const TArray& GridInfo) { const int32 NumGrids = GridInfo.Num(); for (const FOpenVDBSparseVolumeAttributesDesc& AttributesDesc : ImportOptions.Attributes) { for (const FOpenVDBSparseVolumeComponentMapping& Mapping : AttributesDesc.Mappings) { const int32 SourceGridIndex = Mapping.SourceGridIndex; const int32 SourceComponentIndex = Mapping.SourceComponentIndex; if (Mapping.SourceGridIndex != INDEX_NONE) { if (SourceGridIndex >= NumGrids) { return false; // Invalid grid index } if (SourceComponentIndex == INDEX_NONE || SourceComponentIndex >= (int32)GridInfo[SourceGridIndex].NumComponents) { return false; // Invalid component index } } } } return true; } USparseVolumeTextureFactory::USparseVolumeTextureFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { bCreateNew = true; bEditAfterNew = true; bEditorImport = true; SupportedClass = USparseVolumeTexture::StaticClass(); Formats.Add(TEXT("vdb;OpenVDB Format")); } FText USparseVolumeTextureFactory::GetDisplayName() const { return LOCTEXT("SparseVolumeTextureFactoryDescription", "Sparse Volume Texture"); } bool USparseVolumeTextureFactory::ConfigureProperties() { return true; } bool USparseVolumeTextureFactory::ShouldShowInNewMenu() const { return false; } /////////////////////////////////////////////////////////////////////////////// // Create asset bool USparseVolumeTextureFactory::CanCreateNew() const { return false; // To be able to import files and call } UObject* USparseVolumeTextureFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { USparseVolumeTexture* Object = NewObject(InParent, InClass, InName, Flags); // SVT_TODO initialize similarly to UTexture2DFactoryNew return Object; } /////////////////////////////////////////////////////////////////////////////// // Import asset bool USparseVolumeTextureFactory::DoesSupportClass(UClass* Class) { return Class == USparseVolumeTexture::StaticClass(); } UClass* USparseVolumeTextureFactory::ResolveSupportedClass() { return USparseVolumeTexture::StaticClass(); } bool USparseVolumeTextureFactory::FactoryCanImport(const FString& Filename) { const FString Extension = FPaths::GetExtension(Filename); if (Extension == TEXT("vdb")) { return true; } return false; } void USparseVolumeTextureFactory::CleanUp() { Super::CleanUp(); } UObject* USparseVolumeTextureFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, FFeedbackContext* Warn, bool& bOutOperationCanceled) { #if OPENVDB_AVAILABLE GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Parms); TArray ResultAssets; bOutOperationCanceled = false; const bool bIsUnattended = (IsAutomatedImport() || FApp::IsUnattended() || IsRunningCommandlet() || GIsRunningUnattendedScript); // Load file and get info about each contained grid FOpenVDBPreviewData PreviewData; if (!LoadOpenVDBPreviewData(Filename, &PreviewData)) { return nullptr; } FOpenVDBImportOptions ImportOptions = PreviewData.DefaultImportOptions; if (!bIsUnattended) { // Show dialog for import options if (!ShowOpenVDBImportWindow(Filename, PreviewData, &ImportOptions)) { bOutOperationCanceled = true; return nullptr; } } if (!ValidateImportOptions(ImportOptions, PreviewData.GridInfo)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Import options are invalid! This is likely due to invalid/out-of-bounds grid or component indices.")); return nullptr; } // Utility function for computing the bounding box encompassing the bounds of all frames in the SVT. auto ExpandVolumeBounds = [](const FOpenVDBImportOptions& ImportOptions, const TArray& GridInfoArray, FIntVector3& VolumeBoundsMin, FIntVector3& VolumeBoundsMax) { for (const FOpenVDBSparseVolumeAttributesDesc& Attributes : ImportOptions.Attributes) { for (const FOpenVDBSparseVolumeComponentMapping& Mapping : Attributes.Mappings) { if (Mapping.SourceGridIndex != INDEX_NONE) { const FOpenVDBGridInfo& GridInfo = GridInfoArray[Mapping.SourceGridIndex]; VolumeBoundsMin.X = FMath::Min(VolumeBoundsMin.X, GridInfo.VolumeActiveAABBMin.X); VolumeBoundsMin.Y = FMath::Min(VolumeBoundsMin.Y, GridInfo.VolumeActiveAABBMin.Y); VolumeBoundsMin.Z = FMath::Min(VolumeBoundsMin.Z, GridInfo.VolumeActiveAABBMin.Z); VolumeBoundsMax.X = FMath::Max(VolumeBoundsMax.X, GridInfo.VolumeActiveAABBMax.X); VolumeBoundsMax.Y = FMath::Max(VolumeBoundsMax.Y, GridInfo.VolumeActiveAABBMax.Y); VolumeBoundsMax.Z = FMath::Max(VolumeBoundsMax.Z, GridInfo.VolumeActiveAABBMax.Z); } } } }; auto ComputeNumMipLevels = [](const FIntVector3& VolumeBoundsMin, const FIntVector3& VolumeBoundsMax) { int32 Levels = 1; FIntVector3 Resolution = VolumeBoundsMax - VolumeBoundsMin; while (Resolution.X > 1 || Resolution.Y > 1 || Resolution.Z > 1) { Resolution /= 2; ++Levels; } return Levels; }; FIntVector3 VolumeBoundsMin = FIntVector3(INT32_MAX, INT32_MAX, INT32_MAX); FIntVector3 VolumeBoundsMax = FIntVector3(INT32_MIN, INT32_MIN, INT32_MIN); // Import as either single static SVT or a sequence of frames, making up an animated SVT if (!ImportOptions.bIsSequence) { // Import as a static sparse volume texture asset. FScopedSlowTask ImportTask(1.0f, LOCTEXT("ImportingVDBStatic", "Importing static OpenVDB")); ImportTask.MakeDialog(true); FName NewName(InName.ToString() + TEXT("VDB")); UStaticSparseVolumeTexture* StaticSVTexture = NewObject(InParent, UStaticSparseVolumeTexture::StaticClass(), NewName, Flags); ExpandVolumeBounds(ImportOptions, PreviewData.GridInfo, VolumeBoundsMin, VolumeBoundsMax); StaticSVTexture->VolumeResolution = VolumeBoundsMax - VolumeBoundsMin; StaticSVTexture->Frames.SetNum(1); const int32 NumMipLevels = ComputeNumMipLevels(VolumeBoundsMin, VolumeBoundsMax); FSparseVolumeRawSource SparseVolumeRawSource{}; const bool bConversionSuccess = ConvertOpenVDBToSparseVolumeTexture(PreviewData.LoadedFile, ImportOptions, VolumeBoundsMin, SparseVolumeRawSource); if (!bConversionSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to convert OpenVDB file to SparseVolumeTexture: %s"), *Filename); return nullptr; } // Serialize the raw source data into the asset object. // After serializing the data, we generate the next mip level by downsampling the current one (with a call to GenerateMipMap()) // and then repeating the loop until we processed all levels. StaticSVTexture->Frames[0].SetNum(NumMipLevels); for (int32 MipLevel = 0; MipLevel < NumMipLevels; ++MipLevel) { UE::Serialization::FEditorBulkDataWriter RawDataArchiveWriter(StaticSVTexture->Frames[0][MipLevel].RawData); SparseVolumeRawSource.Serialize(RawDataArchiveWriter); if ((MipLevel + 1) < NumMipLevels) { SparseVolumeRawSource = SparseVolumeRawSource.GenerateMipMap(); } } if (ImportTask.ShouldCancel()) { bOutOperationCanceled = true; return nullptr; } ImportTask.EnterProgressFrame(1.0f, LOCTEXT("ConvertingVDBStatic", "Converting static OpenVDB")); ResultAssets.Add(StaticSVTexture); } else { // Import as an animated sparse volume texture asset. // Data from original file is no longer needed; we iterate over all frames later PreviewData.LoadedFile.Empty(); FName NewName(InName.ToString() + TEXT("VDBAnim")); UAnimatedSparseVolumeTexture* AnimatedSVTexture = NewObject(InParent, UAnimatedSparseVolumeTexture::StaticClass(), NewName, Flags); const int32 NumFrames = PreviewData.SequenceFilenames.Num(); FScopedSlowTask ImportTask(NumFrames + 1, LOCTEXT("ImportingVDBAnim", "Importing OpenVDB animation")); ImportTask.MakeDialog(true); // Allocate space for each frame AnimatedSVTexture->Frames.SetNum(NumFrames); std::atomic_bool bErrored = false; std::atomic_bool bCanceled = false; // Compute volume bounds and check sequence files for compatiblity std::mutex VolumeBoundsMutex; ParallelFor(NumFrames, [&bErrored, &VolumeBoundsMutex, &VolumeBoundsMin, &VolumeBoundsMax, &ExpandVolumeBounds, &PreviewData, &ImportOptions](int32 FrameIdx) { if (bErrored.load()) { return; } // Load file and get info about each contained grid const FString& FrameFilename = PreviewData.SequenceFilenames[FrameIdx]; TArray64 LoadedFrameFile; if (!FFileHelper::LoadFileToArray(LoadedFrameFile, *FrameFilename)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file could not be loaded: %s"), *FrameFilename); bErrored.store(true); return; } TArray FrameGridInfo; if (!GetOpenVDBGridInfo(LoadedFrameFile, true, &FrameGridInfo)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to read OpenVDB file: %s"), *FrameFilename); bErrored.store(true); return; } // Sanity check for compatibility for (const FOpenVDBSparseVolumeAttributesDesc& AttributesDesc : ImportOptions.Attributes) { for (const FOpenVDBSparseVolumeComponentMapping& Mapping : AttributesDesc.Mappings) { const uint32 SourceGridIndex = Mapping.SourceGridIndex; if (SourceGridIndex != INDEX_NONE) { if ((int32)SourceGridIndex >= FrameGridInfo.Num()) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file is incompatible with other frames in the sequence: %s"), *FrameFilename); bErrored.store(true); return; } const FOpenVDBGridInfo& OrigSourceGrid = PreviewData.GridInfo[SourceGridIndex]; const FOpenVDBGridInfo& FrameSourceGrid = FrameGridInfo[SourceGridIndex]; if (OrigSourceGrid.Type != FrameSourceGrid.Type || OrigSourceGrid.Name != FrameSourceGrid.Name) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file is incompatible with other frames in the sequence: %s"), *FrameFilename); bErrored.store(true); return; } } } } // Update sequence volume bounds and increment ProcessedFramesCounter { std::lock_guard Lock(VolumeBoundsMutex); ExpandVolumeBounds(ImportOptions, FrameGridInfo, VolumeBoundsMin, VolumeBoundsMax); } }); if (bErrored.load()) { return nullptr; } ImportTask.EnterProgressFrame(1.0f, LOCTEXT("ConvertingVDBAnim", "Converting OpenVDB animation")); const int32 NumMipLevels = ComputeNumMipLevels(VolumeBoundsMin, VolumeBoundsMax); FEvent* AllTasksFinishedEvent = FPlatformProcess::GetSynchEventFromPool(); std::atomic_int FinishedTasksCounter = 0; // Will be incremented even if frame processing failed std::atomic_int ProcessedFramesCounter = 0; // Load individual frames, process/convert them and append them to the resulting asset for (int32 FrameIdx = 0; FrameIdx < NumFrames; ++FrameIdx) { // Increments the atomic counter when going out of scope. Triggers an event once the counter reaches a given value. struct FScopedIncrementer { std::atomic_int& Counter; int32 MaxValue; FEvent* Event; explicit FScopedIncrementer(std::atomic_int& InCounter, int32 InMaxValue, FEvent* InEvent) : Counter(InCounter), MaxValue(InMaxValue), Event(InEvent) {} ~FScopedIncrementer() { if ((Counter.fetch_add(1) + 1) == MaxValue) { Event->Trigger(); } } }; AsyncTask(ENamedThreads::AnyNormalThreadNormalTask, [FrameIdx, NumFrames, &PreviewData, &ImportOptions, &AnimatedSVTexture, AllTasksFinishedEvent, &bErrored, &bCanceled, &FinishedTasksCounter, &ProcessedFramesCounter, &VolumeBoundsMin, &NumMipLevels]() { // Ensure the FinishedTasksCounter will be incremented in all cases FScopedIncrementer Incremeter(FinishedTasksCounter, NumFrames, AllTasksFinishedEvent); if (bErrored.load() || bCanceled.load()) { return; } const FString& FrameFilename = PreviewData.SequenceFilenames[FrameIdx]; UE_LOG(LogSparseVolumeTextureFactory, Display, TEXT("Loading OpenVDB sequence frame #%i %s."), FrameIdx, *FrameFilename); // Load file TArray64 LoadedFrameFile; if (!FFileHelper::LoadFileToArray(LoadedFrameFile, *FrameFilename)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file could not be loaded: %s"), *FrameFilename); bErrored.store(true); return; } FSparseVolumeRawSource SparseVolumeRawSource{}; const bool bConversionSuccess = ConvertOpenVDBToSparseVolumeTexture(LoadedFrameFile, ImportOptions, VolumeBoundsMin, SparseVolumeRawSource); if (!bConversionSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to convert OpenVDB file to SparseVolumeTexture: %s"), *FrameFilename); bErrored.store(true); return; } // Serialize the raw source data from this frame into the asset object. // After serializing the data, we generate the next mip level by downsampling the current one (with a call to GenerateMipMap()) // and then repeating the loop until we processed all levels. AnimatedSVTexture->Frames[FrameIdx].SetNum(NumMipLevels); for (int32 MipLevel = 0; MipLevel < NumMipLevels; ++MipLevel) { UE::Serialization::FEditorBulkDataWriter RawDataArchiveWriter(AnimatedSVTexture->Frames[FrameIdx][MipLevel].RawData); SparseVolumeRawSource.Serialize(RawDataArchiveWriter); if ((MipLevel + 1) < NumMipLevels) { SparseVolumeRawSource = SparseVolumeRawSource.GenerateMipMap(); } } // Increment ProcessedFramesCounter ProcessedFramesCounter.fetch_add(1); }); } // Wait for frames to be processed { int NumFinishedTasks = 0; int NumProcessedFrames = 0; while (NumFinishedTasks < NumFrames) { // We can't block here because we want to regularly update the progress bar and check for user input. const uint32 WaitTimeMS = 2; AllTasksFinishedEvent->Wait(WaitTimeMS); if (!bCanceled.load() && !bErrored.load() && ImportTask.ShouldCancel()) { bCanceled.store(true); } const int NewNumFinishedTasks = FinishedTasksCounter.load(); if (NewNumFinishedTasks > NumFinishedTasks) { const int NewNumProcessedFrames = ProcessedFramesCounter.load(); if (NewNumProcessedFrames > NumProcessedFrames && !bErrored.load()) { const float Progress = float(NewNumProcessedFrames - NumProcessedFrames); ImportTask.EnterProgressFrame(Progress, LOCTEXT("ConvertingVDBAnim", "Converting OpenVDB animation")); } NumFinishedTasks = NewNumFinishedTasks; NumProcessedFrames = NewNumProcessedFrames; } } } FPlatformProcess::ReturnSynchEventToPool(AllTasksFinishedEvent); if (bCanceled.load()) { bOutOperationCanceled = true; return nullptr; } if (bErrored.load()) { return nullptr; } AnimatedSVTexture->VolumeResolution = VolumeBoundsMax - VolumeBoundsMin; ResultAssets.Add(AnimatedSVTexture); } // Now notify the system about the imported/updated/created assets check(ResultAssets.Num() == 1); GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, ResultAssets[0]); return ResultAssets[0]; #else // OPENVDB_AVAILABLE // SVT_TODO Make sure we can also import on more platforms such as Linux. See SparseVolumeTextureOpenVDB.h UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Cannot import OpenVDB asset any platform other than Windows.")); return nullptr; #endif // OPENVDB_AVAILABLE } #endif // WITH_EDITORONLY_DATA #undef LOCTEXT_NAMESPACE #include "Serialization/EditorBulkDataWriter.h"