NGUI渲染流程

2019-10-17  本文已影响0人  白桦叶

0. 概述

本文将从整体类图出发,先对NGUI渲染涉及到几个重点的类的关系有一个整体的了解,接着再讲下各个类的作用,然后通过源码将下整个渲染的流程,最后尝试解答几个问题。本文使用的NGUI版本是3.8.2。

1. 整体类图

NGUI_Class.png

我们从图中可以看到涉及到NGUI渲染流程的类主要有UIRect、UIWidget、UIPanel、UIDrawcall和UIGeometry。

2. 各个类的作用

2.1 UIRect

UIRect作为UIWidget和UIPanel的抽象基类,主要作用是维护4个锚点(左/上/右/下),并根据锚定更新类型在适当的时候更新4个锚点,并提供OnAnchor抽象方法让子类根据锚点更新自己的尺寸。

2.2 UIGeometry

2.3 UIWidget

2.4 UIDrawCall

2.5 UIPanel

3. 渲染流程

[图片上传中...(NGUI_UIPanel.png-f91a26-1571404641111-0)]

3.1 UIPanel的LateUpdate

先遍历根据深度进行排序的静态的Panel列表,并调用UIPanel的UpdateSelf函数更新每个Panel.
再遍历Panel列表根据Panel的RenderQueue类型不同(Automatic/StartAt/Explicit),设置相应的RenderQueue数据并调用Panel的UpdateDrawCalls更新Drawcall。

void LateUpdate ()
    {
#if UNITY_EDITOR
        if (mUpdateFrame != Time.frameCount || !Application.isPlaying)
#else
        if (mUpdateFrame != Time.frameCount)
#endif
        {
            mUpdateFrame = Time.frameCount;

            // Update each panel in order
            for (int i = 0, imax = list.Count; i < imax; ++i)
                list[i].UpdateSelf();

            int rq = 3000;

            // Update all draw calls, making them draw in the right order
            for (int i = 0, imax = list.Count; i < imax; ++i)
            {
                UIPanel p = list[i];

                if (p.renderQueue == RenderQueue.Automatic)
                {
                    p.startingRenderQueue = rq;
                    p.UpdateDrawCalls();
                    rq += p.drawCalls.Count;
                }
                else if (p.renderQueue == RenderQueue.StartAt)
                {
                    p.UpdateDrawCalls();
                    if (p.drawCalls.Count != 0)
                        rq = Mathf.Max(rq, p.startingRenderQueue + p.drawCalls.Count);
                }
                else // Explicit
                {
                    p.UpdateDrawCalls();
                    if (p.drawCalls.Count != 0)
                        rq = Mathf.Max(rq, p.startingRenderQueue + 1);
                }
            }
        }
    }

3.2 UIPanel的UpdateSelf

3.2.1 流程

3.2.2 代码

void UpdateSelf ()
    {
        mUpdateTime = RealTime.time;

        UpdateTransformMatrix();
        UpdateLayers();
        UpdateWidgets();

        if (mRebuild)
        {
            mRebuild = false;
            FillAllDrawCalls();
        }
        else
        {
            for (int i = 0; i < drawCalls.Count; )
            {
                UIDrawCall dc = drawCalls[i];

                if (dc.isDirty && !FillDrawCall(dc))
                {
                    UIDrawCall.Destroy(dc);
                    drawCalls.RemoveAt(i);
                    continue;
                }
                ++i;
            }
        }

        if (mUpdateScroll)
        {
            mUpdateScroll = false;
            UIScrollView sv = GetComponent<UIScrollView>();
            if (sv != null) sv.UpdateScrollbars();
        }
    }

3.3 UIPanel的UpdateWidgets

3.3.1 流程

3.3.2 代码

void UpdateWidgets()
    {
#if UNITY_EDITOR
        bool forceVisible = cullWhileDragging ? false : (Application.isPlaying && mCullTime > mUpdateTime);
#else
        bool forceVisible = cullWhileDragging ? false : (mCullTime > mUpdateTime);
#endif
        bool changed = false;

        if (mForced != forceVisible)
        {
            mForced = forceVisible;
            mResized = true;
        }

        bool clipped = hasCumulativeClipping;

        // Update all widgets
        for (int i = 0, imax = widgets.Count; i < imax; ++i)
        {
            UIWidget w = widgets[i];

            // If the widget is visible, update it
            if (w.panel == this && w.enabled)
            {
#if UNITY_EDITOR
                // When an object is dragged from Project view to Scene view, its Z is...
                // odd, to say the least. Force it if possible.
                if (!Application.isPlaying)
                {
                    Transform t = w.cachedTransform;

                    if (t.hideFlags != HideFlags.HideInHierarchy)
                    {
                        t = (t.parent != null && t.parent.hideFlags == HideFlags.HideInHierarchy) ?
                            t.parent : null;
                    }

                    if (t != null)
                    {
                        for (; ; )
                        {
                            if (t.parent == null) break;
                            if (t.parent.hideFlags == HideFlags.HideInHierarchy) t = t.parent;
                            else break;
                        }

                        if (t != null)
                        {
                            Vector3 pos = t.localPosition;
                            pos.x = Mathf.Round(pos.x);
                            pos.y = Mathf.Round(pos.y);
                            pos.z = 0f;

                            if (Vector3.SqrMagnitude(t.localPosition - pos) > 0.0001f)
                                t.localPosition = pos;
                        }
                    }
                }
#endif
                int frame = Time.frameCount;

                // First update the widget's transform
                if (w.UpdateTransform(frame) || mResized)
                {
                    // Only proceed to checking the widget's visibility if it actually moved
                    bool vis = forceVisible || (w.CalculateCumulativeAlpha(frame) > 0.001f);
                    w.UpdateVisibility(vis, forceVisible || ((clipped || w.hideIfOffScreen) ? IsVisible(w) : true));
                }
                
                // Update the widget's geometry if necessary
                if (w.UpdateGeometry(frame))
                {
                    changed = true;

                    if (!mRebuild)
                    {
                        if (w.drawCall != null)
                        {
                            w.drawCall.isDirty = true;
                        }
                        else
                        {
                            // Find an existing draw call, if possible
                            FindDrawCall(w);
                        }
                    }
                }
            }
        }

        // Inform the changed event listeners
        if (changed && onGeometryUpdated != null) onGeometryUpdated();
        mResized = false;
    }

3.4 UIWidget的UpdateGeometry

3.4.1 流程

3.4.2 代码

public bool UpdateGeometry (int frame)
    {
        // Has the alpha changed?
        float finalAlpha = CalculateFinalAlpha(frame);
        if (mIsVisibleByAlpha && mLastAlpha != finalAlpha) mChanged = true;
        mLastAlpha = finalAlpha;

        if (mChanged)
        {
            mChanged = false;

            if (mIsVisibleByAlpha && finalAlpha > 0.001f && shader != null)
            {
                bool hadVertices = geometry.hasVertices;

                if (fillGeometry)
                {
                    geometry.Clear();
                    OnFill(geometry.verts, geometry.uvs, geometry.cols);
                }

                if (geometry.hasVertices)
                {
                    // Want to see what's being filled? Uncomment this line.
                    //Debug.Log("Fill " + name + " (" + Time.frameCount + ")");

                    if (mMatrixFrame != frame)
                    {
                        mLocalToPanel = panel.worldToLocal * cachedTransform.localToWorldMatrix;
                        mMatrixFrame = frame;
                    }
                    geometry.ApplyTransform(mLocalToPanel);
                    mMoved = false;
                    return true;
                }
                return hadVertices;
            }
            else if (geometry.hasVertices)
            {
                if (fillGeometry) geometry.Clear();
                mMoved = false;
                return true;
            }
        }
        else if (mMoved && geometry.hasVertices)
        {
            if (mMatrixFrame != frame)
            {
                mLocalToPanel = panel.worldToLocal * cachedTransform.localToWorldMatrix;
                mMatrixFrame = frame;
            }
            geometry.ApplyTransform(mLocalToPanel);
            mMoved = false;
            return true;
        }
        mMoved = false;
        return false;
    }

3.5 UIPanel的FillAllDrawCalls

3.5.1 流程

3.5.2 代码

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;
                Texture tx = w.mainTexture;
                Shader sd = w.shader;

                if (mat != mt || tex != tx || sdr != sd)
                {
                    if (dc != null && dc.verts.size != 0)
                    {
                        drawCalls.Add(dc);
                        dc.UpdateGeometry(count);
                        dc.onRender = mOnRender;
                        mOnRender = null;
                        count = 0;
                        dc = null;
                    }

                    mat = mt;
                    tex = tx;
                    sdr = sd;
                }

                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;
                    }
                    else
                    {
                        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);
                    else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null);

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

        if (dc != null && dc.verts.size != 0)
        {
            drawCalls.Add(dc);
            dc.UpdateGeometry(count);
            dc.onRender = mOnRender;
            mOnRender = null;
        }
    }

3.6 UIPanel的FillDrawCall

3.6.1 流程

3.7.2 代码

bool FillDrawCall (UIDrawCall dc)
    {
        if (dc != null)
        {
            dc.isDirty = false;
            int count = 0;

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

                if (w == null)
                {
#if UNITY_EDITOR
                    Debug.LogError("This should never happen");
#endif
                    widgets.RemoveAt(i);
                    continue;
                }

                if (w.drawCall == dc)
                {
                    if (w.isVisible && w.hasVertices)
                    {
                        ++count;
                        
                        if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans);
                        else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null);

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

            if (dc.verts.size != 0)
            {
                dc.UpdateGeometry(count);
                dc.onRender = mOnRender;
                mOnRender = null;
                return true;
            }
        }
        return false;
    }

3.7 UIDrawcall的UpdateGeometry

3.7.1 流程

3.7.2 代码

public void UpdateGeometry (int widgetCount)
    {
        this.widgetCount = widgetCount;
        int count = verts.size;

        // Safety check to ensure we get valid values
        if (count > 0 && (count == uvs.size && count == cols.size) && (count % 4) == 0)
        {
            // Cache all components
            if (mFilter == null) mFilter = gameObject.GetComponent<MeshFilter>();
            if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();

            if (verts.size < 65000)
            {
                // Populate the index buffer
                int indexCount = (count >> 1) * 3;
                bool setIndices = (mIndices == null || mIndices.Length != indexCount);

                // Create the mesh
                if (mMesh == null)
                {
                    mMesh = new Mesh();
                    mMesh.hideFlags = HideFlags.DontSave;
                    mMesh.name = (mMaterial != null) ? "[NGUI] " + mMaterial.name : "[NGUI] Mesh";
                    mMesh.MarkDynamic();
                    setIndices = true;
                }
#if !UNITY_FLASH
                // If the buffer length doesn't match, we need to trim all buffers
                bool trim = (uvs.buffer.Length != verts.buffer.Length) ||
                    (cols.buffer.Length != verts.buffer.Length) ||
                    (norms.buffer != null && norms.buffer.Length != verts.buffer.Length) ||
                    (tans.buffer != null && tans.buffer.Length != verts.buffer.Length);

                // Non-automatic render queues rely on Z position, so it's a good idea to trim everything
                if (!trim && panel.renderQueue != UIPanel.RenderQueue.Automatic)
                    trim = (mMesh == null || mMesh.vertexCount != verts.buffer.Length);

                // NOTE: Apparently there is a bug with Adreno devices:
                // http://www.tasharen.com/forum/index.php?topic=8415.0
#if !UNITY_ANDROID
                // If the number of vertices in the buffer is less than half of the full buffer, trim it
                if (!trim && (verts.size << 1) < verts.buffer.Length) trim = true;
#endif
                mTriangles = (verts.size >> 1);

                if (trim || verts.buffer.Length > 65000)
                {
                    if (trim || mMesh.vertexCount != verts.size)
                    {
                        mMesh.Clear();
                        setIndices = true;
                    }

                    mMesh.vertices = verts.ToArray();
                    mMesh.uv = uvs.ToArray();
                    mMesh.colors32 = cols.ToArray();

                    if (norms != null) mMesh.normals = norms.ToArray();
                    if (tans != null) mMesh.tangents = tans.ToArray();
                }
                else
                {
                    if (mMesh.vertexCount != verts.buffer.Length)
                    {
                        mMesh.Clear();
                        setIndices = true;
                    }

                    mMesh.vertices = verts.buffer;
                    mMesh.uv = uvs.buffer;
                    mMesh.colors32 = cols.buffer;

                    if (norms != null) mMesh.normals = norms.buffer;
                    if (tans != null) mMesh.tangents = tans.buffer;
                }
#else
                mTriangles = (verts.size >> 1);

                if (mMesh.vertexCount != verts.size)
                {
                    mMesh.Clear();
                    setIndices = true;
                }

                mMesh.vertices = verts.ToArray();
                mMesh.uv = uvs.ToArray();
                mMesh.colors32 = cols.ToArray();

                if (norms != null) mMesh.normals = norms.ToArray();
                if (tans != null) mMesh.tangents = tans.ToArray();
#endif
                if (setIndices)
                {
                    mIndices = GenerateCachedIndexBuffer(count, indexCount);
                    mMesh.triangles = mIndices;
                }

#if !UNITY_FLASH
                if (trim || !alwaysOnScreen)
#endif
                    mMesh.RecalculateBounds();

                mFilter.mesh = mMesh;
            }
            else
            {
                mTriangles = 0;
                if (mFilter.mesh != null) mFilter.mesh.Clear();
                Debug.LogError("Too many vertices on one panel: " + verts.size);
            }

            if (mRenderer == null) mRenderer = gameObject.GetComponent<MeshRenderer>();

            if (mRenderer == null)
            {
                mRenderer = gameObject.AddComponent<MeshRenderer>();
#if UNITY_EDITOR
                mRenderer.enabled = isActive;
#endif
            }
            UpdateMaterials();
        }
        else
        {
            if (mFilter.mesh != null) mFilter.mesh.Clear();
            Debug.LogError("UIWidgets must fill the buffer with 4 vertices per quad. Found " + count);
        }

        verts.Clear();
        uvs.Clear();
        cols.Clear();
        norms.Clear();
        tans.Clear();
    }

3.8 UIPanel的UpdateDrawCalls

3.8.1 作用

更新Panel的裁剪区域和Drawcall的位置,旋转角度,sortingorder,renderQueue等信息。

3.8.2 代码

void UpdateDrawCalls ()
    {
        Transform trans = cachedTransform;
        bool isUI = usedForUI;

        if (clipping != UIDrawCall.Clipping.None)
        {
            drawCallClipRange = finalClipRegion;
            drawCallClipRange.z *= 0.5f;
            drawCallClipRange.w *= 0.5f;
        }
        else drawCallClipRange = Vector4.zero;

        int w = Screen.width;
        int h = Screen.height;

        // Legacy functionality
        if (drawCallClipRange.z == 0f) drawCallClipRange.z = w * 0.5f;
        if (drawCallClipRange.w == 0f) drawCallClipRange.w = h * 0.5f;

        // DirectX 9 half-pixel offset
        if (halfPixelOffset)
        {
            drawCallClipRange.x -= 0.5f;
            drawCallClipRange.y += 0.5f;
        }

        Vector3 pos;

        if (isUI)
        {
            Transform parent = cachedTransform.parent;
            pos = cachedTransform.localPosition;

            if (clipping != UIDrawCall.Clipping.None)
            {
                pos.x = Mathf.RoundToInt(pos.x);
                pos.y = Mathf.RoundToInt(pos.y);
            }

            if (parent != null) pos = parent.TransformPoint(pos);
            pos += drawCallOffset;
        }
        else pos = trans.position;

        Quaternion rot = trans.rotation;
        Vector3 scale = trans.lossyScale;

        for (int i = 0; i < drawCalls.Count; ++i)
        {
            UIDrawCall dc = drawCalls[i];

            Transform t = dc.cachedTransform;
            t.position = pos;
            t.rotation = rot;
            t.localScale = scale;

            dc.renderQueue = (renderQueue == RenderQueue.Explicit) ? startingRenderQueue : startingRenderQueue + i;
            dc.alwaysOnScreen = alwaysOnScreen &&
                (mClipping == UIDrawCall.Clipping.None || mClipping == UIDrawCall.Clipping.ConstrainButDontClip);
            dc.sortingOrder = mSortingOrder;
            dc.clipTexture = mClipTexture;
        }
    }

4. 解答问题

4.1 UIWidget的depth如何生效

  1. 我们知道在同一个Panel不同的UIWidget可以通过设置深度depth值,depth越大越显示在前面
  2. UIPanel下widget组件列表按照在widget的depth进行排序,在FillAllDrawCalls遍历widget组件列表将material,texture,shader相同的widget的顶点,UV,Color等几何数据填入同一个Drawcall缓存中。
  3. 上面讲到UIDrawcall是渲染UI元素的载体,UIPanel生成UIDrawcall,UIDrawcall是一个组件,挂载在一个GameObject,这个GameObject上再挂载MeshRender、Mesh、MeshFilter、材质等Unity组件,通过这些组件将UI元素渲染出来。
  4. 最终UI元素渲染顺序是通过MeshRender的sortingOrder和其材质的renderQueue共同决定的。
  5. 下方为RenderQueue,Sortingorder,Sorting Layer如何共同决定物体的渲染顺序

1.RenderQueue 2500以下
1. Sorting Layer/Order in Layer
1. 按照Sorting Layer/Order in Layer 设置的值,越小越优先
2. 无此属性,等同于 Sorting Layer=default ,Order in Layer=0 参与排序
2.RenderQueue 越小越优先
3.RenderQueue 相等,由近到远排序优先
2.RenderQueue 2500以上
1. Sorting Layer/Order in Layer
1. 按照Sorting Layer/Order in Layer 设置的值,越小越优先
2. 无此属性,等同于 Sorting Layer=default ,Order in Layer=0 参与排序
2.RenderQueue 越小越优先
3.RenderQueue 相等,由远到近排序优先

参考链接https://www.jianshu.com/p/0341f0ab9020

  1. UIPanel有sortingorder和RenderQueue类型,sortingorder用来设置对应UIPanel下每个UIDrawcall上MeshRender的sortingorder。所以同一个Panel下的UIdrawcall的MeshRender的sortingorder是一样的。而RenderQueue有3个类型,分别是
    RenderQueue.Automatic,自动。起始值3000,Panel下第一个UIDrawcall起始为之前处理Panel中最大的的rendqueue ,每一个drawcall的RenderQueue 不断+1。
    RenderQueue.StartAt 指定Panel下UIDrawcall的RenderQueue 初始值,每一个drawcall的RenderQueue 不断+1
    RenderQueue.Explicit,指定特定值,Panel下的每个drawcall都是这个值
  2. 代码

4.2 UIPanel什么时候重新绘制

  1. 第一次LateUpdate
  2. 移除Widget的时候Widget的depth等于其Drawcall的起始或者结束深度
  3. 添加Widget的时候,Panel无法为其从现有的Drawcall列表找到复合条件的Drawcall(Widget的material、shader、texture与Drawcall相等且Widget的depth在Drawcall的深度范围内)。

4.3 UIWidget什么时候重新绘制

  1. Widget的最终alpha发生变化
  2. Widget的锚点发生变化
  3. 当调用Widget的MarkAsChanged标记其发生变化的时候
  4. Widget位置或者大小发生变化

4.4 项目如何减少Drawcall

  1. 由UIPanel生成Drawcall的函数FillAllDrawCalls可以知道,按照遍历已经按照深度排序的Widget列表,当碰到Widget的Shader、Texture或者Material不想等的时候,需要重新生成一个Drawcall。
  2. 了解到这机制,减少drawcall的思路就是尽可能将Shader、Texture、Material相同的Widget安排在同一个深度范围内,并且中间尽可能少的穿插与其不同的Widget。
  3. 公司项目减少Drawcall的方法
    3.1 公司项目的界面一般对应一个UIPanel(没有ScrollView或者ListView),静态UI界面主要由图片或者文字,我们定义图片或者文字的层级区间类型,其中文字最高为1000,并且我们界面都是同一个字体,一个界面的所UILabel的深度值都为1000,这样一个界面的所有文字只会产生一个Drawcall。
    3.2 另外使用图集的Widget(UISprite)和使用单一图片的Widget(UITexture)的深度值范围是区分开的,因为UISprite和UITexture一般来说使用的Shader、Texture、Material无法完全一样,通过划分深度范围能有效减少因两者穿插产生多余Drawcall。
    3.3 图片或者文字的层级区间
    • PageMask, //页面遮罩 depth:0
    • BgIcon, //背景Icon depth:100-200
    • AtlasBg, //图集-背景 depth:200-300
    • AtlasBasicComponent, //图集-基础控件 depth:300-400
    • AtalsMainScene, //图集-主界面 depth:200-500
    • DynIcon, //动态图标 depth:500-700
    • UpperAtlas, //图标或者图集上的图集 depth:700-800
    • UpperIcon, //图标之上的图标 depth:800-900
    • Label //字体为1000
上一篇下一篇

猜你喜欢

热点阅读