// Copyright Epic Games, Inc. All Rights Reserved. #include "LandscapeProxyUIDetails.h" #include "Algo/AnyOf.h" #include "Algo/Count.h" #include "Algo/Find.h" #include "Algo/Transform.h" #include "Components/RuntimeVirtualTextureComponent.h" #include "Containers/Array.h" #include "Containers/Map.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Engine/World.h" #include "Fonts/SlateFontInfo.h" #include "HAL/PlatformCrt.h" #include "IDetailPropertyRow.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Landscape.h" #include "LandscapeEditTypes.h" #include "LandscapeInfo.h" #include "LandscapeProxy.h" #include "LandscapeStreamingProxy.h" #include "LandscapeSubsystem.h" #include "Layout/Margin.h" #include "Math/IntPoint.h" #include "Math/IntRect.h" #include "Math/UnrealMathSSE.h" #include "Misc/Attribute.h" #include "RuntimeVirtualTextureSetBounds.h" #include "ScopedTransaction.h" #include "Templates/Casts.h" #include "Types/SlateEnums.h" #include "UObject/LazyObjectPtr.h" #include "UObject/ObjectMacros.h" #include "UObject/ObjectPtr.h" #include "UObject/UObjectIterator.h" #include "UObject/WeakObjectPtr.h" #include "UObject/WeakObjectPtrTemplates.h" #include "VT/RuntimeVirtualTextureVolume.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/IToolTip.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/STextComboBox.h" class IPropertyHandle; class SWidget; class UObject; class URuntimeVirtualTexture; #define LOCTEXT_NAMESPACE "FLandscapeProxyUIDetails" FLandscapeProxyUIDetails::FLandscapeProxyUIDetails() { // Position Precision options are copied from StaticMeshEditorTools.cpp auto PositionPrecisionValueToDisplayString = [](int32 Value) { 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); } }; for (int32 i = MinNanitePrecision; i <= MaxNanitePrecision; i++) { TSharedPtr Option = MakeShared(PositionPrecisionValueToDisplayString(i)); PositionPrecisionOptions.Add(Option); } } TSharedRef FLandscapeProxyUIDetails::MakeInstance() { return MakeShareable( new FLandscapeProxyUIDetails); } void FLandscapeProxyUIDetails::CustomizeDetails( IDetailLayoutBuilder& DetailBuilder ) { TArray> EditingObjects; DetailBuilder.GetObjectsBeingCustomized(EditingObjects); auto GenerateTextWidget = [](const FText& InText, bool bInBold = false) -> TSharedRef { return SNew(STextBlock) .Font(bInBold? IDetailLayoutBuilder::GetDetailFontBold() : IDetailLayoutBuilder::GetDetailFont()) .Text(InText); }; TArray> EditingProxies; Algo::Transform(EditingObjects, EditingProxies, [](TWeakObjectPtr InObject) { return TWeakObjectPtr(Cast(InObject.Get())); }); TArray> LandscapeActors; TArray> EditingStreamingProxies; for (TWeakObjectPtr EditingProxy : EditingProxies) { if (EditingProxy.IsValid()) { if (ALandscape* LandscapeActor = EditingProxy->GetLandscapeActor()) { LandscapeActors.AddUnique(LandscapeActor); } if (ALandscapeStreamingProxy* LandscapeStreamingProxy = Cast(EditingProxy.Get())) { EditingStreamingProxies.Add(LandscapeStreamingProxy); } } } ALandscapeStreamingProxy* LandscapeStreamingProxy = EditingStreamingProxies.IsEmpty() ? nullptr : EditingStreamingProxies[0].Get(); // Hide World Partition specific properties in non WP levels const bool bShouldDisplayWorldPartitionProperties = Algo::AnyOf(EditingProxies, [](const TWeakObjectPtr InProxy) { UWorld* World = InProxy.IsValid() ? InProxy->GetTypedOuter() : nullptr; return UWorld::IsPartitionedWorld(World); }); if (!bShouldDisplayWorldPartitionProperties) { DetailBuilder.HideProperty(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(ALandscape, bAreNewLandscapeActorsSpatiallyLoaded), ALandscape::StaticClass())); } if (LandscapeActors.Num() == 1) { TWeakObjectPtr LandscapeActor = LandscapeActors[0]; if (ULandscapeInfo* LandscapeInfo = LandscapeActor->GetLandscapeInfo()) { IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory("Information", FText::GetEmpty(), ECategoryPriority::Important); FText CreateRVTVolumesTooltip = LOCTEXT("Button_CreateVolumes_Tooltip", "Create volumes for the selected Runtime Virtual Textures."); FText RowDisplayText = LOCTEXT("LandscapeComponentResolution", "Component Resolution (Verts)"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeComponentResolution")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("LandscapeComponentResolutionValue", "{0} x {0}"), LandscapeActor->ComponentSizeQuads+1), true) // Verts ]; RowDisplayText = LOCTEXT("LandscapeComponentCount", "Component Count"); int32 NumComponents = LandscapeActor->LandscapeComponents.Num(); // If displaying streaming proxies, don't show the main landscape actor's component count, which is always 0 : if (!EditingStreamingProxies.IsEmpty()) { NumComponents = EditingStreamingProxies[0]->LandscapeComponents.Num(); if (Algo::AnyOf(EditingStreamingProxies, [NumComponents](const TWeakObjectPtr InStreamingProxy) { return InStreamingProxy.Get()->LandscapeComponents.Num() != NumComponents; })) { NumComponents = -1; } } CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeComponentCount")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget((NumComponents == -1) ? LOCTEXT("MultipleValues", "Multiple Values") : FText::Format(LOCTEXT("LandscapeComponentCountValue", "{0}"), NumComponents), true) ]; RowDisplayText = LOCTEXT("LandscapeComponentSubsections", "Component Subsections"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeComponentSubsections")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("LandscapeComponentSubSectionsValue", "{0} x {0}"), LandscapeActor->NumSubsections), true) ]; FIntRect Rect = LandscapeActor->GetBoundingRect(); FIntPoint Size = Rect.Size(); RowDisplayText = LOCTEXT("LandscapeResolution", "Resolution (Verts)"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeResolution")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("LandscapeResolutionValue", "{0} x {1}"), Size.X+1, Size.Y+1), true) ]; int32 LandscapeCount = LandscapeInfo->StreamingProxies.Num() + (LandscapeInfo->LandscapeActor.Get() ? 1 : 0); RowDisplayText = LOCTEXT("LandscapeCount", "Landscape Proxy Count"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeCount")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("LandscapeProxyCountValue", "{0}"), LandscapeCount), true) ]; int32 TotalComponentCount = LandscapeInfo->XYtoComponentMap.Num(); RowDisplayText = LOCTEXT("TotalLandscapeComponentCount", "Total Component Count"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("TotalLandscapeComponentCount")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("TotalLandscapeComponentCountValue", "{0}"), TotalComponentCount), true) ]; LandscapeInfo->GetLandscapeExtent(Rect.Min.X, Rect.Min.Y, Rect.Max.X, Rect.Max.Y); Size = Rect.Size(); RowDisplayText = LOCTEXT("LandscapeOverallResolution", "Overall Resolution (Verts)"); CategoryBuilder.AddCustomRow(RowDisplayText) .RowTag(TEXT("LandscapeOverallResolution")) .NameContent() [ GenerateTextWidget(RowDisplayText) ] .ValueContent() [ GenerateTextWidget(FText::Format(LOCTEXT("LandscapeOverallResolutionValue", "{0} x {1}"), Size.X + 1, Size.Y + 1), true) ]; // Apply custom widget for CreateVolume. TSharedRef CreateVolumePropertyHandle = DetailBuilder.GetProperty(TEXT("bSetCreateRuntimeVirtualTextureVolumes")); DetailBuilder.EditDefaultProperty(CreateVolumePropertyHandle)->CustomWidget() .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("Button_CreateVolumes", "Create Volumes")) .ToolTipText(CreateRVTVolumesTooltip) ] .ValueContent() .MinDesiredWidth(125.f) [ SNew(SButton) .VAlign(VAlign_Center) .HAlign(HAlign_Center) .ContentPadding(2.f) .Text(LOCTEXT("Button_CreateVolumes", "Create Volumes")) .OnClicked_Lambda([LandscapeActor, this]() { return CreateRuntimeVirtualTextureVolume(LandscapeActor.Get()); }) .IsEnabled_Lambda([LandscapeActor, this]() { return IsCreateRuntimeVirtualTextureVolumeEnabled(LandscapeActor.Get()); }) .ToolTipText_Lambda([LandscapeActor, this, CreateRVTVolumesTooltip]() { return IsCreateRuntimeVirtualTextureVolumeEnabled(LandscapeActor.Get()) ? CreateRVTVolumesTooltip : LOCTEXT("Button_CreateVolumes_Tooltip_Invalid", "No Runtime Virtual Textures found in the world."); }) ]; } TSharedRef NanitePositionPrecisionHandle = DetailBuilder.GetProperty(TEXT("NanitePositionPrecision")); IDetailPropertyRow* DetailRow = DetailBuilder.EditDefaultProperty(NanitePositionPrecisionHandle); // todo don.boogert : this is the handling of disabling of inherited properties on Landscape Proxies. // todo don.boogert : be able to handle override & inherited properties which also have custom UI. if (LandscapeStreamingProxy->IsPropertyInherited(NanitePositionPrecisionHandle->GetProperty())) { if (LandscapeStreamingProxy != nullptr) { if (DetailRow != nullptr) { // Extend the tool tip to indicate this property is inherited FText ToolTipText = NanitePositionPrecisionHandle->GetToolTipText(); DetailRow->ToolTip(FText::Format(LOCTEXT("InheritedProperty", "{0} This property is inherited from the parent Landscape proxy."), ToolTipText)); // Disable the property editing DetailRow->IsEnabled(false); } } } auto GetPositionPrecision = [MinNanitePrecision = this->MinNanitePrecision, &PositionOptions = this->PositionPrecisionOptions, LandscapeActor]() { return PositionOptions[LandscapeActor->GetNanitePositionPrecision() - MinNanitePrecision] ; }; auto SetPositionPrecision = [NanitePositionPrecisionHandle, MinNanitePrecision = this->MinNanitePrecision, &PositionOptions = this->PositionPrecisionOptions, LandscapeActor](TSharedPtr NewValue, ESelectInfo::Type SelectInfo) { if (!LandscapeActor.IsValid()) { return; } if (const int32 Index = PositionOptions.Find(NewValue); Index != INDEX_NONE) { NanitePositionPrecisionHandle->SetValue(Index + MinNanitePrecision); } }; DetailRow->CustomWidget() .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("LandscapeNanitePositionPrecision", "Nanite Position Precision")) .ToolTipText(LOCTEXT("LandscapeNanitePositionPrecisionTooltip", "Precision of Nanite vertex positions in World Space.")) ] .ValueContent() .VAlign(VAlign_Center) [ SNew(STextComboBox) .Font(IDetailLayoutBuilder::GetDetailFont()) .OptionsSource(&PositionPrecisionOptions) .InitiallySelectedItem(GetPositionPrecision()) .OnSelectionChanged_Lambda(SetPositionPrecision) ]; } // Add Nanite buttons : if (!EditingProxies.IsEmpty()) { auto BuildNaniteData = [EditingProxies](UE::Landscape::EBuildFlags InBuildFlags) -> FReply { TArray ProxiesToBuild; Algo::TransformIf(EditingProxies, ProxiesToBuild, [](const TWeakObjectPtr& InProxy) { return InProxy.IsValid(); }, [](const TWeakObjectPtr& InProxy) { return InProxy.Get(); }); if (ProxiesToBuild.IsEmpty()) { return FReply::Unhandled(); } if (UWorld* World = ProxiesToBuild[0]->GetWorld()) { if (ULandscapeSubsystem* LandscapeSubsystem = World->GetSubsystem()) { LandscapeSubsystem->BuildNanite(InBuildFlags, MakeArrayView(ProxiesToBuild)); } } return FReply::Handled(); }; auto GetNumProxiesNeedingRebuild = [EditingProxies](UE::Landscape::EBuildFlags InBuildFlags) -> int32 { const bool bForceRebuild = EnumHasAnyFlags(InBuildFlags, UE::Landscape::EBuildFlags::ForceRebuild); TSet> ProxiesToBuild; for (TWeakObjectPtr EditingProxy : EditingProxies) { if (EditingProxy.IsValid()) { ProxiesToBuild.Add(EditingProxy); // Build all streaming proxies in the case of a ALandscape : if (ALandscape* Landscape = Cast(EditingProxy.Get())) { ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo(); if (LandscapeInfo != nullptr) { Algo::Transform(LandscapeInfo->StreamingProxies, ProxiesToBuild, [](const TWeakObjectPtr& InStreamingProxy) { return InStreamingProxy; }); } } } } return static_cast(Algo::CountIf(ProxiesToBuild, [bForceRebuild](TWeakObjectPtr InProxy) { return (InProxy.IsValid() && (bForceRebuild || !InProxy->IsNaniteMeshUpToDate())); })); }; auto HasAtLeastOneNaniteLandscape = [LandscapeActors]() -> bool { return Algo::FindByPredicate(LandscapeActors, [](TWeakObjectPtr InLandscape) { return (InLandscape.IsValid() && InLandscape->IsNaniteEnabled()); }) != nullptr; }; IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory("Nanite", FText::GetEmpty(), ECategoryPriority::Default); CategoryBuilder.AddCustomRow(FText::FromString(TEXT("Rebuild Nanite Data"))) .RowTag("RebuildNaniteData") [ SNew(SHorizontalBox) .IsEnabled_Lambda([HasAtLeastOneNaniteLandscape] { return HasAtLeastOneNaniteLandscape(); }) + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SButton) .Text(LOCTEXT("BuildNaniteData", "Build Data")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ToolTipText_Lambda([GetNumProxiesNeedingRebuild]() { return FText::Format(LOCTEXT("BuildNaniteDataTooltip", "Builds the Nanite mesh representation from the Landscape data if it's not up to date ({0} {0}|plural(one=actors, other=actors) to build)"), GetNumProxiesNeedingRebuild(UE::Landscape::EBuildFlags::None)); }) .OnClicked_Lambda([BuildNaniteData]() { return BuildNaniteData(UE::Landscape::EBuildFlags::WriteFinalLog); }) .IsEnabled_Lambda([GetNumProxiesNeedingRebuild]() { return (GetNumProxiesNeedingRebuild(UE::Landscape::EBuildFlags::None) > 0); }) ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SButton) .Text(LOCTEXT("RebuildNaniteData", "Rebuild Data")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ToolTipText_Lambda([GetNumProxiesNeedingRebuild]() { return FText::Format(LOCTEXT("RebuildNaniteDataTooltip", "Rebuilds the Nanite mesh representation from the Landscape data ({0} {0}|plural(one=actors, other=actors) to build)"), GetNumProxiesNeedingRebuild(UE::Landscape::EBuildFlags::ForceRebuild)); }) .OnClicked_Lambda([BuildNaniteData]() { return BuildNaniteData(UE::Landscape::EBuildFlags::WriteFinalLog | UE::Landscape::EBuildFlags::ForceRebuild); }) ] ]; } if (LandscapeStreamingProxy != nullptr) { for (TFieldIterator PropertyIterator(LandscapeStreamingProxy->GetClass()); PropertyIterator; ++PropertyIterator) { FProperty* Property = *PropertyIterator; if (Property == nullptr) { continue; } if (LandscapeStreamingProxy->IsPropertyInherited(Property)) { TSharedPtr PropertyHandle = DetailBuilder.GetProperty(Property->GetFName()); if (PropertyHandle->IsValidHandle()) { IDetailPropertyRow* DetailRow = DetailBuilder.EditDefaultProperty(PropertyHandle); // Extend the tool tip to indicate this property is inherited FText ToolTipText = PropertyHandle->GetToolTipText(); DetailRow->ToolTip(FText::Format(LOCTEXT("InheritedProperty", "{0} This property is inherited from the parent Landscape proxy."), ToolTipText)); // Disable the property editing DetailRow->IsEnabled(false); } } else if (LandscapeStreamingProxy->IsPropertyOverridable(Property)) { TSharedPtr PropertyHandle = DetailBuilder.GetProperty(Property->GetFName()); if (PropertyHandle->IsValidHandle()) { const FText TooltipText = LOCTEXT("OverriddenProperty", "Check this box to override the parent landscape's property."); FName PropertyName = Property->GetFName(); IDetailPropertyRow* DetailRow = DetailBuilder.EditDefaultProperty(PropertyHandle); TSharedPtr NameWidget = nullptr; TSharedPtr ValueWidget = nullptr; DetailRow->GetDefaultWidgets(NameWidget, ValueWidget); TAttribute EnabledAttribute = TAttribute::Create([LandscapeStreamingProxy, PropertyName]() -> bool { return LandscapeStreamingProxy->IsSharedPropertyOverridden(PropertyName); }); DetailRow->CustomWidget(/*bShowChildren = */true) .NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SCheckBox) .ToolTipText(TooltipText) .IsChecked_Lambda([EditingStreamingProxies, PropertyName]() -> ECheckBoxState { bool bContainsOverriddenProperty = false; bool bContainsDefaultProperty = false; for (const TWeakObjectPtr StreamingProxy : EditingStreamingProxies) { if (!StreamingProxy.IsValid()) { continue; } const bool bIsPropertyOverridden = StreamingProxy->IsSharedPropertyOverridden(PropertyName); bContainsOverriddenProperty |= bIsPropertyOverridden; bContainsDefaultProperty |= !bIsPropertyOverridden; } if (bContainsOverriddenProperty && bContainsDefaultProperty) { return ECheckBoxState::Undetermined; } return bContainsOverriddenProperty ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([EditingStreamingProxies, PropertyName](ECheckBoxState NewState) { if (NewState == ECheckBoxState::Undetermined) { return; } const bool bChecked = NewState == ECheckBoxState::Checked; const FScopedTransaction Transaction(LOCTEXT("SetSharedPropertyOverride", "Change Property Override")); for (const TWeakObjectPtr StreamingProxy : EditingStreamingProxies) { if (!StreamingProxy.IsValid()) { continue; } StreamingProxy->SetSharedPropertyOverride(PropertyName, bChecked); } }) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .IsEnabled(EnabledAttribute) [ NameWidget->AsShared() ] ] ] .ValueContent() [ SNew(SBox) .IsEnabled(EnabledAttribute) [ ValueWidget->AsShared() ] ] .IsValueEnabled(EnabledAttribute); } } } } } static void GetMissingRuntimeVirtualTextureVolumes(ALandscape* InLandscapeActor, TArray& OutVirtualTextures) { UWorld* World = InLandscapeActor != nullptr ? InLandscapeActor->GetWorld() : nullptr; if (World == nullptr) { return; } TArray FoundVolumes; for (TObjectIterator It(RF_ClassDefaultObject, false, EInternalObjectFlags::Garbage); It; ++It) { if (It->GetWorld() == World) { if (URuntimeVirtualTexture* VirtualTexture = It->GetVirtualTexture()) { FoundVolumes.Add(VirtualTexture); } } } for (URuntimeVirtualTexture* VirtualTexture : InLandscapeActor->RuntimeVirtualTextures) { if (VirtualTexture != nullptr && FoundVolumes.Find(VirtualTexture) == INDEX_NONE) { OutVirtualTextures.Add(VirtualTexture); } } } bool FLandscapeProxyUIDetails::IsCreateRuntimeVirtualTextureVolumeEnabled(ALandscape* InLandscapeActor) const { if (InLandscapeActor == nullptr) { return false; } TArray VirtualTextureVolumesToCreate; GetMissingRuntimeVirtualTextureVolumes(InLandscapeActor, VirtualTextureVolumesToCreate); return VirtualTextureVolumesToCreate.Num() > 0; } FReply FLandscapeProxyUIDetails::CreateRuntimeVirtualTextureVolume(ALandscape* InLandscapeActor) { if (InLandscapeActor == nullptr) { return FReply::Unhandled(); } TArray VirtualTextureVolumesToCreate; GetMissingRuntimeVirtualTextureVolumes(InLandscapeActor, VirtualTextureVolumesToCreate); if (VirtualTextureVolumesToCreate.Num() == 0) { return FReply::Unhandled(); } const FScopedTransaction Transaction(LOCTEXT("Transaction_CreateVolumes", "Create Runtime Virtual Texture Volumes")); for (URuntimeVirtualTexture* VirtualTexture : VirtualTextureVolumesToCreate) { ARuntimeVirtualTextureVolume* NewVolume = InLandscapeActor->GetWorld()->SpawnActor(); NewVolume->VirtualTextureComponent->SetVirtualTexture(VirtualTexture); NewVolume->VirtualTextureComponent->SetBoundsAlignActor(InLandscapeActor); RuntimeVirtualTexture::SetBounds(NewVolume->VirtualTextureComponent); } return FReply::Handled(); } #undef LOCTEXT_NAMESPACE