Browse Source

feat: 添加 Paint 流程

NiceTry12138 6 tháng trước cách đây
mục cha
commit
86e7285f90
3 tập tin đã thay đổi với 220 bổ sung1 xóa
  1. BIN
      UE5/UI/Image/005.png
  2. BIN
      UE5/UI/Image/006.png
  3. 220 1
      UE5/UI/README.md

BIN
UE5/UI/Image/005.png


BIN
UE5/UI/Image/006.png


+ 220 - 1
UE5/UI/README.md

@@ -20,7 +20,7 @@
 
 在 `FEngineLoop::Tick` 中,触发 `FSlateApplication::Get().Tick` 函数,用于 `Slate` 渲染
 
-Slate 的渲染逻辑是每帧重新渲染所有的控件,这当然会带来大量的性能浪费,因为某些控件的变化频率并没有那么高,无需每帧更新,`SInvalidationPanel` 则是当控件的内容发生变化时,只需重新渲染发生变化的部分,而不是整个面板
+`Slate` 的渲染逻辑是每帧重新渲染所有的控件,这当然会带来大量的性能浪费,因为某些控件的变化频率并没有那么高,无需每帧更新,`SInvalidationPanel` 则是当控件的内容发生变化时,只需重新渲染发生变化的部分,而不是整个面板
 
 于是在 `PaintInvalidationRoot` 函数中出现了这么一段内容
 
@@ -57,6 +57,123 @@ if (!Context.bAllowFastPathUpdate || bNeedsSlowPath || GSlateIsInInvalidationSlo
 
 在 `SWindow` 调用到 `SWindow::Paint`,`SWindow` 继承自 `SWidget` 且没有重写 `Paint`,最后还是调用到 `SWidget::Paint` 
 
+### PrePass
+
+![](Image/005.png)
+
+在调用 `DrawWindowAndChildren` 绘制之前,会调用 `DrawPrepass`
+
+```cpp
+// Prepass the window
+DrawPrepass( DrawOnlyThisWindow );
+```
+
+根据注释,可以知道这是在绘制之前的流程
+
+1. 执行自定义流程 `CustomPrepass`
+2. 遍历子节点,对其执行 `Prepass_ChildLoop`
+3. 缓存设计分辨率 `CacheDesiredSize` 
+
+```cpp
+void SWidget::Prepass_Internal(float InLayoutScaleMultiplier)
+{
+	PrepassLayoutScaleMultiplier = InLayoutScaleMultiplier;
+
+	bool bShouldPrepassChildren = true;
+	if (bHasCustomPrepass)
+	{
+		bShouldPrepassChildren = CustomPrepass(InLayoutScaleMultiplier);
+	}
+
+	if (bCanHaveChildren && bShouldPrepassChildren)
+	{
+		FChildren* MyChildren = this->GetChildren();
+		const int32 NumChildren = MyChildren->Num();
+		Prepass_ChildLoop(InLayoutScaleMultiplier, MyChildren);
+		ensure(NumChildren == MyChildren->Num());
+	}
+
+	{
+		CacheDesiredSize(PrepassLayoutScaleMultiplier.Get(1.0f));
+		bNeedsPrepass = false;
+	}
+}
+```
+
+要自定义设计分辨率,需要重写 `ComputeDesiredSize` 函数,这是 `SWidget` 的纯虚函数,每个子类都要重写
+
+```cpp
+virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const = 0;
+```
+
+计算每个控件占据的控件大小,是一个自下而上的计算过程
+
+- 不含子项的控件基于其本质属性计算和缓存其所需的大小
+- 组合其他控件的控件使用特殊逻辑决定其所需的大小
+
+### ArrangeChildren
+
+`ArrangeChildren` 函数负责计算子控件的几何信息并将其添加到 `ArrangedChildren` 列表中
+
+`ArrangeChildren` 会在布局过程中被调用,子类可以重写 `OnArrangeChildren` 以实现自定义的子控件布局逻辑
+
+```cpp
+void SWidget::ArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren, bool bUpdateAttributes) const
+{
+	if (bUpdateAttributes)
+	{
+		FSlateAttributeMetaData::UpdateChildrenOnlyVisibilityAttributes(const_cast<SWidget&>(*this), FSlateAttributeMetaData::EInvalidationPermission::DelayInvalidation, false);
+	}
+
+	OnArrangeChildren(AllottedGeometry, ArrangedChildren);
+}
+```
+
+这里 `OnArrangeChildren` 是一个纯虚函数,需要每个子类重写自己的内容
+
+```cpp
+virtual void OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const = 0;
+```
+
+以 `SCanvas::OnArrangeChildren` 为例
+
+```cpp
+void SCanvas::OnArrangeChildren( const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren ) const
+{
+    for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex)
+    {
+        // ... 做一些计算
+        
+        ArrangedChildren.AddWidget( AllottedGeometry.MakeChild(
+            CurChild.GetWidget(),
+            CurChild.GetPosition() + Offset,
+            Size
+        ));
+    }
+}
+```
+
+以 `SOverlay::OnPaint` 为例,展示使用 `ArrangeChildren` 的使用
+
+```cpp
+for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex)
+{
+    FArrangedWidget& CurArrangedWidget = ArrangedChildren[ChildIndex];
+
+    const int32 CurWidgetsMaxLayerId =
+        CurArrangedWidget.Widget->Paint(
+            NewArgs,
+            CurArrangedWidget.Geometry,
+            MyCullingRect,
+            OutDrawElements,
+            MaxLayerId,
+            InWidgetStyle,
+            bChildrenEnabled);
+}
+```
+
+将 `Widget` 的坐标偏移 和 大小 信息保存到 `ArrangedChildren` 中
+
 ### FGeometry
 
 一个 UI 控件,需要计算自己的尺寸大小、位置坐标,这些信息都可以用 `FGeometry` 来表示
@@ -78,7 +195,109 @@ if (!Context.bAllowFastPathUpdate || bNeedsSlowPath || GSlateIsInInvalidationSlo
 
 在这些计算的时候, `FGeometry` 可以发挥很大的作用
 
+### Paint
+
+根据几何信息(`AllottedGeometry`)和父级剪裁矩形(`MyCullingRect`)计算当前 `Widget` 的剪裁边界(`CullingBounds`)
+
+```cpp
+FSlateRect CullingBounds = CalculateCullingAndClippingRules(AllottedGeometry, MyCullingRect, bClipToBounds, bAlwaysClip, bIntersectClipBounds);
+```
+
+将 `AllottedGeometry` 计算成桌面坐标
+
+混合父级样式和当前 `Widget` 的透明度 
+
+```cpp
+FWidgetStyle ContentWidgetStyle = FWidgetStyle(InWidgetStyle).BlendOpacity(RenderOpacity);
+
+FGeometry DesktopSpaceGeometry = AllottedGeometry;
+DesktopSpaceGeometry.AppendTransform(FSlateLayoutTransform(Args.GetWindowToDesktopTransform()));
+```
+
+根据 `Flag` 指定定时逻辑,更新控件
+
+```cpp
+if (HasAnyUpdateFlags(EWidgetUpdateFlags::NeedsActiveTimerUpdate))
+{
+    // ... do something
+    MutableThis->ExecuteActiveTimers(Args.GetCurrentTime(), Args.GetDeltaTime());
+}
+
+if (HasAnyUpdateFlags(EWidgetUpdateFlags::NeedsTick))
+{
+    // ... do something
+    MutableThis->Tick(DesktopSpaceGeometry, Args.GetCurrentTime(), Args.GetDeltaTime());
+}
+```
+
+> 这里执行 `Tick`,大部分控件的更新逻辑都在 `Tick` 中执行
+
+将当前 `Widget` 压入绘制栈,缓存句柄
+
+如果当前 `Widget` 可见并且支持命中测试,将其添加到命中测试网格 `FHittestGrid` 中
+
+```cpp
+OutDrawElements.PushPaintingWidget(*this, LayerId, PersistentState.CachedElementHandle);
+
+if (bOutgoingHittestability)
+{
+    Args.GetHittestGrid().AddWidget(MutableThis, 0, LayerId, FastPathProxyHandle.GetWidgetSortOrder());
+}
+```
+
+调用 `OnPaint` 由子类自行实现生成实际绘制指令
+
+```cpp
+int32 NewLayerId = OnPaint(UpdatedArgs, AllottedGeometry, CullingBounds, OutDrawElements, LayerId, ContentWidgetStyle, bParentEnabled);
+```
+
+### OnPaint
+
+`SPanel` 和 `SCompoundWidget` 因为存在子节点,需要先调用 `PaintArrangedChildren` 计算每个子节点的坐标、大小
+
+```cpp
+ArrangeChildren(AllottedGeometry, ArrangedChildren);
+```
+
+> 如果是 `SLeafWidget` 则不用,毕竟没有子节点没必要计算
+
+`SPanel` 和 `SCompoundWidget` 再对每个子节点调用 `Paint`
+
+```cpp
+for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex)
+{
+    const FArrangedWidget& CurWidget = ArrangedChildren[ChildIndex];
+
+    if (!IsChildWidgetCulled(MyCullingRect, CurWidget))
+    {
+        const int32 CurWidgetsMaxLayerId = CurWidget.Widget->Paint(NewArgs, CurWidget.Geometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bShouldBeEnabled);
+        MaxLayerId = FMath::Max(MaxLayerId, CurWidgetsMaxLayerId);
+    }
+    else
+    {
+        //SlateGI - RemoveContent
+    }
+}
+```
+
+不过前面都是收集数据、遍历节点,真正绘制的内容其实是下面这样,以 `SButton::OnPaint` 为例
+
+```cpp
+FSlateDrawElement::MakeBox(
+    OutDrawElements,
+    LayerId,
+    AllottedGeometry.ToPaintGeometry(),
+    BrushResource,
+    DrawEffects,
+    BrushResource->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint() * GetBorderBackgroundColor().GetColor(InWidgetStyle)
+);
+```
+
+`FSlateDrawElement` 是 `Slate` 渲染系统的原子绘制单元,封装了 `GPU` 绘制所需的所有元数据
+
+`OutDrawElements` 是由 `SWindow` 创建,用于收集绘制信息。通过遍历所有节点时,由每个节点各自添加各自的绘制指令到 `OutDrawElements` 中
 
+![](Image/006.png)
 
 ## 事件触发