Files
UnrealEngineUWP/Engine/Source/Editor/SparseVolumeTexture/Private/SparseVolumeTextureFactory.cpp

778 lines
26 KiB
C++
Raw Normal View History

// 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 <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::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<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;
}
static TArray<FString> FindOpenVDBSequenceFileNames(const FString& Filename)
{
TArray<FString> 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<FString> 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<int32>::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<uint8> LoadedFile;
TArray<FOpenVDBGridInfo> GridInfo;
TArray<TSharedPtr<FOpenVDBGridInfo>> GridInfoPtrs;
TArray<TSharedPtr<FOpenVDBGridComponentInfo>> GridComponentInfoPtrs;
TArray<FString> 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 <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 = 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<USparseVolumeTexture>(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<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Parms);
TArray<UObject*> 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<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);
}
}
}
};
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<UStaticSparseVolumeTexture>(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<UAnimatedSparseVolumeTexture>(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<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"));
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<uint8> 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<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
#include "Serialization/EditorBulkDataWriter.h"