使用Mask组件的缺点
我们知道项目中可以这样展示圆形图片,一般是Image组件,下面再加上一个圆形Mask。但是这样做有几个缺点:
- 使用Mask会额外消耗多一个Drawcall来创建Mask,做像素剔除。
- Mask不利于层级合并。原本同一图集里的ui可以合并层级,仅需一个Drawcall渲染,如果加入Mask,就会将一个ui整体分割成了Mask下的子ui与其他ui,两者只能各自进行层级合并,至少要两个Drawcall。Mask用得多了,一个ui整体会被分割得四分五裂,就会严重影响层次合并的效率了。
- 产生的圆形不够完美,边上有锯齿现象。
自定义圆形图片
我们使用自己定义的圆形图片,不仅只有一个DrawCall,还能和同一图集的UI进行合并,产生的圆形程度也能自己控制。上面的所有缺点都没有了。所以我们要自定义圆形图片。
继承Image类
我们可以继承Image类,然后重写OnPopulateMesh方法,重新写入圆形形状的渲染顶点,三角面片信息,从而实现我们自己的圆形Image。
圆形可以由若干个以圆形为顶点的等腰三角形片组成正多边形,三角形多了,就近似圆形。所以首先定义一个参数圆形由多少个三角形组成。
然后我们添加顶点信息,首先添加圆心为顶点的信息,大致伪代码是这样的:
public class CircleImage : Image
{
public int segements = 50;
protected override void OnPopulateMesh(VertexHelper vh)
{
//清除原来画的信息
vh.Clear();
UIVertex origin = new UIVertex(); //原点
origin.color = color;
//计算顶点vertexPos
origin.position = vertexPos;
//计算uv0
origin.uv0 =uv0Pos ;
vh.AddVert(origin);
}
}
我们需要计算一个点的position和uv0坐标。原点的position就是(0,0),在圆上的点的坐标就是(半径Cos弧度,半径SIn弧度),半径我们知道,角度可以通过(2pi/三角形个数)求出。
而Uv的值,首先我们可以通过DataUtility.GetOuterUV,得到一个vector4的对象,这个对象的x,y,z,w表示一个矩形,左下,右下,左上,右上。我们的Position的值是可以知道的。因此我们可以通过uv和position的比值来求出对应的uv值,在原点的position是(0,0)。对应的uv值就是(原点position.x比率+uv中心点.x,原点y比率+uv中心点.y)。注意这里是添加要分的分数+1个顶点,有要分的份上个三角形。然后把这些顶点,三角形都添加到VertexHelper 的实例类中。
Vector4 uv4 = overrideSprite == null ? Vector4.zero : DataUtility.GetOuterUV(overrideSprite);
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
float uvWidth = uv4.z - uv4.x;
float uvHeight = uv4.w - uv4.y;
Vector2 uvCenter = new Vector2(uvWidth/2,uvHeight/2);
Vector2 ratio = new Vector2(uvWidth/width,uvHeight/height);
这是计算position和uv的宽高,得出uv和position的比率。
UIVertex origin = new UIVertex();
origin.color = color;
origin.position = Vector3.zero;
origin.uv0 = new Vector2(origin.position.x*ratio.x+uvCenter.x,origin.position.y*ratio.y+uvCenter.y);
vh.AddVert(origin);
float angle = 2 * Mathf.PI / segements;
float curAngle = 0;
float radius = width / 2;
for (int i = 0; i < segements+1; i++)
{
float x = radius * Mathf.Cos(curAngle);
float y = radius * Mathf.Sin(curAngle);
curAngle += angle;
UIVertex originTemp = new UIVertex();
originTemp.color = color;
originTemp.position = new Vector2(x,y);
originTemp.uv0 = new Vector2(originTemp.position.x * ratio.x + uvCenter.x, originTemp.position.y * ratio.y + uvCenter.y);
vh.AddVert(originTemp);
}
for (int i = 0; i < segements; i++)
{
vh.AddTriangle(i+1,0,i+2);
}
这是把顶点和三角形都添加到vh类里面,Image会自动去绘制图形。
就这样,我们就完成了简单的圆形Image制作,但是还是有很多缺陷,例如无法实现精确点击,无法使用锚点,没有暴露参数。但是只有你一步一步修改,就能完成达到你想要的效果,我这里不仅完善了以上的缺陷,还增加了可以按照百分比显示遮罩。想要的童鞋可以参考我的源码。
实现效果:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;
/// <summary>
///1.为什么不用Mask组件
///1.1 增加Drawcall
///1.2 边上有锯齿
///
///2.Position和uv的关系
/// 以rect的中心点为中心,Position左下,右下,左上,右上可以表示为(-200,-200)(200,-200)(-200,200)(200,200)
/// 以左下为中心,Uv可以表示为四个点距离左下的位置(0,0,1,1)
/// uv换算成Position就是让uv填满整个rect uv=position*(uv/position)
/// </summary>
public class CircleImage : Image
{
//圆形由多少块三角形组成
[SerializeField]
public int segements=60;
[SerializeField]
public float showPercent;
//半径
private float radius;
private readonly Color32 GRAY_COLOR = new Color32(60, 60, 60, 255);
private List<Vector3> _vertexList;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
_vertexList = new List<Vector3>();
int realsegments = (int) (segements * showPercent);
//rect的width 和 height
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
radius = width > height ? height / 2 : width / 2;
//获取当前UV信息
Vector4 uv = Vector4.zero;
uv = DataUtility.GetOuterUV(overrideSprite);
//uv为什么是4维变量 对应着离原点的距离x ,y,以及x+width,y+height;
float uvWidth = uv.z - uv.x;
float uvHight = uv.w - uv.y;
Vector2 uvCenter = new Vector2(uvWidth / 2, uvHight / 2);
//uv坐标和rect之间的换算比例
Vector2 converRatio = new Vector2(uvWidth / width, uvHight / height);
//求出一个小三角形的弧度
float radian = (2 * Mathf.PI) / segements;
Vector2 vertexPos = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);
//求出一个小三角形的三个顶点
//通过传入点的顺序 判断是正面(顺时针)还是背面 背面不渲染
UIVertex origin = new UIVertex(); //原点
origin.color = GetOriginColor();
Vector2 uvrela= Vector2.zero;
origin.uv0 = new Vector2(uvrela.x * converRatio.x+uvCenter.x, uvrela.y * converRatio.y+uvCenter.y);
origin.position = vertexPos;
vh.AddVert(origin);
//需要的顶点数等于块数+1
int vertexCount = realsegments + 1;
float curRadian = 0;
for (int i = 0; i < segements+1; i++)
{
float x = Mathf.Cos(curRadian)* radius;
float y = Mathf.Sin(curRadian)* radius;
curRadian += radian;
UIVertex vertexTemp = new UIVertex(); //原点
vertexTemp.color = i < vertexCount ? new Color32(60, 60, 60, 255):(Color32)color;
if (realsegments==0)
{
vertexTemp.color = color;
}
uvrela = new Vector2(x,y);
vertexTemp.uv0 = new Vector2(uvrela.x * converRatio.x + uvCenter.x, uvrela.y * converRatio.y + uvCenter.y);
vertexTemp.position = new Vector2(x,y)+vertexPos;
vh.AddVert(vertexTemp);
_vertexList.Add(vertexTemp.position);
}
int id = 1;
//生成面片生成UV
for (int i = 0; i < segements; i++)
{
//id按照传入三角形的顺序生成
vh.AddTriangle(id,0,id+1);
id++;
}
}
private Color32 GetOriginColor()
{
//随着showPercent从0变到1 ,color从白变成灰
Color32 colorTemp = (Color.white - GRAY_COLOR) * showPercent;
return new Color32((byte) (255 - colorTemp.r),
(byte)(255- colorTemp.g),
(byte)(255 - colorTemp.b),
255);
}
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera,
out localPoint);
return IsValid(localPoint);
}
private bool IsValid(Vector2 localPoint)
{
return GetCrossPointNum(localPoint, _vertexList) %2 ==1;
}
private int GetCrossPointNum(Vector2 localPoint,List<Vector3> vertexList)
{
int count = 0;
Vector3 vert1 = Vector3.zero;
Vector3 vert2 = Vector3.zero;
int vertCount = vertexList.Count;
for (int i = 0; i < vertCount; i++)
{
vert1 = vertexList[i];
vert2 = vertexList[(i + 1) % vertCount];
if (IsYInRang(localPoint,vert1,vert2))
{
if (localPoint.x<GetX(vert1,vert2,localPoint.y))
{
count++;
}
}
}
return count;
}
/// <summary>
///获取相交点的X坐标
/// </summary>
/// <param name="vert1"></param>
/// <param name="vert2"></param>
/// <param name="localPointY"></param>
/// <returns></returns>
private float GetX(Vector3 vert1, Vector3 vert2, float localPointY)
{
float k = (vert1.y - vert2.y) / (vert1.x - vert2.x);
return vert1.x + (localPointY - vert1.y) / k;
}
private bool IsYInRang(Vector2 localPoint, Vector3 vert1, Vector3 vert2)
{
if (vert1.y>vert2.y)
{
return vert1.y > localPoint.y && localPoint.y > vert2.y;
}
else
{
return vert2.y>localPoint.y && localPoint.y>vert1.y;
}
}
}