在使用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++;
}
}
}