UE4/UE5 c++场景直方图
- UE4/UE5 C++绘制编辑器场景直方图
- 绘制原理:
- 元素绘制
- 坐标轴绘制
- 源码处理
UE4/UE5 C++绘制编辑器场景直方图
注:源码包含场景中的像素获取、菜单添加ToolBar
实现效果:
这个是用于美术统计场景中像素元素分布,类似于PS里面的直方图,比如,R值是255的像素,场景中的个数是1000个,那么就是(255,1000)。
绘制原理:
元素绘制
根据不同高度绘制了不同的Box
坐标轴绘制
箭头,一段段的标尺,坐标轴,均由绘制线构成
源码处理
首先创建一个Editor模块:
创建SCompoundWidget
:
.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SCompoundWidget.h"
/**
*
*/
class SLASHEDITOR_API SPixHistogramWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SPixHistogramWidget)
{
}
SLATE_END_ARGS()
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const override;
virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override;
virtual FReply OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override;
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
void DrawPixelOnScreen(const TMap<int, float>& InData, const float InBarWidth,const float InBarGap,
const FGeometry& InAllottedGeometry, FSlateWindowElementList& InOutDrawElements,
const int32 InLayerId,const FVector2D InEdgeSize,const FLinearColor InColor) const;
virtual bool SupportsKeyboardFocus() const override;
void SelectionChanged(TSharedPtr<FString> NewSelection, ESelectInfo::Type SelectInfo);
void GetScreenPixelValue();
void RefreshData();
void ResetMap();
void OnScalingFactorChanged(float InScalingFactor);
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
TArray<TSharedPtr<FString>> Pass;
int CurrentIndex = 0;
FVector2D LinearScale = FVector2D(3.f, 3.f);
FVector2D BottomSize = FVector2D(40.f,40.f);
float HeightScale = 1.f;
const float HeightScalingFactor = 500;
TArray<FColor> PixelsColor;
TMap<int,float> GrayData;
TMap<int,float> RData;
TMap<int,float> GData;
TMap<int,float> BData;
bool IsCtrlDown = false;
float ScalingFactor = 5.f;
};
.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "PixHistogramWidget.h"
#include "Editor.h"
#include "SlateOptMacros.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Input/STextComboBox.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
#define LOCTEXT_NAMESPACE "PixHistogramWidget"
void SPixHistogramWidget::Construct(const FArguments& InArgs)
{
PixelsColor.Init(FColor::White, 65535);
ResetMap();
Pass.Empty();
CurrentIndex = 0;
Pass = {
MakeShared<FString>("R"),
MakeShared<FString>("G"),
MakeShared<FString>("B"),
MakeShared<FString>("Gray"),
MakeShared<FString>("RGB"),
MakeShared<FString>("RGB And Gray")
};
ChildSlot
[
SNew(SBox)
.WidthOverride(500)
.HeightOverride(200)
.HAlign(HAlign_Left)
.VAlign(VAlign_Bottom)
.Padding(FMargin(0, -200, 0, 0))
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Top)
[
SNew(STextComboBox)
.InitiallySelectedItem(Pass[CurrentIndex])
.OptionsSource(&Pass)
.OnSelectionChanged(this, &SPixHistogramWidget::SelectionChanged)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(20, 0, 0, 0)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("scale speed:")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.Value(ScalingFactor)
.MinValue(0.0f)
.MaxValue(100.0f)
.OnValueChanged(this, &SPixHistogramWidget::OnScalingFactorChanged)
]
]
];
bCanSupportFocus = true;
}
void SPixHistogramWidget::SelectionChanged(TSharedPtr<FString> NewSelection, ESelectInfo::Type SelectInfo)
{
CurrentIndex = Pass.Find(NewSelection);
}
void SPixHistogramWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
RefreshData();
}
int32 SPixHistogramWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const
{
const int32 NewLayerId = SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect,
OutDrawElements, LayerId, InWidgetStyle,
bParentEnabled);
const float BarWidth = 1.f * LinearScale.X;
const float BarGap = 0.f * LinearScale.X;
const FVector2D EdgeSize = FVector2D(0, 0);
const FVector2D BackGroundSize = FVector2D((256 * LinearScale.X + EdgeSize.X * 2),
(100 * LinearScale.Y + EdgeSize.Y * 2));
if (CurrentIndex == 0)
{
DrawPixelOnScreen(RData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Red);
}
else if (CurrentIndex == 1)
{
DrawPixelOnScreen(GData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Green);
}
else if (CurrentIndex == 2)
{
DrawPixelOnScreen(BData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Blue);
}
else if (CurrentIndex == 3)
{
DrawPixelOnScreen(GData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Gray);
}
else if (CurrentIndex == 4)
{
DrawPixelOnScreen(RData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Red);
DrawPixelOnScreen(GData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Green);
DrawPixelOnScreen(BData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Blue);
}
else if (CurrentIndex == 5)
{
DrawPixelOnScreen(RData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Red);
DrawPixelOnScreen(GData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Green);
DrawPixelOnScreen(BData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Blue);
DrawPixelOnScreen(GrayData, BarWidth, BarGap,
AllottedGeometry, OutDrawElements,
LayerId, EdgeSize, FLinearColor::Gray);
}
//Background frame drawing
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y), BackGroundSize),
new FSlateBrush(),
ESlateDrawEffect::None,
FLinearColor(0, 0, 0, 0.3)
);
//X Axis ruler
FLinearColor XColor = FLinearColor::White;
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(BottomSize.X + BackGroundSize.X + 20, AllottedGeometry.Size.Y - BottomSize.Y)
},
ESlateDrawEffect::None,
XColor
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X + BackGroundSize.X + 20, AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(BottomSize.X + BackGroundSize.X + 10, AllottedGeometry.Size.Y - BottomSize.Y + 5)
},
ESlateDrawEffect::None,
XColor
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X + BackGroundSize.X + 20, AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(BottomSize.X + BackGroundSize.X + 10, AllottedGeometry.Size.Y - BottomSize.Y - 5)
},
ESlateDrawEffect::None,
XColor
);
//X Axis ruler
int XDefaultSectionNum = FMath::Clamp<int>(8 * LinearScale.X, 2, 256);
const FSlateFontInfo XCoordinateFont = FCoreStyle::GetDefaultFontStyle("Regular", 8);
for (int i = 1; i <= XDefaultSectionNum; ++i)
{
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X + BackGroundSize.X / XDefaultSectionNum * i,
AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(BottomSize.X + BackGroundSize.X / XDefaultSectionNum * i,
AllottedGeometry.Size.Y - BottomSize.Y - 3)
},
ESlateDrawEffect::None,
XColor
);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(
FVector2D(BottomSize.X + BackGroundSize.X / XDefaultSectionNum * i,
AllottedGeometry.Size.Y - BottomSize.Y + 5),
FVector2D(30, 30)),
FString::FromInt(256 / XDefaultSectionNum * i - 1),
XCoordinateFont,
ESlateDrawEffect::None,
FLinearColor::White
);
}
//Y Axis
FLinearColor YColor = FLinearColor::White;
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y - 20)
},
ESlateDrawEffect::None,
YColor
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y - 20),
FVector2D(BottomSize.X - 5, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y - 10)
},
ESlateDrawEffect::None,
YColor
);
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y - 20),
FVector2D(BottomSize.X + 5, AllottedGeometry.Size.Y - BackGroundSize.Y - BottomSize.Y - 10)
},
ESlateDrawEffect::None,
YColor
);
//Y Axis ruler
int YDefaultSectionNum = FMath::Clamp<int>(5 * LinearScale.Y, 2, static_cast<int>(100 / HeightScale * HeightScalingFactor) - 1);
const FSlateFontInfo YCoordinateFont = FCoreStyle::GetDefaultFontStyle("Regular", 8);
for (int i = 1; i <= YDefaultSectionNum; ++i)
{
FSlateDrawElement::MakeLines(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(),
TArray<FVector2D>{
FVector2D(BottomSize.X,
AllottedGeometry.Size.Y - BackGroundSize.Y / YDefaultSectionNum * i - BottomSize.Y),
FVector2D(BottomSize.X + 3,
AllottedGeometry.Size.Y - BackGroundSize.Y / YDefaultSectionNum * i - BottomSize.Y)
},
ESlateDrawEffect::None,
YColor
);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(
FVector2D(BottomSize.X + 5,
AllottedGeometry.Size.Y - BackGroundSize.Y / YDefaultSectionNum * i - BottomSize.Y),
FVector2D(30, 30)),
FString::FromInt(100 / HeightScale * HeightScalingFactor / YDefaultSectionNum * i),
YCoordinateFont,
ESlateDrawEffect::None,
FLinearColor::White
);
}
//Origin of coordinate
const FSlateFontInfo CoordinateFont = FCoreStyle::GetDefaultFontStyle("Regular", 10);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(FVector2D(BottomSize.X - 15, AllottedGeometry.Size.Y - BottomSize.Y),
FVector2D(30, 30)),
FString(TEXT("O")),
CoordinateFont,
ESlateDrawEffect::None,
FLinearColor::White
);
//X Axis labeling
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(
FVector2D(BottomSize.X + BackGroundSize.X + 20, AllottedGeometry.Size.Y - BottomSize.Y - 15),
FVector2D(30, 30)),
FString(TEXT("Color Pass(0 ~ 255)")),
CoordinateFont,
ESlateDrawEffect::None,
XColor
);
//Y Axis labeling
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(
FVector2D(BottomSize.X + 5, AllottedGeometry.Size.Y - BottomSize.Y - BackGroundSize.Y - 20),
FVector2D(30, 30)),
FString(TEXT("Pixel Num")),
CoordinateFont,
ESlateDrawEffect::None,
YColor
);
//Operation reminder
const FSlateFontInfo TooltipsFontInfo = FCoreStyle::GetDefaultFontStyle("Regular", 15);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 12,
AllottedGeometry.ToPaintGeometry(
FVector2D(AllottedGeometry.Size.X / 2 - 425, 0), FVector2D(30, 30)),
FString(TEXT("Adjust the zoom with the mouse wheel. Press and hold Ctrl + mouse wheel to adjust the height.")),
TooltipsFontInfo,
ESlateDrawEffect::None,
FLinearColor::White
);
return NewLayerId;
}
FReply SPixHistogramWidget::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
const float ScrollDelta = MouseEvent.GetWheelDelta();
const float TempScalingFactor = 1.f + ScalingFactor * 0.01f;
if (IsCtrlDown)
{
if (ScrollDelta > 0)
{
HeightScale = HeightScale * TempScalingFactor;
}
else if (ScrollDelta < 0)
{
HeightScale = HeightScale / TempScalingFactor;
}
}
else
{
if (ScrollDelta > 0)
{
LinearScale = LinearScale * TempScalingFactor;
}
else if (ScrollDelta < 0)
{
LinearScale = LinearScale / TempScalingFactor;
}
}
return FReply::Handled();
}
FReply SPixHistogramWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
IsCtrlDown = InKeyEvent.IsControlDown();
return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent);
}
FReply SPixHistogramWidget::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
IsCtrlDown = false;
return SCompoundWidget::OnKeyUp(MyGeometry, InKeyEvent);
}
void SPixHistogramWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
SCompoundWidget::OnMouseEnter(MyGeometry, MouseEvent);
FSlateApplication::Get().SetKeyboardFocus(SharedThis(this), EFocusCause::OtherWidgetLostFocus);
}
void SPixHistogramWidget::DrawPixelOnScreen(const TMap<int, float>& InData, const float InBarWidth,
const float InBarGap,
const FGeometry& InAllottedGeometry,
FSlateWindowElementList& InOutDrawElements,
const int32 InLayerId, const FVector2D InEdgeSize,
const FLinearColor InColor) const
{
for (int i = 0; i < InData.Num(); ++i)
{
const float BarHeight = InData[i] * LinearScale.Y * HeightScale / HeightScalingFactor;
FVector2D BarPosition(i * (InBarWidth + InBarGap) + InEdgeSize.X,
InAllottedGeometry.Size.Y - BarHeight - InEdgeSize.Y);
FVector2D BarSize(InBarWidth, BarHeight);
FSlateDrawElement::MakeBox(
InOutDrawElements,
InLayerId,
InAllottedGeometry.ToPaintGeometry(BarPosition + FVector2D(BottomSize.X, -BottomSize.Y), BarSize),
new FSlateBrush(),
ESlateDrawEffect::None,
InColor
);
}
}
bool SPixHistogramWidget::SupportsKeyboardFocus() const
{
return true;
}
void SPixHistogramWidget::GetScreenPixelValue()
{
PixelsColor.Empty();
FViewport* Viewport = GEditor->GetActiveViewport();
Viewport->ReadPixels(PixelsColor);
}
void SPixHistogramWidget::RefreshData()
{
GetScreenPixelValue();
ResetMap();
//float Gray = 0.299 * Color.R + 0.587 * Color.G + 0.114 * Color.B;
for (const FColor Color : PixelsColor)
{
RData[Color.R] += 1;
GData[Color.G] += 1;
BData[Color.B] += 1;
const int Index = FMath::Clamp<int>((0.299 * Color.R + 0.587 * Color.G + 0.114 * Color.B), 0, 255);
GrayData[Index] += 1;
}
}
void SPixHistogramWidget::ResetMap()
{
GrayData.Empty();
RData.Empty();
GData.Empty();
BData.Empty();
for (int i = 0; i <= 255; ++i)
{
GrayData.Add(i, 0);
RData.Add(i, 0);
GData.Add(i, 0);
BData.Add(i, 0);
}
}
void SPixHistogramWidget::OnScalingFactorChanged(float InScalingFactor)
{
ScalingFactor = InScalingFactor;
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
Module添加内容:
.h
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FSlashEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
void AddProjectToolsMenu(FMenuBarBuilder& MenuBuilder);
void FillProjectToolsMenu(FMenuBuilder& MenuBuilder);
private:
TSharedRef<SDockTab> SpawnPixHistogramTab(const FSpawnTabArgs& Args);
static TSharedRef<FWorkspaceItem> ProjectMenuRoot;
};
.cpp
#include "SlashEditor.h"
#include "LevelEditor.h"
#include "SlashEditorStyle.h"
#include "PixHistogram/PixHistogramWidget.h"
#define LOCTEXT_NAMESPACE "FSlashEditorModule"
TSharedRef<FWorkspaceItem> FSlashEditorModule::ProjectMenuRoot = FWorkspaceItem::NewGroup(
FText::FromString("ProjectToolsRoot"));
void FSlashEditorModule::StartupModule()
{
FSlashEditorStyle::Register();
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
const TSharedPtr<FExtender> MenuExtender = MakeShareable(new FExtender());
MenuExtender->AddMenuBarExtension(
"Help",
EExtensionHook::Before,
nullptr,
FMenuBarExtensionDelegate::CreateRaw(this, &FSlashEditorModule::AddProjectToolsMenu));
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender);
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(FName("PixHistogramTab"),
FOnSpawnTab::CreateRaw(
this,
&FSlashEditorModule::SpawnPixHistogramTab))
.SetGroup(ProjectMenuRoot)
.SetDisplayName(FText::FromString(TEXT("PixHistogram")));
}
void FSlashEditorModule::ShutdownModule()
{
FSlashEditorStyle::Unregister();
}
void FSlashEditorModule::AddProjectToolsMenu(FMenuBarBuilder& MenuBuilder)
{
MenuBuilder.AddPullDownMenu(
LOCTEXT("Menu", "Project Menu"),
LOCTEXT("ToolTip", "Open project menu"),
FNewMenuDelegate::CreateRaw(this, &FSlashEditorModule::FillProjectToolsMenu),
FName(TEXT("Project Menu")),
FName(TEXT("ProjectMenu")));
}
void FSlashEditorModule::FillProjectToolsMenu(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("ProjectMenu", TAttribute<FText>(LOCTEXT("PixHistogramSectionName", "PixHistogram")));
{
FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(MenuBuilder, FName("PixHistogramTab"));
}
MenuBuilder.EndSection();
}
TSharedRef<SDockTab> FSlashEditorModule::SpawnPixHistogramTab(const FSpawnTabArgs& Args)
{
TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
SNew(SPixHistogramWidget)
];
return SpawnedTab;
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FSlashEditorModule, SlashEditor)
.Build.cs
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UMG",
"InputCore"
}
最后成功添加按钮,点击即可得到开头结果: