UE Slate渲染流程(上)
- 作者:admin
- /
- 时间:10月30日
- /
- 浏览:366 次
- /
- 分类:厚积薄发
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
一、前言
本文将深入探讨Slate的渲染流程及其相关细节。将详细讲解Slate如何将UI元素渲染到屏幕上,以及它是如何处理各种渲染细节以实现高效、灵活的UI渲染。此外还将讨论Slate的一些缺点,希望能够帮助你更全面地了解这个框架。如有错误还请多多评论指正。
二、渲染数据的准备
首先来看看渲染数据是怎么准备的,流程如下所示:
已知在FSlateApplication::PrivateDrawWindows会对每个SWindow调用FSlateApplication::DrawWindowAndChildren来收集其中的所有控件的图元信息,所以我们就从这里开始!
DrawWindowAndChildren
其实DrawWindowAndChildren内的做的事情很简单,就是去让每一个SWindow的所有子控件完成渲染。具体代码如下:
void FSlateApplication::DrawWindowAndChildren(constTSharedRef<SWindow>&WindowToDraw,FDrawWindowArgs&DrawWindowArgs){
boolbDrawChildWindows= PLATFORM_MAC;
if(bRenderOffScreen ||(WindowToDraw->IsVisible()&&(!WindowToDraw->IsWindowMinimized()||FApp::UseVRFocus()))){
FSlateWindowElementList&WindowElementList=DrawWindowArgs.OutDrawBuffer.AddWindowElementList(WindowToDraw);
int32MaxLayerId=0;
......
MaxLayerId=WindowToDraw->PaintWindow(GetCurrentTime(),GetDeltaTime(),WindowElementList,......);
.......
bDrawChildWindows =true;
}
if(bDrawChildWindows){
constTArray<TSharedRef<SWindow>>WindowChildren=WindowToDraw->GetChildWindows();
for(int32 ChildIndex=0;ChildIndex<WindowChildren.Num();++ChildIndex){
DrawWindowAndChildren(WindowChildren[ChildIndex],DrawWindowArgs);
}
}
}
首先判断当前窗口是否可见。如果窗口不可见且没有处于离屏渲染模式,那么跳过这个窗口的渲染。但是在Mac平台上,子窗口始终会被绘制,无论父窗口是否可见。
假如窗口可见的话,则完成以下操作:
创建一个FSlateWindowElementList对象,用于存储本次渲染所有的图元信息。
调用SWindow::PaintWindow函数来绘制窗口和其所有子控件,并将其添加到FSlateWindowElementList中。并且会返回一个LayerID。
将bDrawChildWindows设置为true,父窗口都渲染了,子窗口当然也得渲染。
如果bDrawChildWindows为true,则遍历所有子窗口,并调用DrawWindowAndChildren来绘制每个子窗口。
PaintWindow
接下来就是SWindow::PaintWindow,来看看它都做了什么吧!代码如下所示:
int32 SWindow::PaintWindow(doubleCurrentTime,floatDeltaTime,FSlateWindowElementList&OutDrawElements,constFWidgetStyle&InWidgetStyle, bool bParentEnabled ){
......
constboolHittestCleared=HittestGrid->SetHittestArea(GetPositionInScreen(),GetViewportSize());
FPaintArgsPaintArgs(nullptr, GetHittestGrid(),GetPositionInScreen(),CurrentTime,DeltaTime);
FSlateInvalidationContextContext(OutDrawElements, InWidgetStyle);
Context.bParentEnabled = bParentEnabled;
Context.bAllowFastPathUpdate = bAllowFastUpdate &&GSlateEnableGlobalInvalidation;
Context.LayoutScaleMultiplier=FSlateApplicationBase::Get().GetApplicationScale()*GetDPIScaleFactor();
Context.PaintArgs=&PaintArgs;
Context.IncomingLayerId=0;
Context.CullingRect=GetClippingRectangleInWindow();
......
FSlateInvalidationResultResult=PaintInvalidationRoot(Context);
.......
returnResult.MaxLayerIdPainted;
}
首先调用SetHittestArea主要用于更新窗口的点击区域。这个区域包括窗口在屏幕中的位置以及窗口的大小。HittestGrid会根据窗口的大小和位置来判断是否需要更新点击区域。
根据HittestGrid来构建FPaintArgs对象,这是为了在渲染过程中提供有关点击区域的信息,确保控件可以访问到这些信息以正确处理用户输入事件。
创建一个FSlateInvalidationContext对象,用于存储与渲染相关的上下文信息,如图元列表、父控件的可见性以及上面的FPaintArgs对象。
调用PaintInvalidationRoot函数并传入FSlateInvalidationContext对象来绘制窗口及其所有子控件。
这个流程主要处理一些渲染上下文的设置,做好渲染所有控件的前期准备。
PaintInvalidationRoot
接着来到PaintInvalidationRoot,其实这里涉及到Slate中一个重要的优化手段,那就是SInvalidationPanel。现在Slate的逻辑是每帧重新渲染所有的控件,这当然会带来大量的性能浪费,因为某些控件的变化频率并没有那么高,无需每帧更新,SInvalidationPanel则是当控件的内容发生变化时,只需重新渲染发生变化的部分,而不是整个面板。这可以显著减少UI布局侧的计算消耗,从而提高性能。具体代码如下所示:
FSlateInvalidationResult FSlateInvalidationRoot::PaintInvalidationRoot(constFSlateInvalidationContext&Context){
......
FSlateInvalidationResultResult;
.....
TSharedRef<SWidget>RootWidget=GetRootWidget();
......
if(!Context.bAllowFastPathUpdate || bNeedsSlowPath ||GSlateIsInInvalidationSlowPath){
.....
CachedMaxLayerId=PaintSlowPath(Context);
Result.bRepaintedWidgets =true;
}
elseif(!FastWidgetPathList->IsEmpty()){
Result.bRepaintedWidgets =PaintFastPath(Context);
}
Result.MaxLayerIdPainted=CachedMaxLayerId;
returnResult;
}
主要分为两个路径,一个是正常路径也就是调用PaintSlowPath来重新渲染所有的控件,其二就是调用PaintFastPath仅仅渲染本次发生变化的控件,这当然对性能更优。但是在本文中还是沿着PaintSlowPath继续往下,可以更加完整地看到Slate是如何渲染的整个流程。
PaintSlowPath
最后终于快到渲染控件的起点了,来看看SWindow::PaintSlowPath都做了什么!代码如下所示:
int32 SWindow::PaintSlowPath(constFSlateInvalidationContext&Context){
HittestGrid->Clear();
constFSlateRectWindowCullingBounds=GetClippingRectangleInWindow();
constint32LayerId=0;
constFGeometryWindowGeometry=GetWindowGeometryInWindow();
int32MaxLayerId=0;
{
MaxLayerId=Paint(*Context.PaintArgs,WindowGeometry,WindowCullingBounds,*Context.WindowElementList,LayerId,Context.WidgetStyle,Context.bParentEnabled);
}
returnMaxLayerId;
}
调用GetClippingRectangleInWindow获取裁剪矩形,裁剪矩形用于确定在窗口中哪些区域需要绘制。
调用GetWindowGeometryInWindow获取窗口的几何信息,几何信息包括窗口的位置、大小等。
初始化LayerId设置为0,因为SWindow是根结点。
调用SWidget::Paint来渲染窗口以及子控件。
之前的流程主要是设置一些Context信息以及全局的优化手段,到这里终于走到了渲染控件的入口。这里需要注意的是SWindow是继承SCompoundWidget的,所以这里调用的Paint是SWidget内的实现。
Paint
最后绕了这么多层,终于来到了SWidget::Paint,这是递归处理Slate控件树的开始,代码如下所示:
int32 SWidget::Paint(constFPaintArgs&Args,constFGeometry&AllottedGeometry,constFSlateRect&MyCullingRect,FSlateWindowElementList&OutDrawElements, int32 LayerId,constFWidgetStyle&InWidgetStyle, bool bParentEnabled)const{
TSharedRef<SWidget>MutableThis=ConstCastSharedRef<SWidget>(AsShared());
bool bClipToBounds, bAlwaysClip, bIntersectClipBounds;
FSlateRectCullingBounds=CalculateCullingAndClippingRules(AllottedGeometry,MyCullingRect, bClipToBounds, bAlwaysClip, bIntersectClipBounds);
FWidgetStyleContentWidgetStyle=FWidgetStyle(InWidgetStyle).BlendOpacity(RenderOpacity);
FGeometryDesktopSpaceGeometry=AllottedGeometry;
DesktopSpaceGeometry.AppendTransform(FSlateLayoutTransform(Args.GetWindowToDesktopTransform()));
if(HasAnyUpdateFlags(EWidgetUpdateFlags::NeedsTick)){
.....
MutableThis->Tick(DesktopSpaceGeometry,Args.GetCurrentTime(),Args.GetDeltaTime());
}
constboolbInheritedHittestability=Args.GetInheritedHittestability();
SWidget*PaintParentPtr= const_cast<SWidget*>(Args.GetPaintParent());
.......
PersistentState.InitialClipState=OutDrawElements.GetClippingState();
PersistentState.LayerId=LayerId;
PersistentState.bParentEnabled = bParentEnabled;
......
PersistentState.WidgetStyle=InWidgetStyle;
PersistentState.CullingBounds=MyCullingRect;
PersistentState.IncomingUserIndex=Args.GetHittestGrid().GetUserIndex();
FPaintArgsUpdatedArgs=Args.WithNewParent(this);
UpdatedArgs.SetInheritedHittestability(bOutgoingHittestability);
OutDrawElements.PushPaintingWidget(*this,LayerId,PersistentState.CachedElementHandle);
.......
int32NewLayerId=OnPaint(UpdatedArgs,AllottedGeometry,CullingBounds,OutDrawElements,LayerId,ContentWidgetStyle, bParentEnabled);
........
returnNewLayerId;
}
首先处理窗口的裁剪矩形,裁剪矩形用于确定在窗口中哪些区域需要绘制。并且还有透明度以及坐标系转换等操作。
处理Tick相关逻辑,如果包含EWidgetUpdateFlags::NeedsTick,则调用Tick函数。
构造FSlateWidgetPersistentState对象,作为一个SWidget的变量,表示Paint时的状态,包含调用Paint所需的信息。
最后调用子类实现的OnPaint函数,来递归调用整个控件树来完成窗口和子控件的渲染操作。
到了这里可以总结一下Slate是怎么渲染这一颗控件树了,实际上是一个深度遍历的操作。流程如下所示:
- SWindow调用SWidget::Paint,最后调用到SWindow::OnPaint。
- 递归调用各自控件Paint函数,并分发给各控件实现的OnPaint。
- 当前控件如果包含子控件则重复第二步的流程。
- 直到所有的控件全部被渲染完毕。
使用SWindow来举例,SWindow::OnPaint实现其实是直接调用SCompoundWidget::OnPaint的实现来渲染子控件的。SCompoundWidget::OnPaint实现如下所示:
int32 SCompoundWidget::OnPaint(constFPaintArgs&Args,constFGeometry&AllottedGeometry,constFSlateRect&MyCullingRect,FSlateWindowElementList&OutDrawElements, int32 LayerId,constFWidgetStyle&InWidgetStyle, bool bParentEnabled )const{
FArrangedChildrenArrangedChildren(EVisibility::Visible);
{
this->ArrangeChildren(AllottedGeometry,ArrangedChildren);
}
if(ArrangedChildren.Num()>0){
FArrangedWidget&TheChild=ArrangedChildren[0];
........
int32Layer=0;
{
Layer=TheChild.Widget->Paint(Args.WithNewParent(this),TheChild.Geometry,MyCullingRect,OutDrawElements,LayerId+1,CompoundedWidgetStyle,ShouldBeEnabled( bParentEnabled ));
}
returnLayer;
}
returnLayerId;
}
因为SCompoundWidget只有一个子控件所以处理很简单(多个子控件也不复杂,无非是变成一个循环),直接调用其子控件的Paint函数去渲染该控件以及它的子控件。直到整个控件树全部都被处理完毕。到这里应该知道Slate是如何去收集到全部控件的信息的,但是到目前为止还不知道怎么设置图元信息的,咱们接着往下走!
各个控件的OnPaint
在Slate中存储单个图元信息的对象是FSlateDrawElement,并且最终都要放入FSlateWindowElementList以供最后的渲染。OnPaint需要做的就是这些。Slate中提供了FSlateDrawElement::MakeXXX各种辅助方法来帮助创建不同类型的图元。
这里还是介绍一下OnPaint这些参数的含义,可以帮助你的理解它们的作用:
int32 OnPaint(const FPaintArgs&Args,
constFGeometry&AllottedGeometry,
constFSlateRect&,
FSlateWindowElementList&OutDrawElements,
int32 LayerId,
constFWidgetStyle&InWidgetStyle,
bool bParentEnabled) const = 0;
FPaintArgs & Args:上面流程中也有提到,包括点击区域以及当前时间和间隔时间以及父控件指针。
FGeometry & AllottedGeometry:分配给该子控件的几何大小(相对于父控件)。
FSlateRect & MyCullingRect:当前控件的剪裁矩形,可用于判断子控件是否在这个矩形内来决定是否渲染。
int32 LayerId:用于标记当前控件的层级,决定其渲染顺序的前后,并且会影响最后的合批操作。
FWidgetStyle & InWidgetStyle:一般最上层父控件(SWindow对象)的传入的样式。
bool bParentEnabled:代表父控件是否已启用。
首先还是通过SImage来举例,这调用FSlateDrawElement::MakeBox来创建一个矩形图元信息,因为是渲染一张图片所以矩形图元来承载足以。代码如下所示:
int32 SImage::OnPaint(constFPaintArgs&Args,constFGeometry&AllottedGeometry,constFSlateRect&MyCullingRect,FSlateWindowElementList&OutDrawElements, int32 LayerId,constFWidgetStyle&InWidgetStyle, bool bParentEnabled )const{
....
FSlateDrawElement::MakeBox(OutDrawElements,LayerId,AllottedGeometry.ToPaintGeometry(),ImageBrush,DrawEffects,FinalColorAndOpacity);
.....
returnLayerId;
}
FSlateDrawElement::MakeBox的实现如下所示:
void FSlateDrawElement::MakeBox(FSlateWindowElementList&ElementList, uint32 InLayer,
constFPaintGeometry&PaintGeometry,
constFSlateBrush*InBrush,
ESlateDrawEffectInDrawEffects,
constFLinearColor&InTint){
......
if(ShouldCull(ElementList,PaintGeometry,InBrush,InTint)){
return;
}
MakeBoxInternal(ElementList,InLayer,PaintGeometry,InBrush,InDrawEffects,InTint);
}
首先调用ShouldCull函数来判定当前控件是否应该被裁剪,这里面其实包含了很多维度的检验,因为如果被裁剪了就没必要放入图元列表,这个检验的维度主要是以下几个方面:
检查尺寸:如果PaintGeometry的大小(宽度或高度)为0,说明图元没有实际大小,因此应该被剔除。
检查裁剪状态:检查当前的裁剪状态。如果裁剪状态具有零区域(即裁剪矩形的宽度或高度为0),说明图元被完全裁剪掉,因此应该被剔除。
检查Brush:如果InBrush的绘制类型为ESlateBrushDrawType::NoDrawType,说明Brush没有实际的绘制内容,因此应该被剔除。
检查资源:检查Brush的资源是否有效。如果资源处于销毁或不可访问的状态,说明Brush无法正常绘制,因此应该被剔除。
检查颜色透明度:如果InTint的透明度为0,说明图元是完全透明的,没有必要渲染,因此应该被剔除。
检查文本:当绘制文本时,如果InText的长度为0,说明没有实际的文本内容,因此应该被剔除。
完成这些检验就代表这是一个真的需要渲染的图元,接着来看FSlateDrawElement::MakeBoxInternal,代码如下所示:
FSlateDrawElement& FSlateDrawElement::MakeBoxInternal(FSlateWindowElementList&ElementList, uint32 InLayer,
constFPaintGeometry&PaintGeometry,
constFSlateBrush*InBrush,
ESlateDrawEffectInDrawEffects,
constFLinearColor&InTint
){
EElementTypeElementType=(InBrush->DrawAs==ESlateBrushDrawType::Border)?EElementType::ET_Border :EElementType::ET_Box;
FSlateDrawElement&Element=ElementList.AddUninitialized();
FSlateBoxPayload&BoxPayload=ElementList.CreatePayload<FSlateBoxPayload>(Element);
BoxPayload.SetTint(InTint);
BoxPayload.SetBrush(InBrush);
Element.Init(ElementList,ElementType,InLayer,PaintGeometry,InDrawEffects);
returnElement;
}
首先确定当前图元类型,只有在Brush的DrawAs为ESlateBrushDrawType::Border的情况下是EElementType::ET_Border,其他的情况下都是EElementType::ET_Box类型。
调用FSlateWindowElementList::AddUninitialized从中获取一个新的FSlateDrawElement对象。
为Box图元创建对应FSlateBoxPayload对象,用于存储与绘制Box类型图元相关的数据(包括Tint,Brush)。
最后调用FSlateDrawElement::Init,完成FSlateDrawElement对象的初始化。
完成这些后一份完整的Box图元信息就已经全部准备好,并放入FSlateWindowElementList对象中,之后就可以交给Slate的渲染线程去操作。其他图元的创建方式也是大同小异,比如STextBlock控件生成的图元,它是通过FSlateDrawElement::MakeText来创建的,代码如下所示:
void FSlateDrawElement::MakeText(FSlateWindowElementList&ElementList, uint32 InLayer,constFPaintGeometry&PaintGeometry,constFString&InText,constFSlateFontInfo&InFontInfo,ESlateDrawEffectInDrawEffects,constFLinearColor&InTint){
PaintGeometry.CommitTransformsIfUsingLegacyConstructor();
......
if(ShouldCull(ElementList,PaintGeometry,InTint,InText)){
return;
}
if(InTint.A ==0&&!InFontInfo.OutlineSettings.IsVisible()){
return;
}
FSlateDrawElement&Element=ElementList.AddUninitialized();
FSlateTextPayload&DataPayload=ElementList.CreatePayload<FSlateTextPayload>(Element);
DataPayload.SetTint(InTint);
DataPayload.SetText(InText,InFontInfo);
Element.Init(ElementList,EElementType::ET_Text,InLayer,PaintGeometry,InDrawEffects);
}
其实这个流程和创建Box类型的图元类似,其他的图元创建流程上都是创建FSlateDrawElement并设置对应类型的DataPayload并完成初始化操作。
三、Geometry
在生成图元信息的流程中看到使用FGeometry对象,它主要用于描述和管理控件的布局和几何信息。FGeometry包含了控件的位置、大小、缩放等等各种信息,它在控件布局、事件处理、坐标变换、子控件传递以及支持复杂变换等方面发挥重要作用。
在正式开始介绍之前,首先来了解一些前置概念,比如还有一个本地空间和屏幕空间的概念:
本地空间(Local Space):本地空间是指控件自身的坐标系。在本地空间中,控件的原点(0,0)通常位于其左上角,x轴从左到右增加,y轴从上到下增加。本地空间主要用于描述控件的内部布局和尺寸。例如,当设置一个控件的宽度和高度时,这些值是相对于控件自身的本地空间的。
屏幕空间(Screen Space):屏幕空间是指整个屏幕或应用程序窗口的坐标系。在屏幕空间中,原点(0,0)通常位于屏幕或窗口的左上角,x轴从左到右增加,y轴从上到下增加。屏幕空间主要用于描述控件在屏幕或窗口上的位置。例如,当设置一个控件的位置时,这个位置是相对于屏幕或窗口的坐标系的。
在Slate框架中,通常需要在本地空间和屏幕空间之间进行坐标转换。例如在处理控件的布局和点击事件时,需要将本地空间中的坐标转换为屏幕空间中的坐标,以便确定控件在屏幕上的实际位置。
接着来介绍一下Slate的FGeometry对象,重要函数和属性如下所示:
USTRUCT(BlueprintType)
struct SLATECORE_API FGeometry
{
GENERATED_USTRUCT_BODY()
public:
......
FORCEINLINE constFVector2D&GetLocalSize()const{returnSize;}
FORCEINLINE constFSlateRenderTransform&GetAccumulatedRenderTransform()const{returnAccumulatedRenderTransform;}
FORCEINLINE FSlateLayoutTransformGetAccumulatedLayoutTransform()const{returnFSlateLayoutTransform(Scale,AbsolutePosition);}
FORCEINLINE FVector2DGetAbsolutePosition()const{returnAccumulatedRenderTransform.TransformPoint(FVector2D::ZeroVector);}
FORCEINLINE FVector2DGetAbsoluteSize()const{returnAccumulatedRenderTransform.TransformVector(GetLocalSize());}
constFVector2D/*Local*/Size;
constfloat/*Absolute*/Scale;
constFVector2DAbsolutePosition;
constFVector2D/*Local*/Position;
private:
FSlateRenderTransformAccumulatedRenderTransform;
const uint8 bHasRenderTransform :1;
};
Size:表示控件在本地空间(Local Space)中的大小,即控件自身坐标系中的宽度和高度。这个字段主要用于描述控件的内部布局和尺寸。
Scale:表示控件的缩放因子。这个字段用于描述控件在屏幕空间(Screen Space)中的缩放程度。这个值是累积的,包括了控件本身以及其所有父控件的缩放。
AbsolutePosition:表示控件在屏幕空间中的位置,即控件相对于屏幕或应用程序窗口的左上角的坐标。这个字段主要用于描述控件在屏幕上的位置。
Position:表示控件在本地空间中的位置。这个字段主要用于描述当前控件相对于其父控件的位置。
AccumulatedRenderTransform:表示从控件的本地空间到屏幕空间的累积渲染变换。这个变换包括了控件本身以及其所有父控件的渲染变换。这个字段主要用于在渲染过程中对控件应用复杂的变换,例如旋转、缩放和平移等。
bHasRenderTransform:一个布尔值,表示控件是否具有渲染变换。这个字段用于在需要时快速检查控件是否具有渲染变换,以便在渲染过程中进行相应的处理。
这里还有一个需要注意的GetAccumulatedLayoutTransform方法,这里返回的是控件的累积布局变换。这个变换表示了从控件的本地空间到父控件空间的变换,包括所有父控件的变换。它和AccumulatedRenderTransform的差别如下:
AccumulatedLayoutTransform:这个变量表示从控件的本地空间到父控件空间的累积布局变换。这个变换包括了控件本身以及其所有父控件的布局变换。主要用于处理控件的布局计算,例如在确定控件在屏幕上的位置时。可以确保控件在屏幕上的正确布局,同时支持复杂的布局变换,例如缩放和平移等。
AccumulatedRenderTransform:这个变量表示从控件的本地空间到屏幕空间的累积渲染变换。这个变换包括了控件本身以及其所有父控件的渲染变换。主要用于在渲染过程中对控件应用复杂的变换,例如旋转、缩放和平移等。可以确保控件在渲染过程中的正确显示,同时支持复杂的渲染变换。
FGeometry可以在控件的层次结构中传递,这可以在OnPaint的实现中看到传递其FGeometry对象,尤其是针对多个子控件的控件会完成其子控件约束后得到正确的FGeometry对象再被传递,这样可以使得子控件可以根据其父控件的FGeometry信息确定自己的布局。这有助于维护控件层次结构的正确布局。
最后再来谈谈生成图元信息时使用到的FPaintGeometry,其实它和FGeometry的差别并不大,并且一般都是由FGeometry转化而来,它们只是应用场景不同,FGeometry主要关注控件的布局和几何信息,而FPaintGeometry主要关注控件的绘制信息和渲染变换。所以在生成图元流程中都会将FGeometry转化为FPaintGeometry再执行后续的流程。
四、渲染线程需要干什么
现在到了如何提交给GPU去完成渲染的时候了,当然UE也为Slate框架封装了一个类来专门处理Slate的渲染,那就是FSlateRenderer。FSlateRenderer是一个抽象基类,用于定义Slate框架的通用渲染接口。为了实现跨平台渲染,UE提供了针对不同图形API和平台的FSlateRenderer的派生类,如FSlateRHIRenderer(它就是Slate的渲染器),当然UE也封装了FSlateD3DRenderer和FSlateOpenGLRenderer,只有特殊情况会启用其它两个。当然还有一个特殊的FSlateNullRenderer,它是一个空渲染器,它不执行任何实际的渲染操作。主要用于不需要图形输出的场景,例如服务器或者某些命令行工具。FSlateRenderer的初始化是在FEngineLoop::PreInitPostStartupScreen中完成的,代码如下所示:
if (!IsRunningDedicatedServer()&&!IsRunningCommandlet()){
TSharedPtr<FSlateRenderer>SlateRenderer=GUsingNullRHI?
FModuleManager::Get().LoadModuleChecked<ISlateNullRendererModule>("SlateNullRenderer").CreateSlateNullRenderer():
FModuleManager::Get().GetModuleChecked<ISlateRHIRendererModule>("SlateRHIRenderer").CreateSlateRHIRenderer();
TSharedRef<FSlateRenderer>SlateRendererSharedRef=SlateRenderer.ToSharedRef();
{
SCOPED_BOOT_TIMING("CurrentSlateApp.InitializeRenderer");
FSlateApplication&CurrentSlateApp=FSlateApplication::Get();
CurrentSlateApp.InitializeRenderer(SlateRendererSharedRef);
}
.......
}
如果是DS或者命令行工具的环境下是不会创建FSlateRenderer的,并且根据GUsingNullRHI是否为true来创建SlateRHIRenderer还是SlateNullRenderer。当然正常情况下都是SlateRHIRenderer。随后调用FSlateApplication::InitializeRenderer完成其初始化,归FSlateApplication对象持有。
bool FSlateApplication::InitializeRenderer(TSharedRef<FSlateRenderer>InRenderer, bool bQuietMode ){
Renderer=InRenderer;
boolbResult=Renderer->Initialize();
......
return bResult;
}
几个重要的类
首先就是FSlateRHIRenderer,一些重要的函数和属性如下所示:
class FSlateRHIRenderer:publicFSlateRenderer{
.......
voidDrawWindow_RenderThread(FRHICommandListImmediate& RHICmdList, FViewportInfo& ViewportInfo, FSlateWindowElementList& WindowElementList, const struct FSlateDrawWindowCommandParams& DrawParams);
voidDrawWindows_Private( FSlateDrawBuffer& InWindowDrawBuffer );
virtual FSlateDrawBuffer&GetDrawBuffer() override;
FSlateDrawBufferDrawBuffers[NumDrawBuffers];
uint8 FreeBufferIndex;
TUniquePtr<FSlateElementBatcher>ElementBatcher;
TSharedPtr<FSlateRHIRenderingPolicy>RenderingPolicy;
......
}
FreeBufferIndex和DrawBuffers:还是经典的多缓冲,因为Slate是多线程渲染的,所以当GPU正在渲染时,为了让CPU侧能够继续工作,就可拿一个新的Buffer来交替使用,即避免了数据竞争问题,又尽可能地榨干CPU侧的性能,体现在Slate的每帧Tick最开始时会调用GetDrawBuffer,获取最新的FSlateDrawBuffer用于后续流程中存储渲染数据。
TSharedPtr RenderingPolicy:它主要封装Slate的RHI渲染逻辑,包括渲染状态的设置、资源管理和DrawCall执行等等。
TUniquePtr ElementBatcher:它是完成Slate合批操作的中枢,它负责将Slate控件转化为RenderBatch中,后面会详细讲到。
FSlateElementBatcher,FSlateBatchData,FSlateRenderBatch
接着来看FSlateElementBatcher,如果需要深入了解得结合FSlateBatchData和FSlateRenderBatch一起来看,它们的三者的关系如下所示:
有了这一张图应该更能帮助你理解:
FSlateRenderBatch类起到了将各类Element组织成Render Batch并提交给GPU进行渲染的关键作用。它存储了Batch中元素的渲染状态、顶点数据、索引数据等,并支持自定义绘制和实例化渲染,以实现高效且灵活的Slate UI元素渲染。FSlateRenderBatch的一些重要属性和方法如下所示是:
class FSlateRenderBatch{
public:
........
public:
.......
FShaderParamsShaderParams;
constFSlateClippingState*ClippingState;
constFSlateShaderResource*ShaderResource;
ICustomSlateElement*CustomDrawer;
int32 LayerId;
int8 SceneIndex;
ESlateBatchDrawFlagDrawFlags;
ESlateShaderShaderType;
**ESlateDrawPrimitiveDrawPrimitiveType;
ESlateDrawEffectDrawEffects;
uint8 bIsMergable :1;
uint8 bIsMerged :1;
.......
};
ShaderParams:FShaderParams结构体定义了一组着色器参数。这些参数在渲染过程中将传递给像素着色器。
ClippingState:指向FSlateClippingState的指针,表示与RenderBatch关联的裁剪状态。主要用于限制Slate控件在屏幕上的可见区域。
ShaderResource:指向FSlateShaderResource的指针,表示与RenderBatch关联的着色器资源。这通常是一个纹理或其他渲染资源。RenderBatch中的所有元素将使用相同的着色器资源进行渲染。
CustomDrawer:ICustomSlateElement类型的指针,表示与Batch关联的自定义渲染逻辑。这用于实现特定于的渲染逻辑。
LayerId:表示Batch所在的Layer。并且LayerId用于对Batch进行排序,以确保正确的绘制顺序。
SceneIndex:表示Batch所在的场景索引。
DrawFlags:表示Batch的绘制标志。这用于控制Batch的渲染行为,例如混合模式、剔除模式等。
ShaderType:表示Batch使用的着色器类型。
DrawPrimitiveType:表示Batch的所渲染的图元类型,图元类型决定了Batch中元素的几何形状,例如三角形列表、线段列表等。
了解FSlateRenderBatch之后,再来看看作为FSlateRenderBatch容器的FSlateBatchData,FSlateBatchData类的主要作用是承载RenderBatch。这些数据在渲染过程中将提交给GPU进行渲染。此外FSlateBatchData还提供了一些方法来完成RenderBatch的合批操作。重要的方法和属性如下所示:
class FSlateBatchData{
public:
.......
SLATECORE_API voidMergeRenderBatches();
FSlateRenderBatch&AddRenderBatch(.......);
private:
voidCombineBatches(FSlateRenderBatch& FirstBatch, FSlateRenderBatch& SecondBatch, FSlateVertexArray& FinalVertices, FSlateIndexArray& FinalIndices);
private:
......
TArray<FSlateRenderBatch>RenderBatches;
FSlateVertexArrayFinalVertexData;
FSlateIndexArrayFinalIndexData;
int32 NumBatches;
};
RenderBatches:是一个用于存储RenderBatches的数组。
FinalVertexData和FinalIndexData:分别表示最终的顶点数据和索引数据。这些数据在渲染过程中将提交给GPU。
NumBatches:表示最终RenderBatch的数量。这与RenderBatches的数组长度并不同,因为它表示经过合批操作后的RenderBatch数量。
AddRenderBatch(...):用于添加一个新的FSlateRenderBatch对象,并且返回该FSlateRenderBatch引用。
MergeRenderBatches():用于合并具有相似渲染状态的RenderBatch,通过合并RenderBatch可以减少渲染时的DrawCall数量,从而提高性能。
CombineBatches(…..):在MergeRenderBatches中被调用,来完成RenderBatch的合并操作(顶点数据的合并操作等)。
最后再来看看FSlateElementBatcher,代码如下所示:
class FSlateElementBatcher{
public:
SLATECORE_API voidAddElements( FSlateWindowElementList& ElementList );
private:
voidAddElementsInternal(const FSlateDrawElementArray& DrawElements, const FVector2D& ViewportSize);
voidAddCachedElements(FSlateCachedElementData& CachedElementData, const FVector2D& ViewportSize);
template<ESlateVertexRoundingRounding>
voidAddQuadElement( const FSlateDrawElement& DrawElement );
template<ESlateVertexRoundingRounding>
voidAddBoxElement( const FSlateDrawElement& DrawElement );
template<ESlateVertexRoundingRounding>
voidAddTextElement( const FSlateDrawElement& DrawElement )
template<ESlateVertexRoundingRounding>
voidAddShapedTextElement( const FSlateDrawElement& DrawElement );
..........
FSlateRenderBatch&CreateRenderBatch(..........);
private:
......
FSlateBatchData*BatchData;
FSlateRenderingPolicy*RenderingPolicy;
......
};
FSlateElementBatcher中包含了FSlateRenderingPolicy指针以及FSlateBatchData指针,并且FSlateBatchData指针是从FSlateWindowElementList中获取的,每个FSlateWindowElementList都包含一个FSlateBatchData对象,并且最后渲染时都会从中获取数据,所以FSlateElementBatcher最重要的还是起到构造FSlateRenderBatch的作用。
后续流程中通过AddElements来生成FSlateRenderBatch对象的,根据图元类型的不同会分发到不同的AddXXXElement,和之前生成图元MakeXXX类似。最后都会调用CreateRenderBatch来构造FSlateRenderBatch对象。关于最后渲染数据是怎么生成的以及谁负责都已经知道,接下来看看具体的Slate如何完成渲染的。
下一篇:UE Slate渲染流程(下)
这是侑虎科技第1703篇文章,感谢作者不知名书杯供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/lllzwj
再次感谢不知名书杯的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)