UE4 CanvasPanel 渲染批次合并分析优化
在 UE4 中,CanvasPanel 可以进行批次合并,前提是在项目设置中开启 Explicit Canvas Child ZOrder 选项,但是在使用过程中,发现批次合并并不是很理想,比如下面这种情况
在上图中,第一个 Canvas Panel 下面有两个子控件,一个 Image,还有另一个 Canvas Panel。第二个 Canvas 下面再放一个 Image,两个 Image 控件使用同一张纹理(或者是同一图集中的两张图片),在实际运行后,这两张图片并没有合并到同一个渲染批次中。
为什么会导致这个问题?
打开 SConstraintCanvas.cpp,找到 OnPaint
函数,这个函数就是 Slate Tick 第二次从上到下遍历控件树,生成 Vertex Buffer 和 批次合并过程中被调用到(第一次遍历是从下到上计算Desired Size)。OnPaint
函数一开始就调用 ArrangeLayeredChildren
函数,这个函数会对子控件按 ZOrder 进行排序,然后按排序后的结果遍历,做两件事情,一是计算控件 LocalSize 和 LocalPosition,二是判断子控件是否需要新的一层 Layer 进行绘制,同一层绘制的子控件最终会合并到同一批次中,所以我们要关心的代码就是下面这段
bool bNewLayer = true;
if (bExplicitChildZOrder)
{
// Split children into discrete layers for the paint method
bNewLayer = false;
if (CurSlot.ZOrder > LastZOrder + DELTA)
{
bNewLayer = true;
LastZOrder = CurSlot.ZOrder;
}
}
ArrangedChildLayers.Add(bNewLayer);
bExplicitChildZOrder
就是我们在项目设置中开启的 Explicit Canvas Child ZOrder 选项if (CurSlot.ZOrder > LastZOrder + DELTA)
这里其实就等价于if(CurSlot.ZOrder != LastZOrder)
,按 ZOrder 从小到大排序后,如果后面一个子控件的ZOrder不等于前面一个子控件,后面那个子控件就需要在新的一层中绘制,问题就出在这个地方,在遍历的for
循环外面,LastZOrder
的初始化值是-FLT_MAX
,这样第一个子控件的if (CurSlot.ZOrder > LastZOrder + DELTA)
肯定是true
,就导致 Canvas Panel 下面的第一个子控件肯定是要新建一层来绘制,所以就导致上面的情况,两张图片不能合并批次。
回到 OnPaint
函数中,在 ArrangeLayeredChildren
调用之后,声明了一个 ChildLayerId
的变量,初始值是 LayerId + 1
,这里也是有问题的,也会导致新建一层绘制子控件。
怎么优化?
要进行优化很简单,一是将 ArrangeLayeredChildren
函数中 LastZOrder
的初始化值改为 SlotOrder[0].ZOrder
,二是将 OnPaint
函数中 ChildLayerId
初始化值改为 LayerId
,如下
void SConstraintCanvas::ArrangeLayeredChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren, FArrangedChildLayers& ArrangedChildLayers) const
{
...
float LastZOrder = SlotOrder[0].ZOrder;
...
}
int32 SConstraintCanvas::OnPaint(...) const
{
...
int32 ChildLayerId = LayerId;
...
}
总结
- 内容控件的父类
SCompoundWidget
的OnPaint
中也是直接新建一层绘制子控件,也可以考虑将这里优化一下,这样Button
等控件也可以合并批次了 - ZOrder 只是用来在兄弟控件中进行排序时的依据,它本身的值并没有意义,换句话说,两个兄弟控件,它们的ZOrder是0, 1还是100,200,都没区别,不影响排序,除此之外,ZOrder没有其他作用
补充
这篇文章是之前的一个笔记的完善,当时用的引擎版本是 4.18,上面对于 Canvas Panel 的优化部分官方也已经在 4.20 的版本中修改了,只不过官方修改的代码有点让人看着头大,官方代码是这样的
bool bNewLayer = true;
if (bExplicitChildZOrder)
{
// Split children into discrete layers for the paint method
bNewLayer = false;
if (CurSlot.ZOrder > LastZOrder + DELTA)
{
if (ArrangedChildLayers.Num() > 0)
{
bNewLayer = true;
}
LastZOrder = CurSlot.ZOrder;
}
}
ArrangedChildLayers.Add(bNewLayer);
多加了一个 if (ArrangedChildLayers.Num() > 0)
来判断是不是第一个控件,效果是一样的,只不过看着难受,一眼看上去像个特殊处理似的。。
至于 SCompoundWidget
还是没变
所以如果用的版本是4.20之前的,要么升级一下引擎,要么自己手动改下引擎代码,让 Canvas Panel 的批次合并更合理些