概述
Slate系统是UE的一套UI解决方案,UMG系统也是依赖Slate系统实现的。
问题:
- Slate系统是如何组织的?
- 控件树的父子关系是如何绑定的?
- Slate系统是如何渲染的?
- slate渲染结构和流程是如何组织的?
- 如何进行合批?
结构
SWidget控件类型
SWidget是Slate系统中所有控件的父类。
控件有三种类型。
叶控件 - 不带子槽的控件。如显示一块文本的 STextBlock。其原生便了解如何绘制文本。
面板 - 子槽数量为动态的控件。如垂直排列任意数量子项,形成一些布局规则的 SVerticalBox。
合成控件 - 子槽显式命名、数量固定的控件。如拥有一个名为 Content 的槽(包含按钮中所有控件)的 SButton。
-- 官方文档
也有一些其他控件直接继承自SWidget,情况比较特殊,暂时忽略。
SWidget 控件树实现
上述控件三种类型中,其中SPanel、SCompoundWidget可以作为父节点,控件之间的父子关系是依赖Slot实现的。父控件引用Slot,Slot引用子控件并且保留子控件相对于父控件的布局信息。UMG的控件树的实现方式类似,以UCanvasPanel为例:
UCanvasPanel 控件树相关源码分析
相关类图
- 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<UCanvasPanelSlot>( Super::AddChild(Content) ); }
class UMG_API UPanelWidget : public UWidget { // ... protected: TArray<UPanelSlot*> Slots; // ... } UPanelSlot* UPanelWidget::AddChild(UWidget* Content) { // ... UPanelSlot* PanelSlot = NewObject<UPanelSlot>(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<UCanvasPanelSlot>(InSlot)->BuildSlot(MyCanvas.ToSharedRef()); } }
class UMG_API UCanvasPanelSlot : public UPanelSlot { // ... private: SConstraintCanvas::FSlot* Slot; // ... } void UCanvasPanelSlot::BuildSlot(TSharedRef<SConstraintCanvas> Canvas) { Slot = &Canvas->AddSlot() [ Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget() ]; SynchronizeProperties(); }
class SLATE_API SConstraintCanvas : public SPanel { public: class FSlot : public TSlotBase<FSlot> { /* 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等多个图像库对应的子类。
渲染流程图
渲染相关类图
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<SWindow>& 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<FSlateBoxPayload>(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 TArrayView<FSlateCachedElementData* const> 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<ESlateVertexRounding Rounding> void FSlateElementBatcher::AddBoxElement(const FSlateDrawElement& DrawElement) { const FSlateBoxPayload& DrawElementPayload = DrawElement.GetDataPayload<FSlateBoxPayload>(); const FColor Tint = PackVertexColor(DrawElementPayload.GetTint()); const FSlateRenderTransform& ElementRenderTransform = DrawElement.GetRenderTransform(); // ... RenderBatch.AddVertex( FSlateVertex::Make<Rounding>( RenderTransform, FVector2D( Position.X, Position.Y ), LocalSize, DrawScale, FVector4(StartUV, Tiling), Tint ) ); //0 RenderBatch.AddVertex( FSlateVertex::Make<Rounding>( 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<FSlateRenderBatch>& 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进行绘制。