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 进行绘制,同一层绘制的子控件最终会合并到同一批次中,所以我们要关心的代码就是下面这段

1
2
3
4
5
6
7
8
9
10
11
12
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,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

void SConstraintCanvas::ArrangeLayeredChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren, FArrangedChildLayers& ArrangedChildLayers) const
{
...

float LastZOrder = SlotOrder[0].ZOrder;

...
}

int32 SConstraintCanvas::OnPaint(...) const
{
...

int32 ChildLayerId = LayerId;

...
}

总结

  • 内容控件的父类 SCompoundWidgetOnPaint 中也是直接新建一层绘制子控件,也可以考虑将这里优化一下,这样 Button 等控件也可以合并批次了
  • ZOrder 只是用来在兄弟控件中进行排序时的依据,它本身的值并没有意义,换句话说,两个兄弟控件,它们的ZOrder是0, 1还是100,200,都没区别,不影响排序,除此之外,ZOrder没有其他作用

补充

这篇文章是之前的一个笔记的完善,当时用的引擎版本是 4.18,上面对于 Canvas Panel 的优化部分官方也已经在 4.20 的版本中修改了,只不过官方修改的代码有点让人看着头大,官方代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 的批次合并更合理些