UE4.25 Slate源码解读

虚幻大学 xuhss 226℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

概述

Slate系统是UE的一套UI解决方案,UMG系统也是依赖Slate系统实现的。
问题:

  • Slate系统是如何组织的?
    • 控件树的父子关系是如何绑定的?
  • Slate系统是如何渲染的?
    • slate渲染结构和流程是如何组织的?
    • 如何进行合批?

结构

SWidget控件类型

SWidget是Slate系统中所有控件的父类。

控件有三种类型。
叶控件 - 不带子槽的控件。如显示一块文本的 STextBlock。其原生便了解如何绘制文本。
面板 - 子槽数量为动态的控件。如垂直排列任意数量子项,形成一些布局规则的 SVerticalBox。
合成控件 - 子槽显式命名、数量固定的控件。如拥有一个名为 Content 的槽(包含按钮中所有控件)的 SButton。
-- 官方文档

386915bffdc556a1711e7afc284ac8f8 - UE4.25 Slate源码解读

也有一些其他控件直接继承自SWidget,情况比较特殊,暂时忽略。

SWidget 控件树实现

上述控件三种类型中,其中SPanel、SCompoundWidget可以作为父节点,控件之间的父子关系是依赖Slot实现的。父控件引用Slot,Slot引用子控件并且保留子控件相对于父控件的布局信息。UMG的控件树的实现方式类似,以UCanvasPanel为例:

UCanvasPanel 控件树相关源码分析

相关类图
f2971029c23a7bc7485dd7c0022aa35a - UE4.25 Slate源码解读

  • UCanvasPanel有一个SConsntraintCanvas的引用,UCanvasPanel功能依赖SConsntraintCanvas实现。(组合关系)
Class UMG_API UCanvasPanel : public UPanelWidget
{
    // ...
protected:
    TSharedPtr<class SConstraintCanvas> MyCanvas;
    // ...
}
  • UCanvasPanel有一个Slot容器,AddChild会生成Slot并与Child互相绑定引用,然后把Slot放入Slot容器。
UCanvasPanelSlot* UCanvasPanel::AddChildToCanvas(UWidget* Content)
{
    return Cast( Super::AddChild(Content) );
}
class UMG\_API UPanelWidget : public UWidget
{
    // ...
protected:
    TArray Slots;
 // ...
}

UPanelSlot* UPanelWidget::AddChild(UWidget* Content)
{
 // ...
 UPanelSlot* PanelSlot = NewObject(this, GetSlotClass(), NAME\_None, NewObjectFlags);
 PanelSlot->Content = Content;
 PanelSlot->Parent = this;

 Content->Slot = PanelSlot;

 Slots.Add(PanelSlot);

 OnSlotAdded(PanelSlot);

 InvalidateLayoutAndVolatility();

 return PanelSlot;
}
  • 当UCanvasPanel增加一个UCanvasPanelSlot,其SConstraintCanvas引用也响应的添加一个FSlot(SConstraintCanvas::FSlot),且UCanvasPanelSlot保存FSlot的引用。
void UCanvasPanel::OnSlotAdded(UPanelSlot* InSlot)
{
    // Add the child to the live canvas if it already exists
    if ( MyCanvas.IsValid() )
    {
        CastChecked(InSlot)->BuildSlot(MyCanvas.ToSharedRef());
 }
}
class UMG\_API UCanvasPanelSlot : public UPanelSlot
{
// ...
private:
    SConstraintCanvas::FSlot* Slot;
// ...
}

void UCanvasPanelSlot::BuildSlot(TSharedRef Canvas)
{
    Slot = &Canvas->AddSlot()
        [
            Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget()
        ];

    SynchronizeProperties();
}
class SLATE\_API SConstraintCanvas : public SPanel
{
public:
    class FSlot : public TSlotBase { /* Offset,Anchors,Alignment 等布局数据... */ }
 // ...
protected:
 TPanelChildren< FSlot > Children;
 // ...
public:
 FSlot& AddSlot()
 {
 Invalidate(EInvalidateWidget::Layout);

 SConstraintCanvas::FSlot& NewSlot = *(new FSlot());
 this->Children.Add( &NewSlot );
 return NewSlot;
 }
 // ...
}
  • 当修改UCanvasPanelSlot的属性时,通用引用也修改了SConstraintCanvas::FSlot对应的属性。
void UCanvasPanelSlot::SetOffsets(FMargin InOffset)
{
    LayoutData.Offsets = InOffset;
    if ( Slot )
    {
        Slot->Offset(InOffset);
    }
}

渲染

Slate渲染由Game线程驱动,收集渲染单元并转换成渲染参数打包推送到渲染线程,渲染线程依据渲染参数分批生成RHICommand,RHIConmand调用图形库API设置渲染状态和绘制。

  • RHICommand是多态的,提供了OpenGL,D3D,Vulkan等多个图像库对应的子类。

83e97066fd92bc7af421a0da5cd1938e - UE4.25 Slate源码解读

渲染流程图

950251accba3a597fd5732e7ff0f3bb9 - UE4.25 Slate源码解读

渲染相关类图

4f2f9b7c1adc731772e21687010a6f09 - UE4.25 Slate源码解读

FSlateApplication::PrivateDrawWindows

遍历所有Window,收集渲染图元信息。

FSlateApplication::DrawPrepass

对控件树进行中序遍历,缓存每个控件的DesiredSize,给后面DrawWindowAndChildren遍历时使用。ComputeDesiredSize行为是多态的,例如:

  • SImage 依据ImageBrush->ImageSize计算。
  • SConstraintCanvas 依据子控件布局计算。

FSlateApplication::DrawWindowAndChildren

从树根开始,依据每个节点的遍历策略遍历,调用Paint函数收集图元信息保存在上下文中。OnPaint行为是多态的,例如:

  • SConstraintCanvas 先遍历计算孩子的布局信息,再遍历孩子的Paint方法。
  • SImage 会调用FSlateDrawElement::MakeBox等方法计算计算自身的图元信息保存在上下文中。

FDrawWindowArgs

  • FSlateDrawBuffer 负载所有Window的图元信息。
  • FSlateWindowElementList 负载Window内所有图元信息。
  • FSlateDrawElement 负载一个元素的图元信息

以SImage的OnPaint为例:

void FSlateApplication::DrawWindowAndChildren( const TSharedRef& WindowToDraw, FDrawWindowArgs& DrawWindowArgs )
{
 // ...
 FSlateWindowElementList& WindowElementList = DrawWindowArgs.OutDrawBuffer.AddWindowElementList(WindowToDraw);
 // ...
 MaxLayerId = WindowToDraw->PaintWindow(
 GetCurrentTime(),
 GetDeltaTime(),
 WindowElementList,
 FWidgetStyle(),
 WindowToDraw->IsEnabled());
 // ...
}
int32 SImage::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
    // ...
    FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), ImageBrush, DrawEffects, FinalColorAndOpacity);
    // ...
    return LayerId;
}
FSlateDrawElement& FSlateDrawElement::MakeBoxInternal(
    FSlateWindowElementList& ElementList,
    uint32 InLayer,
    const FPaintGeometry& PaintGeometry,
    const FSlateBrush* InBrush,
    ESlateDrawEffect InDrawEffects,
    const FLinearColor& InTint
)
{
    EElementType ElementType = (InBrush->DrawAs == ESlateBrushDrawType::Border) ? EElementType::ET_Border : EElementType::ET_Box;

    FSlateDrawElement& Element = ElementList.AddUninitialized();

    const FMargin& Margin = InBrush->GetMargin();
    FSlateBoxPayload& BoxPayload = ElementList.CreatePayload(Element);

 Element.Init(ElementList, ElementType, InLayer, PaintGeometry, InDrawEffects);

 BoxPayload.SetTint(InTint);
 BoxPayload.SetBrush(InBrush);

 return Element;
}

SImage调用了FSlateDrawElement::MakeBox令FSlateWindowElementList增加一个FSlateDrawElement并将自身的图元信息保存其中。

FSlateRHIRenderer::DrawWindows_Private

  • 调用FSlateElementBatcher::AddElements生成渲染参数(顶点数组,索引数组,shader相关参数...)
  • 生成渲染命令闭包放到RHI渲染命令队列中,供渲染线程取出调用。
void FSlateRHIRenderer::DrawWindows_Private(FSlateDrawBuffer& WindowDrawBuffer)
{
    // ...
    for (int32 ListIndex = 0; ListIndex < WindowElementLists.Num(); ++ListIndex)
    {
        // ...
        ElementBatcher->AddElements(ElementList);
        // ...

        // ...
        if (GIsClient && !IsRunningCommandlet() && !GUsingNullRHI)
        {
            ENQUEUE\_RENDER\_COMMAND(SlateDrawWindowsCommand)(
                [Params, ViewInfo](FRHICommandListImmediate& RHICmdList)
                {
                    Params.Renderer->DrawWindow\_RenderThread(RHICmdList, *ViewInfo, *Params.WindowElementList, Params);
                }
            );
        }
    // ...
}

FSlateElementBatcher::AddElements

将 FSlateApplication::PrivateDrawWindows 阶段生成的 FSlateDrawElement 所负载的图元信息,转换成渲染所需的参数封装到FSlateRenderBatch中,放入FSlateWindowElementList的FSlateBatchData成员中,对于缓存/未缓存的数据有不同的处理策略:

void FSlateElementBatcher::AddElements(FSlateWindowElementList& WindowElementList)
{
    // ...
    AddElementsInternal(WindowElementList.GetUncachedDrawElements(), ViewportSize);

    // ...
    const TArrayViewconst> CachedElementDataList = WindowElementList.GetCachedElementDataList();

 if(CachedElementDataList.Num())
 {
 for (FSlateCachedElementData* CachedElementData : CachedElementDataList)
 {
 AddCachedElements(*CachedElementData, ViewportSize);
 }
 }
 // ...
}
  • 未缓存的调用AddElements,AddElements调用AddElementsInternal生成和封装渲染参数,放入FSlateWindowElementList的FSlateBatchData成员中。
void FSlateElementBatcher::AddElementsInternal(const FSlateDrawElementArray& DrawElements, const FVector2D& ViewportSize)
{
    for (const FSlateDrawElement& DrawElement : DrawElements)
    {
        switch ( DrawElement.GetElementType() )
        {
        case EElementType::ET_Box:
        {
            SCOPED\_NAMED\_EVENT\_TEXT("Slate::AddBoxElement", FColor::Magenta);
            STAT(ElementStat_Boxes++);
            DrawElement.IsPixelSnapped() ? AddBoxElement<ESlateVertexRounding::Enabled>(DrawElement) : AddBoxElement<ESlateVertexRounding::Disabled>(DrawElement);
        }
        // ...
    }
}
template
void FSlateElementBatcher::AddBoxElement(const FSlateDrawElement& DrawElement)
{
 const FSlateBoxPayload& DrawElementPayload = DrawElement.GetDataPayload();
 const FColor Tint = PackVertexColor(DrawElementPayload.GetTint());
 const FSlateRenderTransform& ElementRenderTransform = DrawElement.GetRenderTransform();
 // ...

 RenderBatch.AddVertex( FSlateVertex::Make( RenderTransform, FVector2D( Position.X, Position.Y ), LocalSize, DrawScale, FVector4(StartUV, Tiling), Tint ) ); //0
 RenderBatch.AddVertex( FSlateVertex::Make( RenderTransform, FVector2D( Position.X, TopMarginY ), LocalSize, DrawScale, FVector4(FVector2D( StartUV.X, TopMarginV ), Tiling), Tint ) ); //1
 // ...

 RenderBatch.AddIndex( IndexStart + 0 );
 RenderBatch.AddIndex( IndexStart + 1 );
 // ...
}
  • 已缓存的调用AddCachedElements:
    • 遍历 ListsWithNewData 中的FSlateDrawElement,调用AddElementsInternal生成和封装渲染参数,放入FSlateWindowElementList的FSlateBatchData成员中。
    • 直接将 CachedElementData 中所有FSlateRenderBatch放入FSlateWindowElementList的FSlateBatchData成员中。
void FSlateElementBatcher::AddCachedElements(FSlateCachedElementData& CachedElementData, const FVector2D& ViewportSize)
{
    // ...
    for (FSlateCachedElementList* List : CachedElementData.ListsWithNewData)
    {
        // ...
        AddElementsInternal(List->DrawElements, ViewportSize);
        // ...
    }
    // ...
    BatchData->AddCachedBatches(CachedElementData.GetCachedBatches());
    // ...
}

DrawWindow_RenderThread

合并和处理批次,提交渲染参数,调用渲染相关API进行绘制。

void FSlateRHIRenderer::DrawWindow\_RenderThread(FRHICommandListImmediate& RHICmdList, FViewportInfo& ViewportInfo, FSlateWindowElementList& WindowElementList, const struct FSlateDrawWindowCommandParams& DrawCommandParams)
{
    // ...
    RenderingPolicy->BuildRenderingBuffers(RHICmdList, BatchData);

    // ...
    RenderingPolicy->DrawElements
            (
                RHICmdList,
                BackBufferTarget,
                BackBuffer,
                PostProcessBuffer,
                ViewportInfo.bRequiresStencilTest ? ViewportInfo.DepthStencil : EmptyTarget,
                BatchData.GetFirstRenderBatchIndex(),
                BatchData.GetRenderBatches(),
                RenderParams
            );

    // ...
    RHICmdList.EndDrawingViewport(ViewportInfo.ViewportRHI, true, DrawCommandParams.bLockToVsync);
    // ...
}

FSlateRHIRenderingPolicy::BuildRenderingBuffers

合并批次并收集所有batch的顶点/索引数据分别填充到数组中(方便后面一次性提交给GPU)。

void FSlateRHIRenderingPolicy::BuildRenderingBuffers(FRHICommandListImmediate& RHICmdList, FSlateBatchData& InBatchData)
{
    // ...
    InBatchData.MergeRenderBatches();

    // ...
    uint32 RequiredVertexBufferSize = NumBatchedVertices * sizeof(FSlateVertex);
    uint8* VertexBufferData = (uint8*)InRHICmdList.LockVertexBuffer(VertexBuffer, 0, RequiredVertexBufferSize, RLM_WriteOnly);

    uint32 RequiredIndexBufferSize = NumBatchedIndices * sizeof(SlateIndex);
    uint8* IndexBufferData = (uint8*)InRHICmdList.LockIndexBuffer(IndexBuffer, 0, RequiredIndexBufferSize, RLM_WriteOnly);

    FMemory::Memcpy(VertexBufferData, LambdaFinalVertexData.GetData(), RequiredVertexBufferSize);
    FMemory::Memcpy(IndexBufferData, LambdaFinalIndexData.GetData(), RequiredIndexBufferSize);
    // ...
}
  • 调用FSlateBatchData::MergeRenderBatches设置批次顶点/索引偏移(每次绘制时按照偏移读取一段数据进行绘制)并进行合批,注意合批条件:
    • TestBatch.GetLayer() == CurBatch.GetLayer()
    • CurBatch.IsBatchableWith(TestBatch)
void FSlateBatchData::MergeRenderBatches()
{
    // ...
    FillBuffersFromNewBatch(CurBatch, FinalVertexData, FinalIndexData);
    // ...
    if (CurBatch.bIsMergable)
    {
        for (int32 TestIndex = BatchIndex + 1; TestIndex < BatchIndices.Num(); ++TestIndex)
        {
            const TPair<int32, int32>& NextBatchIndexPair = BatchIndices[TestIndex];
            FSlateRenderBatch& TestBatch = RenderBatches[NextBatchIndexPair.Key];
            if (TestBatch.GetLayer() != CurBatch.GetLayer())
            {
                // none of the batches will be compatible since we encountered an incompatible layer
                break;
            }
            else if (!TestBatch.bIsMerged && CurBatch.IsBatchableWith(TestBatch))
            {
                CombineBatches(CurBatch, TestBatch, FinalVertexData, FinalIndexData);

                check(TestBatch.NextBatchIndex == INDEX_NONE);

            }
        }
    }
    // ...
}

void FSlateBatchData::FillBuffersFromNewBatch(FSlateRenderBatch& Batch, FSlateVertexArray& FinalVertices, FSlateIndexArray& FinalIndices)
{
    if(Batch.HasVertexData())
    {
        const int32 SourceVertexOffset = Batch.VertexOffset;
        const int32 SourceIndexOffset = Batch.IndexOffset;

        // At the start of a new batch, just direct copy the verts
        // todo: May need to change this to use absolute indices
        Batch.VertexOffset = FinalVertices.Num();
        Batch.IndexOffset = FinalIndices.Num();

        FinalVertices.Append(&(*Batch.SourceVertices)[SourceVertexOffset], Batch.NumVertices);
        FinalIndices.Append(&(*Batch.SourceIndices)[SourceIndexOffset], Batch.NumIndices);
    }
}
bool IsBatchableWith(const FSlateRenderBatch& Other) const
{
    return
        ShaderResource == Other.ShaderResource
        && DrawFlags == Other.DrawFlags
        && ShaderType == Other.ShaderType
        && DrawPrimitiveType == Other.DrawPrimitiveType
        && DrawEffects == Other.DrawEffects
        && ShaderParams == Other.ShaderParams
        && InstanceData == Other.InstanceData
        && InstanceCount == Other.InstanceCount
        && InstanceOffset == Other.InstanceOffset
        && DynamicOffset == Other.DynamicOffset
        && CustomDrawer == Other.CustomDrawer
        && SceneIndex == Other.SceneIndex
        && ClippingState == Other.ClippingState;
}

FRHICommandList::BeginDrawingViewport

调用FRHICommandListImmediate::ImmediateFlush提交上文提到的所有顶点/索引数组等渲染状态信息。

void FRHICommandList::BeginDrawingViewport(FRHIViewport* Viewport, FRHITexture* RenderTargetRHI)
{
    // ...
    FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThread);
    // ...
}
FORCEINLINE\_DEBUGGABLE void FRHICommandListImmediate::ImmediateFlush(EImmediateFlushType::Type FlushType)
{
    // ...
    GRHICommandList.ExecuteList(*this); // 执行并销毁所有命令
    // ...
}

FSlateRHIRenderingPolicy::DrawElements

为每一个批次生成渲染状态信息和绘制相关RHI命令。

void FSlateRHIRenderingPolicy::DrawElements(
 FRHICommandListImmediate& RHICmdList,
 FSlateBackBuffer& BackBuffer,
 FTexture2DRHIRef& ColorTarget,
 FTexture2DRHIRef& PostProcessTexture,
 FTexture2DRHIRef& DepthStencilTarget,
 int32 FirstBatchIndex,
 const TArray& RenderBatches,
 const FSlateRenderingParams& Params)
{
    // ...
    while (NextRenderBatchIndex != INDEX_NONE)
    {
        // ...
        RHICmdList.SetStreamSource(0, VertexBufferPtr->VertexBufferRHI, RenderBatch.VertexOffset * sizeof(FSlateVertex));
        RHICmdList.DrawIndexedPrimitive(IndexBufferPtr->IndexBufferRHI, 0, 0, RenderBatch.NumVertices, RenderBatch.IndexOffset, PrimitiveCount, RenderBatch.InstanceCount);
        // ...
    }
    // ...
}

FRHICommandList::EndDrawingViewport

再次调用FRHICommandListImmediate::ImmediateFlush执行并销毁所有命令,调用图形库API提交所有渲染状态和绘制命令。

FD3D11DynamicRHI::RHIDrawIndexedPrimitive

绘制命令调用FD3D11DynamicRHI::RHIDrawIndexedPrimitive最终调到ID3D11DeviceContext::DrawIndexed调用图形库API进行绘制。

拓展阅读

转载请注明:xuhss » UE4.25 Slate源码解读

喜欢 (0)

您必须 登录 才能发表评论!