Unity UGUI Batches合批规则详解
在处理UGUI DrawCall问题的时候,我们经常遇到各式各样的问题。
因为看不到UGUI Batches的源码,所以就一直不清楚它的合批规则到底是怎样的。所以今天就来一个一个问题解答,顺序详解一下它的源码:
那么到底具体是怎样?我来总结一下:
源码剖析:
在ui合批的时候,会先给按照depth(ui的层数)以及materialInstanceID,textureID进行排序,如下调用过程如下:
SortForBatching先对每一个ui进行数据准备,调用PrepareDepthEntries();
void SortForBatching(const RenderableUIInstruction* inputInstructions, UInt32 count, RenderableUIInstruction* outputInstructions, int sortBucketGridSize)
{
PROFILER_AUTO(gSort, NULL);
// Create depth block
dynamic_array<DepthSortEntry> depthEntries(kMemTempAlloc);
depthEntries.resize_uninitialized(count);
PrepareDepthEntries(inputInstructions, count, depthEntries.data(), sortBucketGridSize);
std::sort(depthEntries.data(), depthEntries.data() + count);
// Generate new sorted UIInstruction array
for (UInt32 i = 0; i < count; i++)
{
Assert(depthEntries[i].depth >= 0);
const RenderableUIInstruction& instruction = inputInstructions[depthEntries[i].renderIndex];
outputInstructions[i] = instruction;
}
}
在PrepareDepthEntries时候:
static void PrepareDepthEntries(const RenderableUIInstruction* uiInstruction, UInt32 count, DepthSortEntry* output, int sortBucketGridSize)
{
if (count == 0)
return;
DepthSortGrid grid;
grid.Initialize(0);
int maxDepth = 0;
for (int i = 0; i < count; ++i)
{
// if we have a forced new batch
if (SortingForceNewBatch(uiInstruction[i]) != NoBreaking)
{
// try for a run of forced batches
int forcedCount = 0;
for (int j = i; j < count; ++j)
{
// TODO: If this is true through the end the instructions then we never increment i and
// can get into a very long infinite loop as we never hit the i += forcedCount - 1
if (SortingForceNewBatch(uiInstruction[j]) != NoBreaking)
{
// just set the depth to be the
// maxDepth +1
++forcedCount;
output[j].renderIndex = uiInstruction[j].renderDepth;
output[j].depth = ++maxDepth;
continue;
}
// we have run out of forced depths...
// create a new grid for the next set of
// batchable elements
Assert(forcedCount > 0);
i += forcedCount - 1;
grid.Initialize(++maxDepth);
break;
}
}
else
{
int depth = grid.AddAndGetDepthFor(uiInstruction[i], uiInstruction, sortBucketGridSize);
maxDepth = std::max(depth, maxDepth);
output[i].renderIndex = uiInstruction[i].renderDepth;
output[i].depth = depth;
output[i].materialInstanceID = uiInstruction[i].materialInstance.GetInstanceID();
output[i].textureID = uiInstruction[i].textureID;
output[i].texelSize = uiInstruction[i].texelSize;
}
}
}
有一个函数,SortingForceNewBatch(),这个函数会在ui与canvas不共面,或者不是同一个canvas的时候,强制新启一个batch,同时把深度增加。也就是下面这个函数的判断:
inline BatchBreakingReason SortingForceNewBatch(const RenderableUIInstruction& instruction)
{
if (!instruction.isCoplanarWithCanvas)
return NotCoplanarWithCanvas;
if (instruction.isCanvasInjectionIndex)
return CanvasInjectionIndex;
return NoBreaking;
}
排序方法如下:
static bool operator<(const DepthSortEntry& a, const DepthSortEntry& b)
{
// first sort by depths
if (a.depth != b.depth)
return a.depth < b.depth;
// if they're equal, sort by materials
if (a.materialInstanceID != b.materialInstanceID)
return a.materialInstanceID < b.materialInstanceID;
// if they're equal, sort by textures
if (a.textureID != b.textureID)
return a.textureID < b.textureID;
//TODO: we could break 'fast' batching here due
// to not looking at rect clipping / what the clip
// rect is. This is unlikely (due to the next step
// being render order), but it's something to
// investigate at a later time.
// all else being equal... sort by render order
return a.renderIndex < b.renderIndex;
}
我们ui只用了一个canvas,显然,是因为不共面导致的,因此通过调试,发现isCoplanarWithCanvas为false的计算是通过相对于canvas的Z值确定的:
void DoSyncWorldRect(UIInstruction& uiData)
{
MinMaxAABB worldBounds;
TransformAABBSlow(uiData.localBounds, uiData.transform, worldBounds);
ProjectAABBToRect(worldBounds, uiData.globalRect);
uiData.worldBounds = worldBounds;
uiData.isCoplanarWithCanvas = CompareApproximately(worldBounds.m_Min.z, 0.0f, 0.001F) && CompareApproximately(worldBounds.m_Max.z, 0.0f, 0.001F);
uiData.dirtyTypesFlag = kTypeOrder;
}
可以看到,是通过判断worldBounds.m_Max.z worldBounds.m_Min.z是否在0.001f范围内,而这个z是通过旋转影响的,我通过调试发现,在PlaneDistance比较大的时候这个误差比较小,会小于0.001,然而PlaneDistance比较小的时候这个值会大于0.001,我们设置1的时候这个z会达到0.019左右,因此我将0.001改成0.01,发现怎么转向都没问题了。因为转不同的方向不同的ui旋转的计算值的误差不一样,这也就是为什么转不一样的方向drawcall不一样的原因了。
因此大家在优化drawcall的时候一定要注意,防止这种情况的出现,这种情况会导致depth非常大,很多ui都会成为单独的批次。这也算unity的一个坑吧。毕竟z值相差较大也去合批容易造成渲染顺序不正确的问题。