UGUI源代码之Image-Sliced模式
1、目的
本文主要以研究UGUI中九宫图的实现过程,以及考虑能否优化(效果上的优化以及性能上的优化),最终实现出来的效果还需要进一步测试才能用于实际项目中。
2、参考
本文参考Unity官方的UGUI源代码
Github地址:https://github.com/Unity-Technologies/uGUI
3、代码阅读
Image组件有4种模式,在这里我们只解析九宫模式的Image。
static readonly Vector2[] s_VertScratch = new Vector2[4];
static readonly Vector2[] s_UVScratch = new Vector2[4];
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 / pixelsPerUnit, rect);
padding = padding / pixelsPerUnit;
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;
}
s_UVScratch[0] = new Vector2(outer.x, outer.y);
s_UVScratch[1] = new Vector2(inner.x, inner.y);
s_UVScratch[2] = new Vector2(inner.z, inner.w);
s_UVScratch[3] = new Vector2(outer.z, outer.w);
toFill.Clear();
for (int x = 0; x < 3; ++x)
{
int x2 = x + 1;
for (int y = 0; y < 3; ++y)
{
if (!m_FillCenter && x == 1 && y == 1)
continue;
int y2 = y + 1;
AddQuad(toFill,
new Vector2(s_VertScratch[x].x, s_VertScratch[y].y),
new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y),
color,
new Vector2(s_UVScratch[x].x, s_UVScratch[y].y),
new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y));
}
}
}
在这里简单介绍一下Unity GUI渲染的流程:
1、与渲染模型类似,实际上UGUI也是通过计算顶点和三角形生成网格(Mesh),再通过Renderer渲染出来
2、顶点数据中最主要的是位置,颜色,纹理坐标,法线等。也就是说顶点信息中保存着顶点位置、颜色、纹理坐标、法线信息等。
3、Unity中的图片渲染,实际上也是通过生成顶点信息,以及生成三角形等,再把生成的信息转换为Mesh,再进行渲染的过程
从代码可以看出,传进来的是一个VertexHelper的参数,VertexHelper是顶点辅助类,VertexHelper类封装了生成mesh的基本信息以及常用的方法。
if (!hasBorder)
{
GenerateSimpleSprite(toFill, false);
return;
}
这段代码的意思是,如果Sprite没有设置九宫范围,则直接生成Simple模式的图
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 / pixelsPerUnit, rect);
padding = padding / pixelsPerUnit;
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;
}
s_UVScratch[0] = new Vector2(outer.x, outer.y);
s_UVScratch[1] = new Vector2(inner.x, inner.y);
s_UVScratch[2] = new Vector2(inner.z, inner.w);
s_UVScratch[3] = new Vector2(outer.z, outer.w);
这段代码实际上的操作是,计算图片的位置信息,然后把4个顶点位置信息按顺序写进s_VertScratch数组中,还有,计算当前sprite的uv信息(包括图集中的uv和自身的uv),然后把4个顶点的uv信息按顺序写进s_UVScratch数组中,其顺序如图所示:
因此,知道这4个点的位置信息和UV信息即可知道全部点的位置和UV信息了
toFill.Clear();
for (int x = 0; x < 3; ++x)
{
int x2 = x + 1;
for (int y = 0; y < 3; ++y)
{
if (!m_FillCenter && x == 1 && y == 1)
continue;
int y2 = y + 1;
AddQuad(toFill,
new Vector2(s_VertScratch[x].x, s_VertScratch[y].y),
new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y),
color,
new Vector2(s_UVScratch[x].x, s_UVScratch[y].y),
new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y));
}
}
最后,先清空VertexHelper里的顶点信息,然后把计算出来的顶点信息通过AddQuad方法加入到VertexHelper中
其中,VertexHelper是一个用于保存顶点和三角形的类,相当于顶点和三角形信息的容器,常用方法有AddVert添加顶点,以及AddTriangle添加三角形
static void AddQuad(VertexHelper vertexHelper, Vector2 posMin, Vector2 posMax, Color32 color, Vector2 uvMin, Vector2 uvMax)
{
int startIndex = vertexHelper.currentVertCount;
vertexHelper.AddVert(new Vector3(posMin.x, posMin.y, 0), color, new Vector2(uvMin.x, uvMin.y));
vertexHelper.AddVert(new Vector3(posMin.x, posMax.y, 0), color, new Vector2(uvMin.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMax.y, 0), color, new Vector2(uvMax.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMin.y, 0), color, new Vector2(uvMax.x, uvMin.y));
vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}
AddQuad方法是为VetexHelper增加一个矩形,实际上是增加4个顶点,以及两个三角形面。如果不需要填充中心(FillCenter)的话,就不为中心的矩形添加顶点和三角形面就可以了
至此,一张九宫图就实现出来了。
4、思考
从第三部分源代码阅读可以看出,实现九宫图一共需要执行9次AddQuad方法,即一共会产生4x9=36个顶点,以及2x9=18个三角形面,就是说,每次使用九宫图都会比使用Simple模式的图增加32个顶点。(Simple模式的图只需要执行以此AddQuad方法,需要4个顶点和2个三角形)
但是,根据我画的示意图,实际上最少只需要16个顶点就能实现相同的九宫图,而UGUI的九宫图实际上有不少顶点是重复的,那么重复的顶点是否可以复用呢?
5、自定义实现九宫图
实现自定义的九宫图,在这里只写重点部分:
static void AddVertexAndLine(VertexHelper vertexHelper, Vector2[] s_VertScratch, Vector2[] s_UVScratch, bool m_FillCenter)
{
vertexHelper.AddVert(new Vector3(s_VertScratch[0].x, s_VertScratch[3].y), color, new Vector2(s_UVScratch[0].x, s_UVScratch[3].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[1].x, s_VertScratch[3].y), color, new Vector2(s_UVScratch[1].x, s_UVScratch[3].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[2].x, s_VertScratch[3].y), color, new Vector2(s_UVScratch[2].x, s_UVScratch[3].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[3].x, s_VertScratch[3].y), color, new Vector2(s_UVScratch[3].x, s_UVScratch[3].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[0].x, s_VertScratch[2].y), color, new Vector2(s_UVScratch[0].x, s_UVScratch[2].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[1].x, s_VertScratch[2].y), color, new Vector2(s_UVScratch[1].x, s_UVScratch[2].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[2].x, s_VertScratch[2].y), color, new Vector2(s_UVScratch[2].x, s_UVScratch[2].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[3].x, s_VertScratch[2].y), color, new Vector2(s_UVScratch[3].x, s_UVScratch[2].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[0].x, s_VertScratch[1].y), color, new Vector2(s_UVScratch[0].x, s_UVScratch[1].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[1].x, s_VertScratch[1].y), color, new Vector2(s_UVScratch[1].x, s_UVScratch[1].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[2].x, s_VertScratch[1].y), color, new Vector2(s_UVScratch[2].x, s_UVScratch[1].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[3].x, s_VertScratch[1].y), color, new Vector2(s_UVScratch[3].x, s_UVScratch[1].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[0].x, s_VertScratch[0].y), color, new Vector2(s_UVScratch[0].x, s_UVScratch[0].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[1].x, s_VertScratch[0].y), color, new Vector2(s_UVScratch[1].x, s_UVScratch[0].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[2].x, s_VertScratch[0].y), color, new Vector2(s_UVScratch[2].x, s_UVScratch[0].y));
vertexHelper.AddVert(new Vector3(s_VertScratch[3].x, s_VertScratch[0].y), color, new Vector2(s_UVScratch[3].x, s_UVScratch[0].y));
vertexHelper.AddTriangle(4, 0, 1);
vertexHelper.AddTriangle(1, 5, 4);
vertexHelper.AddTriangle(5, 1, 2);
vertexHelper.AddTriangle(2, 6, 5);
vertexHelper.AddTriangle(6, 2, 3);
vertexHelper.AddTriangle(3, 7, 6);
vertexHelper.AddTriangle(8, 4, 5);
vertexHelper.AddTriangle(5, 9, 8);
if (m_FillCenter)
{
vertexHelper.AddTriangle(9, 5, 6);
vertexHelper.AddTriangle(6, 10, 9);
}
vertexHelper.AddTriangle(10, 6, 7);
vertexHelper.AddTriangle(7, 11, 10);
vertexHelper.AddTriangle(12, 8, 9);
vertexHelper.AddTriangle(9, 13, 12);
vertexHelper.AddTriangle(13, 9, 10);
vertexHelper.AddTriangle(10, 14, 13);
vertexHelper.AddTriangle(14, 10, 11);
vertexHelper.AddTriangle(11, 15, 14);
}
因为顶点信息和UV信息都计算好了,所以信息可以直接使用,修改的地方就是循环使用AddQuad方法的地方,改为使用上述方法。
可以看出,自定义实现的九宫图中,我没有循环使用AddQuad方法,而是改为使用AddVert方法和AddTriangle方法手动添加16个顶点和18个三角形面。
需要注意的是:这里自定义的SlicedImage是没有继承Graphic类的,而是继承MonoBehavior类,因此一些变量需要获取(如canvas,rectTransform等),在这里使用简单的挂载获取
6、与NGUI对比
void SlicedFill (List<Vector3> verts, List<Vector2> uvs, List<Color> cols)
{
Vector4 br = border * pixelSize;
if (br.x == 0f && br.y == 0f && br.z == 0f && br.w == 0f)
{
SimpleFill(verts, uvs, cols);
return;
}
Color gc = drawingColor;
Vector4 v = drawingDimensions;
mTempPos[0].x = v.x;
mTempPos[0].y = v.y;
mTempPos[3].x = v.z;
mTempPos[3].y = v.w;
if (mFlip == Flip.Horizontally || mFlip == Flip.Both)
{
mTempPos[1].x = mTempPos[0].x + br.z;
mTempPos[2].x = mTempPos[3].x - br.x;
mTempUVs[3].x = mOuterUV.xMin;
mTempUVs[2].x = mInnerUV.xMin;
mTempUVs[1].x = mInnerUV.xMax;
mTempUVs[0].x = mOuterUV.xMax;
}
else
{
mTempPos[1].x = mTempPos[0].x + br.x;
mTempPos[2].x = mTempPos[3].x - br.z;
mTempUVs[0].x = mOuterUV.xMin;
mTempUVs[1].x = mInnerUV.xMin;
mTempUVs[2].x = mInnerUV.xMax;
mTempUVs[3].x = mOuterUV.xMax;
}
if (mFlip == Flip.Vertically || mFlip == Flip.Both)
{
mTempPos[1].y = mTempPos[0].y + br.w;
mTempPos[2].y = mTempPos[3].y - br.y;
mTempUVs[3].y = mOuterUV.yMin;
mTempUVs[2].y = mInnerUV.yMin;
mTempUVs[1].y = mInnerUV.yMax;
mTempUVs[0].y = mOuterUV.yMax;
}
else
{
mTempPos[1].y = mTempPos[0].y + br.y;
mTempPos[2].y = mTempPos[3].y - br.w;
mTempUVs[0].y = mOuterUV.yMin;
mTempUVs[1].y = mInnerUV.yMin;
mTempUVs[2].y = mInnerUV.yMax;
mTempUVs[3].y = mOuterUV.yMax;
}
for (int x = 0; x < 3; ++x)
{
int x2 = x + 1;
for (int y = 0; y < 3; ++y)
{
if (centerType == AdvancedType.Invisible && x == 1 && y == 1) continue;
int y2 = y + 1;
verts.Add(new Vector3(mTempPos[x].x, mTempPos[y].y));
verts.Add(new Vector3(mTempPos[x].x, mTempPos[y2].y));
verts.Add(new Vector3(mTempPos[x2].x, mTempPos[y2].y));
verts.Add(new Vector3(mTempPos[x2].x, mTempPos[y].y));
uvs.Add(new Vector2(mTempUVs[x].x, mTempUVs[y].y));
uvs.Add(new Vector2(mTempUVs[x].x, mTempUVs[y2].y));
uvs.Add(new Vector2(mTempUVs[x2].x, mTempUVs[y2].y));
uvs.Add(new Vector2(mTempUVs[x2].x, mTempUVs[y].y));
if (!mApplyGradient)
{
cols.Add(gc);
cols.Add(gc);
cols.Add(gc);
cols.Add(gc);
}
else
{
AddVertexColours(cols, ref gc, x, y);
AddVertexColours(cols, ref gc, x, y2);
AddVertexColours(cols, ref gc, x2, y2);
AddVertexColours(cols, ref gc, x2, y);
}
}
}
}
实际上,NGUI在九宫图的实现逻辑上和UGUI无异,同样是使用循环增加顶点和三角形面,增加的顶点数和三角形面也和UGUI的一致
7、最终效果
可以看到,自定义实现的九宫图比UGUI的九宫图少20个顶点
两张九宫图的顶点结构也是一样的