前言
说起描边,有很多种实现手段,尤其是3D的描边,网上一搜一大把,相对比较贫乏的是2D的描边。描边又分为内描边和外描边,内描边就是在自己原图上,靠近边缘的像素,描上想要的颜色,而外描边,就是在图的边缘再扩上一些像素,描上颜色,看似差别不大,但实现方法却是天差地别,本篇主要讲内描边。
原理
如上所说,内描边就是在图上,靠近边缘的像素,描上想要的颜色。所以原图大小并未改变,只是利用了边缘的像素,所以实现也相对简单,就是在自身的材质上做文章,在shader里判断图里每一个像素,是否是靠近边缘(当然有多靠近是我们自定义的),如果是,就给描边色,不是,就还是原图色。
第一步:找边
那么在shader里如何判断一个像素是边缘像素呢,我们看下图
这是一棵树,我们可以通过图很容易看出,除了树的主体部分,其余的像素都是镂空的,也就是镂空的部分Alpha是0,树的部分Alpha大于0,那我们就想到一个方法,是否能从Alpha着手去找边呢。那树的边缘Alpha有什么特征呢,不也是大于0吗,可是它周边总有一些像素,Alpha是0,所以我们可以在shader里,针对每个像素的周边像素,去判断是否有Alpha是0,如果有,那它就是边。上代码
这短短几句就是核心算法,baseUv就是当前像素的uv,offsetUv就是该uv的偏移值,minAlpha就是alpha的最小值
简单说一下这个代码,第一步,根据偏移uv算出它这个邻居的uv,其中_TexelSize是纹素,untiy可以直接拿到,cocos就得自己传了,仔细看上面那棵树的截图,尺寸是256 * 283,那么纹素就是(1 / 256, 1 / 283),_OutlineSize是我们自定义的边缘尺寸,就是上面说的你想有多靠近边缘的那些像素的尺寸。然后再得到这个uv的像素的Alpha值,和传进来的minAlpha比较,谁小就返回谁,这样如果遇到Alpha是0的像素,我们的返回值就是0了。这里有一行代码要注意,
float isEdge = step(uv.x, 0.0) + step(1.0, uv.x) + step(uv.y, 0.0) + step(1.0, uv.y);
这是在判断是否是已经超了图的边界了,我们都知道图的uv的xy就是从0到1的(repeat的不考虑),如果判断到这个边界了,那我就强制给它返回0。
第二步:遍历周边8个像素得到最小Alpha
就是利用第一步的找边算法,逐一遍历该像素周边的8个像素,得到最小Alpha,如果是0,那就是边了,给我们定义的边缘色,大于0,那就给原图色,是不是很简单
优缺点
优点:
内描边的优点很明显,就是很轻,我们不需要复杂的代码,很简单的shader就能实现,可以自定义每个图的边缘尺寸和边缘颜色,自由性很强;
缺点:
但自由性强是把双刃剑,也就有了缺点,因为不同的图,尺寸大小不一样,所以我们上面给的纹素值也就不一样,所以不同物体,我们要用不同材质,而不同材质也就导致无法合批,描边物体少无所谓,如果有大量的不同物体要描边,那drawcall要考虑。
还有个问题,比较隐蔽,就是它对有很多零碎小窟窿的图不友好,在小窟窿边缘的像素,有可能判断边缘的时候直接跨过了窟窿的尺寸,判断成没有边的像素了,那就会绘制错误。
还有个问题,就是在图的配置上,一定不能勾Packable
勾了这个,它会自动把碎图合图集,这样shader在判断边缘的时候,可能两张图在图集里挨得太近,它判断不出是边缘了,原理和上面的小窟窿一样。
结论
所以,如果你的描边数量比较小,或是都是相同物体,没那么零碎镂空,单图或是图集中图和图之间距离足够大,那内描边是最好的选择。反之,就可以考虑我后面要介绍的外描边了。
下一篇:2D描边-外描边