NGUI渲染机制——从顶点和UV说起

相信来到这里的你和我一样好奇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的时候,可视整张贴图为Simple
        if (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、颜色放入缓存中,跟踪数据可知,这些数据此时被放入了UIGeometry
        for (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内所有DrawCall
        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();


        //生成适应的DrawCall
        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;

                //如果此组件材质(在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中的任意一种的话,
                //就创建一个新的DrawCall
                if (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;
        }

        //最后一个DrawCall
        if (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);
 #else
                if (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_FLASH
                if (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就可以渲染出图像了,这就是从原始输入到我们之所见的大致流程。更多细节我也希望进一步地探索,并分享出来。

猜你喜欢

转载自blog.csdn.net/weixin_42596388/article/details/80891207