Unity_UGUI实现圆形按钮的底层实现

在使用Unity的UGUI过程中,我们通常会遇到一种需求就是会,创建一个圆形的按钮或者图标。

先说说正常实现这么一个圆形组件的步骤:

1、创建Image组件Img1,作为父组件
2、在父组件下面创建子Image组件Img2,
3、在Img1中添加Mask(Script)组件,然后给父组件赋一个图片

这个图片的周围是透明的,中间是圆形的纯色填充,用来做父组件的image
4、然后我们给子组件img2赋上我们需要显示的图片

最终父组件中Mask组件会帮我们做遮挡,然后将子组件img2显示成圆形:

这样我们的一个圆形的图标就简单的显示出来。

但是这种方法有一个很明显的缺点就是:Mask组件,这个组件会增加一次drawcall,也就是我们的绘制压力。所以这不是我们最优的解决方案。

图片渲染到屏幕的底层过程大致如下:

首先获取图形的顶点信息,然后将这些顶点信息传给GPU缓存
然后,经过我们的顶点着色器,几何着色器,片元着色器,最后经过混合测试,然后再输出到我们的屏幕上。

因为在第一种方法里面我们使用了Mask遮罩,所以就需要先把遮罩图片画一遍,再画我们需要的图片。

那如果我们直接画一个圆形的图片,岂不是就可以省去Mask遮罩带来的多余的drawcall。

下面就是我们自己控制渲染过程,将图片只画一个圆形的部分:

1、圆形在绘制过程中的组成

我们将一个圆形的图片放大,就可以看见其实圆形显示其实并不是圆滑的,其实在绘制的过程中圆形的显示其实都是由一个个三角形拼接而成:

拼接三角形越多,圆形就越圆滑。

2、当我们将一张图片显示屏幕上的时候,我们是通过将图片转换成纹理,然后通过纹理的信息控制显示的区域。

纹理的信息在Unity中获取到的是一个Vector4类型的信息,为什么我们的纹理信息是Vector4呢?

上面的坐标系中,矩形就代表了一张纹理,x,y,z,w四个点是纹理的左下角和右上角的坐标,当我们有了这四个点,那么那么我们就可以完整的表示一张纹理在坐标系中信息,大小,宽高,中心点等。所以我们获取的纹理的Vector4类型的数据就是代表纹理中的x,y,z,w四个分量。

3、uv坐标和rect坐标之间的换算比例

我们对于纹理的操作相当于将一张图片贴在物体上面,所以物体上的坐标需要和纹理上的对应的坐标有一个换算关系,

这个换算比例就是物体的宽比纹理的宽,得到宽上面的比例

物体的高比纹理的高,得到高方向上的比例

当我们知道物体上面的坐标的时候,乘以对应方向上的比例,我们就得到了对应方向上uv的坐标

4、获得顶点的信息

这是最重要的一步,因为在将图片渲染到屏幕的第一步就是获取图形的顶点,因为我们现在要在一个正方形的面上找到对应圆形纹理的顶点,所以我们在正方形的物体上找到所有对应的点,然后依次给每一个点成比例,就找到了纹理上对应的点

我们可以从使用半径r*sinλ,r*cosλ就可以得到圆上的点的坐标。

这个时候有可能会有人奇怪,为什么这个这正方的物体坐标轴原点在中心,而不是像纹理坐标一样在左下角,因为我们在Unity的界面有这么一个设置:

轴点是在正方形的正中间,所以我们计算这个正方形上面的坐标的时候,使用的坐标系的原点就是在正中间,也就相当于一个局部坐标的概念,也是为了方便我们的计算。

此时我们需要的基础的知识大概就是这些,然后我们需要重载一个函数,这个函数就是OnPopulateMesh,这个函数Unity使用它进行纹理的操作,然后我们重载它,然后Unity就会使用我们自己实现的函数进行操作。

PS:函数的名字里面带Mesh,也就是物体网格,但是目前这里我们可以不关心mesh是什么,简单的理解这个渲染过程就可以。

下面是我们挂在image组件上的脚本的代码,我们只需要重载我们刚才说的那个函数:

里面我还带了一些讲解,方便理解。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Sprites;

// 我们直接让脚本集成Image类,也就是组件的类,然后我们就可以使用这个组件里面的一些成员变量
public class CircleImage : Image
{
    // 这个数组就代表圆形是由多少个三角形拼成的,数字越大越圆滑,由我们自己定义
    private int segements;

    // 这个函数就是我们重载的函数,需要使用override关键字
    protected override void OnPopulateMesh(VertexHelper vh)
    {
        /* VertexHelper 类是Unity帮我们封装的可以操作顶点的类 */

        segements = 100;

        // 原本这个组件就会有顶点的一些信息传进来,比如我们在Unity界面设置的一些信息,但是
        // 因为现在我们需要使用自己的信息,所以就把这里面的顶点信息清除掉。
        // 把原本的数据清除掉
        vh.Clear();
        // 获取图片的宽高
        float width = rectTransform.rect.width;
        float height = rectTransform.rect.height;

        // 获取当前纹理的UV信息,UV是四维坐标,
        // overrideSprite是我们给Image组件赋的图片,需要显示什么图片,就在Unity的Inspector界面拖动赋值
        Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
        // 获取UV的中心点,给圆心用
        // X轴上z-x,就是uv的宽,然后除以2 + X,就是中心点的X坐标,高同理
        float uvWidth = uv.z - uv.x;
        float uvHeight = uv.w - uv.y;
        Vector2 uvCenter = new Vector2(uvWidth * 0.5f + uv.x, uvHeight * 0.5f + uv.y);
        // uv宽高/位置宽高 = 比例系数,如果我们知道位置坐标,乘以比例系数就可以得到对应的uv坐标
        Vector2 convertRatio = new Vector2(uvWidth / width, uvHeight / height);

        // 生成圆心点
        // 求出每一块的弧度
        float radian = (2 * Mathf.PI) / segements;
        // 半径,自己定的
        float radius = width * 0.5f;

        // 使用临时变量 ,防止因为构造的延迟,导致设置的值不对
        // 因为如果不使用这个临时的变量,而在下面顶点计算的时候,来新构造一个Vector2,这个时候有可能因为构造的延时,获取到的上一次旧值
        // 这个临时的值,我们先用他来储存原点的位置
        Vector2 TmpPosition = Vector2.zero;

        // 我们要画的第一个点就是圆心点,所以我们使用UIVertex类将我们的圆心点的信息都存进去
        UIVertex origin = new UIVertex();
        // 圆心顶的颜色就是image的颜色就可以
        origin.color = color;
        // 圆心在中心点的位置
        origin.position = TmpPosition;
        // 通过换算,得到原点所对应的贴图的位置,原点的Position * 比例 = uv坐标
        origin.uv0 = new Vector2(origin.position.x * convertRatio.x, origin.position.y * convertRatio.y);
        // 将生成的顶点添加到顶点信息里面去 
        vh.AddVert(origin);

        // 圆边上的顶点数 = 块数+1
        int vertexCnt = segements + 1;
        float curRadian = 0;
        for (int i = 0; i < vertexCnt; i++)
        {
            // 通过半径乘sin或者cos来获得圆边上的点的坐标Position
            float x = Mathf.Cos(curRadian) * radius;
            float y = Mathf.Sin(curRadian) * radius;
            // 当前点计算完成之后,将角度进行累加
            curRadian += radian;
            // 新构造一个UIVertex变量,使用这个变量储存当前点的信息
            UIVertex vertexTmp = new UIVertex();
            vertexTmp.color = color;
            // Vector2 构造的延时,导致下一步获取的值 ,并不是构造出来的值,而是position的默认值
            TmpPosition = new Vector2(x, y);
            vertexTmp.position = TmpPosition;
            vertexTmp.uv0 = new Vector2(vertexTmp.x * convertRatio.x + uvCenter.x, vertexTmp.y * convertRatio.y + uvCenter.y);
            vh.AddVert(vertexTmp);
        }

        // 通过顶点信息,来描述三角形的面片信息
        // 三角形的面片数量就是绘制的三角形的数量
        int id = 1;
        for (int i = 0; i < segements; i++)
        {
            // 传入三个顶点的顺序,按照你传入的顺序进行绘制,
            // 参数是顶点ID
            // 顶底的ID大小就是你传入的顺序,第一个传入ID就是1
            vh.AddTriangle(id, 0, id + 1);
            id++;
        }
    }
}

 

 


版权声明:本文为DY_1024原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。