// Copyright Epic Games, Inc. All Rights Reserved. #include "SparseVolumeTextureFactory.h" #include "SparseVolumeTexture/SparseVolumeTexture.h" #include "SparseVolumeTexture/SparseVolumeTextureData.h" #if WITH_EDITOR #include "SparseVolumeTextureOpenVDB.h" #include "SparseVolumeTextureOpenVDBUtility.h" #include "OpenVDBImportOptions.h" #include "Misc/Paths.h" #include "Misc/ScopedSlowTask.h" #include "Misc/FileHelper.h" #include "Async/Async.h" #include "Async/ParallelFor.h" #include "AssetImportTask.h" #include "Editor.h" #include "EditorFramework/AssetImportData.h" #include "ObjectTools.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::Float16; } // Assign the components of the input grids to the components of the output SVT. const TSharedPtr* DensityComponentInfoPtr = GridComponentInfo.FindByPredicate([](const TSharedPtr& GridComponent) { return GridComponent->Name == TEXT("density"); }); const int32 NumNonDensityComponentInfos = GridComponentInfo.Num() - 1 - (DensityComponentInfoPtr ? 1 : 0); // -1 because there is always a element in the list // Optimized density assignment: density as 8bit unorm in Attributes A and all other components in Attributes B as 16bit float. This only works if there is a maximum of 4 non-density components. // We also don't use this assignment if there are 3 non-density components as these will be padded to a 4 component format anyways, so we might as well put all 4 components into a single texture. const bool bOptimizedDensityAssignment = (NumNonDensityComponentInfos <= 4 && NumNonDensityComponentInfos != 3) && DensityComponentInfoPtr != nullptr; if (bOptimizedDensityAssignment) { // Assign density to the first channel of Attributes A and set format to 8 bit unorm ImportOptions->Attributes[0].Mappings[0].SourceGridIndex = (*DensityComponentInfoPtr)->Index; ImportOptions->Attributes[0].Mappings[0].SourceComponentIndex = (*DensityComponentInfoPtr)->ComponentIndex; ImportOptions->Attributes[0].Format = ESparseVolumeAttributesFormat::Unorm8; // All the other components go into Attributes B with 16 bit float uint32 DstComponentIdx = 0; for (const TSharedPtr& GridComponent : GridComponentInfo) { if (!ensure(DstComponentIdx <= 3) || GridComponent->Index == INDEX_NONE || &GridComponent == DensityComponentInfoPtr) { continue; } ImportOptions->Attributes[1].Mappings[DstComponentIdx].SourceGridIndex = GridComponent->Index; ImportOptions->Attributes[1].Mappings[DstComponentIdx].SourceComponentIndex = GridComponent->ComponentIndex; ++DstComponentIdx; } ImportOptions->Attributes[1].Format = ESparseVolumeAttributesFormat::Float16; } else { 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; } 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 = nullptr; // This factory supports multiple classes, so SupportedClass needs to be nullptr 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() || Class == UStaticSparseVolumeTexture::StaticClass() || Class == UAnimatedSparseVolumeTexture::StaticClass(); } UClass* USparseVolumeTextureFactory::ResolveSupportedClass() { // SVT_TODO: Do we need to return UStaticSparseVolumeTexture::StaticClass() or UAnimatedSparseVolumeTexture::StaticClass() here instead? Using the base class seems to work. 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) { return ImportInternal(InClass, InParent, InName, Flags, Filename, Parms, bOutOperationCanceled, false /*bIsReimport*/); } bool USparseVolumeTextureFactory::CanReimport(UObject* Obj, TArray& OutFilenames) { #if OPENVDB_AVAILABLE UStreamableSparseVolumeTexture* StreamableSVT = Cast(Obj); if (StreamableSVT && StreamableSVT->AssetImportData) { StreamableSVT->AssetImportData->ExtractFilenames(OutFilenames); return true; } #endif // OPENVDB_AVAILABLE return false; } void USparseVolumeTextureFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) { #if OPENVDB_AVAILABLE UStreamableSparseVolumeTexture* StreamableSVT = Cast(Obj); if (StreamableSVT && ensure(NewReimportPaths.Num() == 1)) { StreamableSVT->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]); } #endif // OPENVDB_AVAILABLE } EReimportResult::Type USparseVolumeTextureFactory::Reimport(UObject* Obj) { #if OPENVDB_AVAILABLE UStreamableSparseVolumeTexture* StreamableSVT = Cast(Obj); if (!StreamableSVT) { return EReimportResult::Failed; } // Make sure file is valid and exists const FString Filename = StreamableSVT->AssetImportData->GetFirstFilename(); if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE || !FactoryCanImport(Filename)) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Reimport failed! Filename '%s' is invalid or no such file exists."), *Filename); return EReimportResult::Failed; } bool OutCanceled = false; if (!ImportInternal(StreamableSVT->GetClass(), StreamableSVT->GetOuter(), *StreamableSVT->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled, true /*bIsReimport*/)) { if (OutCanceled) { return EReimportResult::Cancelled; } return EReimportResult::Failed; } return EReimportResult::Succeeded; #else UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Cannot import OpenVDB asset any platform other than Windows.")); return EReimportResult::Failed; #endif // OPENVDB_AVAILABLE } UObject* USparseVolumeTextureFactory::ImportInternal(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, const FString& Filename, const TCHAR* Parms, bool& bOutOperationCanceled, bool bIsReimport) { #if OPENVDB_AVAILABLE GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Parms); TArray ResultAssets; bOutOperationCanceled = false; const bool bIsUnattended = (IsAutomatedImport() || FApp::IsUnattended() || IsRunningCommandlet() || GIsRunningUnattendedScript); FOpenVDBPreviewData PreviewData; // Use the provided preview data, if any bool bCollectedData = false; if (bIsUnattended && AssetImportTask) { if (UOpenVDBImportOptionsObject* TaskOptions = Cast(AssetImportTask->Options)) { PreviewData = TaskOptions->PreviewData; bCollectedData = true; } } // Otherwise, load file and get info about each contained grid if (!bCollectedData) { 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); } } } }; 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); ExpandVolumeBounds(ImportOptions, PreviewData.GridInfo, VolumeBoundsMin, VolumeBoundsMax); UE::SVT::FTextureData TextureData{}; FTransform FrameTransform = FTransform::Identity; const bool bConversionSuccess = ConvertOpenVDBToSparseVolumeTexture(PreviewData.LoadedFile, ImportOptions, VolumeBoundsMin, TextureData, FrameTransform); if (!bConversionSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to convert OpenVDB file to SparseVolumeTexture: %s"), *Filename); return nullptr; } UStaticSparseVolumeTexture* StaticSVTexture = NewObject(InParent, UStaticSparseVolumeTexture::StaticClass(), InName, Flags); const bool bInitSuccess = StaticSVTexture->Initialize(MakeArrayView(&TextureData, 1), MakeArrayView(&FrameTransform, 1)); if (!bInitSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to initialize SparseVolumeTexture: %s"), *Filename); return nullptr; } 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(); const int32 NumFrames = PreviewData.SequenceFilenames.Num(); FScopedSlowTask ImportTask(NumFrames + 1, LOCTEXT("ImportingVDBAnim", "Importing OpenVDB animation")); ImportTask.MakeDialog(true); // Allocate space for each frame TArray UncookedFramesData; TArray FrameTransforms; UncookedFramesData.SetNum(NumFrames); FrameTransforms.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")); 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, &UncookedFramesData, &FrameTransforms, AllTasksFinishedEvent, &bErrored, &bCanceled, &FinishedTasksCounter, &ProcessedFramesCounter, &VolumeBoundsMin]() { // 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; } UE::SVT::FTextureData TextureData{}; FTransform FrameTransform = FTransform::Identity; const bool bConversionSuccess = ConvertOpenVDBToSparseVolumeTexture(LoadedFrameFile, ImportOptions, VolumeBoundsMin, TextureData, FrameTransform); if (!bConversionSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to convert OpenVDB file to SparseVolumeTexture: %s"), *FrameFilename); bErrored.store(true); return; } UncookedFramesData[FrameIdx] = MoveTemp(TextureData); FrameTransforms[FrameIdx] = FrameTransform; // 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; } // By default, the resulting package (already created) and object (about to be) will have the name of the imported file. // However, since the file is part of an entire sequence that we imported, it would be confusing if the resulting asset was named "file_0000" instead of "file", // so we attempt to rename both package and object here. // Don't try to rename the SVT if we are doing a reimport. FName NewObjectName = InName; if (!bIsReimport) { FString NewFileName = GetVDBSequenceBaseFileName(Filename, false /*bDiscardNumbersOnly*/); // GetVDBSequenceBaseFileName() discards the number as well as underscores and invalid chars at the end of the filename. // We still need to ensure that there are no additional invalid characters in the filename. NewFileName = ObjectTools::SanitizeObjectName(NewFileName); // Don't try to rename the package if it's the transient package or the new filename is empty if (!NewFileName.IsEmpty() && (InParent != GetTransientPackage() && InParent->IsA())) { const FString PackageName = InParent->GetName(); // Contains name with path int32 LastSeparatorIndex = 0; const bool bFoundSeparator = PackageName.FindLastChar(TEXT('/'), LastSeparatorIndex); // Get the substring containing the path only, without the file name const FString PackagePath = bFoundSeparator ? PackageName.Left(LastSeparatorIndex) : FString(); const FString NewPackageName = PackagePath / NewFileName; UPackage* ExistingPackage = FindPackage(InParent->GetOuter(), *NewPackageName); if (!ExistingPackage) { InParent->Rename(*NewPackageName, nullptr, REN_DontCreateRedirectors); NewObjectName = *NewFileName; } } } UAnimatedSparseVolumeTexture* AnimatedSVTexture = NewObject(InParent, UAnimatedSparseVolumeTexture::StaticClass(), NewObjectName, Flags); const bool bInitSuccess = AnimatedSVTexture->Initialize(UncookedFramesData, FrameTransforms); if (!bInitSuccess) { UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("Failed to initialize SparseVolumeTexture: %s"), *Filename); return nullptr; } ResultAssets.Add(AnimatedSVTexture); } // Now notify the system about the imported/updated/created assets check(ResultAssets.Num() == 1); CastChecked(ResultAssets[0])->AssetImportData->Update(Filename); 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