Unity

[unity/UI]Unity的UI究竟为什么可以合批

2019-04-23  本文已影响184人  江枫枫Maple

前言

      我们知道,如果物体的材质相同,顶点数满足unity动态合批的要求,那么unity就可以实现动态合批,进而减少drawcall。
      但是我最近就有一些困惑,为什么unity的UI可以实现使用同样的材质,但是纹理可以使用图集中的任意一张纹理呢?毫无疑问,肯定是通过图集中图片的UV值不同,使用不同的UV对图集纹理进行采样才可以达到这样的效果。我们知道,我们编写shader后,创建材质球使用自定义的shader,可以在面板上修改材质某张纹理的uv偏移值与缩放值。

unity中材质面板,Tiling纹理缩放,Offset纹理偏移

      但是如果场景中存在多个物体,使用相同材质时,一旦改变了材质的纹理缩放或位移值,所有物体的采样结果都会被改变。

使用相同材质的不同物体

      很显然,直接改变材质的UV值是行不通的,因为一旦改变了材质上纹理的UV值,所有使用了该材质的物体引用的纹理UV值都会被改变掉。那么unity到底是怎么做的才能实现这样的效果呢?

1.UI/Default代码研究

      首先,我想到的是,既然是对图集纹理进行采样,而且又不能统一更改材质的纹理UV值,我们通常写的shader都是直接根据模型UV值对主纹理进行采样,那会不会是shader中对MainTexture进行了什么神奇的处理,让图片采样只根据指定的UV值进行采样呢?
      我去官网下载了shader代码,找到了UI/Default的具体实现:

            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = v.texcoord;

                OUT.color = v.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                return color;
            }

      看了上面的代码,我们可以基本确定,没有在shader中做什么特别神奇的MainTexture处理。但是我们还是可以发现一些不同的地方,这里上面的变量_Color_TextureSampleAdd_ClipRect并没有暴露在面板上,可以看出来这三个变量是通过某些脚本传递给shader的。
      我们知道,伴随着Defalut材质的一般使用的是Image组件、Text组件。这两个组件会绘制顶点与三角形,然后使用指定的材质进行渲染。所以会不会是Image组件或Text组件中使用了什么算法,计算过图片UV值,并把上面三个变量填充好传给shader的呢?

2.Image组件代码研究

      因为unity的ui代码已经开源了,所以我们很幸运的可以看到Image的源码是怎么实现的,因为Image组件代码很多,所以这里就只贴出比较主要的绘制顶点的函数:

        /// <summary>
        /// Update the UI renderer mesh.
        /// </summary>
        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (activeSprite == null)
            {
                base.OnPopulateMesh(toFill);
                return;
            }

            switch (type)
            {
                case Type.Simple:
                    if (!useSpriteMesh)
                        GenerateSimpleSprite(toFill, m_PreserveAspect);
                    else
                        GenerateSprite(toFill, m_PreserveAspect);
                    break;
                case Type.Sliced:
                    GenerateSlicedSprite(toFill);
                    break;
                case Type.Tiled:
                    GenerateTiledSprite(toFill);
                    break;
                case Type.Filled:
                    GenerateFilledSprite(toFill, m_PreserveAspect);
                    break;
            }
        }

      我们可以看到,这个函数是用来刷新UI渲染的,unity对图片的四种类型分别进行了处理,这里我们就只看一下最简单的Simple模式的代码:

        /// <summary>
        /// Generate vertices for a simple Image.
        /// </summary>
        void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
        {
            Vector4 v = GetDrawingDimensions(lPreserveAspect);
            var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;

            var color32 = color;
            vh.Clear();
            vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(uv.x, uv.y));
            vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(uv.x, uv.w));
            vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(uv.z, uv.w));
            vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(uv.z, uv.y));

            vh.AddTriangle(0, 1, 2);
            vh.AddTriangle(2, 3, 0);
        }

       /// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
        private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
        {
            var padding = activeSprite == null ? Vector4.zero : Sprites.DataUtility.GetPadding(activeSprite);
            var size = activeSprite == null ? Vector2.zero : new Vector2(activeSprite.rect.width, activeSprite.rect.height);

            Rect r = GetPixelAdjustedRect();
            // Debug.Log(string.Format("r:{2}, size:{0}, padding:{1}", size, padding, r));

            int spriteW = Mathf.RoundToInt(size.x);
            int spriteH = Mathf.RoundToInt(size.y);

            var v = new Vector4(
                padding.x / spriteW,
                padding.y / spriteH,
                (spriteW - padding.z) / spriteW,
                (spriteH - padding.w) / spriteH);

            if (shouldPreserveAspect && size.sqrMagnitude > 0.0f)
            {
                PreserveSpriteAspectRatio(ref r, size);
            }

            v = new Vector4(
                r.x + r.width * v.x,
                r.y + r.height * v.y,
                r.x + r.width * v.z,
                r.y + r.height * v.w
            );

            return v;
        }

       public void AddVert(Vector3 position, Color32 color, Vector2 uv0);

     就是在这里了,首先拿到绘制的尺寸v,也就是四个顶点的位置,然后根据activeSprite拿到纹理的UV值。我们可以看到AddVert函数中,第三个值是绘制的顶点中填充的uv0也就是这个得到的UV值,而shader中也会根据这个uv值对MainTexture进行采样。

3.小实验

      我们已经知道计算顶点与UV值的操作是在image中进行的,其实unity有一个组件可以自己控制采样的uv值,就是RawImage组件,相比Image组件,RawImage组件更为精简,因为没有处理Image中的四种图片样式。
      其实Image组件中帮我们做的操作其实就相当于(是相当于,其实计算比这复杂的多)在RawImage中设置了不同的UV偏移值。这样就可以做到,每个组件使用的UV值不同,而不是改变统一使用材质上的UV值。

修改RawImage中的UV值

总结

     我们最开始的想法是修改材质中的UV值,但是这样是不行的,因为改变了材质UV值后所有物体都会跟着改变。Unity使用了一个巧妙的办法,也就是在建模(绘制顶点/三角形)的时候,就把得到的图集中纹理的UV采样值填充到mesh的UV中。所以材质使用的都是同一个材质,也都是对MainTexture进行采样,只不过每个图片的mesh中存储的UV值都是不同的。

上一篇下一篇

猜你喜欢

热点阅读