NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

Unreal 的  UMG 提供了相当多的UI Widget,并允许你在蓝图中做很多事情。但是,有时可能仍需要创建自定义Widget来填补空白。这篇文章中将介绍使用自定义渲染创建Slate Widget的基础知识。

UMG Widget涉及两个类:

  • 一个 UWidget 派生类,可以在编辑器中与之交互
  • 一个 SCompoundWidget 类,处理低级功能和渲染

让我们看一个展示切片的简单UMG Widget,它具有给定的起始角度和圆弧大小。我们可以控制颜色和不透明度,并且可以选择使用图像来渲染它:

1、最小UMG Widget类

首先,我们将添加没有任何功能的类,并确保我们看到新的小部件出现在编辑器面板中。

这是一个 UMG 小部件的最小化头文件:

UCLASS()
class CUSTOMWIDGET_API USlice : public UWidget
{
  GENERATED_BODY()
public:
  virtual void ReleaseSlateResources(bool bReleaseChildren) override;

protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  TSharedPtr<SSlateSlice> MySlice;
};

对应的CPP文件如下:

void USlice::ReleaseSlateResources(bool bReleaseChildren)
{
  MySlice.Reset();
}

TSharedRef<SWidget> USlice::RebuildWidget()
{
  MySlice = SNew(SSlateSlice);
  return MySlice.ToSharedRef();
}

请注意,在创建新UMG Widget时,始终必须实现这两个方法 - RebuildWidget负责构建 Slate Widget, ReleaseSlateResources负责清理资源。

2、最小Slate Widget类

这是 UMG Widget类引用的 Slate 类:

class CUSTOMWIDGET_API SSlateSlice : public SCompoundWidget
{
public:
  SLATE_BEGIN_ARGS(SSlateSlice)
  {}
  SLATE_END_ARGS()

  void Construct(const FArguments& InArgs);
};

以及对应的CPP文件:

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlateSlice::Construct(const FArguments& InArgs)
{
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION和END_SLATE_FUNCTION_BUILD_OPTIMIZATION经常在Construct函数周围使用,因为它最终可能会产生一个非常复杂的表达式,编译器可能会花费大量时间来尝试优化。这些宏关闭并再次打开优化。

3、传递Widget属性

为了让我们的UMG Widget绘制切片,需要添加一些属性来确定它的外观。首先,我们将BrushAngleArcSize添加到 UMG  Widget:

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance")
  FSlateBrush Brush;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance")
  float Angle;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Appearance")
  float ArcSize;

我们还需要在 Slate 类中添加类似的属性。它们都作为参数添加到 SLATE_BEGIN_ARGS / SLATE_END_ARGS宏中,以及类中的常规成员变量:

  SLATE_BEGIN_ARGS(SSlateSlice)
  {}
  SLATE_ARGUMENT(FSlateBrush*, Brush)
  SLATE_ARGUMENT(float, Angle)
  SLATE_ARGUMENT(float, ArcSize)
  SLATE_END_ARGS()
  
  ...
  
protected:
  FInvalidatableBrushAttribute Brush;
  float Angle;
  float ArcSize;  

RebuildWidget方法需要将这些属性从 UMG Widget传递给 Slate Widget

TSharedRef<SWidget> USlice::RebuildWidget()
{
  MySlice = SNew(SSlateSlice)
    .Brush(&Brush)
    .Angle(Angle)
    .ArcSize(ArcSize);
  return MySlice.ToSharedRef();
}

Slate Widget现在将这些属性作为Construct方法的参数获取:

void SSlateSlice::Construct(const FArguments& InArgs)
{
  Brush = FInvalidatableBrushAttribute(InArgs._Brush);
  Angle = InArgs._Angle;
  ArcSize = InArgs._ArcSize;
}

在类头中定义的每个SLATE_ARGUMENT作为成员添加到FArguments结构中,并在其名称前加上下划线。Construct方法只是将这些值复制到类成员变量中,以便稍后在渲染Widget时使用它们。

4、Widget渲染

现在我们终于可以添加OnPaint方法了,它处理Widget的实际渲染。

在这一点上,我不会详细介绍所有参数 - 以下是我们在以下方法中引用的参数:

  • AllottedGeometry为我们提供了Widget的位置和大小
  • OutDrawElements接收用于渲染Widget的绘制元素
  • InWidgetStyle为我们提供了应该应用的颜色和不透明度

FSlateDrawElement 是渲染 Slate Widget的主力。它具有绘制框、线、文本、样条线等的方法。我们将在这里使用顶点(在 2D 空间中)和索引来渲染任意网格。

我们需要为顶点填充 FSlateVertex 结构的 TArray,以及定义从顶点构建的三角形的 SlateIndex (uint32) 索引值的 TArray。

为了渲染切片,我们在中心添加一个顶点,然后沿弧添加边缘顶点。然后索引缓冲区使用中心和一对连续的边顶点定义三角形。每个顶点的颜色是通过使用通过小部件样式传入的颜色和不透明度以及小部件的整体颜色和不透明度来调制画笔颜色来计算的。

如果画笔使用图像,我们也可以为顶点设置 UV 坐标——我暂时不考虑它。

int32 SSlateSlice::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
  FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle,
  bool bParentEnabled) const
{
  const FVector2D Pos = AllottedGeometry.GetAbsolutePosition();
  const FVector2D Size = AllottedGeometry.GetAbsoluteSize();
  const FVector2D Center = Pos + 0.5 * Size;
  const float Radius = FMath::Min(Size.X, Size.Y) * 0.5f;

  const FSlateBrush* SlateBrush = Brush.GetImage().Get();
  FLinearColor LinearColor = ColorAndOpacity.Get() * InWidgetStyle.GetColorAndOpacityTint() * SlateBrush->GetTint(InWidgetStyle);
  FColor FinalColorAndOpacity = LinearColor.ToFColor(true);

  const int NumSegments = FMath::RoundToInt(ArcSize / 10.0f);
  TArray<FSlateVertex> Vertices;
  Vertices.Reserve(NumSegments + 3);

  // Add center vertex
  Vertices.AddZeroed();
  FSlateVertex& CenterVertex = Vertices.Last();

  CenterVertex.Position = Center;
  CenterVertex.Color = FinalColorAndOpacity;

  // Add edge vertices
  for (int i = 0; i < NumSegments + 2; ++i)
  {
    const float CurrentAngle = FMath::DegreesToRadians(ArcSize * i / NumSegments + Angle);
    const FVector2D EdgeDirection(FMath::Cos(CurrentAngle), FMath::Sin(CurrentAngle));
    const FVector2D OuterEdge(Radius*EdgeDirection);

    Vertices.AddZeroed();
    FSlateVertex& OuterVert = Vertices.Last();

    OuterVert.Position = Center + OuterEdge;
    OuterVert.Color = FinalColorAndOpacity;
  }

  TArray<SlateIndex> Indices;
  for (int i = 0; i < NumSegments; ++i)
  {
    Indices.Add(0);
    Indices.Add(i);
    Indices.Add(i + 1);
  }

  const FSlateResourceHandle Handle = FSlateApplication::Get().GetRenderer()->GetResourceHandle( *SlateBrush );
  FSlateDrawElement::MakeCustomVerts(
        OutDrawElements,
        LayerId,
        Handle,
        Vertices,
        Indices,
        nullptr,
        0,
        0
    );
  return LayerId;
}

这允许我们以不同的大小和颜色渲染切片:

5、更新Widget属性

为了使编辑器正常工作,我们需要在 Slate 小部件中的属性在 UMG 小部件中发生更改时更新它们。我们通过重写SynchronizeProperties函数来做到这一点:

void USlice::SynchronizeProperties()
{
  Super::SynchronizeProperties();
  MySlice->SetBrush(&Brush);
  MySlice->SetAngle(Angle);
  MySlice->SetArcSize(ArcSize);
}

Slate 小部件需要以下 setter 函数:

void SSlateSlice::SetBrush(FSlateBrush* InBrush)
{
  Brush.SetImage(*this, InBrush);
}

void SSlateSlice::SetAngle(float InAngle)
{
  Angle = InAngle;
}

void SSlateSlice::SetArcSize(float InArcSize)
{
  ArcSize = InArcSize;
}

现在,只要在编辑器中更改任何属性,切片就会立即更新。

6、设置Widget类别

除非另行指定,否则Widget在 UMG 设计器的Widget面板中显示为未分类。要设置类别,请添加对GetPaletteCategory函数的覆盖:

#if WITH_EDITOR
const FText USlice::GetPaletteCategory()
{
  return LOCTEXT("CustomPaletteCategory", "My custom category!");
}
#endif

7、后续工作

本文涉及的源码可以从 GitHub 库中下载。

在能够与 Unreal 中的常规 UMG Widget相提并论之前,仍有许多工作要做。例如,属性不支持动画,也不支持属性绑定。我也没有讨论鼠标事件——UMG(和 Slate)中的所有Widget都是矩形的,因此在不规则形状的Widget上正确处理鼠标需要一些技巧。

实现自定义Widget的一个潜在原因是优化 - 例如,实现一个组合多个元素呈现的单个Widget可能会更快 - 我将在不久的将来探讨这一点。


原文链接:Custom widgets in Unreal

BimAnt翻译整理,转载请标明出处