UGUI 中实现多边形网格 Image

这是之前写的一篇关于 UGUI 优化的文章,换了博客平台之后,重新再发一遍,之前的文章也翻译过官方关于 Ugui 优化的一系列文章,总结其内容,可以将 ugui 的优化归结为以下三类:

  • 资源的优化
  • Drawcall 的优化
  • Overdraw 的优化
  • Rebuild 的优化

资源的优化主要是图片的导入设置,以及图片的压缩格式选择,导入设置中重点注意两个选项:Read/Write Enable 和 Generate MipMaps,当开启 Read/Write 时,将占用两倍内存资源,关闭时将不能使用 Texture.GetPixel() 等函数来读取像素信息,如不需要请将该选项关闭,开启 Generate MipMaps 是增加 1.33被内存占用,对于 UI 资源来说,不需要开启此选项。

对于图片压缩格式,可以采用平台硬件支持的类型,Android 平台的 ETC 和 ETC2,需要注意的是前者不支持透明通道,需要分离 Alpha 通道;ETC2 是 OpenGL ES 3.0的一部分,则在某些低端机型上不支持,此时将会采用 RGBA32 格式(新版本可以通过 ETC2 Fallback 设置),另外 Android 平台默认采用 RGB(A) Compressed ETC2 格式。IOS 平台可选的有 ASTC 和 PVRTC,ASTC 的效果比较差,但是性能最好,如果美术可以接受的话,可以考虑使用,平台默认采用 RGB(A) Compressed PVRTC 2/4 Bits 格式。

Drawcall 的优化其实主要是图集打包策略的选择,以及合批处理,关于哪些情况会中断合批,前面的文章也详细地做了介绍,同时,Unity 的 Profiler 当前提供了 UI 模块,能够很清楚的知道哪个对象引起合批中断,中断原因,帮助我们很好的进行界面调优。

Rebuild 通常可能是 UGUI 优化的重灾区,每一次 Rebuild 都需要重新的生成合批的网格,造成 Rebuild 的原因有很多,之前的文章也介绍过,Rebuild 优化宗旨是,尽可能减少 Rebuild 次数,尽可能减少 Rebuild 影响范围,主要实施方案是:

  • 采用动静分离,对于动态 ui 元素,使用单独的 Canvas 进行渲染,
  • 最好采用 TMP 文字来替代 UGUI 的文字渲染,

造成 Overdraw 的原因前文也介绍过,因为 UGUI 生成的网格都是矩形,而 UI 的渲染是在半透明队列,所以多层 ui 的叠加是造成 overdraw 的主要原因,针对 Overdraw 的优化,主要总结三点:

  • 避免全透明 Image,有些时候为了接收 UI 某个事件而使用全透明 Image,虽然其不可见,但是仍然会进行一遍渲染;
  • 对于 Sliced 类型,如果只需要渲染边框,避免勾选 FillCenter 选项(将不会生成中间部分网格);
  • 对于不规则图片,可以采用本文介绍的多边形组件;

下面详细介绍多边形组件的实现方式,以下是之前原文:

在使用UGUI Image组件显示不规则图片、镂空图片时,Image总是会创建一个四边形网格来显示图形,渲染过程中,GPU需要对完全透明的区域进行计算,这不利于性能的优化,一个解决办法是采用多边形网格显示图形,来减少这种不必要的消耗。

下面是Image组件和多边形显示组件的网格对比


下面是Image和多边形的Overdraw对比

整个方案的实现过程包括以下几个步骤:

  1. 生成图集,这里推荐使用 Texture Packer,这里要求导出 tpsheet 格式。
  2. 导入图集、生成多边形,这里需要从AssetStore下载TexturePackerImporter(已经包含的项目中)。到导入插件之后,将 tpsheet 文件和图集一起导入项目中,导入之后TexturePackerImporter会自动将图集转换成带多边形的Sprite。
  3. 使用UIPolyImage组件替换 Image 组件。(该组件目前只支持 Simple 模式)

如果需要根据图形做射线检测,在 UIPolyImage/Image 组件上添加PolyRaycastFilter 组件。因为该组件需要读取贴图的像素,所以需要将贴图的 readAndWrite 属性勾选。

示例项目:这里

多边形Image组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace UnityEngine.UI
{
public class UIPolyImage : Image
{
protected override void OnPopulateMesh(VertexHelper vh)
{
if (overrideSprite == null)
{
base.OnPopulateMesh(vh);
return;
}

switch (type)
{
case Type.Simple:
case Type.Filled:
case Type.Sliced:
case Type.Tiled:
GenerateSimpleSprite(vh, preserveAspect);
break;
}
}

void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
Vector4 v = GetDrawingDimensions(lPreserveAspect);
var color32 = color;

float width = v.z - v.x;
float height = v.w - v.y;

//将sprite.pivot进行归一化
Vector2 spritePivot = new Vector2(sprite.pivot.x / sprite.rect.width, sprite.pivot.y / sprite.rect.height);
Vector2 pivotOffset = Vector2.Scale(spritePivot - rectTransform.pivot, new Vector2(width, height));

Vector3 scale = new Vector3(1, 1, 1);
scale.x = width / sprite.bounds.size.x;
scale.y = height / sprite.bounds.size.y;
Matrix4x4 trsMT = Matrix4x4.TRS(pivotOffset, Quaternion.identity, scale);

vh.Clear();
for (int i = 0; i < sprite.vertices.Length; i++)
{
vh.AddVert(trsMT.MultiplyPoint3x4(sprite.vertices[i]), color32, sprite.uv[i]);
}

for (int i = 0; i < sprite.triangles.Length; i += 3)
{
vh.AddTriangle(sprite.triangles[i], sprite.triangles[i + 1], sprite.triangles[i + 2]);
}
}

public Rect GetDrawingRect()
{
Vector4 v = GetDrawingDimensions(preserveAspect);
return Rect.MinMaxRect(v.x, v.y, v.z, v.w);
}

/// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
{
var padding = overrideSprite == null ? Vector4.zero : UnityEngine.Sprites.DataUtility.GetPadding(overrideSprite);
var size = overrideSprite == null ? Vector2.zero : new Vector2(overrideSprite.rect.width, overrideSprite.rect.height);

Rect r = GetPixelAdjustedRect();
// 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);

var v = new Vector4(
padding.x / spriteW,
padding.y / spriteH,
(spriteW - padding.z) / spriteW,
(spriteH - padding.w) / spriteH);

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;
}
}

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;
}
}
}

射线检测过滤组件

这里实现方式是通过获取像素的 Alpha 值来进行判断的,所以一个明显的缺点就是需要将贴图的 Read/Write 设置为true,造成图片资源内存占用翻倍。当然还有一些其他的方法也可以实现功能,比如设置多边形碰撞、多边形检测等等,后续如果有机会的话,会补充其他的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace UnityEngine.UI
{
/// <summary>
/// 用于检测不规则图形的射线检测方法,由于需要进行多边形的射线检测,
/// 效率相对较低,所以,确认自己必须进行不规则射线检测时,才使用
/// 该组件。
/// </summary>
[RequireComponent(typeof(UIPolyImage))]
public class PolyRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
{
private UIPolyImage image
{
get
{
if (m_Image == null)
m_Image = GetComponent<UIPolyImage>();
return m_Image;
}
}

private RectTransform rectTransform
{
get
{
if (m_RectTransform == null)
m_RectTransform = GetComponent<RectTransform>();
return m_RectTransform;
}
}

private UIPolyImage m_Image;
private RectTransform m_RectTransform;

public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (image.sprite == null)
return false;

//首先转换到本地坐标系中,方便下面的计算
Vector2 localP = rectTransform.InverseTransformPoint(sp);

Rect pixelRect = image.GetDrawingRect();
if (!pixelRect.Contains(localP, false))
return false;

Vector2 delta = localP - pixelRect.min;
Vector2 normalizedDelta = new Vector2(delta.x / pixelRect.width, delta.y / pixelRect.height);
int x = Mathf.CeilToInt(normalizedDelta.x * image.sprite.rect.width+image.sprite.rect.xMin);
int y = Mathf.CeilToInt(normalizedDelta.y * image.sprite.rect.height+image.sprite.rect.yMin);

Color pixel = image.sprite.texture.GetPixel(x, y);

return !Mathf.Approximately(pixel.a, 0f);
}
}
}

基于多边形检测

判断某点是否在多边形内,通常采用的方法有交点法、回转数法,本文采用的是交点法来计算,具体原理网上有更详细的文章来介绍,这里直接上代码,工程源码里对应PolyMeshRaycastFilter.cs 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(UIPolyImage))]
public class PolyMeshRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
{
private UIPolyImage image
{
get
{
if (m_Image == null)
m_Image = GetComponent<UIPolyImage>();
return m_Image;
}
}

private RectTransform rectTransform
{
get
{
if (m_RectTransform == null)
m_RectTransform = GetComponent<RectTransform>();
return m_RectTransform;
}
}

private UIPolyImage m_Image;
private RectTransform m_RectTransform;

// Detail: http://alienryderflex.com/polygon/
bool IsPointInPoly(List<Vector2> verts, Vector2 point)
{
int i, j = verts.Count - 1;
bool oddNodes = false;
float x = point.x;
float y = point.y;

for (i = 0; i < verts.Count; i++)
{
if ((verts[i].y < y && verts[j].y >= y || verts[j].y < y && verts[i].y >= y) &&
(verts[i].x <= x || verts[j].x <= x))
{
oddNodes ^= (verts[i].x + (y - verts[i].y) / (verts[j].y - verts[i].y) * (verts[j].x - verts[i].x) < x);
}
j = i;
}

return oddNodes;
}

public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (image == null || image.poly == null)
return false;

Vector2 localP = rectTransform.InverseTransformPoint(sp);
if (!image.meshBounds.Contains(localP))
return false;

return IsPointInPoly(image.poly, localP);
}
}

基于 PolygonCollider2D 检测

这种方法是采用 Unity 的 2D 多边形碰撞体 PolygonCollider2D,其提供了相应的检测方法,对应工程的 PolyColliderRaycastFilter.cs 文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// <summary>
/// 基于 PolygonCollider2D 组件的射线检测,系统提供的 Physics2DRaycaster 虽然也能
/// 实现类似功能,但是会需要多个 Raycaster 来实现。
/// </summary>
[RequireComponent(typeof(PolygonCollider2D))]
public class PolyColliderRaycastFilter : MonoBehaviour, ICanvasRaycastFilter
{
PolygonCollider2D polyCollider
{
get
{
if (m_PolyCollider == null)
m_PolyCollider = GetComponent<PolygonCollider2D>();
return m_PolyCollider;
}
}

private RectTransform rectTransform
{
get
{
if (m_RectTransform == null)
m_RectTransform = transform as RectTransform;
return m_RectTransform;
}
}

private PolygonCollider2D m_PolyCollider;
private RectTransform m_RectTransform;

public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (polyCollider == null)
return false;

if (eventCamera != null)
{
//sp不包含 z坐标值,所以需要单独计算z值
float zDistance2Cam = rectTransform.position.z - eventCamera.transform.position.z;
Vector3 sp3 = new Vector3(sp.x, sp.y, zDistance2Cam);
return polyCollider.OverlapPoint(eventCamera.ScreenToWorldPoint(sp3));
}
else
{
//Overlay 模式下,屏幕坐标xy = 世界坐标xy,z = 0
return polyCollider.OverlapPoint(sp);
}
}
}

总结

本文主要介绍了优化 Overdraw 的一种方法,如果在项目中遇上 Overdraw 严重的情况,可以考虑采用这种方法,后续有时间再介绍 ugui 的其他优化内容。