UGUI源代码之Image-Sliced模式

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数组中,其顺序如图所示:
UGUI源代码之Image-Sliced模式
因此,知道这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源代码之Image-Sliced模式
UGUI源代码之Image-Sliced模式
可以看到,自定义实现的九宫图比UGUI的九宫图少20个顶点

UGUI源代码之Image-Sliced模式
两张九宫图的顶点结构也是一样的

上一篇:UGUI学习笔记之Rect Transform组件


下一篇:UGUI 源码之 CanvasUpdateRegistry、ICanvasElement