从NGUI的UIScrollview的实现原理延伸到ngui的层次,合并,drawcall生成原理
记上次面试被一个主程说,你连NGUI底层探索的欲望都没有,你还说你对编程感兴趣
想想也是,人家代码摆在那给你看你连看都不看,还说自己对学习技术有热忱。
而且当初确实是好奇UIScrollview怎么实现的,所以趁今天刚好做这个需求就看看NGUI底层是怎么实现的。
一、基本结构
首先实现一个UIScrollview基本的结构是
ScrollviewPanel
-Grid
-Item
首先在ScrollviewPanel这个控件下挂上UIScrollview这个脚本。unity就会自动帮你挂上UIPanel和Rigidbody这两个component。
这里很重要的是UIScrollview和UIPanel这两个类,这两个就是为什么能只显示固定窗口界面的关键。这个之后再讲。
接着是Grid,Grid上会挂UIGrid这个脚本。这个脚本是负责把它下面挂的所有item有序的排列好,Grid就像一个有多格的收纳盒,每次层的大小是一样的。
最后是Item,Item上挂自己的脚本控制自己的UI摆放。放个BoxCollider作为碰撞(点击)触发,再放一个UIDragScrollview,在Scrollview这一栏上拖入ScrollviewPanel这个控件(没有拖动的话在UI打开的时候系统也会找最近的带有UIScrollview的父节点)。
二、原理
原理比较复杂,涉及到好几块,我们拆分一下,从小的开始。
以上面UI为例
1.首先是每一个item,item上面挂自己控制的脚本,比如删除自己或者其他的什么处理,处理完后要告诉UIScrollview和UIGrid要刷新列表。
2.接着要把中间的多个Item放到一个大框框里,并规定好格子大小,有序的摆放。
这个用UIGrid实现。
UIGrid 代码逻辑相对简单,在UIGrid里,遍历所有item,最顶端item的坐标为(0,0)。根据摆放的方向(横向或竖向),如上图是竖向,根据设定好的Cell Width和Cell Height。计算好下一个的坐标就行了。
2.接下来是UIScrollview,首先按上面说的,ScrollviewPanel上面要先挂上UIPanel,这个简单来说是作为渲染用的,所以当你有这么一块需要特殊处理的模块需要渲染当然需要UIPanel专门控制。
而且在UIPanel上有个Clipping功能。这个功能是用作裁剪用,
也就是中间这一块红色区域,当超框时要做什么处理,是淡化还是直接裁掉还是不处理,
这些东西就在这里设置。
这里就需要好好讲讲。为什么可以让某些item显示某些不显示,某些item只显示一部分。
这里就涉及到UIPanel这个比较底层的东西。
首先我们看UIPanel的LateUpdate函数。
这个每帧更新的函数做了什么操作。这里看到主要就两个
UpdateSelf()和LateUpdatePanel().
首先看UpdateSelf()
这里可以看到。首先各种Update,具体就不说了,说一个UpdateWidget(),为什么要UpdateWidget呢。
这是因为NGUI是根据UIWidget来作为渲染单位,一个UIWidget就一个渲染(这样不就巨多渲染?!所以NGUI会合并Drawcall,这个之后讲)
这也是为什么基本的显示单位向UIlabel和UISprite都继承UIWidget。
这时我们在看看UIWidget脚本,看看它的OnStart()函数。
只做CreatePanel()操作
这里最重要的是
这里说明CreatePanel其实不是真的去Create一个Panel,而是找父节点或者自己节点上的UIPanel,去把自己这个Widget添加到Panel的WidgetList里。
这就是为什么UIPanel被称作Randerer渲染器的原因,一个UIWidget是没有办法渲染的,所有的控件是通过找到UIPanel,再在UIPanel那去渲染。
这也是为什么需要UpdateWidget的原因。
所以回到上面的UpdateSelf()。更新完Widgets后。接着到FillAllDrawcalls()这个函数。
这个函数很重要,就是上面说的如果这么多widget那Drawcall不就要爆了。这个函数就是合并可以合并的drawcall。
(这函数代码太多,截断讲吧)
具体怎么合并的呢。
1.调SortWidgets()排序Widget。
这里可以看到,先比较Depth大小。大的往前排。
如果Depth一样看Material
如果material一样不改变排位,
如果不一样,如果有上一个Material不为空退一位,如果自己不为空进一位。
如果都为空就按实例的id排位。
其实目的就是,把material按前后拍到一起,再按Depth拍到一起。
这段代码是就是负责把drawcall合并。
首先获取上个环节的widgetList,获取每个widget。
评断这个widget的Material,Texture,Shader是否和上一个widget的这些属性是否一样。
如果一样的话就不调SubmitDrawcall这个函数。
接着看这些属性是否为空,如果为空就去Create一个新的Drawcall。
如果不为空就改变这个Drawcall的Depth从哪到哪
把这些数据写到内存。
循环判断。
这样就把用一样的Material,Texture,Shader的widget生成的drawcall合并成一个。
接着看SubmitDrawcall这个函数。
这里我们关注drawCalls.Add(dc);这行代码,这里把这个drawcall加到这个drawcallList里。
这里就把UIPanel的LateUpdate()里的UpdateSelf()基本看完。
接下来就是第二部分LateUpdatePanel()
定位到里面
看到不管是什么渲染队列方式,都是调用UpdateDrawcalls().所以直接看里面。
这里面代码又比较多。主要看这里
简单来说就是各种计算算出这个transform的位置旋转缩放在显示屏中的各种属性数据和这个Drawcall的渲染方式队列裁剪方式裁剪范围等这些数据。
这样就完成了每帧刷新每个widget的transform和drawcall数据刷新。
接下来就要到UIDrawcall.cs里看看渲染。
具体不多说了。我们看为什么那让一些一些widget只显示一部分。也就是解答一开始的问题
为什么可以让某些item显示某些不显示,某些item只显示一部分。
首先我们会看到在之前调用的SubmitDrawcall()中有dc.Set()这个函数。这里会看到最后会到UIDrawcall的CreateMaterial()这个函数里。
在这里进行判断,根据设置好的alpha裁剪还是soft裁剪来选择不同的shader。
而由于这里不同shader会产生不同的动态材质mDynamicMat。
也是由于这个不同的动态材质使得下一步的渲染裁剪得到实现。
我们找到这个函数。(忽略箭头。。。)
这里可以看到下面,当这个widget是需要裁剪的时候会计算上面的动态材质的裁剪坐标和大小,如果是SoftClip再计算强度。
这里就是为什么可以做到某些item显示某些不显示,某些item只显示一部分。的所有原因了!!!
要了解底层真是不容易啊