Files
UnrealEngine/Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureFactory.cpp
2025-05-18 13:04:45 +08:00

795 lines
29 KiB
C++

// 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 <atomic>
#include <mutex>
#define LOCTEXT_NAMESPACE "USparseVolumeTextureFactory"
DEFINE_LOG_CATEGORY_STATIC(LogSparseVolumeTextureFactory, Log, All);
static void ComputeDefaultOpenVDBGridAssignment(const TArray<TSharedPtr<FOpenVDBGridComponentInfo>>& 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<FOpenVDBGridComponentInfo>* DensityComponentInfoPtr = GridComponentInfo.FindByPredicate([](const TSharedPtr<FOpenVDBGridComponentInfo>& GridComponent) { return GridComponent->Name == TEXT("density"); });
const int32 NumNonDensityComponentInfos = GridComponentInfo.Num() - 1 - (DensityComponentInfoPtr ? 1 : 0); // -1 because there is always a <None> 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<FOpenVDBGridComponentInfo>& 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<FOpenVDBGridComponentInfo>& 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 <None> option to leave channels empty
FOpenVDBGridComponentInfo NoneGridComponentInfo;
NoneGridComponentInfo.Index = INDEX_NONE;
NoneGridComponentInfo.ComponentIndex = INDEX_NONE;
NoneGridComponentInfo.Name = TEXT("<None>");
NoneGridComponentInfo.DisplayString = TEXT("<None>");
Result.GridComponentInfoPtrs.Add(MakeShared<FOpenVDBGridComponentInfo>(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<FOpenVDBGridInfo>(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<FOpenVDBGridComponentInfo>(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<SWindow> ParentWindow;
if (FModuleManager::Get().IsModuleLoaded("MainFrame"))
{
IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked<IMainFrameModule>("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<SWindow> Window = SNew(SWindow)
.Title(NSLOCTEXT("UnrealEd", "OpenVDBImportOptionsTitle", "OpenVDB Import Options"))
.SizingRule(ESizingRule::Autosized)
.AutoCenter(EAutoCenter::None)
.ClientSize(ImportWindowSize)
.ScreenPosition(WindowPosition);
TArray<TSharedPtr<ESparseVolumeAttributesFormat>> SupportedFormats =
{
MakeShared<ESparseVolumeAttributesFormat>(ESparseVolumeAttributesFormat::Float32),
MakeShared<ESparseVolumeAttributesFormat>(ESparseVolumeAttributesFormat::Float16),
MakeShared<ESparseVolumeAttributesFormat>(ESparseVolumeAttributesFormat::Unorm8)
};
TSharedPtr<SOpenVDBImportWindow> 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<FOpenVDBGridInfo>& 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<USparseVolumeTexture>(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<FString>& OutFilenames)
{
#if OPENVDB_AVAILABLE
UStreamableSparseVolumeTexture* StreamableSVT = Cast<UStreamableSparseVolumeTexture>(Obj);
if (StreamableSVT && StreamableSVT->AssetImportData)
{
StreamableSVT->AssetImportData->ExtractFilenames(OutFilenames);
return true;
}
#endif // OPENVDB_AVAILABLE
return false;
}
void USparseVolumeTextureFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
{
#if OPENVDB_AVAILABLE
UStreamableSparseVolumeTexture* StreamableSVT = Cast<UStreamableSparseVolumeTexture>(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<UStreamableSparseVolumeTexture>(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<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Parms);
TArray<UObject*> 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<UOpenVDBImportOptionsObject>(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<FOpenVDBGridInfo>& 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<UStaticSparseVolumeTexture>(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<UE::SVT::FTextureData> UncookedFramesData;
TArray<FTransform> 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<uint8> LoadedFrameFile;
if (!FFileHelper::LoadFileToArray(LoadedFrameFile, *FrameFilename))
{
UE_LOG(LogSparseVolumeTextureFactory, Error, TEXT("OpenVDB file could not be loaded: %s"), *FrameFilename);
bErrored.store(true);
return;
}
TArray<FOpenVDBGridInfo> 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<std::mutex> 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<uint8> 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<UPackage>()))
{
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<UAnimatedSparseVolumeTexture>(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<UStreamableSparseVolumeTexture>(ResultAssets[0])->AssetImportData->Update(Filename);
GEditor->GetEditorSubsystem<UImportSubsystem>()->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