相信来到这里的你和我一样好奇NGUI是如何将我们的原始输入加工成为最终呈现出来的样子的,一言以蔽之,这个过程就是生成顶点、UV、颜色等数据并将它们传入Mesh中使用MeshRenderer进行渲染,但实际过程中需要涉及到各种模块的管理、对效率的优化和对表现的提升等等。


让我们先从一张图开始吧。

为了更好地理解这张图,我们可以先从一条线索入手,那就是在渲染过程中被传递的数据,即顶点和UV等。

顶点和UV

作为图形学的基础,关于顶点和UV的介绍和探讨相信已经有很多了,本文不再赘述。这里所要关心的是,站在NGUI的视角上我们是如何看待或者是如何使用顶点和UV的呢?它们在NGUI的渲染过程中到底发挥了什么样的作用呢?

顶点

顶点是界定所要渲染区域的最基本的元素。在三维空间中,往往还要涉及到对顶点的空间转换,而在二维空间的NGUI中,情况就比较单纯了。

先上两张图,第一张是我们所见的普通视图。

第二张是实际渲染时候的网格图,我们可以看到顶点组成了大量的三角形,这些都是最基本的图元。

UV

UV即u,v纹理贴图坐标,在渲染过程中会根据UV坐标对贴图进行采样。而在NGUI中,使用的贴图主要有三种情况,一种是直接使用原始贴图,第二种是使用图集,还有一种是字体。

以图集为例,左上角四分之一部分的贴图取样对应的UV为(0.0,1.0),(0.0,0.5),(0.5,0.5),(0.5,1.0),大概是这样的。

好了,做好基本工作,我们就可以真正开始探索了。接下来,我会根据图中的各个步骤开始逐一剖析,揭秘NGUI渲染流程的大致脉络。


渲染流程

1、在继承自UIWidget的组件中生成顶点、UV、颜色等数据,并缓存到UIGeometry中

与用户最直接相关的一步,就是我们的输入被各种组件用不同的方法生成顶点、UV和颜色,从而表现出不同的效果。以Sliced类型的UISprite为例,整个Spite被分成9个部分,包括內域和外域,其中外域包含4边和4角,內域顶点所围成的面由內域贴图渲染,外域顶点所围成的面由外域贴图渲染。

下图展示了Simple(右)和Sliced(左)中顶点的区别

    //由下面代码可以看出,Sliced类型的做法就是在顶点坐标和UV中根据border划分出一道内边界,从而分出内域和外域,//内域的顶点由内域贴图渲染,外域顶点由外域贴图渲染void SlicedFill (List<Vector3> verts, List<Vector2> uvs, List<Color> cols){//border以内的贴图可视为Simple,以外无法被拉伸Vector4 br = border * pixelSize;//当border为0的时候,可视整张贴图为Simpleif (br.x == 0f && br.y == 0f && br.z == 0f && br.w == 0f){SimpleFill(verts, uvs, cols);return;}Color gc = drawingColor;//渲染后图像4条边相对中心点的位置,xyzw分别对应左下右上Vector4 v = drawingDimensions;//左下的外边界坐标mTempPos[0].x = v.x;mTempPos[0].y = v.y;//右上的外边界坐标mTempPos[3].x = v.z;mTempPos[3].y = v.w;if (mFlip == Flip.Horizontally || mFlip == Flip.Both){//左下的内边界坐标mTempPos[1].x = mTempPos[0].x + br.z;//右上的内边界坐标mTempPos[2].x = mTempPos[3].x - br.x;//mOuterUV为原始贴图,mInnerUV为被border界定的内贴图mTempUVs[3]. x = mOuterUV. xMin;mTempUVs[2]. x = mInnerUV. xMin;mTempUVs[1]. x = mInnerUV. xMax;mTempUVs[0]. x = mOuterUV. xMax;}//类似的,对纵向坐标进行处理...//将计算好的顶点、UV、颜色放入缓存中,跟踪数据可知,这些数据此时被放入了UIGeometryfor (int x = 0; x < 3; ++x){int x2 = x + 1;for (int y = 0; y < 3; ++y){if (centerType == AdvancedType.Invisible && x == 1 && y == 1) continue;int y2 = y + 1;//用4个边界坐标即mTempPos生成顶点数据,可以理解为将mTempPos的所有x和所有y两两组合为最后的顶点verts.Add(new Vector3(mTempPos[x].x, mTempPos[y].y));verts.Add(new Vector3(mTempPos[x].x, mTempPos[y2].y));verts.Add(new Vector3(mTempPos[x2].x, mTempPos[y2].y));verts.Add(new Vector3(mTempPos[x2].x, mTempPos[y].y));//类似的,对UV和颜色进行处理...}}}

2、数据从UIGeometry被写入UIDrawCall的缓冲区,准备渲染

这一步主要由UIPanel来管理,在有组件被标记上Change时(比如改变了内容、深度变化等),UIPanel会为对应的组件(继承自UIWidget)找到适应的UIDrawcall,而一旦找不到适应的UIDrawCall,就会将UIPanel内的UIDrawcall全部回收,然后重新制造适应的UIDrawcall。
关键在于,什么是所谓的“适应”呢?我们可以通过以下这段代码一探究竟。

    void FillAllDrawCalls (){//回收Panel内所有DrawCallfor (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();//生成适应的DrawCallfor (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;//如果此组件材质(在NGUI中一般体现为图集和字体)、贴图、Shader中有一样不同于(深度)相邻的组件的话,//就把上一个DrawCall放入DrawCall池,至此完成一个DrawCall的真正创建if (mat != mt || tex != tx || sdr != sd){if (dc != null && dc.verts.Count != 0){drawCalls.Add(dc);dc.UpdateGeometry(count);dc.onRender = mOnRender;mOnRender = null;count = 0;dc = null;}mat = mt;tex = tx;sdr = sd;}//如果此组件有材质(在NGUI中一般体现为图集和字体)、贴图、Shader中的任意一种的话,//就创建一个新的DrawCallif (mat != null || sdr != null || tex != null){if (dc == null){dc = UIDrawCall.Create(this, mat, tex, sdr);dc.depthStart = w.depth;dc.depthEnd = dc.depthStart;dc.panel = this;dc.onCreateDrawCall = onCreateDrawCall;}else{int rd = w.depth;if (rd < dc.depthStart) dc.depthStart = rd;if (rd > dc.depthEnd) dc.depthEnd = rd;}w.drawCall = dc;++count;//将上文所提到的顶点、UV、颜色等数据从UIGeometry写到UIDrawCall中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;}//最后一个DrawCallif (dc != null && dc.verts.Count != 0){drawCalls.Add(dc);dc.UpdateGeometry(count);dc.onRender = mOnRender;mOnRender = null;}}

像这样,通过合并减少DrawCall的数量,从而减少对CPU的开销,提升性能。这也就是为什么我们要将材质相同的组件在深度上尽量集中放置。

3、使用UIDrawCall中的数据建立网格,并通过MeshRenderer渲染

好了,原料都准备好了,但是没有机器怎么开工呢?好在Unity提供了这样的机器,NGUI可以将生成的顶点、UV、颜色、法线、切线等数据制作成网格,然后通过MeshRenderer将网格渲染出来。这一部分可以说像一座桥梁,连接了NGUI和Unity。

    public void UpdateGeometry (int widgetCount){this.widgetCount = widgetCount;int vertexCount = verts.Count;// 安全性检测,确保获得(至少是格式上)正确的数据,这里主要看的是顶点、UV和颜色对不对应if (vertexCount > 0 && (vertexCount == uvs.Count && vertexCount == cols.Count) && (vertexCount % 4) == 0){//颜色处理if (mColorSpace == ColorSpace.Uninitialized)mColorSpace = QualitySettings.activeColorSpace;if (mColorSpace == ColorSpace.Linear){for (int i = 0; i < vertexCount; ++i){var c = cols[i];c.r = Mathf.GammaToLinearSpace(c.r);c.g = Mathf.GammaToLinearSpace(c.g);c.b = Mathf.GammaToLinearSpace(c.b);c.a = Mathf.GammaToLinearSpace(c.a);cols[i] = c;}}// 缓存下MeshFilter,主要用来获取网格if (mFilter == null) mFilter = gameObject.GetComponent<MeshFilter>();if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();if (vertexCount < 65000){// 这里是计算实际组成最后的三角面(最小的面单元)的顶点数,如果发生了变化就重新生成三角面的顶点序列int indexCount = (vertexCount >> 1) * 3;bool setIndices = (mIndices == null || mIndices.Length != indexCount);// 创建网格if (mMesh == null){mMesh = new Mesh();mMesh.hideFlags = HideFlags.DontSave;mMesh.name = (mMaterial != null) ? "[NGUI] " + mMaterial.name : "[NGUI] Mesh";if (dx9BugWorkaround == 0) mMesh.MarkDynamic();setIndices = true;}//其他处理,主要是顶点的修整...//放入数据,准备渲染mMesh.SetVertices(verts);mMesh.SetUVs(0, uvs);mMesh.SetColors(cols);#if UNITY_5_4 || UNITY_5_5_OR_NEWER//放入法线、切线等数据mMesh.SetUVs(1, (uv2.Count == vertexCount) ? uv2 : null);mMesh.SetNormals((norms.Count == vertexCount) ? norms : null);mMesh.SetTangents((tans.Count == vertexCount) ? tans : null);#elseif (uv2.Count != vertexCount) uv2.Clear();if (norms.Count != vertexCount) norms.Clear();if (tans.Count != vertexCount) tans.Clear();mMesh.SetUVs(1, uv2);mMesh.SetNormals(norms);mMesh.SetTangents(tans);#endif
#endif//计算三角面的顶点序列if (setIndices){mIndices = GenerateCachedIndexBuffer(vertexCount, indexCount);mMesh.triangles = mIndices;}#if !UNITY_FLASHif (trim || !alwaysOnScreen)
#endif//使用顶点重新计算边界mMesh.RecalculateBounds();mFilter.mesh = mMesh;}//为了性能考虑,如果顶点数太多了就报错,提醒进行优化,实际开发中这种限制很有必要else{mTriangles = 0;if (mMesh != null) mMesh.Clear();Debug.LogError("Too many vertices on one panel: " + vertexCount);}//设置好MeshRenderer参数后,开始渲染if (mRenderer == null) mRenderer = gameObject.GetComponent<MeshRenderer>();if (mRenderer == null){mRenderer = gameObject.AddComponent<MeshRenderer>();//对MeshRenderer进行一系列的设置...}UpdateMaterials();}else{if (mFilter.mesh != null) mFilter.mesh.Clear();Debug.LogError("UIWidgets must fill the buffer with 4 vertices per quad. Found " + vertexCount);}//清空数据...}

最后通过MeshRenderer,Unity就可以渲染出图像了,这就是从原始输入到我们之所见的大致流程。更多细节我也希望进一步地探索,并分享出来。

NGUI渲染机制——从顶点和UV说起相关推荐

  1. android 重绘如何能不闪一下屏幕_浏览器渲染机制——重绘重排

    性能优化中,减少重绘重排应该是一种很好的优化方式,我们具体看一下什么情况下会造成重绘重排,为什么减少重绘重排可以做到优化,怎么样减少重绘重排. 浏览器渲染过程 我们先看看当浏览器拿到服务端返回的资源时 ...

  2. Android渲染机制和丢帧分析

    http://blog.csdn.net/bd_zengxinxin/article/details/52525781 自己编写App的时候,有时会感觉界面卡顿,尤其是自定义View的时候,大多数是因 ...

  3. 浏览器渲染机制面试_面试官不讲码德,问我Chrome浏览器的渲染原理(6000字长文)...

    前言 对于HTML,css和JavaScript是如何变成页面的,这个问题你了解过吗?浏览器究竟在背后都做了些什么事情呢?让我们去了解浏览器的渲染原理,是通往更深层次的开发必不可少的事情,能让我们更深 ...

  4. 从架构到源码:一文了解Flutter渲染机制

    简介:Flutter从本质上来讲还是一个UI框架,它解决的是一套代码在多端渲染的问题.在渲染管线的设计上更加精简,加上自建渲染引擎,相比ReactNative.Weex以及WebView等方案,具有更 ...

  5. 浏览器渲染机制面试_【前端面试必考题】页面渲染机制(一)

    页面渲染机制这部分内容会分成两篇来进行讲解,这两篇里我们准备聊一下页面的渲染的过程,包括页面的加载.DOM 树的构建.CSSOM 树的构建.渲染树的构建和最后的渲染过程等.浏览器的渲染机制和网页的优化 ...

  6. beetl 页面标签_Beetl 2.9.0 发布,修改 HTML 标签的渲染机制

    本次发布主要修改了 HTML 标签的渲染机制,HTMLTagSupportWrapper2 采用延迟渲染 在2.9.0版本,HTML 标签内部渲染是使用tagBody变量,渲染的时候会调用此变量的to ...

  7. jq动态渲染后获取不到元素高度_浏览器的渲染机制

    面试肯定会问到这个吧~ So:再一次的屡屡浏览器的渲染机制~ 在渲染一开始会先从网络层获取请求文档(HTML.XML)的内容,然后再进行以下基本流程 3.1 解析HTML => DOM树 从HT ...

  8. js一定要放在body的最底部么?聊聊浏览器的渲染机制

    今天看了一篇前段大全推送的"js一定要放在body的最底部么?聊聊浏览器的渲染机制".正好今天在<javascript Dom 编程艺术>上看到有说,<scrip ...

  9. html弹出层很字体模糊了,由CSS3 transform 字体模糊问题揭示出浏览器渲染机制

    为了实现垂直.水平居中效果,经常会用到 position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); 但是在实际项目中, ...

最新文章

  1. 时隔这么长时间,又回来写博客了
  2. git push 的符号笔有什么用_Git自救指南(一)——工欲善其事,必先利其器,基本概念概览...
  3. FFMPEG结构体分析之AVPacket
  4. Android下创建一个输入法
  5. 增强中获取屏幕值的一句很实用代码…
  6. 软考信息系统项目管理师_项目风险管理---软考高级之信息系统项目管理师019
  7. bash: !: event not found
  8. 5 Java NIO Scatter 与Gather-翻译
  9. 小D课堂 - 零基础入门SpringBoot2.X到实战_第2节 SpringBoot接口Http协议开发实战_7、开发必备工具PostMan接口工具介绍和使用...
  10. 2021年1月6日运行Python脚本的一些说明与教程
  11. Effective C++学习笔记(条款1-34)
  12. Traceback (most recent call last)解决方法
  13. Jzoj5603 xiz
  14. 数据仓库介绍(一) - 数据来源
  15. 机器学习中 熵的理解
  16. 你喜欢天长地久,还是曾经拥有?
  17. 读书笔记:杨家成的英语学习之路(附带作者人生感悟)
  18. spring boot 项目启动无法访问,排查
  19. 解释java程序所使用的命令是,【单选题】Java 源程序的解释命令是
  20. Windows2014使用NBU备份实现Oracle11g本地恢复和异地恢复

热门文章

  1. 具有不安全、不正确或缺少SameSite属性的Cookie
  2. java实现自举_实现语言的自举 - 沙枣的个人空间 - OSCHINA - 中文开源技术交流社区...
  3. EasyPark共享停车位的设计与实现
  4. SQL之数据定义、数据操纵
  5. 2022.12六级真题第3套(共6页)
  6. 美团酒旅数据治理实践案例分享
  7. css加号图标_一步步打造自己的纯CSS单标签图标库
  8. Whither Speech Recognition: 25年又一个25年
  9. 五、HTML5单页框架View.js介绍 - View.js的比较优势
  10. 06-void类型和never类型