Unity想要实现圆形图片,需要使用Mask,不是基于Mesh实现的,这里我们自己写一个基于Mesh的圆形图片组件。

新建Scene,命名为“CircleImage”,在Scripts文件夹内新建“CircleImage”文件夹。

Mask圆形遮罩

Mask圆形遮罩

缺点:锯齿感强,增加DrawCall

CircleImage

在“CircleImage”文件夹内新建同名脚本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;

public class CircleImage : Image
{
/// <summary>
/// 三角面个数
/// </summary>
[SerializeField] private int segments = 100;
[SerializeField] private float showPercent = 1;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
int realSegment = (int)(segments * showPercent);

Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
Vector2 uvCenter = new Vector2(uvWidth * 0.5f, uvHeight * 0.5f);
Vector2 convertRatio = new Vector2(uvWidth/width, uvHeight/height);//uv系数,因为图片可能被放大或缩小

float radian = (2 * Mathf.PI) / segments;//单片弧度
float radius = width * 0.5f;//圆半径,按宽度算

UIVertex origin = new UIVertex();//UGUI有自己的顶点数据结构
origin.color = color;//使用Graphic父类提供的Color属性
origin.position = Vector3.zero;//顶点位置信息
origin.uv0 = new Vector2(origin.position.x * convertRatio.x + uvCenter.x,origin.position.y * convertRatio.y + uvCenter.y);//顶点uv坐标
vh.AddVert(origin);//添加到数据载体类

int vertexCount = realSegment + 1;//绘制圆的边所需要的顶点数为三角面个数 + 1,注意最开始和最后的顶点是重合的
float curRadian = 0;
for (int i = 0; i < vertexCount; i++)
{
float x = Mathf.Cos(curRadian) * radius;//unity使用弧度计算
float y = Mathf.Sin(curRadian) * radius;
curRadian += radian;

UIVertex vertexTemp = new UIVertex();
vertexTemp.color = color;
vertexTemp.position = new Vector2(x, y);
vertexTemp.uv0 = new Vector2(vertexTemp.position.x * convertRatio.x + uvCenter.x, vertexTemp.position.y * convertRatio.y + uvCenter.y);
vh.AddVert(vertexTemp);
}

//绘制三角面
int id = 1;
for (int i = 0;i < realSegment;i++)
{
vh.AddTriangle(id, 0, id + 1);
id++;
}
}
}

OnPopulateMesh,Image组件的绘制方法。

VertexHelper,一个绘制数据载体类,提供了一系列方法,GPU通过这个类来获取顶点数据、片元数据等。

Class VertexHelper | Unity UI | 1.0.0 (unity3d.com)

DataUtility,一个获取Sprite的UV和Size等数据的工具类。这里的GetOuterUV方法获取的是图像的完整UV,如果一个Sprite是九宫格格式,InnerUV指的就是九宫格内部的UV。

注意,Sprite的OuterUV是一个Vector4,而一般的UV是Vector2,因为OuterUV和InnerUV是对应的,必须要Vector4才能计算出外部和内部的区别。Vector4(x,y,z,w)

InnerUV和OuterUV

overrideSprite是Image组件提供的变量,它指的就是“覆盖在上层”的Sprite,因为有些时候Image的sprite变量为空,而overrideSprite一般并不会为空

原理图

绘制原理

绘制原理

在代码中,顶点的ID是按照添加到VertexHelper.AddVertex的顶点的顺序决定的,第一个ID编号是0

只有顺时针绘制,GPU才能视为正面。默认背面是剔除的。

坐标原理

坐标原理

想要把uv正确设置出来,需要在乘转换系数(convertRatio)的同时再添加uv的偏移量。

顶点在Rect组件中的位置 * convertRatio + uvCenter

CircleImage编辑器拓展

在Scripts——CircleImage文件夹内新建Editor文件夹,在其中新建CircleImageEditor脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(CircleImage))]
[CanEditMultipleObjects]
public class CircleImageEditor : UnityEditor.UI.ImageEditor
{
SerializedProperty mSegments;
SerializedProperty mFillPercent;
protected override void OnEnable()
{
base.OnEnable();
mSegments = serializedObject.FindProperty("segments");
mFillPercent = serializedObject.FindProperty("showPercent");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.PropertyField(mSegments);
EditorGUILayout.Slider(mFillPercent,0,1,new GUIContent("showPercent"));
serializedObject.ApplyModifiedProperties();
if(GUI.changed)//这一段没必要,仅供参考
{
EditorUtility.SetDirty(target);
}
}
}

技能CD效果

我们修改一下CircleImage的代码,就能实现技能CD的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;

public class CircleImage : Image
{
/// <summary>
/// 三角面个数
/// </summary>
[SerializeField] private int segments = 100;
[SerializeField] private float showPercent = 1;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
int realSegment = (int)(segments * showPercent);

Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
Vector2 uvCenter = new Vector2(uvWidth * 0.5f, uvHeight * 0.5f);
Vector2 convertRatio = new Vector2(uvWidth/width, uvHeight/height);

float radian = (2 * Mathf.PI) / segments;
float radius = width * 0.5f;

UIVertex origin = new UIVertex();
byte temp = System.Convert.ToByte(255 * showPercent);
origin.color = new Color32(temp, temp, temp, 255);//圆心的顶点颜色进行渐变

//圆心的位置为(0.5f,0.5f),但我们需要按照rectTransform.pivot作偏移,否则圆心会跟着pivot移动,注意pivot也是0~1,我们需要乘width和height
//向量指向被减者,向量的方向就是圆心移动的方向,就能明白了
Vector2 originPos = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);
Vector2 vertPos = Vector2.zero;//圆心顶点的坐标,用于计算uv,不受pivot和rectTrans的影响

origin.position = originPos;//圆心位置坐标,受pivot和rectTrans的影响
origin.uv0 = new Vector2(vertPos.x * convertRatio.x + uvCenter.x,vertPos.y * convertRatio.y + uvCenter.y);//
vh.AddVert(origin);

int vertexCount = realSegment + 1;
float curRadian = 0;
Vector2 posTemp;//顶点的坐标,用于计算uv,不受pivot和rectTrans的影响
for (int i = 0; i < segments + 1; i++)//每次都要绘制全部
{
float x = Mathf.Cos(curRadian) * radius;
float y = Mathf.Sin(curRadian) * radius;
curRadian += radian;

UIVertex vertexTemp = new UIVertex();
if (i < vertexCount - 1)//在指定绘制范围内的顶点,保持原色
{
vertexTemp.color = color;
}
else if(showPercent >= 1)//防止绘制范围为全图时,最后一个顶点没有保持原色
{
vertexTemp.color = color;
}
else//在指定绘制范围外的
{
vertexTemp.color = new Color32(60, 60, 60, 255);
}
posTemp = new Vector2(x, y);//顶点的坐标,用于计算uv,不受pivot和rectTrans的影响
vertexTemp.position = posTemp + originPos;//顶点位置坐标,受pivot和rectTrans的影响
vertexTemp.uv0 = new Vector2(posTemp.x * convertRatio.x + uvCenter.x, posTemp.y * convertRatio.y + uvCenter.y);//
vh.AddVert(vertexTemp);
}

int id = 1;
for (int i = 0;i < segments;i++)//绘制全部
{
vh.AddTriangle(id, 0, id + 1);
id++;
}
}
}

代码中加注释的地方就是重点修改的地方,注意阴影绘制的方式和pivot中心点的换算。