给物体边缘加高光轮廓的办法,付Demo(增加一组算法)
最近我迷上了Unity,经过一番研究,对一些技术点有了些许心得,在此分享给大家。
1. 边缘光方法(Rim Light)
Unity官方教程中有相关例子,其核心代码为:
half rim = 1.0 - saturate(dot (normalize(IN.viewDir), IN.worldNormal));
o.Emission = _RimColor.rgb * pow (rim, _RimPower);
其中,IN.viewDir 是当前视角向量,IN.worldNormal 是物体的法线。dot 函数用于计算视角向量和法线向量的点积,该点积的值等于视角向量和法线向量夹角的余弦值(Cos值),具体情况如下图所示:
Cos值的范围是从1到0,通过 1 - Cos 计算后,其范围变为从0到1,并且在夹角为90度时达到最大值。这一特性恰好可以用来模拟侧光的强度,因为与视角成90度的部分光线最强,也就是我们所需要的边缘光。
为了强化边缘发亮的效果,我们使用 pow 函数(即 rim 的 _RimPower 次方)对这个值的变化率进行放大。下面进行对比说明:
- 没有经过
pow函数放大变化率的边缘光,由于cos函数的变化比较平缓,会导致大片区域被染色。 - 经过
pow函数放大变化率后,就能够呈现出边缘发亮的效果。下图大致体现了放大前后变化率的曲线。
这种边缘光方法在处理复杂几何形体时效果较好。然而,在处理平直的物体时,边缘光会变得不明显;对于方形物体,边缘光几乎难以看见。这是因为正方形每个面的法线方向都是一致的,无法体现出变化和轮廓。
另外,这种方法在描绘凹的几何体时,凹的部分(包括法线贴图造成的凹凸)的边缘也都会被绘制出来,但这并非真正意义上的边缘轮廓,仅仅是一种侧光效果。
该方法的优点在于实现简单,只需对官方的Shader进行改写,添加计算边缘光的代码,即可实现边缘光效果。在需要显示边缘光时,动态切换Shader即可,基本不需要代码干预,效率较高。
2. 单个物体轮廓渲染方法
这种方法的实现较为复杂,下面仅介绍大致思路:
- 将需要渲染轮廓的物体放置在一个单独的层中。
- 在该层中设置一个禁用(disable)的摄像机,将其
culling mask设置为渲染物体所在的层。 - 主摄像机的
culling mask保持为everything。 - 生成一个
RenderTexture。 - 使用
RenderWithShader方法,让禁用的辅助摄像机指定一个单色渲染的Shader(只需要轮廓,不需要进行光照计算),将物体的轮廓渲染到一个RenderTexture中。 - 继续使用单色Shader,采用类似Unity自带Blur的方法,将物体轮廓图上下左右移动几个像素,然后叠加在一起,得到一个比原来轮廓更大、边缘模糊的轮廓图,并将其存储到一个临时的
RenderTexture中。 - 将大的轮廓图和原始轮廓图进行叠加,消除中间清晰轮廓的部分,从而得到一个完整带透明度的轮廓图。
- 在主摄像机的
OnRenderImage函数中,将这个透明轮廓图和主摄像机渲染的图像进行Alpha混合,即可产生一个完整且不被遮挡的轮廓效果。
效果如下图所示,明显比侧光效果要好很多。不过,这种方法的开销较大,并且需要较多的代码支持。
小Demo说明
附上一个小Demo,其中包含了上述两种方法。在Demo中,鼠标可以控制镜头进行平移、旋转和缩放操作。鼠标划过物体时显示侧光效果,点击物体时显示清晰轮廓效果,再次点击则消除效果。同时,还可以设置边缘颜色、宽度和模糊度。目标物体按照材质进行分组,方便处理,并且可以将轮廓变换成细线,颜色设置为绿色。
优化内容
对代码进行了优化,减少了一组 RenderTexture 的使用,因为该资源比较耗费性能,同时也减少了计算的次数。另外,增加了一组可以同时完成模糊和裁剪操作的Shader,这种Shader在资源使用上更加节省,但边缘会相对模糊。
两种算法效果比较
- 多次模糊再裁剪的算法:边缘光滑,模型的锯齿在边缘线处被平滑处理,但开销略大。
- 一次同时模糊、裁剪算法:边缘较模糊,锯齿也被等比放大,但开销较省。
可以根据实际需求选择合适的算法。总体而言,由于这是全屏效果,与被渲染物体的数量无关,所以基本上显示轮廓物体的多少不会影响效率。
附件里包含了两种方法的代码和Shader。其中,RimLight方法一个Shader就能对应一种材质效果,不需要代码支持;后一种方法则需要代码和Shader配合,类似官方的Image Effect效果。