Slate的渲染

Slate是逐窗口渲染,如果是手机的话,就只要画一个就行了。

窗口的创建,依赖于平台,在Windows和Mac上要根据操作系统的API创建,为了屏蔽这个差异性,Unreal使用FGenericWindow作为基类,然后从这个FGenericWindow派生不同平台的窗口子类,比如FWindowsWindow这个类,然后在其内部持有HWND,windows窗口系统的窗口句柄,我们在虚函数里面调用Win32的函数对这个窗口进行操作。

Slate有一个逻辑窗口类,以及相应的平台窗口类,逻辑窗口类是SWindow,也就是正常的控件,它持有一个

TSharedPtr<FGenericWindow> NativeWindow;//native window handle

这样做的目的是,为了让窗口这种依赖于平台的也和其它控件的创建和操作保持一致。

../_images/SWindow.png

这是SWindow的参数类,里面有很多,比如窗口类型(普通的,弹出窗口,模态的),还有风格(FWindowStyle),标题,创建的时候是否居中,左上角位置,是否支持透明,初始的透明度,有很多很多属性。在Construct里面进行一些额外的处理,并且赋值完毕后。

之后,可以通过FSlateApplication的AddWindow函数来创建平台窗口。

virtual TSharedRef<SWindow> AddWindow( TSharedRef<SWindow> InSlateWindow, const bool bShowImmediately = true ) override;//这个bShowImmediately控制这个窗口是否立即创建RenderTarget,用于窗口的绘制
TSharedRef<SWindow> FSlateApplication::AddWindow(TSharedRef<SWindow> InSlateWindow, const bool bShowImmediately = true)
{
    //这个函数会排序,把这个窗口和之前打开过的窗口排个序,保持ZOrder,主要用于鼠标事件的穿透路由,从顶层窗口散播下去
	FSlateWindowHelper::ArrangeWindowToFront(SlateWindows, InSlateWindow);
    //创建平台窗口,为这个SWindow逻辑窗口创建相应的native window handle
	TSharedRef<FGenericWindow> NewWindow = MakeWindow( InSlateWindow, bShowImmediately );
    
    //立即创建back buffer,图形API需要传入native window的句柄,创建back buffer
    if(bShowImmediately)
    {
        InSlateWindow->ShowWindow();//这个函数会创建back buffer
        
        //设置焦点...
    }
    
    return InSlateWindow;
}

ShowWindow这个函数比较重要,里面为会这个窗口创建相应的backbuffer以及相应的正交透视矩阵:

void SWindow::ShowWindow()
{
	if(NativeWindow) //判断关联的native window是否已经创建
	{
		//这个函数,获取application的渲染器,然后创建视口,创建backbuffer
		FSlateApplicationBase::Get().GetRenderer()->CreateViewport(SharedThis(this));
	}
	
	//...
}

Application也和SWindow差不多,也有逻辑Application和平台Application,平台Application主要是针对不同平台子类的Application进行派生,比如FWindowsApplication,它从GenericApplication派生,然后FSlateApplicationBase持有GenericApplication的单例。这样做的目的是处理不同平台的消息派发。

Windows的消息循环在这里,这些在FWindowsApplication里面。

可惜的是,操作系统的鼠标、键盘消息、窗口、图形渲染全是native的,这些都需要进一步封装。

FSlateApplicationBase类持有FSlateRenderer,Slate的渲染器,它同样也有相应的子类,总共3个,FSlateD3DRenderer,FSlateOpenGLRenderer,FSlateRHIRenderer,第一个和第二个是裸着调用图形API的,第三个是封装了图形模块的RHI,这个RHI封装了OpenGL和DirectX和Vulkan等图形API,在不同平台进行切换。Slate可以编写单独的应用,会使用第一个和第二个,如果是正常的流程,则是第三个。

void FSlateRHIRenderer::CreateViewport(const TSharedRef<SWindow> Window)
{
	FViewportInfo* NewInfo = new FViewportInfo();//创建一个FViewportInfo类
    
    //拿到native window handle
    TSharedRef<FGenericWindow> NativeWindow = Window->GetNativeWindow().ToSharedRef();
    NewInfo->OSWindow = NativeWindow->GetOSWindowHandle();//获取native window hand
    NewInfo->Width = Width;
    NewInfo->Height = Height;
    NewInfo->DesiredWidth = Width;
    NewInfo->DesiredHeight = Height;
    //创建正交投影矩阵,画UI用的
    NewInfo->ProjectionMatrix = CreateProjectionMatrix(Width, Height);
    
    //这个viewport存储了backbuffer,像素格式,交换链
    NewInfo->ViewportRHI = RHICreateViewport(NewInfo->OSWindow, Width, Height, bFullscreen, NewInfo->PixelFormat);
    
    WindowToViewportInfo.Add(&Window.Get(), NewInfo);//加入到渲染器的map里面,SWindow做Key,FViewportInfo做值
    //后续逐窗口渲染的时候,通过SWindow查找FViewportInfo,然后画到窗口的back buffer上面
}

这里FSlateRHIRenderer把backbuffer抽象成了FViewportRHI,但是另外两个裸着调用图形API的渲染器是直接存了backbuffer的,如果想学RHI,可以对照着这3个类看看。

FSlateApplication的初始化和渲染器的初始化在FEngineLoop::PreInitPreStartupScreen里面,还有一些基础风格的初始化也在里面。

FSlateApplication::InitHighDPI(bForceEnableHighDPI);//这个dpi会根据操作系统的设置动态改变

FSlateApplication::Create();//初始化平台Application,如果是Windows,则会注册操作系统的消息回调

FSlateApplication::InitializeCoreStyle();

TSharedPtr<FSlateRenderer> SlateRenderer = GUsingNullRHI ?
FModuleManager::Get().LoadModuleChecked<ISlateNullRendererModule>("SlateNullRenderer").CreateSlateNullRenderer() :
FModuleManager::Get().GetModuleChecked<ISlateRHIRendererModule>("SlateRHIRenderer").CreateSlateRHIRenderer();
TSharedRef<FSlateRenderer> SlateRendererSharedRef = SlateRenderer.ToSharedRef();//创建slate渲染器

FSlateApplication& CurrentSlateApp = FSlateApplication::Get();
CurrentSlateApp.InitializeRenderer(SlateRendererSharedRef);//初始化渲染器

../_images/dpi.png

dpi是这个,主要是如果这个改动了,程序也要通过WIN32的API获取dpi值,然后把所有控件跟着一起缩放。

FSlateRHIRenderer的Initialize函数会初始化一些成员变量:

bool FSlateRHIRenderer::Initialize()
{
	LoadUsedTextures();//加载控件用到的画刷,也就是贴图,没用到的,不加载,会创建显存buffer

	RenderingPolicy = MakeShareable(new FSlateRHIRenderingPolicy(SlateFontServices.ToSharedRef(), ResourceManager.ToSharedRef()));//创建渲染策略,这个在FSlateRHIRenderer的DrawWindow函数里面会使用

	ElementBatcher = MakeUnique<FSlateElementBatcher>(RenderingPolicy.ToSharedRef());//合批器,把一些相同图集的控件给合批了的类
    
	return true;
}

FSlateRHIRenderer持有一个FSlateDrawBuffer,这个是FSlateWindowElementList的数组,每个窗口一个FSlateWindowElementList,每一帧都会生成控件的顶点数据、索引数据,还有其它数据放置在这个FSlateWindowElementList里面,FSlateWindowElementList存了一个FSlateBatchData,FSlateBatchData则是FSlateRenderBatch的数组,每个控件一个FSlateRenderBatch,如果合批器合批了,则可能多个控件合并到一个FSlateRenderBatch。

FSlateRnederBatch的成员变量
    
FShaderParams ShaderParams;//着色器参数

FSlateVertexArray* SourceVertices;//顶点数组

FSlateIndexArray* SourceIndices;//索引数组

int32_t LayerId;//当前这个控件的层,合批器合批的时候,会判断两个控件的Layer是否一样,一样的话,就合并顶点、索引

ESlateBatchDrawFlag DrawFlags;

ESlateShader ShaderType;

ESlateDrawPrimitive DrawPrimitiveType;

ESlateDrawEffect DrawEffects;

合批控件的条件非常苛刻,在绘制每一帧前,会合批一下:

在ElementBatch.cpp的MergeRenderBatches函数里面,会合批控件:

//首先根据层从小到大稳定排序
TArray<TPair<int32, int32>, TInlineAllocator<100, TMemStackAllocator<>>> BatchIndices;

{
	BatchIndices.AddUninitialized(RenderBatches.Num());
	
	for(int32 Index = 0; Index < RenderBatches.Num(); ++Index)
	{
		BatchIndices[Index].Key = Index;
		BatchIndices[Index].Value = RenderBatches[Index].GetLayer();//获取一个FSlateRenderBatch的layer
	}
	
	//稳定排序,根据layer进行排序
	BatchIndices.StableSort
	{
		[](const TPair<int32, int32>& A, const TPair<int32, int32>& B)
		{
			return A.Value < B.Value;
		}
	}
}

//从前往后开始合并
NumBatches = 0;
NumLayers = 0;

int32 CurLayerId = INDEX_NONE;//当前render batch的layer id
int32 PrevLayerId = INDEX_NONE;//前面render batch的layer id

FirstRenderBatchIndex = BatchIndices[0].Key;

FSlateRenderBatch* PrevBatch = nullptr;
//从前往后开始合并
for(int32 BatchIndex = 0; BatchIndex < BatchIndices.Num(); ++BatchIndex)
{
    const TPair<in32, int32>& BatchIndexPair = BatchIndices[BatchIndex];
    
    CurLayerId = CurBatch.GetLayer();//获取当前batch的layer
    
    if(PrevLayerId != CurLayerId)
    {
        ++NumLayers;//当前render batch和前面的不一样,增加NumLayers
    }
    
    if(PrevBatch != nullptr)
    {
        PrevBatch->NextBatchIndex = BatchIndexPair.Key;//用链表串起来
    }
    
    ++NumBatches;
    
    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())
            {
                break;
            }
            else if(!TestBatch.bIsMerged && CurBatch.IsBatchableWith(TestBatch))
            {
                CombineBatches(CurBatch, TestBatch, FinalVertexData, FinalIndexData);//合并batch
            }
        }
    }
    PrevBatch = &CurBatch;
}
//class FSlateRenderBatch
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;
}

需要满足很多条件,才可以合批,着色器资源也要一样(这个是DirectX的描述符,或者是OpenGL的纹理ID),层要一样,图元拓扑也要一样。

控件的渲染

控件首先需要自下而上递归,计算一次固定大小(desired size),然后自上而下递归,计算布局,把整个窗口的几何大小分配给每个控件,为啥要两次递归?因为控件有些指定自动大小的,会进行布局的计算,而不会使用图片的固定大小。

然后绘制是第三次递归,总共遍历3次控件树,还有一次是消息事件的路由,总共4次递归,有3次可以合并,绘制,分配几何大小,2D碰撞网格的构建可以合并,这些都在OnPaint里面处理。

布局的计算是在OnPaint一开头,可以查看布局计算这篇文章。

持续更新。