1.为什么想到使用RawImage来实现圆角矩形呢
(1)优化简介:相信研究过Unity性能优化的同学都知道,我们开发过程中要尽量避免不必要的Drawcall产生,因为一个Drawcall耗费的性能往往比起顶点数面片之类的都要大。
(2)处理方式:我们使用UGUI的时候通常为了节约不必要的开支都会给图片打包,使他们属于同一图集,这样Unity会自动帮助我们批处理掉这些,所有位于该图集下的Image组件只要顺序不被其他图集或者Text这一类的组件打乱,就可以合并成一个Drawcall,大量提高运行的效率。
(3)简单粗暴的实现方式:初接触Unity的时候,我们实现圆角矩形通常都是一个Mask下覆盖一个圆角矩形样式的底图使用遮罩的方式来实现这样的图形。
(4)Mask带来的弊端:这样使用的Mask会消耗最少3个Drawcall,站在性能优化上的角度来说这是很恐怖的,并且边角锯齿比较明显(具体为什么会消耗最少3个Drawcall有兴趣的同学可以去了解一下Mask的工作原理,这里我就不废话了)这样为了显示一个图案就耗费了大量的性能显然是不划算的。
(5)优化方式:为了减少Drawcall过多的产生我们就从UnityUGUI的实现方式来考虑,这样只要把UGUI的Rawimage组件默认的形状改变成我们需要的就会把Drawcall降为1。
2.怎么实现
(1)系统API:Unity内部给我们的RawImage提供了一个OnPopulateMesh接口用来绘制图形的顶点,我们就可以直接使用该接口来实现我们的需求。
(2)绘制步骤:首先是绘制顶点,OnPopulateMesh接口有一个参数VertexHelper 这里面就存储了我们用来绘制图案的一些数据,position是顶点坐标 uv是我们图片纹理的坐标 颜色默认也可以自己指定。把顶点,uv,颜色等数据填好填充进我们的变量告诉给Unity,这样Unity就会帮我们把这些渲染所需的数据发送给Gpu最后就能把这些信息组成的图案显示在屏幕上了。
关于画圆角矩形的方法我这里参照了这篇文章上的一些思路 http://www.cnblogs.com/blueberryzzz/p/9521233.html
具体我们先看图
(3)参数介绍:一张图默认的宽高分别是100,顶点坐标的原点在图片的中心也就是(50,50)位置,uv的原点在图片左下角(-50,-50)位置。
(4)绘制圆角矩形思路:由于我们不能直接画三角形和矩形以外的图形,这之外的图形应该都是按这种方式拼凑出来的,所以我们把这个矩形分成6个三角形和四个90°的扇形。每个扇形用若干个三角形来模拟。这样我们就将一个圆角矩形,划分为最基础的图案了。
(5)计算思路:我们可以把这个扇形理解为四分之一的圆,每个圆内切各自位置的边角,扇形半径等于圆的半径,设定每一个三角形为等腰三角形,为了方便我们先把一个扇形拆分为6个等腰三角形这样每一个三角形顶角的角度就是90/6=15度但由于Unity的Mathf.Cos方法传入的参数是弧度值所以我们不能直接用角度,先转换成弧度值再传给Unity帮我们计算夹角的余弦值(float)(Math.PI/2/6)(6是三角形的个数 这里要强转类型为float不然Unity计算的时候会自动省略小数点后面的数字导致计算错误)
(6)计算三角形顶点:由于组成扇形的若干三角形有一个共用顶点也就是圆心,这里先单独提取出来,然后按照逆时针旋转的方式第二个顶点就是跟我们圆心水平或者垂直的那个顶点(以上图扇形举例,圆心是第一个顶点,圆心右边那个就是我们的第二个顶点),这样我们就有一个三角形所需要的两个顶点了,第三个顶点我们根据圆的标准方程(x-a)²+(y-b)²=r² 以点(a,b)为圆心r为半径得出x坐标等于a+r*CosA,y坐标等于b+r*SinA 这样计算出来的结果就是我们要求的那个点的坐标,也就是三角形的最后一个顶点,这样三个顶点都有了就能得出我们的第一个三角形了,然后第二个三角形由于顶点还是用的圆心那个,第一个顶点保持不变,该三角形的其中一条边与上一三角形的一条边重合,故顶点一致,依然只需要求一个顶点,方式也是用上面的方式,由于是逆时针旋转,夹角递增,之前的15度变为现在的30度。
思路就这些了,下面贴代码。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;
public class UIRoundedRawImage : RawImage
{
public float Radius = 10f;//内切圆半径 图片的一半差不多就是一个圆了 这里相当于图片十分之一的长度
public int TriangleNum = 6;//每个扇形三角形个数 个数越大弧度越平滑
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
float tw = rectTransform.rect.width;//图片的宽
float th = rectTransform.rect.height;//图片的高
float twr = tw / 2;
float thr = th / 2;
if (Radius < 0)
Radius = 0;
float radius = tw / Radius;//半径这里需要动态计算确保不会被拉伸
if (radius > twr)
radius = twr;
if (radius < 0)
radius = 0;
if (TriangleNum <= 0)
TriangleNum = 1;
UIVertex vert = UIVertex.simpleVert;
vert.color = color;
//左边矩形
AddVert(new Vector2(-twr, -thr + radius), tw, th, vh, vert);
AddVert(new Vector2(-twr, thr - radius), tw, th, vh, vert);
AddVert(new Vector2(-twr + radius, thr - radius), tw, th, vh, vert);
AddVert(new Vector2(-twr + radius, -thr + radius), tw, th, vh, vert);
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(0, 2, 3);
//中间矩形
AddVert(new Vector2(-twr + radius, -thr), tw, th, vh, vert);
AddVert(new Vector2(-twr + radius, thr), tw, th, vh, vert);
AddVert(new Vector2(twr - radius, thr), tw, th, vh, vert);
AddVert(new Vector2(twr - radius, -thr), tw, th, vh, vert);
vh.AddTriangle(4, 5, 6);
vh.AddTriangle(4, 6, 7);
//右边矩形
AddVert(new Vector2(twr - radius, -thr + radius), tw, th, vh, vert);
AddVert(new Vector2(twr - radius, thr - radius), tw, th, vh, vert);
AddVert(new Vector2(twr, thr - radius), tw, th, vh, vert);
AddVert(new Vector2(twr, -thr + radius), tw, th, vh, vert);
vh.AddTriangle(8, 9, 10);
vh.AddTriangle(8, 10, 11);
List<Vector2> CirclePoint = new List<Vector2>();//圆心列表
Vector2 pos0 = new Vector2(-twr + radius, -thr + radius);//左下角圆心
Vector2 pos1 = new Vector2(-twr, -thr + radius);//决定首次旋转方向的点
Vector2 pos2;
CirclePoint.Add(pos0);
CirclePoint.Add(pos1);
pos0 = new Vector2(-twr + radius, thr - radius);//左上角圆心
pos1 = new Vector2(-twr + radius, thr);
CirclePoint.Add(pos0);
CirclePoint.Add(pos1);
pos0 = new Vector2(twr - radius, thr - radius);//右上角圆心
pos1 = new Vector2(twr, thr - radius);
CirclePoint.Add(pos0);
CirclePoint.Add(pos1);
pos0 = new Vector2(twr - radius, -thr + radius);//右下角圆心
pos1 = new Vector2(twr - radius, -thr);
CirclePoint.Add(pos0);
CirclePoint.Add(pos1);
float degreeDelta = (float)(Mathf.PI / 2 / TriangleNum);//每一份等腰三角形的角度 默认6份
List<float> degreeDeltaList = new List<float>() { Mathf.PI, Mathf.PI / 2, 0, (float)3 / 2 * Mathf.PI };
for (int j = 0; j < CirclePoint.Count; j += 2)
{
float curDegree = degreeDeltaList[j / 2];//当前的角度
AddVert(CirclePoint[j], tw, th, vh, vert);//添加扇形区域所有三角形公共顶点
int thrdIndex = vh.currentVertCount;//当前三角形第二顶点索引
int TriangleVertIndex = vh.currentVertCount - 1;//一个扇形保持不变的顶点索引
List<Vector2> pos2List = new List<Vector2>();
for (int i = 0; i < TriangleNum; i++)
{
curDegree += degreeDelta;
if (pos2List.Count == 0)
{
AddVert(CirclePoint[j + 1], tw, th, vh, vert);
}
else
{
vert.position = pos2List[i - 1];
vert.uv0 = new Vector2(pos2List[i - 1].x + 0.5f, pos2List[i - 1].y + 0.5f);
}
pos2 = new Vector2(CirclePoint[j].x + radius * Mathf.Cos(curDegree), CirclePoint[j].y + radius * Mathf.Sin(curDegree));
AddVert(pos2, tw, th, vh, vert);
vh.AddTriangle(TriangleVertIndex, thrdIndex, thrdIndex + 1);
thrdIndex++;
pos2List.Add(vert.position);
}
}
}
protected Vector2[] GetTextureUVS(Vector2[] vhs, float tw, float th)
{
int count = vhs.Length;
Vector2[] uvs = new Vector2[count];
for (int i = 0; i < uvs.Length; i++)
{
uvs[i] = new Vector2(vhs[i].x / tw + 0.5f, vhs[i].y / th + 0.5f);//矩形的uv坐标 因为uv坐标原点在左下角,vh坐标原点在中心 所以这里加0.5(uv取值范围0~1)
}
return uvs;
}
protected void AddVert(Vector2 pos0, float tw, float th, VertexHelper vh, UIVertex vert)
{
vert.position = pos0;
vert.uv0 = GetTextureUVS(new[] { new Vector2(pos0.x, pos0.y) }, tw, th)[0];
vh.AddVert(vert);
}
}
效果展示
圆角矩形:默认半径图片10分之一,三角个数6
圆形:默认半径2分之一,三角个数36
使用方式
新建一个只有RectTransform的组件,然后挂载该脚本到组件上即可,需要修改参数的话打开Debug选项
需要用到RawImage的地方使用即可,一般用于网络游戏的头像显示,因为RawImage无法合并批次,所以尽量别让其打乱其他Image的顺序即可。