Files
UnrealEngine/Engine/Source/Developer/NaniteUtilities/Public/NaniteLayout.h
2025-05-18 13:04:45 +08:00

1262 lines
38 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#if WITH_EDITOR
#include "CoreMinimal.h"
#include "NaniteDefinitions.h"
#include "PropertyHandle.h"
#include "PropertyCustomizationHelpers.h"
#include "IDetailGroup.h"
#include "IDetailChildrenBuilder.h"
#include "DetailWidgetRow.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SUniformWrapPanel.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SFilePathPicker.h"
#include "Widgets/Input/STextComboBox.h"
#include "Widgets/Input/SVectorInputBox.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "Engine/EngineTypes.h"
#include "Misc/ScopedSlowTask.h"
#include "EditorDirectories.h"
#include "EngineAnalytics.h"
#include "FbxMeshUtils.h"
#if NANITE_ASSEMBLY_DATA
#include "PropertyCustomizationHelpers.h"
#endif
class IDetailCategoryBuilder;
class IDetailChildrenBuilder;
class IDetailLayoutBuilder;
class IDetailGroup;
namespace Nanite
{
#define LOCTEXT_NAMESPACE "NaniteLayout"
template< typename StructType, typename CopyFuncType >
IDetailPropertyRow& AddDefaultRow( IDetailCategoryBuilder& CategoryBuilder, StructType& Struct, FName PropertyName, CopyFuncType CopyFunc )
{
TSharedPtr<FStructOnScope> TempStruct = MakeShared< FStructOnScope >( StructType::StaticStruct() );
StructType::StaticStruct()->CopyScriptStruct( TempStruct->GetStructMemory(), &Struct, 1 );
IDetailPropertyRow* PropertyRow = CategoryBuilder.AddExternalStructureProperty( TempStruct, PropertyName );
PropertyRow->GetPropertyHandle()->SetOnPropertyValueChanged( FSimpleDelegate::CreateLambda(
[ &Struct, TempStruct, CopyFunc ]
{
StructType* TempStruct2 = (StructType*)TempStruct->GetStructMemory();
CopyFunc( Struct, *TempStruct2 );
}
));
PropertyRow->GetPropertyHandle()->SetOnChildPropertyValueChanged( FSimpleDelegate::CreateLambda(
[ &Struct, TempStruct, CopyFunc ]
{
StructType* TempStruct2 = (StructType*)TempStruct->GetStructMemory();
CopyFunc( Struct, *TempStruct2 );
}
));
return *PropertyRow;
}
template< typename StructType, typename MemberType >
IDetailPropertyRow& AddDefaultRow( IDetailCategoryBuilder& CategoryBuilder, StructType& Struct, MemberType (StructType::*MemberPointer), FName PropertyName )
{
return AddDefaultRow(CategoryBuilder, Struct, PropertyName,
[MemberPointer]( StructType& Dst, StructType& Src )
{
Dst.*MemberPointer = Src.*MemberPointer;
} );
}
#define NANITE_ADD_DEFAULT_ROW(PropertyName) \
AddDefaultRow( NaniteSettingsCategory, NaniteSettings, GET_MEMBER_NAME_CHECKED( FMeshNaniteSettings, PropertyName ), \
[]( FMeshNaniteSettings& Dst, FMeshNaniteSettings& Src ) \
{ \
Dst.PropertyName = Src.PropertyName; \
})
template<typename TMeshType, bool bSupportsForceEnable, bool bSupportsHighRes>
class FSettingsLayout : public TSharedFromThis<FSettingsLayout<TMeshType, bSupportsForceEnable, bSupportsHighRes>>
{
public:
typedef TMeshType MeshType;
public:
FSettingsLayout()
{
// Position options
PositionPrecisionOptions.Add(MakeShared<FString>(LOCTEXT("PositionPrecisionAuto", "Auto").ToString()));
for (int32 i = DisplayPositionPrecisionMin; i <= DisplayPositionPrecisionMax; i++)
{
PositionPrecisionOptions.Add(MakeShared<FString>(PositionPrecisionValueToDisplayString(i)));
}
// Normal options
const FText NormalAutoText = FText::Format(LOCTEXT("NormalPrecisionAuto", "Auto ({0} bits)"), 8); //TODO: Just use Auto=8 for now
NormalPrecisionOptions.Add(MakeShared<FString>(NormalAutoText.ToString()));
for (int32 i = DisplayNormalPrecisionMin; i <= DisplayNormalPrecisionMax; i++)
{
NormalPrecisionOptions.Add(MakeShared<FString>(NormalPrecisionValueToDisplayString(i)));
}
// Tangent options
const FText TangentAutoText = FText::Format(LOCTEXT("TangentPrecisionAuto", "Auto ({0} bits)"), 7); //TODO: Just use Auto=7 for now
TangentPrecisionOptions.Add(MakeShared<FString>(TangentAutoText.ToString()));
for (int32 i = DisplayTangentPrecisionMin; i <= DisplayTangentPrecisionMax; i++)
{
TangentPrecisionOptions.Add(MakeShared<FString>(TangentPrecisionValueToDisplayString(i)));
}
// Bone weight options
const FText BoneWeightAutoText = FText::Format(LOCTEXT("BoneWeightAuto", "Auto ({0} bits)"), 8); //TODO: Just use Auto=8 for now
BoneWeightPrecisionOptions.Add(MakeShared<FString>(BoneWeightAutoText.ToString()));
BoneWeightPrecisionOptions.Add(MakeShared<FString>(LOCTEXT("BoneWeightRigid", "Rigid (0 bits)").ToString()));
for (int32 i = DisplayBoneWeightPrecisionMin; i <= DisplayBoneWeightPrecisionMax; i++)
{
BoneWeightPrecisionOptions.Add(MakeShared<FString>(BoneWeightPrecisionValueToDisplayString(i)));
}
// Residency options
const FText ResidencyMinimalText = FText::Format(LOCTEXT("ResidencyMinimum", "Minimal ({0}KB)"), NANITE_ROOT_PAGE_GPU_SIZE >> 10);
ResidencyOptions.Add(MakeShared<FString>(ResidencyMinimalText.ToString()));
for (int32 i = DisplayMinimumResidencyExpRangeMin; i <= DisplayMinimumResidencyExpRangeMax; i++)
{
ResidencyOptions.Add(MakeShared<FString>(MinimumResidencyValueToDisplayString(1 << i), false));
}
ResidencyOptions.Add(MakeShared<FString>(LOCTEXT("ResidencyFull", "Full").ToString()));
}
~FSettingsLayout()
{
}
/** Position Precision range selectable in the UI. */
static const int32 DisplayPositionPrecisionAuto = MIN_int32;
static const int32 DisplayPositionPrecisionMin = -6;
static const int32 DisplayPositionPrecisionMax = 13;
static int32 PositionPrecisionIndexToValue(int32 Index)
{
check(Index >= 0);
if (Index == 0)
{
return DisplayPositionPrecisionAuto;
}
else
{
int32 Value = DisplayPositionPrecisionMin + (Index - 1);
Value = FMath::Min(Value, DisplayPositionPrecisionMax);
return Value;
}
}
static int32 PositionPrecisionValueToIndex(int32 Value)
{
if (Value == DisplayPositionPrecisionAuto)
{
return 0;
}
else
{
Value = FMath::Clamp(Value, DisplayPositionPrecisionMin, DisplayPositionPrecisionMax);
return Value - DisplayPositionPrecisionMin + 1;
}
}
/** Display string to show in menus. */
static FString PositionPrecisionValueToDisplayString(int32 Value)
{
check(Value != DisplayPositionPrecisionAuto);
if(Value <= 0)
{
return FString::Printf(TEXT("%dcm"), 1 << (-Value));
}
else
{
const float fValue = static_cast<float>(FMath::Exp2((double)-Value));
return FString::Printf(TEXT("1/%dcm (%.3gcm)"), 1 << Value, fValue);
}
}
/** Normal Precision range selectable in the UI. */
static const int32 DisplayNormalPrecisionAuto = -1;
static const int32 DisplayNormalPrecisionMin = 5;
static const int32 DisplayNormalPrecisionMax = 15;
static int32 NormalPrecisionIndexToValue(int32 Index)
{
check(Index >= 0);
if (Index == 0)
{
return DisplayNormalPrecisionAuto;
}
else
{
int32 Value = DisplayNormalPrecisionMin + (Index - 1);
Value = FMath::Min(Value, DisplayNormalPrecisionMax);
return Value;
}
}
static int32 NormalPrecisionValueToIndex(int32 Value)
{
if (Value == DisplayNormalPrecisionAuto)
{
return 0;
}
else
{
Value = FMath::Clamp(Value, DisplayNormalPrecisionMin, DisplayNormalPrecisionMax);
return Value - DisplayNormalPrecisionMin + 1;
}
}
/** Display string to show in menus. */
static FString NormalPrecisionValueToDisplayString(int32 Value)
{
check(Value != DisplayNormalPrecisionAuto);
return FString::Printf(TEXT("%d bits"), Value);
}
/** Tangent Precision range selectable in the UI. */
static const int32 DisplayTangentPrecisionAuto = -1;
static const int32 DisplayTangentPrecisionMin = 4;
static const int32 DisplayTangentPrecisionMax = 12;
static int32 TangentPrecisionIndexToValue(int32 Index)
{
check(Index >= 0);
if (Index == 0)
{
return DisplayTangentPrecisionAuto;
}
else
{
int32 Value = DisplayTangentPrecisionMin + (Index - 1);
Value = FMath::Min(Value, DisplayTangentPrecisionMax);
return Value;
}
}
static int32 TangentPrecisionValueToIndex(int32 Value)
{
if (Value == DisplayTangentPrecisionAuto)
{
return 0;
}
else
{
Value = FMath::Clamp(Value, DisplayTangentPrecisionMin, DisplayTangentPrecisionMax);
return Value - DisplayTangentPrecisionMin + 1;
}
}
/** Display string to show in menus. */
static FString TangentPrecisionValueToDisplayString(int32 Value)
{
check(Value != DisplayTangentPrecisionAuto);
return FString::Printf(TEXT("%d bits"), Value);
}
/** Bone Weight Precision range selectable in the UI. */
static const int32 DisplayBoneWeightPrecisionAuto = -1;
static const int32 DisplayBoneWeightPrecisionRigid = 0;
static const int32 DisplayBoneWeightPrecisionMin = 4;
static const int32 DisplayBoneWeightPrecisionMax = 16;
static int32 BoneWeightPrecisionIndexToValue(int32 Index)
{
check(Index >= 0);
if (Index == 0)
{
return DisplayBoneWeightPrecisionAuto;
}
else if (Index == 1)
{
return DisplayBoneWeightPrecisionRigid;
}
else
{
int32 Value = DisplayBoneWeightPrecisionMin + (Index - 2);
Value = FMath::Min(Value, DisplayBoneWeightPrecisionMax);
return Value;
}
}
static int32 BoneWeightPrecisionValueToIndex(int32 Value)
{
if (Value == DisplayBoneWeightPrecisionAuto)
{
return 0;
}
else if (Value == DisplayBoneWeightPrecisionRigid)
{
return 1;
}
else
{
Value = FMath::Clamp(Value, DisplayBoneWeightPrecisionMin, DisplayBoneWeightPrecisionMax);
return Value - DisplayBoneWeightPrecisionMin + 2;
}
}
/** Display string to show in menus. */
static FString BoneWeightPrecisionValueToDisplayString(int32 Value)
{
check(Value != DisplayBoneWeightPrecisionAuto);
check(Value != DisplayBoneWeightPrecisionRigid);
return FString::Printf(TEXT("%d bits"), Value);
}
/** Residency range selectable in the UI. */
static const int32 DisplayMinimumResidencyMinimalIndex = 0;
static const int32 DisplayMinimumResidencyExpRangeMin = 5;
static const int32 DisplayMinimumResidencyExpRangeMax = 15;
static const int32 DisplayMinimumResidencyFullIndex = DisplayMinimumResidencyExpRangeMax - DisplayMinimumResidencyExpRangeMin + 2;
static uint32 MinimumResidencyIndexToValue(int32 Index)
{
if (Index == DisplayMinimumResidencyMinimalIndex)
{
return 0;
}
else if (Index == DisplayMinimumResidencyFullIndex)
{
return MAX_uint32;
}
else
{
return 1u << (DisplayMinimumResidencyExpRangeMin + Index - 1);
}
}
static int32 MinimumResidencyValueToIndex(uint32 Value)
{
if (Value == 0)
{
return DisplayMinimumResidencyMinimalIndex;
}
else if (Value == MAX_uint32)
{
return DisplayMinimumResidencyFullIndex;
}
else
{
int32 Exp = (int32)FMath::CeilLogTwo(Value);
return FMath::Clamp(Exp, DisplayMinimumResidencyExpRangeMin, DisplayMinimumResidencyExpRangeMax) - DisplayMinimumResidencyExpRangeMin + 1;
}
}
/** Display string to show in menus. */
static FString MinimumResidencyValueToDisplayString(uint32 Value)
{
if (Value < 1024)
{
return FString::Printf(TEXT("%dKB"), Value);
}
else
{
return FString::Printf(TEXT("%dMB"), Value >> 10);
}
}
const FMeshNaniteSettings& GetSettings() const
{
return NaniteSettings;
}
void UpdateSettings(const FMeshNaniteSettings& InSettings)
{
NaniteSettings = InSettings;
}
/** Returns true if settings have been changed and an Apply is needed to update the asset. */
bool IsApplyNeeded() const
{
const MeshType* MeshAsset = GetMesh();
check(MeshAsset);
return MeshAsset->NaniteSettings != NaniteSettings;
}
/** Apply current Nanite settings to the mesh. */
void ApplyChanges()
{
TMeshType* MeshAsset = GetMesh();
check(MeshAsset);
{
FFormatNamedArguments Args;
Args.Add(TEXT("MeshName"), FText::FromString(MeshAsset->GetName()));
FScopedSlowTask SlowTask(0, FText::Format(LOCTEXT("ApplyNaniteChanges", "Applying changes to {MeshName}..."), Args), true);
SlowTask.MakeDialog();
MeshAsset->Modify();
MeshAsset->NaniteSettings = NaniteSettings;
FProperty* ChangedProperty = FindFProperty<FProperty>(TMeshType::StaticClass(), GET_MEMBER_NAME_CHECKED(TMeshType, NaniteSettings));
FPropertyChangedEvent Event(ChangedProperty);
MeshAsset->PostEditChangeProperty(Event);
}
RefreshTool();
}
protected:
FReply OnApply()
{
ApplyChanges();
return FReply::Handled();
}
ECheckBoxState IsEnabledChecked() const
{
bool bEnabled = NaniteSettings.bEnabled;
if constexpr (bSupportsForceEnable)
{
const MeshType* MeshAsset = GetMesh();
bEnabled |= (MeshAsset && MeshAsset->IsNaniteForceEnabled());
}
return bEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void OnEnabledChanged(ECheckBoxState NewState)
{
NaniteSettings.bEnabled = NewState == ECheckBoxState::Checked ? true : false;
}
void OnPositionPrecisionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
int32 NewValueInt = PositionPrecisionIndexToValue(PositionPrecisionOptions.Find(NewValue));
if (NaniteSettings.PositionPrecision != NewValueInt)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("PositionPrecision"), *NewValue.Get());
}
NaniteSettings.PositionPrecision = NewValueInt;
}
}
void OnNormalPrecisionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
int32 NewValueInt = NormalPrecisionIndexToValue(NormalPrecisionOptions.Find(NewValue));
if (NaniteSettings.NormalPrecision != NewValueInt)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("NormalPrecision"), *NewValue.Get());
}
NaniteSettings.NormalPrecision = NewValueInt;
}
}
void OnTangentPrecisionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
int32 NewValueInt = TangentPrecisionIndexToValue(TangentPrecisionOptions.Find(NewValue));
if (NaniteSettings.TangentPrecision != NewValueInt)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("TangentPrecision"), *NewValue.Get());
}
NaniteSettings.TangentPrecision = NewValueInt;
}
}
void OnBoneWeightPrecisionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
int32 NewValueInt = BoneWeightPrecisionIndexToValue(BoneWeightPrecisionOptions.Find(NewValue));
if (NaniteSettings.BoneWeightPrecision != NewValueInt)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("BoneWeightPrecision"), *NewValue.Get());
}
NaniteSettings.BoneWeightPrecision = NewValueInt;
}
}
void OnResidencyChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
int32 NewValueInt = MinimumResidencyIndexToValue(ResidencyOptions.Find(NewValue));
if (NaniteSettings.TargetMinimumResidencyInKB != NewValueInt)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("MinimumResidency"), *NewValue.Get());
}
NaniteSettings.TargetMinimumResidencyInKB = NewValueInt;
}
}
float GetKeepPercentTriangles() const
{
return NaniteSettings.KeepPercentTriangles * 100.0f; // Display fraction as percentage.
}
void OnKeepPercentTrianglesChanged(float NewValue)
{
// Percentage -> fraction.
NaniteSettings.KeepPercentTriangles = NewValue * 0.01f;
}
void OnKeepPercentTrianglesCommitted(float NewValue, ETextCommit::Type TextCommitType)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("KeepPercentTriangles"), FString::Printf(TEXT("%.1f"), NewValue));
}
OnKeepPercentTrianglesChanged(NewValue);
}
float GetTrimRelativeError() const
{
return NaniteSettings.TrimRelativeError;
}
void OnTrimRelativeErrorChanged(float NewValue)
{
NaniteSettings.TrimRelativeError = NewValue;
}
float GetFallbackPercentTriangles() const
{
return NaniteSettings.FallbackPercentTriangles * 100.0f; // Display fraction as percentage.
}
void OnFallbackPercentTrianglesChanged(float NewValue)
{
// Percentage -> fraction.
NaniteSettings.FallbackPercentTriangles = NewValue * 0.01f;
}
void OnFallbackPercentTrianglesCommitted(float NewValue, ETextCommit::Type TextCommitType)
{
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.NaniteSettings"), TEXT("FallbackPercentTriangles"), FString::Printf(TEXT("%.1f"), NewValue));
}
OnFallbackPercentTrianglesChanged(NewValue);
}
float GetFallbackRelativeError() const
{
return NaniteSettings.FallbackRelativeError;
}
void OnFallbackRelativeErrorChanged(float NewValue)
{
NaniteSettings.FallbackRelativeError = NewValue;
}
int32 GetDisplacementUVChannel() const
{
return NaniteSettings.DisplacementUVChannel;
}
void OnDisplacementUVChannelChanged(int32 NewValue)
{
NaniteSettings.DisplacementUVChannel = NewValue;
}
FString GetHiResSourceFilename() const
{
if constexpr (bSupportsHighRes)
{
if (const TMeshType* MeshAsset = GetMesh())
{
return MeshAsset->GetHiResSourceModel().SourceImportFilename;
}
}
return FString();
}
void SetHiResSourceFilename(const FString& NewSourceFile)
{
if constexpr (bSupportsHighRes)
{
//Reimport with new file
TMeshType* MeshAsset = GetMesh();
if (!MeshAsset)
{
return;
}
if (MeshAsset->GetHiResSourceModel().SourceImportFilename.Equals(NewSourceFile))
{
return;
}
MeshAsset->GetHiResSourceModel().SourceImportFilename = NewSourceFile;
// Trigger a reimport with new file
FbxMeshUtils::ImportStaticMeshHiResSourceModelDialog(MeshAsset);
RefreshTool();
}
}
bool DoesHiResDataExists() const
{
if constexpr (bSupportsHighRes)
{
const TMeshType* MeshAsset = GetMesh();
if (MeshAsset)
{
return (MeshAsset->GetHiResMeshDescription() != nullptr);
}
}
return false;
}
bool IsHiResDataEmpty() const
{
return !DoesHiResDataExists();
}
FReply OnImportHiRes()
{
if constexpr (bSupportsHighRes)
{
if (TMeshType* MeshAsset = GetMesh())
{
MeshAsset->GetHiResSourceModel().SourceImportFilename = FString();
FbxMeshUtils::ImportStaticMeshHiResSourceModelDialog(MeshAsset);
//If we import a hires we should enable Nanite
NaniteSettings.bEnabled = true;
ApplyChanges();
}
}
return FReply::Handled();
}
FReply OnRemoveHiRes()
{
if constexpr (bSupportsHighRes)
{
if (TMeshType* MeshAsset = GetMesh())
{
MeshAsset->GetHiResSourceModel().SourceImportFilename = FString();
FbxMeshUtils::RemoveStaticMeshHiRes(MeshAsset);
RefreshTool();
}
}
return FReply::Handled();
}
FReply OnReimportHiRes()
{
if constexpr (bSupportsHighRes)
{
if (TMeshType* MeshAsset = GetMesh())
{
FbxMeshUtils::ImportStaticMeshHiResSourceModelDialog(MeshAsset);
RefreshTool();
}
}
return FReply::Handled();
}
FReply OnReimportHiResWithNewFile()
{
if constexpr (bSupportsHighRes)
{
if (TMeshType* MeshAsset = GetMesh())
{
MeshAsset->GetHiResSourceModel().SourceImportFilename = FString();
FbxMeshUtils::ImportStaticMeshHiResSourceModelDialog(MeshAsset);
RefreshTool();
}
}
return FReply::Handled();
}
bool IsSkeletalMesh() const
{
return TIsDerivedFrom<TMeshType, USkeletalMesh>::Value;
}
public:
void AddToDetailsPanel(
TWeakObjectPtr<TMeshType> WeakMeshPtr,
IDetailLayoutBuilder& DetailBuilder,
int32 SortOrder,
bool bInitiallyCollapsed
)
{
typedef FSettingsLayout<TMeshType, bSupportsForceEnable, bSupportsHighRes> TSettingsType;
const FText NaniteCategoryName = LOCTEXT("NaniteSettingsCategory", "Nanite Settings");
IDetailCategoryBuilder& NaniteSettingsCategory = DetailBuilder.EditCategory("NaniteSettings", NaniteCategoryName, ECategoryPriority::Uncommon);
NaniteSettingsCategory.SetSortOrder(SortOrder);
NaniteSettingsCategory.InitiallyCollapsed(bInitiallyCollapsed);
auto CategoryContentLambda = [WeakMeshPtr]()
{
TMeshType* MeshAsset = WeakMeshPtr.Get();
if (MeshAsset && MeshAsset->IsNaniteEnabled())
{
if constexpr (bSupportsHighRes)
{
if (!MeshAsset->GetHiResSourceModel().SourceImportFilename.IsEmpty())
{
return LOCTEXT("NaniteSettingsCategory_Imported", "[Imported]");
}
}
}
return FText::GetEmpty();
};
auto CategoryContentTooltipLambda = [WeakMeshPtr]()
{
TMeshType* MeshAsset = WeakMeshPtr.Get();
if (MeshAsset && MeshAsset->IsNaniteEnabled())
{
if constexpr (bSupportsHighRes)
{
if (!MeshAsset->GetHiResSourceModel().SourceImportFilename.IsEmpty())
{
return FText::Format(LOCTEXT("NaniteSettingsCategory_ImportedTooltip", "The Nanite high resolution data is imported from file {0}"), FText::FromString(MeshAsset->GetHiResSourceModel().SourceImportFilename));
}
}
}
return FText::GetEmpty();
};
NaniteSettingsCategory.HeaderContent
(
SNew( SHorizontalBox )
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SBox)
.Padding(FMargin(5.0f, 0.0f))
[
SNew(STextBlock)
.Text_Lambda(CategoryContentLambda)
.ToolTipText_Lambda(CategoryContentTooltipLambda)
.Font(IDetailLayoutBuilder::GetDetailFontItalic())
]
]
);
TSharedPtr<SCheckBox> NaniteEnabledCheck;
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("Enabled", "Enabled") )
.RowTag("EnabledNaniteSupport")
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("EnabledNaniteSupport", "Enable Nanite Support"))
]
.ValueContent()
[
SAssignNew(NaniteEnabledCheck, SCheckBox)
.IsChecked(this, &TSettingsType::IsEnabledChecked)
.OnCheckStateChanged(this, &TSettingsType::OnEnabledChanged)
];
}
TAttribute<bool> NaniteEnabledAttr = TAttribute<bool>::Create(TAttribute<bool>::FGetter::CreateLambda([NaniteEnabledCheck]() -> bool { return NaniteEnabledCheck->IsChecked(); }));
TAttribute<bool> NaniteEnabledAndNoHiResDataAttr = TAttribute<bool>::Create(TAttribute<bool>::FGetter::CreateLambda([this, NaniteEnabledCheck]() -> bool {return NaniteEnabledCheck->IsChecked() && IsHiResDataEmpty(); }));
NANITE_ADD_DEFAULT_ROW( bPreserveArea )
.IsEnabled( NaniteEnabledAttr );
NANITE_ADD_DEFAULT_ROW( bExplicitTangents )
.IsEnabled( NaniteEnabledAttr );
NANITE_ADD_DEFAULT_ROW( bLerpUVs )
.IsEnabled( NaniteEnabledAttr );
{
TSharedPtr<STextComboBox> ComboBox;
NaniteSettingsCategory.AddCustomRow(LOCTEXT("PositionPrecision", "Position Precision"))
.RowTag("PositionPrecision")
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("PositionPrecision", "Position Precision"))
.ToolTipText(LOCTEXT("PositionPrecisionTooltip", "Precision of vertex positions."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SAssignNew(ComboBox, STextComboBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.OptionsSource(&PositionPrecisionOptions)
.InitiallySelectedItem(PositionPrecisionOptions[PositionPrecisionValueToIndex(NaniteSettings.PositionPrecision)])
.OnSelectionChanged(this, &TSettingsType::OnPositionPrecisionChanged)
];
}
{
TSharedPtr<STextComboBox> ComboBox;
NaniteSettingsCategory.AddCustomRow(LOCTEXT("NormalPrecision", "Normal Precision"))
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("NormalPrecision", "Normal Precision"))
.ToolTipText(LOCTEXT("NormalPrecisionTooltip", "Precision of vertex normals."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SAssignNew(ComboBox, STextComboBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.OptionsSource(&NormalPrecisionOptions)
.InitiallySelectedItem(NormalPrecisionOptions[NormalPrecisionValueToIndex(NaniteSettings.NormalPrecision)])
.OnSelectionChanged(this, &TSettingsType::OnNormalPrecisionChanged)
];
}
{
TSharedPtr<STextComboBox> ComboBox;
NaniteSettingsCategory.AddCustomRow(LOCTEXT("TangentPrecision", "Tangent Precision"))
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("TangentPrecision", "Tangent Precision"))
.ToolTipText(LOCTEXT("TangentPrecisionTooltip", "Precision of vertex tangents."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SAssignNew(ComboBox, STextComboBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.OptionsSource(&TangentPrecisionOptions)
.InitiallySelectedItem(TangentPrecisionOptions[TangentPrecisionValueToIndex(NaniteSettings.TangentPrecision)])
.OnSelectionChanged(this, &TSettingsType::OnTangentPrecisionChanged)
]
.Visibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateLambda([this]()
{
return NaniteSettings.bExplicitTangents ? EVisibility::Visible : EVisibility::Hidden;
})));
}
if(IsSkeletalMesh())
{
TSharedPtr<STextComboBox> ComboBox;
NaniteSettingsCategory.AddCustomRow(LOCTEXT("BoneWeightPrecision", "Bone Weight Precision"))
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("BoneWeightPrecision", "Bone Weight Precision"))
.ToolTipText(LOCTEXT("BoneWeightPrecisionTooltip", "Precision of vertex bone weights."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SAssignNew(ComboBox, STextComboBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.OptionsSource(&BoneWeightPrecisionOptions)
.InitiallySelectedItem(BoneWeightPrecisionOptions[BoneWeightPrecisionValueToIndex(NaniteSettings.BoneWeightPrecision)])
.OnSelectionChanged(this, &TSettingsType::OnBoneWeightPrecisionChanged)
];
}
{
TSharedPtr<STextComboBox> ComboBox;
NaniteSettingsCategory.AddCustomRow(LOCTEXT("MinimumResidency", "Minimum Residency"))
.RowTag("MinimumResidency")
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("MinimumResidencyRootGeometry", "Minimum Residency (Root Geometry)"))
.ToolTipText(LOCTEXT("ResidencyTooltip", "How much should always be in memory. The rest will be streamed. Higher values require more memory, but also mitigate streaming pop-in issues."))
]
.ValueContent()
[
SAssignNew(ComboBox, STextComboBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.OptionsSource(&ResidencyOptions)
.InitiallySelectedItem(ResidencyOptions[MinimumResidencyValueToIndex(NaniteSettings.TargetMinimumResidencyInKB)])
.OnSelectionChanged(this, &TSettingsType::OnResidencyChanged)
];
}
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("KeepTrianglePercent", "Keep Triangle Percent") )
.RowTag("KeepTrianglePercent")
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("KeepTrianglePercent", "Keep Triangle Percent"))
.ToolTipText(LOCTEXT("KeepTrianglePercentTooltip", "Percentage of triangles to keep. Reduce to optimize for disk size."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.MinValue(0.0f)
.MaxValue(100.0f)
.Value(this, &TSettingsType::GetKeepPercentTriangles)
.OnValueChanged(this, &TSettingsType::OnKeepPercentTrianglesChanged)
.OnValueCommitted(this, &TSettingsType::OnKeepPercentTrianglesCommitted)
];
}
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("TrimRelativeError", "Trim Relative Error") )
.RowTag("TrimRelativeError")
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("TrimRelativeError", "Trim Relative Error"))
.ToolTipText(LOCTEXT("TrimRelativeErrorTooltip", "Trim all detail with less than this relative error. Error is calculated relative to the mesh's size.\nIncrease to optimize for disk size."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.MinValue(0.0f)
.Value(this, &TSettingsType::GetTrimRelativeError)
.OnValueChanged(this, &TSettingsType::OnTrimRelativeErrorChanged)
];
}
NANITE_ADD_DEFAULT_ROW( GenerateFallback )
.IsEnabled(NaniteEnabledAndNoHiResDataAttr);
NANITE_ADD_DEFAULT_ROW( FallbackTarget )
.IsEnabled( NaniteEnabledAndNoHiResDataAttr );
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("FallbackTrianglePercent", "Fallback Triangle Percent") )
.RowTag("FallbackTrianglePercent")
.IsEnabled( NaniteEnabledAndNoHiResDataAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("FallbackTrianglePercent", "Fallback Triangle Percent"))
.ToolTipText(LOCTEXT("FallbackTrianglePercentTooltip", "Reduce until no more than this percentage of triangles remain when generating a fallback\nmesh that will be used anywhere the full detail Nanite data can't,\nincluding platforms that don't support Nanite rendering."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.MinValue(0.0f)
.MaxValue(100.0f)
.Value(this, &TSettingsType::GetFallbackPercentTriangles)
.OnValueChanged(this, &TSettingsType::OnFallbackPercentTrianglesChanged)
.OnValueCommitted(this, &TSettingsType::OnFallbackPercentTrianglesCommitted)
]
.Visibility(TAttribute<EVisibility>::Create( TAttribute<EVisibility>::FGetter::CreateLambda([this]()
{
return NaniteSettings.FallbackTarget == ENaniteFallbackTarget::PercentTriangles ? EVisibility::Visible : EVisibility::Hidden;
} )));
}
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("FallbackRelativeError", "Fallback Relative Error") )
.RowTag("FallbackRelativeError")
.IsEnabled( NaniteEnabledAndNoHiResDataAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("FallbackRelativeError", "Fallback Relative Error"))
.ToolTipText(LOCTEXT("FallbackRelativeErrorTooltip", "Reduce until at least this amount of error is reached relative to its size\nwhen generating a fallback mesh that will be used anywhere the full detail Nanite data can't,\nincluding platforms that don't support Nanite rendering."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.MinValue(0.0f)
.Value(this, &TSettingsType::GetFallbackRelativeError)
.OnValueChanged(this, &TSettingsType::OnFallbackRelativeErrorChanged)
]
.Visibility(TAttribute<EVisibility>::Create( TAttribute<EVisibility>::FGetter::CreateLambda([this]()
{
return NaniteSettings.FallbackTarget == ENaniteFallbackTarget::RelativeError ? EVisibility::Visible : EVisibility::Hidden;
} )));
}
//Source import filename
{
FString FileFilterText = TEXT("Filmbox (*.fbx)|*.fbx|All files (*.*)|*.*");
NaniteSettingsCategory.AddCustomRow( LOCTEXT("NANITE_SourceImportFilename", "Source Import Filename") )
.RowTag("NANITE_SourceImportFilename")
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("NANITE_SourceImportFilename", "Source Import Filename"))
.ToolTipText(LOCTEXT("NANITE_SourceImportFilenameTooltip", "The file path that was used to import this hi res nanite mesh."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SFilePathPicker)
.BrowseButtonImage(FAppStyle::GetBrush("PropertyWindow.Button_Ellipsis"))
.BrowseButtonStyle(FAppStyle::Get(), "HoverHintOnly")
.BrowseButtonToolTip(LOCTEXT("NaniteSourceImportFilenamePathLabel_Tooltip", "Choose a nanite hi res source import file"))
.BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN))
.BrowseTitle(LOCTEXT("NaniteSourceImportFilenameBrowseTitle", "Nanite hi res source import file picker..."))
.FilePath(this, &TSettingsType::GetHiResSourceFilename)
.FileTypeFilter(FileFilterText)
.OnPathPicked(this, &TSettingsType::SetHiResSourceFilename)
];
}
{
NaniteSettingsCategory.AddCustomRow( LOCTEXT("DisplacementUVChannel", "Displacement UV Channel") )
.IsEnabled( NaniteEnabledAttr )
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(LOCTEXT("DisplacementUVChannel", "Displacement UV Channel"))
.ToolTipText(LOCTEXT("DisplacementUVChannelTooltip", "UV channel to use when sampling displacement maps."))
]
.ValueContent()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<int32>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.MinValue(0)
.MaxValue(4)
.Value(this, &TSettingsType::GetDisplacementUVChannel)
.OnValueChanged(this, &TSettingsType::OnDisplacementUVChannelChanged)
];
}
NANITE_ADD_DEFAULT_ROW( DisplacementMaps )
.IsEnabled( NaniteEnabledAttr );
NANITE_ADD_DEFAULT_ROW( MaxEdgeLengthFactor )
.IsEnabled( NaniteEnabledAttr );
#if NANITE_VOXEL_DATA
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( NumRays )
.IsEnabled( NaniteEnabledAttr );
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( VoxelLevel )
.IsEnabled( NaniteEnabledAttr );
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( RayBackUp )
.IsEnabled( NaniteEnabledAttr );
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( bSeparable )
.IsEnabled( NaniteEnabledAttr );
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( bVoxelNDF )
.IsEnabled( NaniteEnabledAttr );
// VOXELTODO
NANITE_ADD_DEFAULT_ROW( bVoxelOpacity )
.IsEnabled( NaniteEnabledAttr );
#endif
#if NANITE_ASSEMBLY_DATA
// Generate a list of meshes referenced in the assembly data, where applicable, and show the number of instances
// per part. NOTE: They cannot be edited currently without re-importing
if (NaniteSettings.NaniteAssemblyData.IsValid())
{
auto CountNodes = [&Nodes = NaniteSettings.NaniteAssemblyData.Nodes](int32 PartIndex)
{
int32 Count = 0;
for (const auto& Node : Nodes)
{
if (Node.PartIndex == PartIndex)
{
++Count;
}
}
return Count;
};
static const FName PathToParts = TEXT("NaniteSettings.NaniteAssemblyData.Parts");
TSharedPtr<IPropertyHandle> PartsProperty = DetailBuilder.GetProperty(PathToParts);
check(PartsProperty.IsValid());
const FText AssemblyRefsGroupName = LOCTEXT("NaniteAssemblyRefs", "Nanite Assembly References");
IDetailGroup& AssemblyRefsGroup = NaniteSettingsCategory.AddGroup("NaniteAssemblyRefs", AssemblyRefsGroupName);
AssemblyRefsGroup.ToggleExpansion(false); // prefer collapsed to not take up too much real estate
for (int32 PartIndex = 0; PartIndex < NaniteSettings.NaniteAssemblyData.Parts.Num(); PartIndex++)
{
TSharedPtr<IPropertyHandle> PartProperty = PartsProperty->GetChildHandle(PartIndex);
check(PartProperty.IsValid());
TSharedPtr<IPropertyHandle> MeshObjectPathProperty = PartProperty->GetChildHandle(GET_MEMBER_NAME_CHECKED(FNaniteAssemblyPart, MeshObjectPath));
check(MeshObjectPathProperty.IsValid());
AssemblyRefsGroup.AddPropertyRow(MeshObjectPathProperty.ToSharedRef())
.OverrideResetToDefault(FResetToDefaultOverride::Hide())
.CustomWidget(true)
.NameContent()
[
SNew(STextBlock)
.Text(LOCGEN_FORMAT_ORDERED(LOCTEXT("PartIndex_Instances_FmtN", "Part {0} (Instances: {1})"), PartIndex, CountNodes(PartIndex)))
]
.ValueContent()
[
SNew(SObjectPropertyEntryBox)
.PropertyHandle(MeshObjectPathProperty)
.ThumbnailPool(DetailBuilder.GetThumbnailPool())
];
}
}
#endif
//Nanite import button
{
NaniteSettingsCategory.AddCustomRow(LOCTEXT("NaniteHiResButtons", "Nanite Hi Res buttons"))
.RowTag("NaniteHiResButtons")
.ValueContent()
.HAlign(HAlign_Fill)
[
SNew(SUniformWrapPanel)
+ SUniformWrapPanel::Slot() // Nanite apply changes
[
SNew(SButton)
.OnClicked(this, &TSettingsType::OnApply)
.IsEnabled(this, &TSettingsType::IsApplyNeeded)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("ApplyChanges", "Apply Changes"))
.Font(DetailBuilder.GetDetailFont())
]
]
+ SUniformWrapPanel::Slot() // Nanite import button
[
SNew(SButton)
.OnClicked(this, &TSettingsType::OnImportHiRes)
.IsEnabled(this, &TSettingsType::IsHiResDataEmpty)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("NaniteImportHiRes", "Import"))
.Font(DetailBuilder.GetDetailFont())
]
]
+ SUniformWrapPanel::Slot() // Nanite remove button
[
SNew(SButton)
.OnClicked(this, &TSettingsType::OnRemoveHiRes)
.IsEnabled(this, &TSettingsType::DoesHiResDataExists)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("NaniteRemoveHiRes", "Remove"))
.Font(DetailBuilder.GetDetailFont())
]
]
+ SUniformWrapPanel::Slot() // Nanite reimport button
[
SNew(SButton)
.OnClicked(this, &TSettingsType::OnReimportHiRes)
.IsEnabled(this, &TSettingsType::DoesHiResDataExists)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("NaniteReimportHiRes", "Reimport"))
.Font(DetailBuilder.GetDetailFont())
]
]
+ SUniformWrapPanel::Slot() // Nanite reimport with new file button
[
SNew(SButton)
.OnClicked(this, &TSettingsType::OnReimportHiResWithNewFile)
.IsEnabled(this, &TSettingsType::DoesHiResDataExists)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
.Text(LOCTEXT("NaniteReimportHiResWithNewFile", "Reimport New File"))
.Font(DetailBuilder.GetDetailFont())
]
]
];
}
}
inline MeshType* GetMesh() const
{
if (OnGetMesh.IsBound())
{
return OnGetMesh.Execute();
}
return nullptr;
}
inline void RefreshTool()
{
if (OnRefreshTool.IsBound())
{
OnRefreshTool.Execute();
}
}
TDelegate<MeshType* ()> OnGetMesh;
TDelegate<void()> OnRefreshTool;
protected:
TArray<TSharedPtr<FString>> PositionPrecisionOptions;
TArray<TSharedPtr<FString>> NormalPrecisionOptions;
TArray<TSharedPtr<FString>> TangentPrecisionOptions;
TArray<TSharedPtr<FString>> BoneWeightPrecisionOptions;
TArray<TSharedPtr<FString>> ResidencyOptions;
FMeshNaniteSettings NaniteSettings;
};
#undef LOCTEXT_NAMESPACE
}
#endif // WITH_EDITOR