关于Graphic组件
Graphic组件是UGUI中比较重要的一个组件,例如Image,RawImag,MaskableGraphic 可遮罩的图形组件 这些都是继承自Graphic的
它必须要有 CanvasrRenderer组件以级RectTransform 组件,并且一个对象只允许挂载一个。
RectTransform:
RectTransform继承于Transform,在 Transform 基础上,RectTransform 增加了 轴心(pivot)、锚点(anchors)、和 尺寸变化量(sizeDelta)。
其中anchors和pivot都是坐标原点在左下角的0-1向量空间,0-1代表的是比例。
anchors的向量空间是子UI相对父UI的比例位置,pivot的向量空间是相对UI本身的比例位置,这个组件是比较常见的。
CanvasRenderer:当然不用说了,如果 没有这个组件,是不会去通知根节点的Canvas 进行渲染的;
关于它的继承关系
Graphic->UIBehaviour,ICanvasElement
关于它的一些使用 例子
这里有个简单的例子:
重写了这个 onPopulateMesh 方法,它的作用是计算元素网格数据
填充网格,计算上下左右四个角的
[ExecuteInEditMode]
public class SimpleGraphic : Graphic
{
protected override void OnPopulateMesh(VertexHelper vh)
{
Debug.Log("vh"+vh.currentIndexCount+"VertCount"+vh.currentVertCount);
//两个对角 (0,0) (0.5,0.5)是中心点 (1,1)
Vector2 corner1 = Vector2.zero;
Vector2 corner2 = Vector2.zero;
corner1.x = 0f;
corner1.y = 0f;
corner2.x = 1f;
corner2.y = 1f; //宽高比
corner1.x -= rectTransform.pivot.x;
corner1.y -= rectTransform.pivot.y;
corner2.x -= rectTransform.pivot.x;
corner2.y -= rectTransform.pivot.y;
//宽高保持一致
corner1.x *= rectTransform.rect.width;
corner1.y *= rectTransform.rect.height;
corner2.x *= rectTransform.rect.width;
corner2.y *= rectTransform.rect.height;
//重新计算
vh.Clear();
UIVertex vert = UIVertex.simpleVert;
//添加四个点 左下左上,右上右下
vert.position = new Vector2(corner1.x, corner1.y);
vert.color = color;
vh.AddVert(vert);
vert.position = new Vector2(corner1.x, corner2.y);
vert.color = color;
vh.AddVert(vert);
vert.position = new Vector2(corner2.x, corner2.y);
vert.color = color;
vh.AddVert(vert);
vert.position = new Vector2(corner2.x, corner1.y);
vert.color = color;
vh.AddVert(vert);
//添加三角形到 缓冲区中
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
}
大概效果就是这样:
那么它是如何被渲染出来的呢?
首先这个方法会在填充网格数据的时候调用。
在onEnable时 ,
会把自己graphic和对应的Canvas的引用注册到GraphicRegistry的 m_Graphics 字典里 ,
GraphicRegistry. RegisterGraphicForCanvas
如果没有canvas就会去上级节点获取所有的canvas
然后
关于它的调用顺序:
onEnble->SetAllDirty-> Rebuild->UpdateGeometry> DoMeshGeneration
关于Rebuild
判断当前是哪个Dirty 然后进行更新,首次会SetAllDirty 因此
UpdateGeometry 以级UpdateMaterial 都会被调用
Graphic.UpdateGeometry 更新几何
判断是否使用遗产mesh生成
遗产用Mesh 不是用VertextHelper
接着是 做网格数据的生成
OnPopulateMesh 填充网格数据 也就是顶点 数据 填充到 s_vertexHelper变量里,然后 取得 所有的IMeshModifier 组件
把s_vertexHelper作为参数 传递过去。
然后进行通知网格数据改变,进行网格数据修改
然后再填充到workerMesh上
最后 调用SetMesh 把workerMesh作为参数传递到canvasRender组件里,
最后再提交数据到根对象canvas上,进行判断合批等等
最后再提交数据出去 进行渲染。
关于Unity UGUI做的合批
合批这部分Unity貌似没公开,然后可以通过下面这个文章参考
基于4.6的 有点旧
UGUI优化源码合批
canvas底下的 更新方法,通过过一个静态的 委托 willRenderCanvases进行注册
关于它的 注册的时机,在CanvasUpdateRegistry的构造函数里
关于 CanvasUpdateRegistry.PerformUpdate
CanvasUpdateRegistry它是这个单例类,负责所有画布 元素的rebuild注册;
因为所有画布元素都是继承自 ICanvasElement 接口的。
CanvasUpdateRegistry在构造出来的时候,就把PerformUpdate注册进去了
willRenderCanvases 就是底层的SendWillRenderCanvas,网格重建,
关于它的调用次数的监听,以及那些元素导致了这个方法调用
可以查考这个 链接,具体触发机制我也不太清楚,一般来说应该是 Canvas的 layout或者Graphic发生变化时,Unity3D 底层帮我们进行 网格合批,等一系列判断处理后,再发送这个事件给UGUI,然后触发这个PreformUpdate方法。 然后它会去通知注册进来的 元素,layoutQueue,以及GraphicQueue,然后调用它们的Rebuild方法, 关于Graphic的Rebuild方法是会根据本身的 dirty 标志位,判断是否真的需要Rebuild。
关于PerformUpdate的事情:
- Layout的Rebuild
- Graphic的Rebuild
1与2的不同点就在于 Layout要先按照子物体多少进行排序,而2并没有。
具体逻辑:
会从m_LayoutRebuildQueue 按子物体的多少进行排序,越多的越靠前,然后进行遍历 调用 rebuild,分别是
顺序是 preLayout ,layout ,postLayout,0,1,2 意思就是 布局Rebuild 以级先后
最后再 是 layoutComlete.
接着是进行Render了 ,从preRender 到LaterRender的 Rebuild
render的 Rebuild 是存在 m_GraphicRebuildQueue 这个队列里的
然后遍历 调用 graphicUpdateComplete 表示渲染 完成
以上这些 RebuildQueue是如何添加的呢
其实是在 调用SetXXXDirty的时候,标志脏,在Graphic中
SetVerticesDirty 会添加到 m_GraphicRebuildQueue 当中,
SetLaoutDirty 会添加到 m_LayoutRebuildQueue 中
SetMaterialDirty 会添加到m_GraphicRebuildQueue 中
而这些脏标志会 什么时候调用呢
一个是OnTransformParentChanged
在父亲Transform发生变化时, 旋转,位置,缩放等等;
以级OnRectTransformDimensionsChange
在RectTransform维度发生变化时
如果关联的RectTransform的维度被更改,则调用此回调。这个调用也会对所有的子rect转换进行调用,即使子转换本身没有改变——它可能会改变,这取决于它的锚定
SetVerticesDirty还会在 颜色发生变化时调用.
SetMaterialDirty还会在 材质进行替换时调用
SetAllDirty 会在 OnEnable,Reset,OnDidApplyAnimationProperties 动画属性变化时调用
因此呢,项目里隐藏,可以改变Alpha,或者移到看不见的地方,这样就不会 触发SetAllDirty了 而是render 或者是vertices的更新了。
此外
画布分离,动静分离的优化就很有必要了。
总结下, Graphic的SetDirty 会添加到 CanvasUpdateRegistry的 单例的 m_layoutRebuildQueue 以级 m_GraphicRebuildQueue中
这个单例在构造出来的时候,会把 performUpdate 注册到
canvas的 静态事件变量 willRenderCanvases 里,
PerformUpdate 会先处理 layOutReBuildQueue,进行按子物体量进行排序,再判断是否可以ReBuild再依次调用 preLayout,layout,lateyout 3个Rebuild
最后才是layoutComplete , 然后 类似的 处理GraphicRebuildQueue;
而这个Rebuild 实际上是由Graphic处理的,Graphic是继承自ICanvasElement的,而对于Graphic而言呢,只在PreRender的时候,进行了 几何学,材质的处理。
ICanvasElement 接口
最后 OnPopulateMesh 只是 在UpdateGeometry 时,调用了;
渲染过程的调用堆栈,在UnityEditor模式下
因为我使用了快捷键进行了激死激活,因此可以看到调用了key事件
HanleKeyEvent的派发过程
因此它的调用顺序在编辑器下是
UnityEditor.DrivenRectTransformUndo:ForceUpdateCanvases ()
UnityEngine.Canvas:ForceUpdateCanvases ()
UnityEngine.Canvas:SendWillRenderCanvases 事件
UnityEngine.UI.CanvasUpdateRegistry:PerformUpdate
UnityEngine.UI.Graphic:Rebuild (CanvasUpdate)
ImeshModifier mesh改变接口
关于Graphic 的优化Tips 技巧:
- 减少SetXXXDirty 次数,减少激死激活次数,例如消失隐藏,可以使用alpha或者位置的偏移来代替 激死激活,
- Canvas动静分离
- UI界面 动画尽量不要使用 动画控制器,使用DOTween等动画插件来实现界面动画。