在Unity中绘制等宽的描边
对于描边的思考 描边可以说是一个特别关键的效果,不仅仅是二次元卡通渲染需要用到描边,在用户交互的方面,描边也是一个增强用户交互的关键效果。 一般的描边的做法是绘制一个沿物体空间顶点法线(或是记录在顶点色中的描边方向)外扩的模型背面,这种做法在绝大部分情况都看上去不错,但是描边的深度测试会有一些小瑕疵,同时在物体距离摄像机较近的时候,描边会显得较粗,此外这种描边没有抗锯齿的效果,绘制模型的背面也让造成了性能的浪费。另外一种方法是使用Multiple Render Targets,渲染出一个模型的剪影,然后使用类似高斯模糊的办法,对采样进行偏移,这样可以渲染出一个较好的可以有抗锯齿效果的描边,但是仅限于模型向外的描边,缺少模型内部的描边效果。 最好的描边应该是能够支持模型外描边、内描边、材质描边的描边效果,pencil +实现了这些效果,但是效率不是很高,这里有相关的演示(我也是看了这个之后才决定用安吉拉的模型的)。我看到的较好的方案应该还是L-灵刃的使用退化四边形生成描边的办法,github上也分享了源码。 这篇博客中介绍的描边,是基于我上一篇博客中讲的世界空间中绘制等宽线条的方法,使用DrawProcedural绘制的等宽的描边。我认为只有等宽的描边,才是最能表现二次元画面特征的描边。这里的“等宽”,并不是说线条的宽度处处相等,线条当然可以控制每一部分的粗细,但是这个控制的粗细是基于一个固定值的相对粗细(也就是存在顶点色中的描边粗细值),当粗细值相同时,不管是画面的哪个部分的描边的粗细(不管是内描边还是外描边),都应该是相同的。 实现描边时需要注意的点 首先参考退化四边形的案例,需要先对模型文件进行预处理。这里我做了简化,只去寻找两个三角面共用的边,忽略了只属于一个三角面的边的情况(事实上我觉得这样看上去的视觉效果也蛮不错的)。一条共用边对应了这条边的两个顶点,两侧的两个三角形和这两个三角形对应的额外的两个顶点。这里都用序号来表示,需要6个int值(事实上可以忽略两个三角形的编号,就能存在一个int4里了)。判断一条边共同属于两个三角形,就相当于判断两个三角形中的某两个顶点的序号是相同的(实际上顺序是相反的)。但是实际操作中,即使是相同的顶点,在两个三角形中顶点的序号也不一定是相同的,因此需要先把两个相同的顶点(使用距离来判断)合并成一个顶点,这也就是我使用vertRemapping这个数组的目的。剩下的就是循环所有三角形,获取共用边的算法部分了,尽可能的优化一下,不然三角面一多运算的时间要很久。 有了共用边的数据,通过SkinnedMeshRenderer.BakeMesh()可以获取到当前帧每个顶点的物体空间的坐标,就能进行描边的计算了。使用DrawProcedural时顶点的数量可以是公用边数量的两倍,这样需要在Geometry Shader中把顶点数目从2扩充到6,或者是在绘制时将顶点数量设置成共用边数量的六倍,可能后者效率会高一点,不过思考的时候会有点乱,这里就使用Geometry Shader的方法了。 如果是像之前的博客介绍的,以共用边两个顶点为中心,同时向左右两侧外扩的话,会因为深度测试的原因,导致描边部分被模型遮挡,这个问题比较严重,他直接导致了外描边和内描边的粗细不一样,也导致了在一条描边中会露出一部分模型的问题。这里采用的方法是仅向外侧描边,在计算是不是轮廓边的时候同时计算需要描边的方向,使用这个方向向外扩展描边,最后效果还蛮不错的。 要实现风格化描边的话,除了使用顶点色来控制描边的粗细之外,还能使用一张贴图作为描边的笔刷,在绘制描边的时候采样这张贴图,本篇博客就暂不使用这种方法了。 具体的实现描边的操作 对当前模型获取到所有的共用边对应的四个顶点序号,严格保持顺序! 每一帧使用SkinnedMeshRenderer.BakeMesh,获取所有顶点当前的物体空间的坐标。 传入顶点坐标,顶点重映射数组,共用边信息,如果需要的话还要传顶点色到描边的Shader中。 使用DrawProcedural绘制描边,顶点数量为共用边的数量的两倍。 在顶点着色器中,计算共用边四个顶点的裁剪空间(实际上用的是屏幕空间)的坐标,判断这条边是不是轮廓边,同时记录描边外扩的方向,相当于对于每一条边(每两个点)存两个bool变量。 在几何体着色器中,计算共用边两个顶点的屏幕空间的坐标,计算出两个点之间的向量,计算与之相垂直的外扩的方向,根据两个向量,计算出描边的四个顶点的裁剪空间的坐标,并赋予uv的值。 在片元着色器中,根据uv计算出描边的颜色,可以采样贴图,也可以直接返回计算的颜色。 由于整个描边的操作较为复杂,我尽可能多的写了注释。 OutlineObject.cs 定义共用边的结构体,也定义用于保存共用边数据的ScriptableObject。 using UnityEngine; namespace ZZNEWCLEAR13.Outline { [System.Serializable] public class OutlineObject : ScriptableObject { [System.Serializable] public class MeshOutlineInfo { public string meshName; //尽量不要都显示出来,不然很卡。。 //顶点、法线、切线和顶点色 [HideInInspector] public Vector3[] vertices; [HideInInspector] public Vector3[] normals; [HideInInspector] public Vector4[] tangents; [HideInInspector] public Color[] colors; //vertRemapping把相同位置的顶点编号映射到第一个该位置顶点的编号 [HideInInspector] public int[] vertRemapping; //三角形对应的顶点编号 [HideInInspector] public Vector3Int[] triangles; public Line[] commonLines; } public MeshOutlineInfo outlineInfo; } [System....