画地为Mask,随心所欲的高效遮罩组件[Unity]
在上一篇博文"扔掉遮罩,更好的圆形Image组件"中,笔者改变Image的顶点数据,使得Image呈圆形显示,避免了Mask的使用,从而节省Drawcall消耗,提高渲染效率了。这也启发了笔者,有没有可能通过同样原理实现Mask,做到在某些需要显示特定形状Icon的场景下,替代Unity原生Mask,且能保有节省Drawcall,减少渲染像素点,实现精确点击等优点?经过一番折腾,就有了MeshMask组件。
组件效果#
MeshMask遮罩效果图
可以看到无论Mask形状是凸边形还是复杂的凹边形,都能准确地将Mask形状数据序列化成顶点,面片数据,
提供给需要Mask的图片修改渲染顶点,达到遮罩效果。组件用法类似于Unity Mask,且效率优于Unity Mask。插件已上传至Github[点击下载], 欢迎试用~
效率对比#
使用原生Mask,10个Icon占用了15个Drawcall
使用MeshMask,10个Icon仅占用1个Drawcall
Scene切换到Overdraw模式:红框为Mask的Overdraw;蓝框为MeshMask的Overdraw
从上面三张图可以看到MeshMask相比Unity的Mask,在减少Drawcall消耗、Overdraw消耗等两方面都是完胜的。
Drawcall消耗###
这10个icon都打包在同一图集的,使用Unity Mask,没办法享受图层合并,消耗了15个Drawcall;使用MeshMask的情况下,看截图里Batches为2,除去摄像机占用的1个Batch,10个icon仅占用1个Batch,即1个Drawcall。在Drawcall资源如此昂贵的情况下(一般机器都会要求Drawcall在200以下),这种性能节省效果非常显著。
Overdraw消耗###
而看图三的Overdraw,使用Unity Mask的红框部分,被Mask的图片全部绘制一次,Unity Mask再做像素剔除,被Mask的部分又绘制了一次,总共需要绘制两次,且有一次是绘制了完全用不到的区域。使用MeshMask的蓝框部分,因为是靠改变顶点绘制出来的icon,因此仅有被Mask部分被绘制了一次。
面片消耗###
当然,使用MeshMask的Image需要消耗比普通Image多一些的顶点和面片,观察Stats面板,使用MeshMsk的10个icon多占用1.3K的顶点和面片,即1个icon占用130个顶点,面片。然而GPU渲染顶点,面片的效率非常高(市面手机GPU渲染多边形数基本上2000-10000+万多边形/每秒以上),这点消耗跟Drawcall比起来就微不足道了。
小结###
在渲染上,GPU、CPU两者的性能瓶颈往往是CPU;GPU的性能瓶颈往往是像素点填充率(Overdraw导致),CPU的性能瓶颈往往是Drawcall。所以,渲染性能排查,几项指标关注优先级应该是:Drawcall > Overdraw > 面片
组件使用#
MeshMask插件目录结构
插件里有MeshMask、MeshImage、MeshButton三个UI组件
MeshMask组件Inspector面板
MeshMask组件作用类似Unity Mask,依赖了Image及PolygonCollider2D组件,带有[根据Image组件生成Mask]、[根据Collider组件生成Mask]两个菜单项,支持两种方式生成Mask数据。
被遮罩GameOjecct的Inspector面板
MeshImage、MeshButton组件挂在需要被遮罩的GameObject上,设置好MeshMask对象,就能获得数据,实现遮罩或者精确点击。
组件实现#
不同于CircleImage,只需要简单的对圆形进行顶点,面片计算;MeshMask要考虑几个点:
- 需要能对所有可能的图形进行顶点,面片计算。
- 考虑顶点,面片计算需要读取Image,且有一定性能开销,所以不能在Run-time中实时计算数据,需要预先计算好vertices,triangle数据,并序列化存放在GameObject中,运行时读取。
- 保证MeshMask灵活性,除了根据Image进行顶点,面片计算,希望像PS一样,提供路径工具,让开发可以可视化地新增、修改Mask形状。
- 对所有图形支持像素级点击判断
其中做顶点,面片计算这一步比较麻烦,涉及以下几个技术点:
图片处理流程
边缘检测##
边缘检测算法算是图形学应用最广泛最基础的算法了,主要原理是滤波器对图形进行滤波从而得到梯度图像,通过判断梯度图像的某像素点灰度值是否超过阈值,就能判断该点是否为边缘点。笔者采用了简单的Sobel算子边缘检测算法。
Sobel算子:3x3的矩形滤波器
A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像灰度值
图像某像素点灰度值
通常,为了提高效率 使用不开平方的近似值
这里拿米老鼠图来做示例图,看看Sobel边缘检测的效果。
原图
sobel边缘检测后的灰度图
可以看到算法效果不错,但我们并不需要这么多边缘“信息”,只需要最外围的边缘“信息”。因此将非透明区域都填充成统一的颜色,再做边缘检测。
最终效果:理想的外围边缘
离散化##
获得了外围边缘信息后,下一步需要做离散化:剔除冗余信息,并将边缘信息以有序集合的形式表示。这个有序集合,就是渲染底层所需要的顶点数据。
冗余顶点:对于边缘的直线,除直线首尾两点外,其他点都是冗余可剔除的。
有序集合:集合点依次连接起来,就如同用笔按逆时针/顺时针方向画出来的边缘图形。
笔者挑选了边缘点集中x最小的点作为起始点,以顺时针顺序查找邻接点的方法来计算有序顶点集。
算法步骤:
- 选择边缘点集x最小的点为起始点,当前点
- 查找当前点周边8个像素点是否有边缘点,如都没有就继续向外围一圈,直到找到边缘点。
- 当找到多个边缘点情况下,比较当前点与各边缘点所呈夹角,选夹角最小的边缘点作为邻接点。
- 若邻接点即为起始点,则算法结束,否则继续
- 判断邻接点与有序顶点集最后一个点是否共边,若共边则删除最后一个点
- 将邻接点加入有序顶点集
- 设置邻接点为当前点,重复步骤2
删除共边顶点图示:当C即将加入顶点集中,发现ABC三点共边的情况,删除中间点B
三角化##
三角化(Triangulation)也是图形学应用较多的算法了,特别是在3D建模、游戏领域。三角化是指从一组已知点集中,构建出三角形网格。随着构建条件不同,三角化算法也不同。像最近LowPoly绘画风格比较热门,一些滤镜软件会支持LowPoly转换。软件在将一张普通图像转换位LowPoly图像的过程中,除了一样要做边缘检测,离散化外,在三角化这一步,需要生成显示质量较高的三角形,不能有过于狭长的三角形,就需要用Delaunay算法。在我们这个场景下,对生成的三角形并没有特殊要求,不需要用上复杂的Delaunay算法,Unity3d wiki社区上提供了一个简单的三角化算法,刚好适用。
算法原理
从点集中随机挑选三点组成三角形,然后遍历其他点,看是否有点落在三角形内,如果三角形内无点则为合格三角形。循环此过程直到所有点都被处理。
可视化编辑##
经过前面处理,我们已经拿到了顶点数据、面片数据。笔者希望组件能将这些顶点数据可视化,以便让使用者直观了解处理结果。Unity自带的PolygonCollider2D组件,正好适用。
public sealed class PolygonCollider2D : Collider2D
{
....
public void SetPath(int index, Vector2[] points);
}
通过SetPath接口将顶点数据传入PolygonCollider2D 组件,PolygonCollider2D完美地生成米老鼠的路径。在一开始实验中,笔者惊奇地发现组件竟然也对顶点做了三角化处理。遗憾地是,组件并没有提供接口获取三角化结果,Unity社区的技术人员也承认此点,说Unity的未来版本可能会考虑暴露此接口,并建议自己做三角化处理,就是前面所说的算法(汗.. = . = ||)。通过下图比较,可以看到组件跟算法的三角化结果还是有所不同的。
顶点数据传入PolygonCollider2D后的效果
算法处理后的三角化效果
利用PolygonCollider2D组件除了让我们可以看到顶点结果,还可以通过Inspector上的[Edit Collider]按钮微调,顶点的位置,做出更理想的Mask效果。
甚至,我们可以直接利用PolygonCollider2D组件,从无到有地编辑Mask形状后,再三角化处理获得面片数据。
直接用PolygonCollider2D编辑出来的“爱心”
渲染##
已经有了顶点数据,面片数据,终于到了最后的渲染步骤。笔者利用MeshMask组件存放这些数据,并不直接渲染MeshMask,而是在MeshMask子节点下添加MeshImage组件,进行修改顶点渲染。
在5.3版本里,Unity提供了BaseMeshEffect类,是Unity提供给开发者用于给Graphic进行二次修改绘制的类,我们可以在ModifyMesh方法中修改VertexHelper携带的顶点,面片,uv等数据来改变渲染。(在5.3之前的版本,对应的类和接口是BaseVertexEffect、ModifyVertices)
MeshImage继承BaseMeshEffect,在ModifyMesh里先将VertexHelper的原有数据清空,获取MeshMask的顶点、面片数据,经过坐标转换后将再传给VertexHelper。
public abstract class BaseMeshEffect : UIBehaviour, IMeshModifier
{
public abstract void ModifyMesh(VertexHelper vh);
}
public class MeshImage : BaseMeshEffect{
...
public override void ModifyMesh(VertexHelper vh)
{
if (this.enabled)
{
vh.Clear();
_uiVertices.Clear();
if (mask)
{
if (mask.vertices != null && mask.triangles != null)
{
float tw = image.rectTransform.rect.width;
float th = image.rectTransform.rect.height;
Vector4 uv = image.overrideSprite != null ? DataUtility.GetOuterUV(image.overrideSprite) : Vector4.zero;
float uvCenterX = (uv.x + uv.z) * image.rectTransform.pivot.x;
float uvCenterY = (uv.y + uv.w) * image.rectTransform.pivot.y;
float uvScaleX = (uv.z - uv.x) / tw;
float uvScaleY = (uv.w - uv.y) / th;
List<Vector3> vertices = this.mask.vertices.Select(
x => { return this.transform.InverseTransformPoint(this.mask.transform.TransformPoint(x)); }).ToList();
for (int i = 0; i < mask.vertices.Count; i++)
{
UIVertex v = new UIVertex();
v.color = image.color;
v.position = vertices[i];
v.uv0 = new Vector2(v.position.x * uvScaleX + uvCenterX, v.position.y * uvScaleY + uvCenterY);
_uiVertices.Add(v);
}
vh.AddUIVertexStream(_uiVertices, mask.triangles);
}
}
}
}
}
拖动MeshImage的位置,图片外显区域始终限定在米老鼠Mask内
像素级精确点击##
如上篇博文所讲,为了实现精确点击,Unity提供了eventAlphaThreshold字段,但有着Sprite占用双倍内存,无法合入图集等缺陷。而MeshButton组件正好解决了痛点。MeshButton实现ICanvasRaycastFilter接口类,实现IsRaycastLocationValid方法,在方法内获取MeshMask的顶点数据,通过Ray-Crossing算法就可以判断点击点是否在区域内。
public class MeshButton : UIBehaviour, ICanvasRaycastFilter
{
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera){
//Stopwatch sw = new Stopwatch();
//sw.Start();
Sprite sprite = image.overrideSprite;
if (sprite == null)
return true;
bool ret = true;
if (this.mask != null && this.mask.vertices != null)
{
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(image.rectTransform, screenPoint, eventCamera, out local);
List<Vector2> vertices = this.mask.vertices.Select(
x =>
{
Vector3 p = this.transform.InverseTransformPoint(this.mask.transform.TransformPoint(x));
return new Vector2(p.x, p.y);
}).ToList();
ret = ImageUtil.Contains(local, vertices);
}
//sw.Stop();
//UnityEngine.Debug.Log("点击检测耗时:" + sw.ElapsedTicks + " tick");
return ret;
}
}
关于MeshMask#
- MeshMask组件适合用来显示特殊形状的Icon。MeshMask并不能完全取代Unity Mask,在需要显示特殊形状Icon时作为Unity Mask的替代方案,能达到提高渲染效率的目的,减少Unity Mask的不必要使用。
- 被Mask的图片如果被移出Mask范围外,会因为Sprite Wrap mode而出现边缘像素拉伸,或者贴图重复的问题,这个问题暂时不能很好解决,因为Sprite Wrap mode必须设置为clamp或者repeat,就会出现这种问题。只能设置为clamp后,人为为贴图边缘留1px的透明边解决。好在,做特殊形状Icon的使用场景下,基本无须担心这个问题。
画地为Mask,随心所欲的高效遮罩组件[Unity]的更多相关文章
- 丢掉Mask遮罩,更好的圆形Image组件[Unity]
写在前面 全文解析圆形Image组件的实现原理,取关键代码介绍算法细节,源码已经上传Github下载地址,欢迎下载试用. 一.Unity原生Image组件实现圆形图片的缺陷 Mask渲染消耗 许多游戏 ...
- cc.Mask. 纯代码拉伸遮罩
var imgBoxInner = new cc.Node('imgBoxInner');var mask = imgBoxInner.addComponent(cc.Mask);mask.alpha ...
- Jquery之ShowLoading遮罩组件
From:http://www.cnblogs.com/eczhou/archive/2012/12/18/2822788.html 一.遮罩用途及效果 ShowLoading这个jQuery插件设计 ...
- (转)Jquery之ShowLoading遮罩组件
本文转载自:http://www.cnblogs.com/eczhou/archive/2012/12/18/2822788.html 一.遮罩用途及效果 ShowLoading这个jQuery插件设 ...
- 微软 SqlHelper代码、功能、用法介绍:高效的组件
数据访问组件SqlHelper数据访问组件是一组通用的访问数据库的代码,在所有项目中都可以用,一般不需要修改.本节使用的是Microsoft提供的数据访问助手,其封装很严密,且应用简单. 首先要先添加 ...
- CSS遮罩mask
前面的话 CSS遮罩是2008年4月由苹果公司添加到webkit引擎中的.遮罩提供一种基于像素级别的,可以控制元素透明度的能力,类似于png24位或png32位中的alpha透明通道的效果.本文将详细 ...
- 使用 mask 实现视频弹幕人物遮罩过滤
经常看一些 LOL 比赛直播的小伙伴,肯定都知道,在一些弹幕网站(Bilibili.虎牙)中,当人物与弹幕出现在一起的时候,弹幕会"巧妙"的躲到人物的下面,看着非常的智能. 简单的 ...
- 【UGUI源码分析】Unity遮罩之Mask详细解读
遮罩,顾名思义是一种可以掩盖其它元素的控件.常用于修改其它元素的外观,或限制元素的形状.比如ScrollView或者圆头像效果都有用到遮罩功能.本系列文章希望通过阅读UGUI源码的方式,来探究遮罩的实 ...
- Egret 矢量绘图、遮罩、碰撞检测
矢量绘图: 1. 为矢量绘图绘制外边 - graphics.lineStype() private createGameScene():void { console.log("Runtime ...
随机推荐
- PHP生成随机水印图片
基于PHP的GD图形库,自己生成一张图片.仅限初识GD库,实例学习. 一.需求 网站的布局用到了类似慕课网课程列表的风格,每一个课程是一个banner图,图下面是标题加简介.因为课程的数量较大没有为所 ...
- Git修改提交注释
修改本地最近一次已提交的注释 git commit --amend 如果已经上传到了github上,因此github的提交和已修改的提交不一样,推送到远程可以用下面命令强制修改 git push or ...
- 在Windows的DOS中运行java编程中的问题
1.苦恼着我的就是找不到或无法加载主类!
- VisualVM监控远程主机上的JAVA应用程序
使用VisualVM监控远程主机上JAVA应用程序时,需要开启远程主机上的远程监控访问,或者在远程JAVA应用程序启动时,开启远程监控选项,两种方法,选择其中一种就可以开启远程监控功能,配置完成后就可 ...
- 《连载 | 物联网框架ServerSuperIO教程》- 17.支持实时数据库,高并发保存测点数据。附:3.4 发布与版本更新说明。
1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...
- sql-update语句多表级联更新
在数据表更新时,可能会出现一种情况,就是更新的内容是来源于其他表的,这个时候,update语句中就加了from,下面为一个范例: update a set a.name=b.name,a.value= ...
- 面向对象的三大特征——封装、继承、多态(&常用关键字)
一.封装 Encapsulation 在面向对象程式设计方法中,封装是指,一种将抽象性函式接口的实作细节部份包装.隐藏起来的方法. 封装的概念(针对服务器开发,保护内部,确保服务器不出现问题) 将类的 ...
- Xamarin android 的WebClient Json下载并存储本地及sqlite数据库
这一点雕虫小技可能对熟悉的人来说已经不值一提.但是我想,既然这些都是常用的功能,集成在一起做个笔记也有点意义吧. 首先,json 是传递数据的事实标准了.所以先说一下将它从服务器端下载下来..net ...
- Android之利用正则表达式校验邮箱、手机号、密码、身份证号码等
概述 现在Android应用在注册的时候基本会校验邮箱.手机号.密码.身份证号码其中一项或多项,特此收集了相关的正则表达式给大家分享.除了正则表达式,文章末尾提供Demo中有惊喜哦! 具体验证的图片效 ...
- 非负矩阵分解(1):准则函数及KL散度
作者:桂. 时间:2017-04-06 12:29:26 链接:http://www.cnblogs.com/xingshansi/p/6672908.html 声明:欢迎被转载,不过记得注明出处哦 ...