1、为什么要使用镜像
在游戏开发过程中,我们经常会为了节省 美术图片资源大小,美术会将两边相同的图片进行切一半来处理。如下所示一个按钮 需要 400 * 236,然而美术只需要切一张 74*236的大小就可以了。这样一来图集就可以容纳更多的图片。内存占用也更少。
2.实现方案
- 拷贝一张图片,然后把 scale改成-1,这种方法比较笨,需要多加一张图片,操作起来也很不方便。没啥好讲的。
- 拷贝原有的图片顶点,进行对称处理。如下图所示。
3.在哪里拿到mesh顶点?修改顶点?
BaseMeshEffect :是用于实现网格效果的抽象类实现IMeshModifier接口,是Shadow、Outline等效果的基类。
从Unity uGUI原理解析-Graphic可以知道,Graphic 在执行 DoMeshGeneration 时会获取到当前GameObject上的所有实现 IMeshModifier 的组件。 并且会调用 ModifyMesh 方法来修改当前Graphic 的Mesh数据。BaseMeshEffect 的类结构图如下:
我们编写一个MirrorImage来继承BaseMeshEffect,拿到 VertexHelper,获取网格的顶点流数据,然后进行下面的镜像,映射操作:
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UI;
public enum MirrorType
{
Horizontal,
Vertical,
HorizontalAndVertical
}
[RequireComponent(typeof(Image))]
public class MirrorImage : BaseMeshEffect
{
public MirrorType mirrorType = MirrorType.Horizontal;
public Dictionary<Image.Type, IMirror> MirrorDic = new Dictionary<Image.Type, IMirror>()
{
{Image.Type.Simple, new SimpleMirror()},
{Image.Type.Sliced, new SlicedMirror()},
{Image.Type.Tiled, new TiledMirror()},
{Image.Type.Filled, new FilledMirror()},
};
public Image image => graphic as Image;
public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive()) return;
Image.Type imageType = (graphic as Image).type;
List<UIVertex> vertices = new List<UIVertex>();
vh.GetUIVertexStream(vertices);
vh.Clear();
MirrorDic[imageType].Draw(image, vertices, mirrorType);
vh.AddUIVertexTriangleStream(vertices);
}
[Button("Set Native Size")]
public void SetNativeSize()
{
if (image.sprite != null)
{
float w = image.sprite.rect.width / image.pixelsPerUnit;
float h = image.sprite.rect.height / image.pixelsPerUnit;
image.rectTransform.anchorMax = image.rectTransform.anchorMin;
float x = mirrorType == MirrorType.Horizontal || mirrorType == MirrorType.HorizontalAndVertical ? 2 : 1;
float y = mirrorType == MirrorType.Vertical || mirrorType == MirrorType.HorizontalAndVertical ? 2 : 1;
image.rectTransform.sizeDelta = new Vector2(w * x, h * y);
image.SetAllDirty();
}
}
}
当然除了继承BaseMeshEffect,当然还可以直接继承Image,重写OnPopulateMesh。
4.如何实现顶点的镜像?
很简单 假设中心的是 center( 0,0),需要水平镜像的点 A(-1,0) 镜像后的点就是 B(-1,0)需要满足 A 到Center的距离 == B到center的距离:
所以我们先求出镜像的中心点 center的位置 ,因为矩形有自己的中心点位置,我们需要求出镜像的中心点位置在改矩形下的坐标,这么说可能有点绕看下图:
矩形的宽w:100,h:100,矩形自身的中心点(蓝色的圈) 这边为称为O点,在Unity中是以 Rect中的 x,y代表的是坐下角的点 既 (-75,-50),
对于Rect(x,y) 不理解的话可以看下GPT的回答,可能比我讲的清楚:
那么镜像真正的中心点坐标
- center.x = rect.x + rect.width;
- center.y = rect.y + rect.height;
那么要镜像的点 A 镜像后为B ,需要求出A到center的长度大小然后取反 + 矩形中心点与镜像中心点的偏移
B.x = -(A.x - center.x) + (rect.x+rect.width/2)
B.y = -(A.y - center.y) + (rect.x+rect.width/2)
逻辑分析完了,直接看代码吧:
using System.Collections.Generic;
using UnityEngine;
public static class MirrorUtlis
{
public static void Mirror(Rect rect,List<UIVertex> uiVertices,MirrorType type)
{
int count = uiVertices.Count;
switch (type)
{
case MirrorType.Horizontal:
Mirror(rect, uiVertices, type, count);
break;
case MirrorType.Vertical:
Mirror(rect, uiVertices, type, count);
break;
case MirrorType.HorizontalAndVertical:
Mirror(rect, uiVertices, MirrorType.Horizontal, count);
Mirror(rect, uiVertices, MirrorType.Vertical, 2 * count);
break;
}
RemoveVertices(uiVertices);
}
private static void Mirror(Rect rect, List<UIVertex> uiVertices, MirrorType type, int count)
{
for (int i = 0; i < count; i++)
{
UIVertex vertex = uiVertices[i];
switch (type)
{
case MirrorType.Horizontal:
vertex = HorizontalMirror(rect, vertex);
break;
case MirrorType.Vertical:
vertex = VerticalMirror(rect, vertex);
break;
case MirrorType.HorizontalAndVertical:
vertex = HorizontalMirror(rect, vertex);
vertex = VerticalMirror(rect, vertex);
break;
}
uiVertices.Add(vertex);
}
}
private static UIVertex HorizontalMirror(Rect rect, UIVertex vertex)
{
float center = rect.width / 2 + rect.x;
vertex.position.x = -(vertex.position.x - center) + rect.x + rect.width/2;
return vertex;
}
private static UIVertex VerticalMirror(Rect rect, UIVertex vertex)
{
float center = rect.height / 2 + rect.y;
vertex.position.y = -(vertex.position.y - center) + rect.y + rect.height/2;
return vertex;
}
// 移除构不成三角形的顶点
private static void RemoveVertices(List<UIVertex> uiVertices)
{
int end = uiVertices.Count;
for (int i = 2; i < end; i += 3)
{
UIVertex v1 = uiVertices[i];
UIVertex v2 = uiVertices[i - 1];
UIVertex v3 = uiVertices[i - 2];
if (v1.position == v2.position ||
v1.position == v3.position ||
v2.position == v3.position)
{
// 移动到尾部
ChaneVertices(uiVertices, i - 1, end - 3);
ChaneVertices(uiVertices, i - 2, end - 2);
ChaneVertices(uiVertices, i, end - 1);
end -= 3;
}
}
if(end < uiVertices.Count)
uiVertices.RemoveRange(end,uiVertices.Count - end);
}
private static void ChaneVertices(List<UIVertex> uiVertices,int a,int b)
{
(uiVertices[a], uiVertices[b]) = (uiVertices[b], uiVertices[a]);
}
}
在顶点镜像前我们需要对顶点进行顶点映射,什么时时映射?
如上图所示,原来图片是白色区域大小,顶点为白色图片的四个顶点,因为要做对称,所以需要留出一半的位置来增加映射后的顶点。
在不同模式下的顶点映射需要做不同处理,在Unity中有一下几种模式:
- Simple: 如上面的图所示,对顶点的位置 除以 2 即可比较简单
- Sliced:九宫格模式下因为要保留九宫格的顶点位置,不能让九宫格保留的位置发生形变,就不能直接除以2来处理,需要做平移处理。具体实现下面在讲
- Filed:平铺模式下是不需要在镜像网格顶点的,因为平铺下的图片顶点是已经增加好的了,我们只需要对UV做镜像就可以了
- Filled:暂时未实现该模式下的,以后有时间在研究。
由于有多种模式的处理我们将实现接口化,方便我们的管理:定义一个IMirror接口:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public interface IMirror
{
void Draw(Image image,List<UIVertex> uiVertices,MirrorType type);
}
Simpe下的顶点映射:
public class SimpleMirror : IMirror
{
public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
{
ChangeVertices(image.rectTransform.rect,uiVertices,type);
MirrorUtlis.Mirror(image.rectTransform.rect,uiVertices,type);
}
// 改变原有的顶点位置 (做一半映射) 如果是 Horizontal:左半部分 Vertical:上半部分 HorizontalAndVertical:左上四分之一
private void ChangeVertices(Rect rect,List<UIVertex> uiVertices,MirrorType type)
{
for (int i = 0; i < uiVertices.Count; i++)
{
UIVertex vertex = uiVertices[i];
switch (type)
{
case MirrorType.Horizontal:
vertex = HorizontalVertex(rect, vertex);
break;
case MirrorType.Vertical:
vertex = VerticalVertex(rect, vertex);
break;
case MirrorType.HorizontalAndVertical:
vertex = HorizontalVertex(rect, vertex);
vertex = VerticalVertex(rect, vertex);
break;
}
uiVertices[i] = vertex;
}
}
// 水平映射
private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
{
vertex.position.x = (vertex.position.x + rect.x) / 2;// - rect.width / 2;
return vertex;
}
// 垂直映射
private UIVertex VerticalVertex(Rect rect,UIVertex vertex)
{
vertex.position.y = (rect.y + vertex.position.y) / 2 + rect.height / 2;
return vertex;
}
}
Sliced下的顶点映射:
我们可以看到如下映射的主要方法:
// 水平映射
private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
{
if (vertex.position.x == s_VertScratch[0].x || vertex.position.x == s_VertScratch[1].x) return vertex;
vertex.position.x -= rect.width / 2;
return vertex;
}
时机上非常简单,就是直接对 x 做矩形宽度/2 的平移,比较难的是我们需要知道什么顶点需要做平移?
这个需要看一下Image的源码是如何实现顶点的处理:
/// <summary>
/// Generate vertices for a 9-sliced Image.
/// </summary>
private void GenerateSlicedSprite(VertexHelper toFill)
{
if (!hasBorder)
{
GenerateSimpleSprite(toFill, false);
return;
}
Vector4 outer, inner, padding, border;
if (activeSprite != null)
{
outer = Sprites.DataUtility.GetOuterUV(activeSprite);
inner = Sprites.DataUtility.GetInnerUV(activeSprite);
padding = Sprites.DataUtility.GetPadding(activeSprite);
border = activeSprite.border;
}
else
{
outer = Vector4.zero;
inner = Vector4.zero;
padding = Vector4.zero;
border = Vector4.zero;
}
Rect rect = GetPixelAdjustedRect();
Vector4 adjustedBorders = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
padding = padding / multipliedPixelsPerUnit;
s_VertScratch[0] = new Vector2(padding.x, padding.y);
s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);
s_VertScratch[1].x = adjustedBorders.x;
s_VertScratch[1].y = adjustedBorders.y;
s_VertScratch[2].x = rect.width - adjustedBorders.z;
s_VertScratch[2].y = rect.height - adjustedBorders.w;
for (int i = 0; i < 4; ++i)
{
s_VertScratch[i].x += rect.x;
s_VertScratch[i].y += rect.y;
}
......
......
}
这段源码不全我截取了,计算图片的位置信息,然后把4个顶点位置信息按顺序写进s_VertScratch数组中。这四个位置分别对应一下位置:
即九宫格裁剪后映射到Image顶点上的四个位置,所以当我们向做水平映射的时候只需要平移和 3和4 x轴相等的顶点,与1和2 x轴相等的顶点保留原来的位置。如下图可以很直观的看出来
至于怎么算出这四个九宫格映射的顶点就直接拷贝Image源码的实现就好了。
贴一下完整代码:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SlicedMirror : IMirror
{
private Image image;
// 九宫格的四个分界点
private Vector2[] s_VertScratch = new Vector2[4];
public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
{
this.image = image;
SetVertScratch();
ChangeVertices(image.rectTransform.rect,uiVertices,type);
MirrorUtlis.Mirror(image.rectTransform.rect,uiVertices,type);
}
private void ChangeVertices(Rect rect,List<UIVertex> uiVertices,MirrorType type)
{
for (int i = 0; i < uiVertices.Count; i++)
{
UIVertex vertex = uiVertices[i];
switch (type)
{
case MirrorType.Horizontal:
vertex = HorizontalVertex(rect, vertex);
break;
case MirrorType.Vertical:
vertex = VerticalVertex(rect, vertex);
break;
case MirrorType.HorizontalAndVertical:
vertex = HorizontalVertex(rect, vertex);
vertex = VerticalVertex(rect, vertex);
break;
}
uiVertices[i] = vertex;
}
}
// 水平映射
private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
{
if (vertex.position.x == s_VertScratch[0].x || vertex.position.x == s_VertScratch[1].x) return vertex;
vertex.position.x -= rect.width / 2;
return vertex;
}
// 垂直映射
private UIVertex VerticalVertex(Rect rect,UIVertex vertex)
{
if (vertex.position.y == s_VertScratch[2].y || vertex.position.y == s_VertScratch[3].y) return vertex;
vertex.position.y += rect.height / 2;
return vertex;
}
private void SetVertScratch()
{
Sprite activeSprite = image.sprite;
Vector4 padding, border;
if (activeSprite != null)
{
padding = UnityEngine.Sprites.DataUtility.GetPadding(activeSprite);
border = activeSprite.border;
}
else
{
padding = Vector4.zero;
border = Vector4.zero;
}
Rect rect = image.GetPixelAdjustedRect();
var multipliedPixelsPerUnit = image.pixelsPerUnit * image.pixelsPerUnitMultiplier;
Vector4 adjustedBorders = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
padding /= multipliedPixelsPerUnit;
s_VertScratch[0] = new Vector2(padding.x, padding.y);
s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);
s_VertScratch[1].x = adjustedBorders.x;
s_VertScratch[1].y = adjustedBorders.y;
s_VertScratch[2].x = rect.width - adjustedBorders.z;
s_VertScratch[2].y = rect.height - adjustedBorders.w;
for (int i = 0; i < 4; ++i)
{
s_VertScratch[i].x += rect.x;
s_VertScratch[i].y += rect.y;
}
}
private Vector4 GetAdjustedBorders(Vector4 border, Rect adjustedRect)
{
Rect originalRect = image.rectTransform.rect;
for (int axis = 0; axis <= 1; axis++)
{
float borderScaleRatio;
if (originalRect.size[axis] != 0)
{
borderScaleRatio = adjustedRect.size[axis] / originalRect.size[axis];
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
float combinedBorders = border[axis] + border[axis + 2];
if (adjustedRect.size[axis] < combinedBorders && combinedBorders != 0)
{
borderScaleRatio = adjustedRect.size[axis] / combinedBorders;
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
}
return border;
}
}
Tiled:模式下的映射
在平铺模式下顶点都是完整的,不需要做镜像处理,只需要修改没一块对应的UV对称即可,我们固定开始位置为1,不做翻转,那么
2位置所构成的顶点UV.y:就需要做对称处理
4位置所构成的顶点UV.x:就需要做对称处理
3位置所构成的顶点UV.x和y :都需要做对称处理
如何判断某几个顶点的UV需要做对称?
我们知道一个三角面由三个顶点构成,那么我么只需要找出三个顶点的中心位置,假设sprite的宽高都为w = 100,h = 100,构成的三角形区域的中心点分别为
1位置 =>(50,50),
2位置 => (50,150),
3位置 => (150,150),
4位置 => (150,50),
我们对中心的的
1 => x / W = 0.5
2 => x / W = 1.5
。。
我们对结果 %2 那么 结果为 1 的就需要翻转了;
uv怎么翻转
outerUv = UnityEngine.Sprites.DataUtility.GetOuterUV(image.sprite);
outerUV中的 x,y就代表右下角的点,z就代表宽,w:代表高,假设A镜像UV后的点为B,那么就满足:
A 到 (0,0)的距离 == B到(0+z)的距离,所以
镜像后的 A.x = outer.z -( A.x - outerUv.x )
贴一下完整代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TiledMirror : IMirror
{
private Vector4 outerUv;
public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
{
outerUv = UnityEngine.Sprites.DataUtility.GetOuterUV(image.sprite);
ChangeUv(uiVertices,type);
}
// uv翻转
private void ChangeUv(List<UIVertex> uiVertices,MirrorType type)
{
Vector3 cellMinP = uiVertices[0].position;
Vector3 cellMaxP = uiVertices[2].position;
float w = cellMaxP.x - cellMinP.x;
float h = cellMaxP.y - cellMinP.y;
for (int i = 0; i < uiVertices.Count; i+= 3)
{
UIVertex v1 = uiVertices[i];
UIVertex v2 = uiVertices[i+1];
UIVertex v3 = uiVertices[i+2];
float centerX = GetCenter(v1, v2, v3, true) - cellMaxP.x;
float centerY = GetCenter(v1, v2, v3, false) - cellMaxP.y;
bool changeX = Mathf.Ceil(centerX / w) % 2 != 0;
bool changeY = Mathf.Ceil(centerY / h) % 2 != 0;
if (changeX && (type == MirrorType.Horizontal || type == MirrorType.HorizontalAndVertical))
{
v1 = HorizontalUv(v1);
v2 = HorizontalUv(v2);
v3 = HorizontalUv(v3);
}
if (changeY && (type == MirrorType.Vertical || type == MirrorType.HorizontalAndVertical))
{
v1 = VerticalUv(v1);
v2 = VerticalUv(v2);
v3 = VerticalUv(v3);
}
uiVertices[i] = v1;
uiVertices[i + 1] = v2;
uiVertices[i + 2] = v3;
}
}
// 获取三个顶点的中心位置
private float GetCenter(UIVertex v1,UIVertex v2,UIVertex v3,bool isX)
{
float min = Mathf.Min(
Mathf.Min(isX ? v1.position.x : v1.position.y,isX ? v2.position.x : v2.position.y),
Mathf.Min(isX ? v1.position.x : v1.position.y,isX ? v3.position.x : v3.position.y));
float max = Mathf.Max(
Mathf.Max(isX ? v1.position.x : v1.position.y,isX ? v2.position.x : v2.position.y),
Mathf.Max(isX ? v1.position.x : v1.position.y,isX ? v3.position.x : v3.position.y));
return (min + max) / 2;
}
private UIVertex HorizontalUv(UIVertex vertex)
{
vertex.uv0.x = outerUv.x + outerUv.z - vertex.uv0.x;
return vertex;
}
private UIVertex VerticalUv(UIVertex vertex)
{
vertex.uv0.y = outerUv.y + outerUv.w - vertex.uv0.y;
return vertex;
}
}
总结:
实现过程中,思路理清之后实现基本上是不难,但是需要去理解Unity Image的实现,针对绘制不同模式的生成网格顶点的实现,知道图篇是怎么绘制上去的,三个顶点构成一个面,Rect中的(x,y,z,w)分别代表什么。然后就是计算计算。