1.一張圖片是如何顯示在屏幕上的
一張圖片渲染到unity界面中的大致流程。
2.我們要做什么
我們要做的就是在CPU中將圖片的矩形頂點數據修改成圓角矩形的頂點信息,之后Unity會將修改后的頂點數據發到GPU中,並設置對應的shader,GPU就會根據我們發送的頂點數據將圖片渲染成我們所要的圓角矩形圖片。
3.怎么做
由於Unity已經幫我們做了將數據發送到GPU的工作,我們只需要在代碼中去修改要傳送頂點數據就可以了。
Unity的Image組件提供了OnPopulateMesh接口。這個接口就是用來更新渲染時用的renderer mesh的頂點信息的的。我們直接重寫這個函數,來修改頂點數據。
<1>我們先來看一下一張Simple類型的圖片的頂點信息是如何組織的。
/// <summary>
/// Update the UI renderer mesh.
/// </summary>
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (activeSprite == null)
{
base.OnPopulateMesh(toFill);
return;
}
switch (type)
{
case Type.Simple:
GenerateSimpleSprite(toFill, m_PreserveAspect);
break;
case Type.Sliced:
GenerateSlicedSprite(toFill);
break;
case Type.Tiled:
GenerateTiledSprite(toFill);
break;
case Type.Filled:
GenerateFilledSprite(toFill, m_PreserveAspect);
break;
}
}
/// <summary>
/// Generate vertices for a simple Image.
/// </summary>
void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
Vector4 v = GetDrawingDimensions(lPreserveAspect);
var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
var color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(uv.x, uv.y));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(uv.x, uv.w));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(uv.z, uv.w));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(uv.z, uv.y));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
v是頂點坐標信息,uv是貼圖坐標信息,vh是用來存儲這些信息的變變量。
每個點的位置信息(相對中軸線的位置),默認顏色,uv坐標組成了一個頂點信息放到了vh中,然后再告訴vh如何去畫三角行,就可以了。
之后unity會將vh中的信息傳到GPU,然后將圖片展示在屏幕上。
<2>我們如何將一張圖片的頂點信息和三角形信息改成我們要的圓角矩形
首先,我們將一張圖分成6個三角形和四個90°的扇形。每個扇形用若干個三角形來模擬。這樣我們就將一個圓角矩形,划分成了GPU能認識的三角形了。
我們以扇形的半徑,構成扇形的三角形的數量作為變量,就可以算出每個我們需要的頂點的坐標了。具體的實現見代碼。
實現代碼:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.Sprites;
using System.Collections.Generic;
namespace GFramework
{
public class SimpleRoundedImage : Image
{
//每個角最大的三角形數,一般5-8個就有不錯的圓角效果,設置Max防止不必要的性能浪費
const int MaxTriangleNum = 20;
const int MinTriangleNum = 1;
public float Radius;
//使用幾個三角形去填充每個角的四分之一圓
[Range(MinTriangleNum, MaxTriangleNum)]
public int TriangleNum;
protected override void OnPopulateMesh(VertexHelper vh)
{
Vector4 v = GetDrawingDimensions(false);
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
var color32 = color;
vh.Clear();
//對radius的值做限制,必須在0-較小的邊的1/2的范圍內
float radius = Radius;
if (radius > (v.z - v.x) / 2) radius = (v.z - v.x) / 2;
if (radius > (v.w - v.y) / 2) radius = (v.w - v.y) / 2;
if (radius < 0) radius = 0;
//計算出uv中對應的半徑值坐標軸的半徑
float uvRadiusX = radius / (v.z - v.x);
float uvRadiusY = radius / (v.w - v.y);
//0,1
vh.AddVert(new Vector3(v.x, v.w - radius), color32, new Vector2(uv.x, uv.w - uvRadiusY));
vh.AddVert(new Vector3(v.x, v.y + radius), color32, new Vector2(uv.x, uv.y + uvRadiusY));
//2,3,4,5
vh.AddVert(new Vector3(v.x + radius, v.w), color32, new Vector2(uv.x + uvRadiusX, uv.w));
vh.AddVert(new Vector3(v.x + radius, v.w - radius), color32, new Vector2(uv.x + uvRadiusX, uv.w - uvRadiusY));
vh.AddVert(new Vector3(v.x + radius, v.y + radius), color32, new Vector2(uv.x + uvRadiusX, uv.y + uvRadiusY));
vh.AddVert(new Vector3(v.x + radius, v.y), color32, new Vector2(uv.x + uvRadiusX, uv.y));
//6,7,8,9
vh.AddVert(new Vector3(v.z - radius, v.w), color32, new Vector2(uv.z - uvRadiusX, uv.w));
vh.AddVert(new Vector3(v.z - radius, v.w - radius), color32, new Vector2(uv.z - uvRadiusX, uv.w - uvRadiusY));
vh.AddVert(new Vector3(v.z - radius, v.y + radius), color32, new Vector2(uv.z - uvRadiusX, uv.y + uvRadiusY));
vh.AddVert(new Vector3(v.z - radius, v.y), color32, new Vector2(uv.z - uvRadiusX, uv.y));
//10,11
vh.AddVert(new Vector3(v.z, v.w - radius), color32, new Vector2(uv.z, uv.w - uvRadiusY));
vh.AddVert(new Vector3(v.z, v.y + radius), color32, new Vector2(uv.z, uv.y + uvRadiusY));
//左邊的矩形
vh.AddTriangle(1, 0, 3);
vh.AddTriangle(1, 3, 4);
//中間的矩形
vh.AddTriangle(5, 2, 6);
vh.AddTriangle(5, 6, 9);
//右邊的矩形
vh.AddTriangle(8, 7, 10);
vh.AddTriangle(8, 10, 11);
//開始構造四個角
List<Vector2> vCenterList = new List<Vector2>();
List<Vector2> uvCenterList = new List<Vector2>();
List<int> vCenterVertList = new List<int>();
//右上角的圓心
vCenterList.Add(new Vector2(v.z - radius, v.w - radius));
uvCenterList.Add(new Vector2(uv.z - uvRadiusX, uv.w - uvRadiusY));
vCenterVertList.Add(7);
//左上角的圓心
vCenterList.Add(new Vector2(v.x + radius, v.w - radius));
uvCenterList.Add(new Vector2(uv.x + uvRadiusX, uv.w - uvRadiusY));
vCenterVertList.Add(3);
//左下角的圓心
vCenterList.Add(new Vector2(v.x + radius, v.y + radius));
uvCenterList.Add(new Vector2(uv.x + uvRadiusX, uv.y + uvRadiusY));
vCenterVertList.Add(4);
//右下角的圓心
vCenterList.Add(new Vector2(v.z - radius, v.y + radius));
uvCenterList.Add(new Vector2(uv.z - uvRadiusX, uv.y + uvRadiusY));
vCenterVertList.Add(8);
//每個三角形的頂角
float degreeDelta = (float)(Mathf.PI / 2 / TriangleNum);
//當前的角度
float curDegree = 0;
for (int i = 0; i < vCenterVertList.Count; i++)
{
int preVertNum = vh.currentVertCount;
for (int j = 0; j <= TriangleNum; j++)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
Vector3 vPosition = new Vector3(vCenterList[i].x + cosA * radius, vCenterList[i].y + sinA * radius);
Vector3 uvPosition = new Vector2(uvCenterList[i].x + cosA * uvRadiusX, uvCenterList[i].y + sinA * uvRadiusY);
vh.AddVert(vPosition, color32, uvPosition);
curDegree += degreeDelta;
}
curDegree -= degreeDelta;
for (int j = 0; j <= TriangleNum - 1; j++)
{
vh.AddTriangle(vCenterVertList[i], preVertNum + j + 1, preVertNum + j);
}
}
}
private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
{
var padding = overrideSprite == null ? Vector4.zero : DataUtility.GetPadding(overrideSprite);
Rect r = GetPixelAdjustedRect();
var size = overrideSprite == null ? new Vector2(r.width, r.height) : new Vector2(overrideSprite.rect.width, overrideSprite.rect.height);
//Debug.Log(string.Format("r:{2}, size:{0}, padding:{1}", size, padding, r));
int spriteW = Mathf.RoundToInt(size.x);
int spriteH = Mathf.RoundToInt(size.y);
if (shouldPreserveAspect && size.sqrMagnitude > 0.0f)
{
var spriteRatio = size.x / size.y;
var rectRatio = r.width / r.height;
if (spriteRatio > rectRatio)
{
var oldHeight = r.height;
r.height = r.width * (1.0f / spriteRatio);
r.y += (oldHeight - r.height) * rectTransform.pivot.y;
}
else
{
var oldWidth = r.width;
r.width = r.height * spriteRatio;
r.x += (oldWidth - r.width) * rectTransform.pivot.x;
}
}
var v = new Vector4(
padding.x / spriteW,
padding.y / spriteH,
(spriteW - padding.z) / spriteW,
(spriteH - padding.w) / spriteH);
v = new Vector4(
r.x + r.width * v.x,
r.y + r.height * v.y,
r.x + r.width * v.z,
r.y + r.height * v.w
);
return v;
}
}
}
Editor代碼:
using System.Linq;
using UnityEngine;
using UnityEditor.AnimatedValues;
using UnityEngine.UI;
using UnityEditor;
using UnityEditor.UI;
namespace GFramework
{
[CustomEditor(typeof(SimpleRoundedImage), true)]
//[CanEditMultipleObjects]
public class SimpleRoundedImageEditor : ImageEditor
{
SerializedProperty m_Radius;
SerializedProperty m_TriangleNum;
SerializedProperty m_Sprite;
protected override void OnEnable()
{
base.OnEnable();
m_Sprite = serializedObject.FindProperty("m_Sprite");
m_Radius = serializedObject.FindProperty("Radius");
m_TriangleNum = serializedObject.FindProperty("TriangleNum");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
SpriteGUI();
AppearanceControlsGUI();
RaycastControlsGUI();
bool showNativeSize = m_Sprite.objectReferenceValue != null;
m_ShowNativeSize.target = showNativeSize;
NativeSizeButtonGUI();
EditorGUILayout.PropertyField(m_Radius);
EditorGUILayout.PropertyField(m_TriangleNum);
this.serializedObject.ApplyModifiedProperties();
}
}
}
需要注意的點:
①UV坐標是[0-1]的,不隨image的寬和高變換的,所以在做uv映射的時候要將uv坐標做等比例的處理,不然會出現斷層的情況。
②在計算頂點信息的時候,要注意Pivot對頂點坐標的影響(直接照搬Image的處理就可以了)
③注意沒有貼圖的時候的處理,要讓這張圖片顯示默認顏色。
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
④因為直接繼承Image類的類在Inspector面板上不會顯示新定義的public變量,所以我們還要寫一個SimpleRoundedImageEditor.cs來將新定義的圓角矩形半徑和構成一個90°扇形的三角型的展示在面板上,順便隱藏一下圖片的類型,因為只實現了simple類型圖片的圓角矩形。
4.效果
5.關於效率
Mask | SimpleRoundedImage | |
---|---|---|
DrawCall | 3 | 1 |
頂點數 | 4 | 30個左右(一般每個扇形由6個三角型組成就可以達到較好的效果),頂點數量可以接受。 |
總結:如果在相同mask且之間沒有相互遮擋的情況下,unity會對drawCall進行動態批處理,所以Mask數量的增加對drawCall的影響很小,只有在有多個不同mask或mask相互遮擋的情況下,每個mask會額外增加2次DrawCall。對DrawCall數量有較大的影響,但這種情況較少。
所以SimpleRoundedImage在大多數情況下對效率的提升並不明顯。但通過修改頂點的方式實現圓角的方式會比使用遮罩實現圓角更加靈活方便。
代碼鏈接:https://github.com/blueberryzzz/UIAndShader/tree/master/UIAndShader/Assets/SimpleRoundedImage