// 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 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 class FSettingsLayout : public TSharedFromThis> { public: typedef TMeshType MeshType; public: FSettingsLayout() { // Position options PositionPrecisionOptions.Add(MakeShared(LOCTEXT("PositionPrecisionAuto", "Auto").ToString())); for (int32 i = DisplayPositionPrecisionMin; i <= DisplayPositionPrecisionMax; i++) { PositionPrecisionOptions.Add(MakeShared(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(NormalAutoText.ToString())); for (int32 i = DisplayNormalPrecisionMin; i <= DisplayNormalPrecisionMax; i++) { NormalPrecisionOptions.Add(MakeShared(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(TangentAutoText.ToString())); for (int32 i = DisplayTangentPrecisionMin; i <= DisplayTangentPrecisionMax; i++) { TangentPrecisionOptions.Add(MakeShared(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(BoneWeightAutoText.ToString())); BoneWeightPrecisionOptions.Add(MakeShared(LOCTEXT("BoneWeightRigid", "Rigid (0 bits)").ToString())); for (int32 i = DisplayBoneWeightPrecisionMin; i <= DisplayBoneWeightPrecisionMax; i++) { BoneWeightPrecisionOptions.Add(MakeShared(BoneWeightPrecisionValueToDisplayString(i))); } // Residency options const FText ResidencyMinimalText = FText::Format(LOCTEXT("ResidencyMinimum", "Minimal ({0}KB)"), NANITE_ROOT_PAGE_GPU_SIZE >> 10); ResidencyOptions.Add(MakeShared(ResidencyMinimalText.ToString())); for (int32 i = DisplayMinimumResidencyExpRangeMin; i <= DisplayMinimumResidencyExpRangeMax; i++) { ResidencyOptions.Add(MakeShared(MinimumResidencyValueToDisplayString(1 << i), false)); } ResidencyOptions.Add(MakeShared(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(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(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 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 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 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 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 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::Value; } public: void AddToDetailsPanel( TWeakObjectPtr WeakMeshPtr, IDetailLayoutBuilder& DetailBuilder, int32 SortOrder, bool bInitiallyCollapsed ) { typedef FSettingsLayout 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 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 NaniteEnabledAttr = TAttribute::Create(TAttribute::FGetter::CreateLambda([NaniteEnabledCheck]() -> bool { return NaniteEnabledCheck->IsChecked(); })); TAttribute NaniteEnabledAndNoHiResDataAttr = TAttribute::Create(TAttribute::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 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 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 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::Create(TAttribute::FGetter::CreateLambda([this]() { return NaniteSettings.bExplicitTangents ? EVisibility::Visible : EVisibility::Hidden; }))); } if(IsSkeletalMesh()) { TSharedPtr 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 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) .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) .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) .Font(IDetailLayoutBuilder::GetDetailFont()) .MinValue(0.0f) .MaxValue(100.0f) .Value(this, &TSettingsType::GetFallbackPercentTriangles) .OnValueChanged(this, &TSettingsType::OnFallbackPercentTrianglesChanged) .OnValueCommitted(this, &TSettingsType::OnFallbackPercentTrianglesCommitted) ] .Visibility(TAttribute::Create( TAttribute::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) .Font(IDetailLayoutBuilder::GetDetailFont()) .MinValue(0.0f) .Value(this, &TSettingsType::GetFallbackRelativeError) .OnValueChanged(this, &TSettingsType::OnFallbackRelativeErrorChanged) ] .Visibility(TAttribute::Create( TAttribute::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) .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 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 PartProperty = PartsProperty->GetChildHandle(PartIndex); check(PartProperty.IsValid()); TSharedPtr 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 OnGetMesh; TDelegate OnRefreshTool; protected: TArray> PositionPrecisionOptions; TArray> NormalPrecisionOptions; TArray> TangentPrecisionOptions; TArray> BoneWeightPrecisionOptions; TArray> ResidencyOptions; FMeshNaniteSettings NaniteSettings; }; #undef LOCTEXT_NAMESPACE } #endif // WITH_EDITOR