最近在研究模型勾边的Shader,看到网上早有相关文章,其中一篇制作思路与我自己想的方法不谋而合,而它用层来控制更具灵活性,很有学习价值,因此转载过来作为资料保存。
---------------------------------------------------------------------------------------------------------------------
总结一下一般游戏中3D模型各种勾边方法遇到的工程性问题
以前做过简单的rim light勾边,几何勾边,这次又做了后处理的勾边,工程化的时候,都遇到很多问题,简单总结一下。
首先是火炬之光勾边效果,类似轮廓光的实现,简单的卡通渲染也是通过类似的算法加采样色阶图实现。
火炬中的勾边相当于为角色添加内测光的效果,即通过计算标准散射点积运算来确定顶点法线N和光线向量L之间角度的余弦,用以确定顶点或像素接收到多少光线:s=L·N。
算法的优势就是:
实现比较简单,通过调整参数就可以开关内测勾边光的效果。可以使用顶点法线,这样更省,火炬中就是vertex Shader中处理的。
算法评估:
效果一般,而且勾边的效果与模型本身的几何关系密切。某些特殊的几何效果很差,例如也无法处理片儿的勾边。
方案简单,使用的指令数有限
不同物体可以有不同勾边的粗细,颜色。
与深度排序、半透明绘制等因素无关。
简单粗暴的法线放大几何勾边法:
算法简单描述:
将模型的顶点沿法线方向外移一定距离得到一个比原模型稍大的模型,绘制时采用剔除正面绘制背面的方式,将模型绘制为边界颜色,
再按正常方式绘制原来的模型,结果就产生了一定宽度的勾边,勾边的宽度可以通过将顶点沿法线外移的距离来控制。
优点:
模型多绘制一遍,消耗较小,简单粗暴。
缺点:
1、因为与模型表面法线相关,所以对于片儿的边缘可能会有勾边错误,例如扇子
2、绘制边缘的pass不能写深度且需要检测深度,因此会产生因为渲染顺序导致的遮挡问题。
后处理方法:
在场景绘制结束后,使用特定的shader再次绘制需要勾边的角色一遍到某RenderTarget上,处理此Rendertarget找到模型轮廓边界,并融合到场景中。
找边的方法很多,例如可以对这张图进行边缘检测算法(如Sobel)。或者直接对其做高斯模糊,通过alpha的值来找到编辑并且还能方便的融合到背景(暗黑三)
缺点就是:
增加了时空的消耗:
时间上:一次全屏的绘制,需要勾边的模型多一次绘制。
空间上,需要申请一张全屏大小的RenderTarget。
问题:
第二次绘制模型时是否开启深度检测会有两种不同的效果,均有一些问题:
一、关闭深度检测:
勾出的边不会被深度遮挡,因此不管在什么角度,都可以看到完整的模型的勾边效果:
上图,模型的边界不会被石头挡住
二、开启深度检测:
绘制纯色模型时会被前置的角色或者模型挡住,因此在勾边模型与前置模型的交界处会有勾边。
蜘蛛与草的边界处会有勾边效果
绘制纯色模型时会被前置的角色或者模型挡住,因此在勾边模型与前置模型的交界处会有勾边。
暗黑三中算法类似,例如暗黑中被模型挡住的门的勾边效果:(为方便查看,勾了粗边)。
暗黑中被角色遮挡的门的勾边
-----------------------------------------------------------------------------------------------------------------------
给物体边缘加高光轮廓的办法
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.单个物体轮廓渲染方法
这个方法实现比较复杂,只能介绍大致思路:
1.把要渲染轮廓的物体放在一个单独的层里
2.在层里设置一个disable的摄像机,culling mask是渲染物体所在层。
3.主摄像机保持culling mask是everything
4.生成1个renderTexture
5.把那个disable的辅助摄像机用RenderWithShader方法,指定一个单色渲染的shader(只需要轮廓,不需要光照计算)渲染物体的轮廓到一个renderTexture
6.继续用单色Shader,用Unity自带Blur类似的方法,把物体轮廓图上下左右移动几个像素,叠加在一起,得到一个比原来轮廓大,边缘模糊的轮廓图,存到一个临时renderTexture
7.把大的轮廓图和原始轮廓图叠加,把中间清晰轮廓部分消除掉,就能得到一个完整带透明度的轮廓图。
8.在主摄像机的OnRenderImage里,把这个透明轮廓图和主摄像机渲染的图像进行alpha混合,就能产生一个完整并且不被遮挡的轮廓效果了。
效果如图:
明显效果比侧光好多了。不过这种方法开销比较大,而且需要很多代码的支持。
附上一个小Demo,包含了上述两种方法。
鼠标可以控制镜头平移、旋转、缩放。鼠标划过物体显示侧光效果,点击物体显示清晰轮廓效果,再次点击效果消除。
可以设置边缘颜色 和宽度 模糊度
目标按照材质进行分组以方便处理
变换成细线 绿色
---------------------------------------------------------------------------------------------------------------------
优化了代码,减少了一组RenderTexture,这个东西还是很耗资源的,减少了计算的次数。
另外增加了一组模糊、裁剪一次完成的shader,资源上更节省,不过边缘较模糊。
两种算法效果比较:
多次模糊再裁剪的算法,边缘光滑,模型的锯齿在边缘线被光滑了,开销略大
一次同时模糊、裁剪算法,边缘较模糊,锯齿也被等比放大,但开销较省。
根据需要使用。
总体上因为是全屏效果,与被渲染物体数量无关,所以基本上显示轮廓物体的多少不会影响效率。
---------------------------------------------------------------------------------------------------------------------
总结一下一般游戏中3D模型各种勾边方法遇到的工程性问题
以前做过简单的rim light勾边,几何勾边,这次又做了后处理的勾边,工程化的时候,都遇到很多问题,简单总结一下。
首先是火炬之光勾边效果,类似轮廓光的实现,简单的卡通渲染也是通过类似的算法加采样色阶图实现。
火炬中的勾边相当于为角色添加内测光的效果,即通过计算标准散射点积运算来确定顶点法线N和光线向量L之间角度的余弦,用以确定顶点或像素接收到多少光线:s=L·N。
算法的优势就是:
实现比较简单,通过调整参数就可以开关内测勾边光的效果。可以使用顶点法线,这样更省,火炬中就是vertex Shader中处理的。
算法评估:
效果一般,而且勾边的效果与模型本身的几何关系密切。某些特殊的几何效果很差,例如也无法处理片儿的勾边。
方案简单,使用的指令数有限
不同物体可以有不同勾边的粗细,颜色。
与深度排序、半透明绘制等因素无关。
简单粗暴的法线放大几何勾边法:
算法简单描述:
将模型的顶点沿法线方向外移一定距离得到一个比原模型稍大的模型,绘制时采用剔除正面绘制背面的方式,将模型绘制为边界颜色,
再按正常方式绘制原来的模型,结果就产生了一定宽度的勾边,勾边的宽度可以通过将顶点沿法线外移的距离来控制。
优点:
模型多绘制一遍,消耗较小,简单粗暴。
缺点:
1、因为与模型表面法线相关,所以对于片儿的边缘可能会有勾边错误,例如扇子
2、绘制边缘的pass不能写深度且需要检测深度,因此会产生因为渲染顺序导致的遮挡问题。
后处理方法:
在场景绘制结束后,使用特定的shader再次绘制需要勾边的角色一遍到某RenderTarget上,处理此Rendertarget找到模型轮廓边界,并融合到场景中。
找边的方法很多,例如可以对这张图进行边缘检测算法(如Sobel)。或者直接对其做高斯模糊,通过alpha的值来找到编辑并且还能方便的融合到背景(暗黑三)
缺点就是:
增加了时空的消耗:
时间上:一次全屏的绘制,需要勾边的模型多一次绘制。
空间上,需要申请一张全屏大小的RenderTarget。
问题:
第二次绘制模型时是否开启深度检测会有两种不同的效果,均有一些问题:
一、关闭深度检测:
勾出的边不会被深度遮挡,因此不管在什么角度,都可以看到完整的模型的勾边效果:
上图,模型的边界不会被石头挡住
二、开启深度检测:
绘制纯色模型时会被前置的角色或者模型挡住,因此在勾边模型与前置模型的交界处会有勾边。
蜘蛛与草的边界处会有勾边效果
绘制纯色模型时会被前置的角色或者模型挡住,因此在勾边模型与前置模型的交界处会有勾边。
暗黑三中算法类似,例如暗黑中被模型挡住的门的勾边效果:(为方便查看,勾了粗边)。
暗黑中被角色遮挡的门的勾边
-----------------------------------------------------------------------------------------------------------------------
给物体边缘加高光轮廓的办法
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.单个物体轮廓渲染方法
这个方法实现比较复杂,只能介绍大致思路:
1.把要渲染轮廓的物体放在一个单独的层里
2.在层里设置一个disable的摄像机,culling mask是渲染物体所在层。
3.主摄像机保持culling mask是everything
4.生成1个renderTexture
5.把那个disable的辅助摄像机用RenderWithShader方法,指定一个单色渲染的shader(只需要轮廓,不需要光照计算)渲染物体的轮廓到一个renderTexture
6.继续用单色Shader,用Unity自带Blur类似的方法,把物体轮廓图上下左右移动几个像素,叠加在一起,得到一个比原来轮廓大,边缘模糊的轮廓图,存到一个临时renderTexture
7.把大的轮廓图和原始轮廓图叠加,把中间清晰轮廓部分消除掉,就能得到一个完整带透明度的轮廓图。
8.在主摄像机的OnRenderImage里,把这个透明轮廓图和主摄像机渲染的图像进行alpha混合,就能产生一个完整并且不被遮挡的轮廓效果了。
效果如图:
明显效果比侧光好多了。不过这种方法开销比较大,而且需要很多代码的支持。
附上一个小Demo,包含了上述两种方法。
鼠标可以控制镜头平移、旋转、缩放。鼠标划过物体显示侧光效果,点击物体显示清晰轮廓效果,再次点击效果消除。
可以设置边缘颜色 和宽度 模糊度
目标按照材质进行分组以方便处理
变换成细线 绿色
---------------------------------------------------------------------------------------------------------------------
优化了代码,减少了一组RenderTexture,这个东西还是很耗资源的,减少了计算的次数。
另外增加了一组模糊、裁剪一次完成的shader,资源上更节省,不过边缘较模糊。
两种算法效果比较:
多次模糊再裁剪的算法,边缘光滑,模型的锯齿在边缘线被光滑了,开销略大
一次同时模糊、裁剪算法,边缘较模糊,锯齿也被等比放大,但开销较省。
根据需要使用。
总体上因为是全屏效果,与被渲染物体数量无关,所以基本上显示轮廓物体的多少不会影响效率。