// Copyright Epic Games, Inc. All Rights Reserved. #include "StatsPages/StaticMeshLightingInfoStatsPage.h" #include "GameFramework/Actor.h" #include "Engine/GameViewportClient.h" #include "Components/StaticMeshComponent.h" #include "Misc/ConfigCacheIni.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Widgets/SBoxPanel.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Widgets/Text/STextBlock.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SComboButton.h" #include "Components/LightComponent.h" #include "Editor.h" #include "IPropertyTableRow.h" #include "BusyCursor.h" #include "ScopedTransaction.h" #include "StaticMeshComponentLODInfo.h" #include "Widgets/Input/STextEntryPopup.h" #include "Engine/LevelStreaming.h" #include "Engine/MapBuildDataRegistry.h" #define LOCTEXT_NAMESPACE "Editor.StatsViewer.StaticMeshLightingInfo" FStaticMeshLightingInfoStatsPage& FStaticMeshLightingInfoStatsPage::Get() { static FStaticMeshLightingInfoStatsPage* Instance = NULL; if( Instance == NULL ) { Instance = new FStaticMeshLightingInfoStatsPage; } return *Instance; } /** Helper class to generate statistics */ struct StaticMeshLightingInfoStatsGenerator { StaticMeshLightingInfoStatsGenerator(UWorld* InWorld) { World = InWorld != nullptr ? InWorld : GWorld; } UWorld* World; /** The lights in the world which the system is scanning. */ TArray AllLights; void AddRequiredLevels( EStaticMeshLightingInfoObjectSets InObjectSet, UWorld* InWorld, TArray& OutLevels ) { switch (InObjectSet) { case StaticMeshLightingInfoObjectSets_CurrentLevel: { check(InWorld); OutLevels.AddUnique(InWorld->GetCurrentLevel()); } break; case StaticMeshLightingInfoObjectSets_SelectedLevels: { TArray>& SelectedLevels = InWorld->GetSelectedLevels(); for(auto It = SelectedLevels.CreateIterator(); It; ++It) { ULevel* Level = *It; if (Level) { OutLevels.AddUnique(Level); } } if (OutLevels.Num() == 0) { // Fall to the current level... check(InWorld); OutLevels.AddUnique(InWorld->GetCurrentLevel()); } } break; case StaticMeshLightingInfoObjectSets_AllLevels: { if (InWorld != NULL) { // Add main level. OutLevels.AddUnique(InWorld->PersistentLevel); // Add secondary levels. for (ULevelStreaming* StreamingLevel : InWorld->GetStreamingLevels()) { if (StreamingLevel) { if (ULevel* Level = StreamingLevel->GetLoadedLevel()) { OutLevels.AddUnique( Level ); } } } } } break; } } /** Add an item to the output objects array */ void AddItem(UStaticMeshComponent* InComponent, AActor* InActor, TArray< TWeakObjectPtr >& OutObjects) { int32 TextureLightMapMemoryUsage; int32 TextureShadowMapMemoryUsage; int32 VertexLightMapMemoryUsage; int32 VertexShadowMapMemoryUsage; int32 StaticLightingResolution; bool bTextureMapping; bool bHasLightmapTexCoords; if(InComponent != NULL && InComponent->GetEstimatedLightAndShadowMapMemoryUsage( TextureLightMapMemoryUsage, TextureShadowMapMemoryUsage, VertexLightMapMemoryUsage, VertexShadowMapMemoryUsage, StaticLightingResolution, bTextureMapping, bHasLightmapTexCoords) ) { UStaticMeshLightingInfo* Entry = NewObject(); Entry->AddToRoot(); OutObjects.Add(Entry); Entry->StaticMeshActor = InActor; Entry->StaticMeshComponent = InComponent; Entry->StaticMesh = InComponent->GetStaticMesh(); // Show all of the lightmap texture names so we can correlate meshes with the actual texture data // for debugging encoding concerns. Only do this if we have static lighting enabled/built (LODData exists) if (InComponent->LODData.Num()) { const FMeshMapBuildData* MeshMapBuildData = InComponent->GetMeshMapBuildData(InComponent->LODData[0]); if (MeshMapBuildData && MeshMapBuildData->LightMap) { TArray Textures; FLightMap2D* LightMap2D = MeshMapBuildData->LightMap->GetLightMap2D(); if (LightMap2D) { LightMap2D->GetReferencedTextures(Textures); } for (UTexture2D* Texture : Textures) { Entry->LightmapTextureNames.Add(Texture->GetName()); } } } Entry->TextureLightMapMemoryUsage = (float)TextureLightMapMemoryUsage / 1024.0f; Entry->TextureShadowMapMemoryUsage = (float)TextureShadowMapMemoryUsage / 1024.0f; Entry->VertexLightMapMemoryUsage = (float)VertexLightMapMemoryUsage / 1024.0f; Entry->VertexShadowMapMemoryUsage = (float)VertexShadowMapMemoryUsage / 1024.0f; Entry->StaticLightingResolution = StaticLightingResolution; Entry->bTextureMapping = bTextureMapping; Entry->bHasLightmapTexCoords = bHasLightmapTexCoords; Entry->UpdateNames(); // Find the lights relevant to the primitive. TArray LightMapRelevantLights; TArray ShadowMapRelevantLights; for (int32 LightIndex = 0; LightIndex < AllLights.Num(); LightIndex++) { ULightComponent* Light = AllLights[LightIndex]; // Only add enabled lights if (Light->GetVisibleFlag() && Light->AffectsPrimitive(InComponent)) { // Check whether the light should use a light-map or shadow-map. const bool bHasStaticLighting = Light->HasStaticLighting(); if (bHasStaticLighting) { LightMapRelevantLights.Add(Light); } // only allow for shadow maps if shadow casting is enabled else if (Light->CastShadows && Light->CastStaticShadows) { ShadowMapRelevantLights.Add(Light); } } } Entry->LightMapLightCount = LightMapRelevantLights.Num(); Entry->ShadowMapLightCount = ShadowMapRelevantLights.Num(); } } /** Add items to the output object array according to the input object set */ void Generate(EStaticMeshLightingInfoObjectSets InObjectSet, TArray< TWeakObjectPtr >& OutObjects) { /** The levels we are gathering information for. */ TArray Levels; // Fill the light list for (TObjectIterator LightIt; LightIt; ++LightIt) { ULightComponent* const Light = *LightIt; const bool bLightIsInWorld = Light->GetOwner() && !Light->GetOwner()->HasAnyFlags(RF_ClassDefaultObject) && World->ContainsActor(Light->GetOwner()); if (bLightIsInWorld) { if (Light->HasStaticLighting() || Light->HasStaticShadowing()) { // Add the light to the system's list of lights in the world. AllLights.Add(Light); } //append to the list of levels in use AddRequiredLevels( InObjectSet, World, Levels ); } } if (Levels.Num() > 0) { // Iterate over static mesh components in the list of levels... for (TObjectIterator SMCIt; SMCIt; ++SMCIt) { AActor* Owner = (*SMCIt)->GetOwner(); if (Owner && !Owner->HasAnyFlags(RF_ClassDefaultObject) ) { int32 Dummy; ULevel* CheckLevel = Owner->GetLevel(); if ((CheckLevel != NULL) && (Levels.Find(CheckLevel, Dummy))) { AddItem(*SMCIt, Owner, OutObjects); } } } } } }; void FStaticMeshLightingInfoStatsPage::Generate( TArray< TWeakObjectPtr >& OutObjects ) const { StaticMeshLightingInfoStatsGenerator Generator( GetWorld() ); Generator.Generate((EStaticMeshLightingInfoObjectSets)ObjectSetIndex, OutObjects); } void FStaticMeshLightingInfoStatsPage::GenerateTotals( const TArray< TWeakObjectPtr >& InObjects, TMap& OutTotals ) const { if(InObjects.Num()) { UStaticMeshLightingInfo* TotalEntry = NewObject(); for( auto It = InObjects.CreateConstIterator(); It; ++It ) { UStaticMeshLightingInfo* StatsEntry = Cast( It->Get() ); TotalEntry->LightMapLightCount += StatsEntry->LightMapLightCount; TotalEntry->TextureLightMapMemoryUsage += StatsEntry->TextureLightMapMemoryUsage; TotalEntry->VertexLightMapMemoryUsage += StatsEntry->VertexLightMapMemoryUsage; TotalEntry->ShadowMapLightCount += StatsEntry->ShadowMapLightCount; TotalEntry->TextureShadowMapMemoryUsage += StatsEntry->TextureShadowMapMemoryUsage; TotalEntry->VertexShadowMapMemoryUsage += StatsEntry->VertexShadowMapMemoryUsage; } OutTotals.Add( TEXT("LightMapLightCount"), FText::AsNumber( TotalEntry->LightMapLightCount ) ); OutTotals.Add( TEXT("TextureLightMapMemoryUsage"), FText::AsNumber( TotalEntry->TextureLightMapMemoryUsage ) ); OutTotals.Add( TEXT("VertexLightMapMemoryUsage"), FText::AsNumber( TotalEntry->VertexLightMapMemoryUsage ) ); OutTotals.Add( TEXT("ShadowMapLightCount"), FText::AsNumber( TotalEntry->ShadowMapLightCount ) ); OutTotals.Add( TEXT("TextureShadowMapMemoryUsage"), FText::AsNumber( TotalEntry->TextureShadowMapMemoryUsage ) ); OutTotals.Add( TEXT("VertexShadowMapMemoryUsage"), FText::AsNumber( TotalEntry->VertexShadowMapMemoryUsage ) ); } } TSharedPtr FStaticMeshLightingInfoStatsPage::GetCustomWidget(TWeakPtr InParentStatsViewer) { if( !CustomWidget.IsValid() ) { SAssignNew(CustomWidget, SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f) .HAlign(HAlign_Fill) [ SAssignNew(SwapComboButton, SComboButton) .ContentPadding(3.f) .OnGetMenuContent( this, &FStaticMeshLightingInfoStatsPage::OnGetSwapComboButtonMenuContent, InParentStatsViewer ) .ButtonContent() [ SNew(STextBlock) .Text(LOCTEXT("Swap", "Swap")) .ToolTipText(LOCTEXT("SwapObjectToolTip", "Swap selected objects between Vertex and Texture lighting")) ] ] +SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f) .HAlign(HAlign_Fill) [ SAssignNew(SetToComboButton, SComboButton) .ContentPadding(3.f) .OnGetMenuContent( this, &FStaticMeshLightingInfoStatsPage::OnGetSetToComboButtonMenuContent, InParentStatsViewer ) .ButtonContent() [ SNew(STextBlock) .Text(LOCTEXT("SetTo", "SetTo")) .ToolTipText(LOCTEXT("SetToToolTip", "Set selected objects to either Vertex or Texture lighting")) ] ]; } return CustomWidget; } void FStaticMeshLightingInfoStatsPage::SwapMappingMethodOnSelectedComponents(TWeakPtr InParentStatsViewer, int32 InStaticLightingResolution) const { if(InParentStatsViewer.IsValid()) { const FScopedBusyCursor BusyCursor; // first get the selected objects in the table TArray SelectedObjects; TSharedPtr PropertyTable = InParentStatsViewer.Pin()->GetPropertyTable(); const TSet< TSharedRef >& Rows = PropertyTable->GetSelectedRows(); for(auto RowIt = Rows.CreateConstIterator(); RowIt; ++RowIt) { TSharedRef Row = *RowIt; UStaticMeshLightingInfo* Entry = Cast( Row->GetDataSource()->AsUObject().Get() ); SelectedObjects.Add(Entry); } if (SelectedObjects.Num() > 0) { const FScopedTransaction Transaction( LOCTEXT("StaticMeshLightingInfoSwap", "DlgStaticMeshLightingInfo:Swap") ); bool bNeedsRefresh = false; for (int32 CompIdx = 0; CompIdx < SelectedObjects.Num(); CompIdx++) { UStaticMeshLightingInfo* Entry = SelectedObjects[CompIdx]; if (Entry->StaticMeshActor.IsValid()) { Entry->StaticMeshActor->Modify(); } if ( Entry->StaticMeshComponent.IsValid()) { Entry->StaticMeshComponent->Modify(); Entry->StaticMeshComponent->SetStaticLightingMapping(!(Entry->bTextureMapping), InStaticLightingResolution); Entry->StaticMeshComponent->InvalidateLightingCache(); Entry->StaticMeshComponent->ReregisterComponent(); } bNeedsRefresh = true; } if (bNeedsRefresh) { InParentStatsViewer.Pin()->Refresh(); } } } } void FStaticMeshLightingInfoStatsPage::OnSwapClicked(TWeakPtr InParentStatsViewer, ESwapOptions::Type InSwapOption) const { switch (InSwapOption) { case ESwapOptions::Swap: { SwapMappingMethodOnSelectedComponents(InParentStatsViewer, 0); } break; case ESwapOptions::SwapAskRes: { GetUserSetStaticLightmapResolution(InParentStatsViewer, true); } break; } } TSharedRef FStaticMeshLightingInfoStatsPage::OnGetSwapComboButtonMenuContent(TWeakPtr InParentStatsViewer) const { FMenuBuilder MenuBuilder( true, NULL ); MenuBuilder.AddMenuEntry( LOCTEXT("Swap", "Swap"), LOCTEXT("SwapToolTip", "Swap between Vertex and Texture"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnSwapClicked, InParentStatsViewer, ESwapOptions::Swap ) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SwapAskRes", "Swap(Res)..."), LOCTEXT("SwapAskResToolTip", "Swap between Vertex and Texture, prompt for Resolution"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnSwapClicked, InParentStatsViewer, ESwapOptions::Swap ) ) ); return MenuBuilder.MakeWidget(); } void FStaticMeshLightingInfoStatsPage::SetMappingMethodOnSelectedComponents(TWeakPtr InParentStatsViewer, bool bInTextureMapping, int32 InStaticLightingResolution) const { if(InParentStatsViewer.IsValid()) { const FScopedBusyCursor BusyCursor; // first get the selected objects in the table TArray SelectedObjects; TSharedPtr PropertyTable = InParentStatsViewer.Pin()->GetPropertyTable(); const TSet< TSharedRef >& Rows = PropertyTable->GetSelectedRows(); for(auto RowIt = Rows.CreateConstIterator(); RowIt; ++RowIt) { TSharedRef Row = *RowIt; UStaticMeshLightingInfo* Entry = Cast( Row->GetDataSource()->AsUObject().Get() ); SelectedObjects.Add(Entry); } if (SelectedObjects.Num() > 0) { const FScopedTransaction Transaction( LOCTEXT("StaticMeshLightingInfoSet", "DlgStaticMeshLightingInfo:Set") ); bool bNeedsRefresh = false; for (int32 CompIdx = 0; CompIdx < SelectedObjects.Num(); CompIdx++) { UStaticMeshLightingInfo* Entry = SelectedObjects[CompIdx]; if (Entry->StaticMeshActor.IsValid()) { Entry->StaticMeshActor->Modify(); } if ( Entry->StaticMeshComponent.IsValid()) { Entry->StaticMeshComponent->Modify(); Entry->StaticMeshComponent->SetStaticLightingMapping(bInTextureMapping, InStaticLightingResolution); Entry->StaticMeshComponent->InvalidateLightingCache(); Entry->StaticMeshComponent->ReregisterComponent(); } bNeedsRefresh = true; } if (bNeedsRefresh) { InParentStatsViewer.Pin()->Refresh(); } } } } void FStaticMeshLightingInfoStatsPage::OnSetToClicked(TWeakPtr InParentStatsViewer, ESetToOptions::Type InSetToOption) const { switch (InSetToOption) { case ESetToOptions::Vertex: { SetMappingMethodOnSelectedComponents(InParentStatsViewer, false, 0); } break; case ESetToOptions::Texture: { SetMappingMethodOnSelectedComponents(InParentStatsViewer, true, 0); } break; case ESetToOptions::TextureAskRes: { GetUserSetStaticLightmapResolution(InParentStatsViewer, false); } break; } } TSharedRef FStaticMeshLightingInfoStatsPage::OnGetSetToComboButtonMenuContent(TWeakPtr InParentStatsViewer) const { FMenuBuilder MenuBuilder( true, NULL ); MenuBuilder.AddMenuEntry( LOCTEXT("SetToVertexMapping", "Set To Vertex"), LOCTEXT("SetToVertexMappingToolTip", "Set to Vertex"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnSetToClicked, InParentStatsViewer, ESetToOptions::Vertex ) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetToTextureMapping", "Set To Texture"), LOCTEXT("SetToTextureMappingToolTip", "Set to Texture"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnSetToClicked, InParentStatsViewer, ESetToOptions::Vertex ) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("SetToTextureMappingAskRes", "Set To Texture(Res)..."), LOCTEXT("SetToTextureMappingAskResToolTip", "Set To Texture, prompt for Resolution"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnSetToClicked, InParentStatsViewer, ESetToOptions::TextureAskRes ) ) ); return MenuBuilder.MakeWidget(); } void FStaticMeshLightingInfoStatsPage::OnResolutionCommitted(const FText& ResolutionText, ETextCommit::Type CommitInfo, TWeakPtr InParentStatsViewer, bool bSwap) const { // If we have a valid resolution int32 NewResolution = INDEX_NONE; if ( CommitInfo == ETextCommit::OnEnter && !ResolutionText.IsEmpty() && ResolutionText.IsNumeric() ) { NewResolution = FCString::Atoi( *ResolutionText.ToString() ); if (NewResolution >= 0) { NewResolution = FMath::Max(((NewResolution + 3) & ~3),4); } } // Remove the popup window if (ResolutionEntryMenu.IsValid()) { ResolutionEntryMenu.Pin()->Dismiss(); } // Do we have a valid resolution to use if (NewResolution != INDEX_NONE) { // Are we wanting to do a swap ex or a setto ex? if ( bSwap ) { SwapMappingMethodOnSelectedComponents(InParentStatsViewer, NewResolution); } else { SetMappingMethodOnSelectedComponents(InParentStatsViewer, true, NewResolution); } } } void FStaticMeshLightingInfoStatsPage::GetUserSetStaticLightmapResolution(TWeakPtr InParentStatsViewer, bool bSwap) const { if(InParentStatsViewer.IsValid()) { int32 DefaultRes = 0; verify(GConfig->GetInt(TEXT("DevOptions.StaticLighting"), TEXT("DefaultStaticMeshLightingRes"), DefaultRes, GLightmassIni)); TSharedRef TextEntry = SNew(STextEntryPopup) .Label(LOCTEXT("StaticMeshLightingInfo_GetResolutionTitle", "Enter Lightmap Resolution")) .HintText(LOCTEXT("StaticMeshLightingInfo_GetResolutionToolTip", "Will round to power of two")) .DefaultText(FText::AsNumber(DefaultRes)) .OnTextCommitted( FOnTextCommitted::CreateSP( this, &FStaticMeshLightingInfoStatsPage::OnResolutionCommitted, InParentStatsViewer, bSwap ) ) .ClearKeyboardFocusOnCommit( false ); ResolutionEntryMenu = FSlateApplication::Get().PushMenu( InParentStatsViewer.Pin().ToSharedRef(), FWidgetPath(), TextEntry, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect( FPopupTransitionEffect::TypeInPopup ) ); } } void FStaticMeshLightingInfoStatsPage::OnEditorNewCurrentLevel( TWeakPtr InParentStatsViewer ) { if(InParentStatsViewer.IsValid()) { const int32 ObjSetIndex = InParentStatsViewer.Pin()->GetObjectSetIndex(); if( ObjSetIndex == StaticMeshLightingInfoObjectSets_CurrentLevel ) { InParentStatsViewer.Pin()->Refresh(); } } } void FStaticMeshLightingInfoStatsPage::OnEditorLevelSelected( TWeakPtr InParentStatsViewer ) { if(InParentStatsViewer.IsValid()) { const int32 ObjSetIndex = InParentStatsViewer.Pin()->GetObjectSetIndex(); if( ObjSetIndex == StaticMeshLightingInfoObjectSets_SelectedLevels ) { InParentStatsViewer.Pin()->Refresh(); } } } void FStaticMeshLightingInfoStatsPage::OnShow( TWeakPtr InParentStatsViewer ) { // register delegates for scene changes we are interested in FEditorDelegates::NewCurrentLevel.AddRaw(this, &FStaticMeshLightingInfoStatsPage::OnEditorNewCurrentLevel, InParentStatsViewer); GWorld->OnSelectedLevelsChanged().AddSP( this, &FStaticMeshLightingInfoStatsPage::OnEditorLevelSelected, InParentStatsViewer ); } void FStaticMeshLightingInfoStatsPage::OnHide() { // unregister delegates GWorld->OnSelectedLevelsChanged().RemoveAll(this); FEditorDelegates::NewCurrentLevel.RemoveAll(this); } #undef LOCTEXT_NAMESPACE