一种UGUI的Outline描边优化方案
性能消耗原因(例如顶点数量的大幅增加)分析
- 描边的文字与美术同学出的效果图有不小的差异。效果图的描边效果连贯而且均匀,而UGUI的Outline组件的效果仅仅只是解决了“温饱问题”,Better Than Not而已,并且这种实现方式带来了其他问题,比如顶点数量的大幅增加。
原因分析
- 通过研究Outline的源码可以看到,Outline组件的原理是在原顶点的基础上,在effectDistance.(x,y), (x, -y), (-x,y), (-x,-y)这4个偏移上ApplyShadowZeroAlloc实现的。亦即Outline组件是在Shadow的基础上实现的,Outline相当于4个不同偏移方向上的Shadow。
- 进一步查看Shadow的源码,发现Shadow的原理实际上是将原始的顶点数据复制一份, 根据设置的偏移量计算复制后的新顶点的位置,并为新的顶点颜色赋值为设置的颜色值。
- 我们可以简单地将Outline的参数EffectDistance调整到一个较大的值即可观察到4次复制。实际上Outline在处理1像素以上的描边时就会出现较大瑕疵,超过1.5像素就已经不可用了。
业界的一般做法
基于Shadow
- Outline的实现是在原始顶点的4个方向ApplyShadow,为了描边效果更好,可以在8个方向甚至16个方向ApplyShadow来实现饱满的描边效果。实现原理与Outline类似。
TextMeshPro
- 注意TextMeshPro使用时需要制作字体文件,即FontAsset。对于英文及数字来说,只需要针对ASCII字符集制作FontAsset即可,但对于中文需要动态生成的文字来说,需要生成的字体文件相对较大,对于手游项目来说,对包量及内存的影响在目前来看都是不能接受的。
- 但TextMeshPro用在可确定的中文字符及ASCII字符上的效果还是不错的,比如用在登录、主界面这类文字不需要动态生成的界面上。
基于Mesh
- 实现思路如下:
- 提取文字原始UV区域,扩大文字绘图区域
- 对文字纹理的每个像素点周围多次采样,应用描边RGBA作为新的点的颜色
- 将原始纹理及采样后的点进行融合
第一次优化
- 采用基于Shadow的实现方式,多次ApplyShadow以使描边效果饱和。
- Outline的实现方式是在(x,-y),(x,y),(-x,y),(-x,-y)4个方向ApplyShadow。模仿Outline的实现有了新的BoldOutline,在原有的4个方向之外增加(x,0),(-x,0),(0,y),(0,-y)4个方向共计8个方向的描边。描边效果较Outline有了一定提升。
- 但,顶点总数爆炸了
- 从理论上计算,Outline对于1个字符是绘制了5次,BoldOutline是绘制了9次,顶点总数及三角形总数相对于无效果的字符分别应该是5倍和9倍的关系。
- 原因分析:
- 在UGUI中Text是由TextGenerator产生顶点数据,通过顶点数据与字体贴图渲染到屏幕上的。
- Text组件继承自MaskableGraphic,MaskableGraphic又继承自Graphic,Text组件实现了protected virtual void OnPopulateMesh(VertexHelper vh)方法为所需的数据赋值。
- 实现这些文字效果的基础是Unity为外部提供的接口IMeshModifier,如果Text组件所在的GameObject中存在实现了IMeshModifier接口的组件,就会调用对应组件的ModifyMesh方法。这样就可以在外部修改Text的顶点数据了。
- 了解了各种文字效果实现的原理,继续来看关键函数ModifyMesh(VertexHelper vh)的具体实现。Text组件生成的顶点数据通过参数类型VertexHelper传入后,需要通过
public void GetUIVertexStream(List<UIVertex> stream)
这个方法获取具体数据。Verts/Tris比值的变化就发生在这个帮助函数中。这个函数将原本共享的三角形顶点做了拆分,按照1个三角形对应3个顶点的数据输出了。 - 引用一位国外Unity开发者的话讲:
GetUIVertexStream takes a nice optimized mesh with shared verts and completely ruins it. Dont call it. Ever.
第二次优化
简单的基于Shadow的实现方式会引起顶点数量的暴涨,第二次优化将方向调整为了基于Mesh。实现思路如下:
在字符原UV边界uvOrigin的基础上,通过描边大小fSize计算描边后的UV边界uvAdd。
将原UV边界uvOrigin,扩展后的UV边界uvAdd,描边大小fSize以及描边颜色color传入Shader。将顶点信息传入Shader时需要注意,UIVertex结构体的成员如下:
- 没有被默认Shader使用的参数是uv1,normal及tangent。这些需要传给Shader的值就塞进这些参数里就可以了,在Shader中只要逆向解出一一对应即可。
对字符贴图像素处理时,从当前像素向四周做8次采样以确定当前像素点的值。采样时需要判断当前点是否是在uvOrigin的范围内,否则的话可能会出现采样到隔壁字符的情况。
- 判断当前点是否在uvOrigin的方法如下:在范围内返回1,不在范围内返回0
- 沿当前像素采样的8个方向偏移,数组已经被解开,避免需要在Shader中用循环来实现。
- 将所有采样点采样的像素值求和,然后除以采样的数量。
在接下来的pass中将字符的原始贴图信息与刚刚计算得到的数据进行融合。
- 需要注意的是,不在uvOrigin中的像素点的color值直接置0.避免uvOrigin之外,uvAdd之内的脏点混进来。
- 于是我们就得到了这样的结果:
使用MeshOutline后Batches数量为2,而Outline因为是复制的原顶点信息,所以无论复制多少份Batches数量都为1(如果不理解的话可参考UGUI的优化方案(1)和UGUI的优化方案(2)).总的来说,这种优化方式带来的是采样次数的增加及顶点数量的相对减少。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SleepyLoser's Blog!
评论