前言
在我们开发游戏过程中,会经常使用Mask来进行图片的裁剪,但是笔者在使用Mask进行裁剪的时候发现锯齿特别严重,因此笔者选择了利用shader进行图形遮罩,详情请看Unity杂文——UGUI基于图集的shader遮罩。
笔者虽然已经利用shader做好了遮罩并应用项目中的,但是在笔者在学习UGUI优化的时候发现Mask不仅有锯齿,也会增加两个DrawCall,因为Mask会把自己和子节点都和外面分开,不会进行合批,这样mask越多,DrawCall就会比较严重,笔者利用Shader进行遮罩虽然也会多一个DrawCall,但是相同的材质会进行合批,
裁剪随然已经改好了,但是笔者发现了不会增加DrawCall的方法。
实现原理
我们在屏幕上看到的图形是GPU渲染出来的,而GPU渲染的最小单位是三角面片,我们从Unity的Scence场景中,切换视图方式为WireFrame或者Shader Wireframe都可以明显看到图片是三角形组成的,而我们要制作出圆形的Image可以利用多个等腰三角形,这样就可以拼接成看似圆形的Image,三角形数量越多就越像圆形。如下图:
实现
首先我们需要自己重写Image,我们要自己实现画图,我们首先查看Image的原码:
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
我们可以看到Image继承了MaskableGraphic,并且实现了ISerializationCallbackReceiver、ILayoutElement、ICanvasRaycastFilter的接口。最关键的其实是MaskableGraphic类,因为这个类主要是负责画图的,我们可以很简单的看到MaskableGraphic类其实继承了Graphic类,在这个类里面有个OnPopulateMesh函数,这个函数就是我们需要重写的函数。
当UI元素生成顶点数据时就会调用OnPopulateMesh函数,我们只需要继承这个函数并且将原来的顶带你数据清除,改写成我们自己设置的圆形的顶带你数据,这样我们就可以画我们需要的圆形了。
由于在Unity中,继承UnityEngine基类的派生类并不能在Inspector面板里显示参数,笔者在制作圆形的Image的时候肯定要设置一些可调节的参数,这样可以应用到更多的场景中,因为笔者就像参考博客一样新建一个BaseImage类去继承Image类,然后自己再写一个CircleImage类去继承BaseImage类,这样我们把可调节的变量放在CircleImage类中,这样就可以通过面板调节参数了。<font color=red>(原Image源码有近千行代码,BaseImage对其进行了部分精简,只支持Simple Image Type,并去掉了eventAlphaThreshold的相关代码。经过删减,得到一个百行代码的BaseImage类,精简版Image就完成了。)</font>
代码分析
完整代码在最后面,因为内容过多,笔者就先写代码分析,您可以先复制最后的完整代码到工程里,然后自己对着代码一步一步进行。
圆形
笔者首先介绍一下笔者设置的允许调节的参数,参数描述都在代码中,代码如下:
[Tooltip("圆形的半径")]
[Range(0, 1)]
public float fillRadius = 1f;
[Tooltip("UV缩放比例")]
[Range(0, 1)]
public float UVScale = 1f;
[Tooltip("圆形的中心点")]
public Vector2 fillCenter = new Vector2(0.5f, 0.5f);
[Tooltip("圆形或扇形填充比例")]
[Range(0, 1)]
public float fillPercent = 1f;
[Tooltip("是否填充圆形")]
public bool fill = true;
[Tooltip("圆环宽度")]
public float thickness = 5;
[Tooltip("圆形")]
[Range(3, 100)]
public int segements = 20; //填充三角形数量
在OnPopulateMesh函数中,函数的参数VertexHelper就是原来图片的顶带你信息,因为我们要重写这些顶点信息,所以我们要清空vh。在我们设置自己的顶点的信息之前,我们需要获得UV信息,获取方法就是DataUtility.GetOuterUV(overrideSprite)。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
float uvScaleX = (uv.z - uv.x) / tw * fillRadius * UVScale;
float uvScaleY = (uv.w - uv.y) / th * fillRadius * UVScale;
...
}
在设置的属性中我们有一个变量segements就是我们需要的三角形数量,正如原理将的,三角形数量越多,越像圆形,但是顶点数据就越多,影响性能,所以我们设置这个参数可以根据需求设置数量,然后我们知道数量后就可以算出顶点的夹角,然后面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形
protected override void OnPopulateMesh(VertexHelper vh)
{
...
//算出每个面片的顶点夹角,面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形
float degreeDelta = (float)(2 * Mathf.PI / segements);
int curSegements = (int)(segements * fillPercent);
...
}
我们可以通过RectTransform获取原图矩形的宽高,笔者这里也添加了一个可以调整的参数圆形半径个圆环宽度,圆环宽度是用来做圆环形状显示的,圆形半径其实就是原图的宽高乘以圆的半径就行了,这里圆的半径其实是一个比例,把原图的比作为1。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
//通过RectTransform获取矩形宽高,计算出半径
float tw = rectTransform.rect.width * fillRadius;
float th = rectTransform.rect.height * fillRadius;
float outerRadius = rectTransform.pivot.x * tw;
float innerRadius = rectTransform.pivot.x * tw - thickness;
...
}
已经有了半径,夹角信息,根据圆形点坐标公式(radius * cosA,radius * sinA)可以算出顶点坐标,每次迭代新建UIVertex,将求出的坐标,color,uv等参数传入,再将UIVertex传给VertexHelper。重复迭代n次,VertexHelper就获得了多边形顶点及圆心点信息了。 这里笔者也设置了参数,UV的缩放和圆的中心点,也是为了适应更多的场景
protected override void OnPopulateMesh(VertexHelper vh)
{
...
float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
float uvScaleX = (uv.z - uv.x) / tw * fillRadius * UVScale;
float uvScaleY = (uv.w - uv.y) / th * fillRadius * UVScale;
float curDegree = 0;
UIVertex uiVertex;
int verticeCount;
int triangleCount;
Vector2 curVertice;
curVertice = Vector2.zero;
verticeCount = curSegements + 1;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
for (int i = 1; i < verticeCount; i++)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
curDegree += degreeDelta;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
...
}
虽然已经传入了所有的顶带你信息,但是GPU还不知道顶点信息之间的关系,不知道顶带你分成了多少个三角形片面,所以还需要把三角形的信息告诉GPU,这里有一个VertexHelper的接口就是AddTriangle(int idx0, int idx1, int idx2)来接受三角形信息。
接口的传入参数并不是UIVertex类型,而是int类型的索引值。哪来的索引?还记得之前往VertexHelper传入了一堆顶点吗?按照传入顺序,第一个顶点,索引记为0,依次类推。每次传入三个顶点的索引,就记录下了一个三角形。
需要注意,GPU 默认是做backface culling(背面剔除)的,GPU只渲染正对屏幕的三角面片,当GPU认为某个三角面片是背对屏幕时,直接丢弃该三角面片,不做渲染。那么GPU怎么判断我们传入的某个三角形是正对屏幕,还是背对屏幕?答案是通过三个顶点的时针顺序,当三个顶点是呈顺时针时,判定为正对屏幕;呈逆时针时,判定为背对屏幕。
VertexHelper收到的第一个顶点是圆心,且算法是按逆时针方向,迭代计算出的多边形顶点,并依次传给VertexHelper。因此按(i, 0, i+1)(i>=1)的规律取索引,就可以保证顶点顺序是顺时针的。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
triangleCount = curSegements*3;
for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
{
vh.AddTriangle(vIdx, 0, vIdx+1);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, 0, 1);
}
...
}
到此我们的圆形算是绘制完成了,但是观测我们的变量可以看出,笔者还支持了圆环的绘制
圆环
圆环的情况稍微复杂:顶点集没有圆心顶点了,只有内环、外环顶点;三角形集也不是简单的切饼式分割,采用一种比较直观的三角形划分,让内外环相邻的顶点类似一根鞋带那样互相连接,来划分三角形。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
float uvScaleX = (uv.z - uv.x) / tw * fillRadius * UVScale;
float uvScaleY = (uv.w - uv.y) / th * fillRadius * UVScale;
float curDegree = 0;
UIVertex uiVertex;
int verticeCount;
int triangleCount;
Vector2 curVertice;
curVertice = Vector2.zero;
verticeCount = curSegements + 1;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
verticeCount = curSegements*2;
for (int i = 0; i < verticeCount; i += 2)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curDegree += degreeDelta;
curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
innerVertices.Add(curVertice);
curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
...
}
点击判断
传统的UGUI的Image的点击判断是只要在矩形内点击,不管是不是透明,都认定为点击到了,笔者从网上学习了一套更好的判断点击的方法,利用的是Ray-Crossing算法。Ray-Crossing算法大概思路是从指定点p发出一条射线,与多边形相交,假若交点个数是奇数,说明点p落在多边形内,交点个数为偶数说明点p在多边形外。
射线选取哪个方向并没有限制,但为了实现起来方便,考虑屏幕点击点为点p,向水平方向右侧发出射线的情况,那么顶点v1,v2组成的线段与射线若有交点q,则点q必定满足两个条件:
v2.y < q.y = p.y > v1.y
p.x < q.x
我们根据这两个条件,逐一跟多边形线段求交点,并统计交点个数,最后判断奇偶即可得知点击点是否在圆形内。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Sprite sprite = overrideSprite;
if (sprite == null)
return true;
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
return Contains(local, outterVertices, innerVertices);
}
private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
{
var crossNumber = 0;
if(!fill)
RayCrossing(p, innerVertices, ref crossNumber);//检测内环
RayCrossing(p, outterVertices, ref crossNumber);//检测外环
return (crossNumber & 1) == 1;
}
/// <summary>
/// 使用RayCrossing算法判断点击点是否在封闭多边形里
/// </summary>
/// <param name="p"></param>
/// <param name="vertices"></param>
/// <param name="crossNumber"></param>
private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
{
for (int i = 0, count = vertices.Count; i < count; i++)
{
var v1 = vertices[i];
var v2 = vertices[(i + 1) % count];
//点击点水平线必须与两顶点线段相交
if (((v1.y <= p.y) && (v2.y > p.y))
|| ((v1.y > p.y) && (v2.y <= p.y)))
{
//只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
{
crossNumber += 1;
}
}
}
}
...
}
SetNativeSize
SetNativeSize的实现比较简单,只要把宽高设置图片的高度就行了。
protected override void OnPopulateMesh(VertexHelper vh)
{
...
public override void SetNativeSize()
{
if (activeSprite != null)
{
float w = activeSprite.rect.width / pixelsPerUnit;
float h = activeSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}
...
}
在这里笔者遇到了一个问题,就是我们怎么能像Image那样调用这个方法呢,笔者参考了Image的原码,Imnage是有一个专门的Editor脚本设置面板显示的,于是笔者就写了一个CircleImageEditor的脚本来控制。只需要脚本继承GraphicEditor,然后通过[CustomEditor(typeof(CircleImage))]标签就可以实现脚本的控制了。
[CustomEditor(typeof(CircleImage))]
public class CircleImageEditor : GraphicEditor
{
public override void OnInspectorGUI() {
DrawDefaultInspector();
CircleImage myScript = (CircleImage)target;
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(EditorGUIUtility.labelWidth);
if (GUILayout.Button("Set Native Size", EditorStyles.miniButtonRight))
{
myScript.SetNativeSize();
}
}
EditorGUILayout.EndHorizontal();
}
}
完整代码
笔者在制作BaseImage的时候并没有继承MaskableGraphic而是自己复制了一份到BaseMaskableGraphic类中,这是因为笔者不喜欢脚本在Inspector面面板中显示m_OnCullStateChanged这个事件,因此笔者复制了一份,只是把这个变量变成了私有,就不在面板显示,如果不介意面板的了可以继续继承MaskableGraphic。
BaseImage
public class BaseImage : BaseMaskableGraphic,ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
[FormerlySerializedAs("m_Frame")]
[SerializeField]
private Sprite m_Sprite; //私有的sorite,内部调用,防止外部修改
//对外公开的sprite属性
public Sprite sprite
{
get { return m_Sprite; }
set{if (SetPropertyUtilityExt.SetClass(ref m_Sprite, value)) SetAllDirty();}
}
[NonSerialized]
private Sprite m_OverrideSprite;
protected BaseImage()
{
useLegacyMeshGeneration = false;
}
public Sprite overrideSprite
{
get { return m_OverrideSprite == null ? sprite : m_OverrideSprite; }
set
{
if (SetPropertyUtilityExt.SetClass(ref m_OverrideSprite, value)) SetAllDirty();
}
}
/// <summary>
/// Image's texture comes from the UnityEngine.Image.
/// </summary>
public override Texture mainTexture
{
get
{
return overrideSprite == null ? s_WhiteTexture : overrideSprite.texture;
}
}
public Sprite activeSprite { get { return overrideSprite != null ? overrideSprite : sprite; } }
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (sprite)
spritePixelsPerUnit = sprite.pixelsPerUnit;
float referencePixelsPerUnit = 100;
if (canvas)
referencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / referencePixelsPerUnit;
}
}
/// <summary>
/// 子类需要重写该方法来自定义Image形状
/// </summary>
/// <param name="vh"></param>
protected override void OnPopulateMesh(VertexHelper vh)
{
base.OnPopulateMesh(vh);
}
#region ISerializationCallbackReceiver
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
}
#endregion
#region ILayoutElement
public virtual void CalculateLayoutInputHorizontal() { }
public virtual void CalculateLayoutInputVertical() { }
public virtual float minWidth { get { return 0; } }
public virtual float preferredWidth
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.x / pixelsPerUnit;
}
}
public virtual float flexibleWidth { get { return -1; } }
public virtual float minHeight { get { return 0; } }
public virtual float preferredHeight
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.y / pixelsPerUnit;
}
}
public virtual float flexibleHeight { get { return -1; } }
public virtual int layoutPriority { get { return 0; } }
#endregion
#region ICanvasRaycastFilter
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
return true;
}
#endregion
}
CircleImage
[AddComponentMenu("UI/Circle Image")]
public class CircleImage : BaseImage
{
[Tooltip("圆形的半径")]
[Range(0, 1)]
public float fillRadius = 1f;
[Tooltip("UV缩放比例")]
[Range(0, 1)]
public float UVScale = 1f;
[Tooltip("圆形的中心点")]
public Vector2 fillCenter = new Vector2(0.5f, 0.5f);
[Tooltip("圆形或扇形填充比例")]
[Range(0, 1)]
public float fillPercent = 1f;
[Tooltip("是否填充圆形")]
public bool fill = true;
[Tooltip("圆环宽度")]
public float thickness = 5;
[Tooltip("圆形")]
[Range(3, 100)]
public int segements = 20;
private List<Vector3> innerVertices;
private List<Vector3> outterVertices;
void Awake()
{
innerVertices = new List<Vector3>();
outterVertices = new List<Vector3>();
}
// Update is called once per frame
void Update () {
if(!fill)
this.thickness = (float)Mathf.Clamp(this.thickness, 0, rectTransform.rect.width / 2);
}
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
innerVertices.Clear();
outterVertices.Clear();
//算出每个面片的顶点夹角,面片数segements与填充比例fillPercent相乘,就知道要用多少个面片来显示圆形/扇形
float degreeDelta = (float)(2 * Mathf.PI / segements);
int curSegements = (int)(segements * fillPercent);
//通过RectTransform获取矩形宽高,计算出半径
float tw = rectTransform.rect.width * fillRadius;
float th = rectTransform.rect.height * fillRadius;
float outerRadius = rectTransform.pivot.x * tw;
float innerRadius = rectTransform.pivot.x * tw - thickness;
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvCenterX = (uv.x + uv.z) * (0.5f + (fillCenter.x - 0.5f) * (uv.z - uv.x));
float uvCenterY = (uv.y + uv.w) * (0.5f + (fillCenter.y - 0.5f) * (uv.w - uv.y));
float uvScaleX = (uv.z - uv.x) / tw * fillRadius * UVScale;
float uvScaleY = (uv.w - uv.y) / th * fillRadius * UVScale;
float curDegree = 0;
UIVertex uiVertex;
int verticeCount;
int triangleCount;
Vector2 curVertice;
if (fill) //圆形
{
curVertice = Vector2.zero;
verticeCount = curSegements + 1;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
for (int i = 1; i < verticeCount; i++)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
curDegree += degreeDelta;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
triangleCount = curSegements*3;
for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
{
vh.AddTriangle(vIdx, 0, vIdx+1);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, 0, 1);
}
}
else//圆环
{
verticeCount = curSegements*2;
for (int i = 0; i < verticeCount; i += 2)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curDegree += degreeDelta;
curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
innerVertices.Add(curVertice);
curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
triangleCount = curSegements*3*2;
for (int i = 0, vIdx = 0; i < triangleCount - 6; i += 6, vIdx += 2)
{
vh.AddTriangle(vIdx+1, vIdx, vIdx+3);
vh.AddTriangle(vIdx, vIdx + 2, vIdx + 3);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, verticeCount - 2, 1);
vh.AddTriangle(verticeCount - 2, 0, 1);
}
}
}
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Sprite sprite = overrideSprite;
if (sprite == null)
return true;
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
return Contains(local, outterVertices, innerVertices);
}
private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
{
var crossNumber = 0;
if(!fill)
RayCrossing(p, innerVertices, ref crossNumber);//检测内环
RayCrossing(p, outterVertices, ref crossNumber);//检测外环
return (crossNumber & 1) == 1;
}
/// <summary>
/// 使用RayCrossing算法判断点击点是否在封闭多边形里
/// </summary>
/// <param name="p"></param>
/// <param name="vertices"></param>
/// <param name="crossNumber"></param>
private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
{
for (int i = 0, count = vertices.Count; i < count; i++)
{
var v1 = vertices[i];
var v2 = vertices[(i + 1) % count];
//点击点水平线必须与两顶点线段相交
if (((v1.y <= p.y) && (v2.y > p.y))
|| ((v1.y > p.y) && (v2.y <= p.y)))
{
//只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
{
crossNumber += 1;
}
}
}
}
/// <summary>
/// Adjusts the image size to make it pixel-perfect.
/// </summary>
/// <remarks>
/// This means setting the Images RectTransform.sizeDelta to be equal to the Sprite dimensions.
/// </remarks>
public override void SetNativeSize()
{
if (activeSprite != null)
{
float w = activeSprite.rect.width / pixelsPerUnit;
float h = activeSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}
}
CircleImageEditor
[CustomEditor(typeof(CircleImage))]
public class CircleImageEditor : GraphicEditor
{
public override void OnInspectorGUI() {
DrawDefaultInspector();
CircleImage myScript = (CircleImage)target;
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(EditorGUIUtility.labelWidth);
if (GUILayout.Button("Set Native Size", EditorStyles.miniButtonRight))
{
myScript.SetNativeSize();
}
}
EditorGUILayout.EndHorizontal();
}
}