【NGUI源码剖析】NGUI的drawcall

转载自
【NGUI源码剖析】NGUI的drawcall
【NGUI源码剖析】深入理解NGUI的drawcall
【NGUI源码剖析】NGUI如何优化drawcall数量
NGUI 渲染流程深入研究 (UIDrawCall UIGeometry UIPanel UIWidget)

0 前言

这篇博客主要介绍NGUI是如何实现渲染的,将UI界面渲染到屏幕上的。

Unity3D 之Mesh 中,我们知道了Unity做一个简单的渲染需要哪些类。

  • MeshFilter 网格过滤器
  • MeshRenderer 网格渲染器

NGUI渲染顺序概述 中,我们已经简单介绍了NGUI是如何管理渲染的。

NGUI用Panel来管理渲染。

  • UIPanel.widgets 表示 Panel 中需要渲染的物体
  • UIPanel.drawCalls 表示 Panel 中的渲染指令.
  • static UIPanel.list 存放所有的 Panel

在渲染时,Panel遍历它管理的所有Widgets. 生成DrawCall命令list.

unity之NGUI 渲染原理 中,
我们知道了NGUI渲染时,可能需要做的哪些操作。

  • 排序
  • 合并DrawCall处理。

那么,这一篇博客,主要根据源码更加详细地了解一些具体实现细节。

1 UIPanel / UIWidget / UIDrawCall

UIPanel是管理渲染的。

静态的UIPanel列表:用于存储所有的Panel的引用。

static public List<UIPanel> list = new List<UIPanel>();

1.1 主要成员变量

每个Panel需要管理在它之上的 Widget

/// <summary>
/// List of widgets managed by this panel. Do not attempt to modify this list yourself.
/// </summary>

[System.NonSerialized]
public List<UIWidget> widgets = new List<UIWidget>();

每个Panel都维护一个 DrawCall 的列表。

/// <summary>
/// List of draw calls created by this panel. Do not attempt to modify this list yourself.
/// </summary>

[System.NonSerialized]
public List<UIDrawCall> drawCalls = new List<UIDrawCall>();

那么,对应的关系其实是这样的:

  • 一个UIPanel可能对应多个UIWidget
  • 而一个UIWidget不一定会对应一个UIDrawCall(可能对应一个UIDrawCall,也可能与其他UIWidget共用一个UIDrawCall)

三者间的关系可以用下图表示:
在这里插入图片描述

1.2 UIPanel的工作流程

UIPanel 函数调用栈:
在这里插入图片描述

渲染时的流程:
在这里插入图片描述

  1. LateUpdate 属于 Unity生命周期的函数。每一帧都会调用;
  2. 执行UIPanel的UpdateSelf : Update the panel, all of its widgets and draw calls. 更新Panel,根据Widgets生成对应的DrawCall列表。( 可能需要根据 Widget 重新生成DrawCall列表)
  3. UpdateDrawCalls : Update all draw calls, making them draw in the right order. 更新所有的DrawCall,使得他们按照正确的顺序绘制。

1.3 UIPanel如何生存UIDrawCall列表

看到drawCalls.Add()的调用全都在FillAllDrawCalls()内部。

	/// <summary>
	/// Fill the geometry fully, processing all widgets and re-creating all draw calls.
	/// </summary>

	void FillAllDrawCalls ()
	{
		for (int i = 0; i < drawCalls.Count; ++i)
			UIDrawCall.Destroy(drawCalls[i]);
		drawCalls.Clear();

		Material mat = null;
		Texture tex = null;
		Shader sdr = null;
		UIDrawCall dc = null;
		int count = 0;

		if (mSortWidgets) SortWidgets();

		for (int i = 0; i < widgets.Count; ++i)
		{
			UIWidget w = widgets[i];

			if (w.isVisible && w.hasVertices)
			{
				Material mt = w.material;
				
				if (onCreateMaterial != null) mt = onCreateMaterial(w, mt);

				Texture tx = w.mainTexture;
				Shader sd = w.shader;

                 // 判断widgets[i-1]与widgets[i]的mateial,texture,shader是否一致
				if (mat != mt || tex != tx || sdr != sd)
				{
					if (dc != null && dc.verts.Count != 0)
					{
                        drawCalls.Add(dc);          //缓存widgets[i-1]的drawcall
						dc.UpdateGeometry(count);
						dc.onRender = mOnRender;
						mOnRender = null;
						count = 0;
                        dc = null;                  //缓存drawcall后,重置drawcall
					}
                    //缓存widgets[i]的material,texture,shader
					mat = mt;
					tex = tx;
					sdr = sd;
				}

				if (mat != null || sdr != null || tex != null)
				{
                    // 创建新的drawcall,设置drawcall相关的数据
					if (dc == null)
					{
						dc = UIDrawCall.Create(this, mat, tex, sdr);
						dc.depthStart = w.depth;
						dc.depthEnd = dc.depthStart;
						dc.panel = this;
						dc.onCreateDrawCall = onCreateDrawCall;
					}
                    //widgets[i-1]与widgets[i]的material,shader,texture相同时,共用一个drawcall
					else
					{
                        // 更新drawcall的深度范围
						int rd = w.depth;
						if (rd < dc.depthStart) dc.depthStart = rd;
						if (rd > dc.depthEnd) dc.depthEnd = rd;
					}

					w.drawCall = dc;

					++count;
					if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans, generateUV2 ? dc.uv2 : null);
					else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null, generateUV2 ? dc.uv2 : null);

					if (w.mOnRender != null)
					{
						if (mOnRender == null) mOnRender = w.mOnRender;
						else mOnRender += w.mOnRender;
					}
				}
			}
			else w.drawCall = null;
		}

        //缓存widgets[widgets.Count-1]的drawcall
		if (dc != null && dc.verts.Count != 0)
		{
			drawCalls.Add(dc);
			dc.UpdateGeometry(count);
			dc.onRender = mOnRender;
			mOnRender = null;
		}
	}

1.4 widgets的生成及作用

UIWidget的定义

在这里插入图片描述

UIPanel与UIWidget的对应关系是一对多的,而UISprite,UI2DSprite,UITexture,UILabel等控件本质上都是UIWidget。

widgets的生成

调用的地方有两个地方.

  • 改变UIWidget的深度时
  • 创建UIPanel时
  1. Set Depth
	/// <summary>
	/// Depth controls the rendering order -- lowest to highest.
	/// </summary>

	public int depth
	{
		get
		{
			// Experiment with a transform-based depth, uGUI style
			//if (mDepth == int.MinValue)
			//{
			//    int val = cachedTransform.GetSiblingIndex();
			//    UIWidget pt = parent as UIWidget;
			//    if (pt != null) val += pt.depth;
			//    return val;
			//}
			return mDepth;
		}
		set
		{
			if (mDepth != value)
			{
				if (panel != null) panel.RemoveWidget(this);
				mDepth = value;
				
				if (panel != null)
				{
					panel.AddWidget(this); // 将控件Widget添加Panel的Widget列表中

					if (!Application.isPlaying)
					{
						panel.SortWidgets();
						panel.RebuildAllDrawCalls();
					}
				}
#if UNITY_EDITOR
				NGUITools.SetDirty(this);
#endif
			}
		}
	}
  1. 创建UIPanel的时候
	/// <summary>
	/// Ensure we have a panel referencing this widget.
	/// </summary>

	public UIPanel CreatePanel ()
	{
		if (mStarted && panel == null && enabled && NGUITools.GetActive(gameObject))
		{
			panel = UIPanel.Find(cachedTransform, true, cachedGameObject.layer);

			if (panel != null)
			{
				mParentFound = false;
				panel.AddWidget(this);
				CheckLayer();
				Invalidate(true);
			}
		}
		return panel;
	}

1.5 drawcall的合并

参考 NGUI如何优化drawcall数量

核心思想在于: 重写 Widget PanelCompareFunc 排序规则.

  1. 加入对于材质,贴图,shader的判断;
  2. 在规划UI界面时,需要统筹规划各个UIPanel的深度。

ref: 可以按照材质是来排序. 因为相同材质/纹理/着色器的Widget可以合并到一个DrawCall中。
当然合并不一定能带来增益。

2 小结

在这里插入图片描述
NGUI的图形工作流程,UIGeometry被UIWidget实例化之后,通过UIWidget的子类,也就是UISprit,UILabel等,在OnFill()函数里算出所需的Geometry缓存(顶点数,UV,Color,法线,切线)。PS:之所以要生成这些数据,是为了之后生成mesh来渲染。

而UIPanel,通过遍历自己子类下所有的UIWidget组件(已经按深度排序),
先创建一个UIDrawCall,然后把该Widget的material,texture,shader对象以及Geometry的缓存传给UIDrawCall,如此反复循环搜索该UIPanel下的每一个Widget,只要是material,texture,shader都和上一个Widget一样的Widget,他们的缓存都传给同一个UIDrawCall,直到循环结束或者碰到一个材质球,贴图,shader对象任一不相同的Widget。
当遇到这种Widget,循环会再创建一个新的UIDrawCall,然后传递material,texture,shader,缓存,如此这般,直到循环完全结束。

每次有新的UIDrawCall产生,UIPanel就会调用上一个UIDrawCall的UpdateGeometry()函数,来创建渲染所需的对象。这些对象分别是MeshFilter,MeshRender,和最重要的Mesh(Mesh的顶点,UV,Color,法线,切线,还有三角面)。这些对象都会像我们正常在游戏中新建Cube一样,依附在创建UIDrawCall时生成的GameObject上以便可以渲染。我们在Editor中是看不到这个GameObject的,是因为创建的时候设置了HideFlags.HideAndDontSave。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值