Detailed explanation of Unity UGUI Batches batch rules
When dealing with UGUI DrawCall, we often encounter various problems.
Problem 1: when processing UGUI batch, it is found that a panel parent node rotates, and the lower UI batch order will be disrupted.
For more questions: Treatment method of batch failure of UGUI 3D interface / Z-axis displacement
Because I can't see the source code of UGUI Batches, I haven't been clear about its batch rules. So let's answer one question by one today, and explain its source code in detail:
The underlying logic is that Z-axis rotation makes it impossible to batch:
Judge whether the z-axis of the control world coordinate bounding box is 0. If it is not 0, it will monopolize a depth. Then sort. The depth is the first priority, and then to materials, maps, etc.
So what exactly is it? Let me summarize:
- First get a list in Hierarchy order
- Calculate the depth of each object.
2.1 the depth increases from 0. If the Z axis of the world bounding box is not 0 (or isCanvasInjectionIndex), you need to monopolize one batch and one depth at the same time. That is, it is equal to the maximum depth of all previous objects + 1, and the depth of the latter object needs + 1.
2.2 depth of general objects. It will judge whether the depth can be shared with the previous object and follow the next process.
2.2.1 divide multiple grids according to the grid (the default size is 120, and then calculate according to the bounding box). (just to speed up the intersection).
2.2.2 calculate which grids are surrounded and intersected by the object, and then judge the intersection of the bounding box with the existing objects in the grid. If it does not intersect, the current depth is used; If it intersects and can be combined, the maximum depth in the intersecting object is used; If it intersects and cannot be matched, the maximum depth + 1 in the intersecting object is used. Batch conditions: no exclusive batch + same material + same map + same clipping switch and clipping rectangle + consistent map A8 format (kTexFormatAlpha8)
2.2.3 add the object to all intersecting grids. If an object with exclusive depth is encountered, the grid data is cleared. That is, subsequent objects do not share depth with previous objects. - Sort: sort by depth - > material - > map - > hierarchy.
- Batch: for the sorted list, check whether it can batch with the previous objects one by one from scratch. Batch matching conditions: no exclusive batch (only isCanvasInjectionIndex is judged) + the same material + the same map + the same clipping switch and clipping rectangle + the same map A8 format (kTexFormatAlpha8). Non SubBatch only judges the first two conditions. Generally, the materials of the UI are the same.
Source code analysis:
When the ui is in batch, it will be sorted according to depth (the number of ui layers), materialInstanceID and textureID. The calling process is as follows:
SortForBatching first prepares data for each ui and calls 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; } }
During 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; } } }
There is a function, SortingForceNewBatch (). This function will force a new batch to start and increase the depth when the ui is not coplanar with the canvas or the canvas is not the same. That is, the judgment of the following function:
inline BatchBreakingReason SortingForceNewBatch(const RenderableUIInstruction& instruction) { if (!instruction.isCoplanarWithCanvas) return NotCoplanarWithCanvas; if (instruction.isCanvasInjectionIndex) return CanvasInjectionIndex; return NoBreaking; }
The sorting method is as follows:
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; }
Our ui uses only one canvas, which is obviously caused by non coplanarity. Therefore, through debugging, it is found that the calculation of isCoplanarWithCanvas is false is determined by the Z value relative to canvas:
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; }
As you can see, it is through the judgment of worldboundaries m_ Max.z worldBounds. m_ Whether min. Z is in the range of 0.001f and this Z is affected by rotation. Through debugging, I found that when the planeresistance is large, the error is small and will be less than 0.001. However, when the planeresistance is small, the value will be greater than 0.001. When we set 1, this Z will reach about 0.019. Therefore, I changed 0.001 to 0.01 and found that there is no problem in how to turn. Because the error of the calculated value of ui rotation in different directions is different, which is why the drawcall in different directions is different.
Therefore, when optimizing drawcall, you must pay attention to prevent this situation. This situation will lead to very large depth, and many UIs will become separate batches. This is also a pit of unity. After all, the large difference of z values is also easy to cause the problem of incorrect rendering order.