Unity Shader实现描边效果

Posted alps_01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader实现描边效果相关的知识,希望对你有一定的参考价值。

http://gad.qq.com/article/detail/28346

 

描边效果是游戏里面非常常用的一种效果,一般是为了凸显游戏中的某个对象,会给对象增加一个描边效果。本篇文章和大家介绍下利用Shader实现描边效果,一起来看看吧。

 

最近又跑回去玩了玩《剑灵》,虽然出了三年了,感觉在现在的网游里面画面仍然算很好的了,剑灵里面走近或者选中NPC的一瞬间,NPC就会出现描边效果,不过这个描边效果是渐变的,会很快减弱最后消失(抓了好久才抓住一张图....)


还有就是最常见的LOL中的塔,我们把鼠标移动到塔上,就会有很明显的描边效果:

 

简单描边效果的原理

 
描 边效果有几种实现方式。其实边缘光效果与描边效果有些类似,适当调整边缘光效果,其实也可以达到凸显要表达的对象的意思。边缘光的实现最为简单,只是在计 算的时候增加了一次计算法线方向与视线方向的夹角计算,用1减去结果作为系数乘以一个边缘光颜色就达到了边缘光的效果,是性能最好的一种方法,关于边缘光 效果,可以参考一下之前的一篇文章:边缘光效果。边缘光的效果如下图所示:
 
原始模型渲染:

使用了边缘光的效果:

 
边 缘光效果虽然简单,但是有很大的局限性,边缘光效果只是在当前模型本身的光照计算时调整了边缘位置的颜色值,并没有达到真正的“描边”(当然,有时候我们 就是想要这种边缘光的效果),而我们希望的描边效果,一般都是在正常模型的渲染状态下,在模型外面扩展出一个描边的效果。既然要让模型的形状有所改变(向 外拓一点),那么肯定就和vertex shader有关系了。而我们的描边效果,肯定就是要让模型更“胖”一点,能够把我们原来的大小包裹住;微观一点来看,一个面,如果我们让它向外拓展,而 我们指的外,也就是这个面的法线所指向的方向,那么就让这个面朝着法线的方向平移一点;再微观一点来看,对于顶点来说,也就是我们的vertex shader真正要写的内容了,我们正常计算顶点的时候,传入的vertex会经过MVP变换,最终传递给fragment shader,那么我们就可以在这一步让顶点沿着法线的方向稍微平移一些。我们在描边后,描边这一次渲染的边缘其实是没有办法和我们正常的模型进行区分 的,为了解决这个问题,就需要用两个Pass来渲染,第一个Pass渲染描边的效果,进行外拓,而第二个Pass进行原本效果的渲染,这样,后面显示的就 是稍微“胖”一点的模型,然后正常的模型贴在上面,把中间的部分挡住,边缘挡不住就露出了描边的部分了。
 

开启深度写入,剔除正面的描边效果

 
知 道了原理,我们来考虑一下外拓的实现,我们可以在vertex阶段获得顶点的坐标,并且有法线的坐标,最直接的方式就是直接用顶点坐标+法线方向*描边粗 细参数,然后用这个偏移的坐标值再进行MVP变换;但是这样做有一个弊端,其实就是我们透视的近大远小的问题,模型上离相机近的地方描边效果较粗,而远的 地方描边效果较细。一种解决的方案是先进行MPV变换,变换完之后再去按照法线方向调整外拓。代码如下:
[csharp] view plain copy
  1. //描边Shader  
  2. //by:puppet_master  
  3. //2017.1.5  
  4.   
  5. Shader "ApcShader/Outline"  
  6. {  
  7.     //属性  
  8.     Properties{  
  9.         _Diffuse("Diffuse", Color) = (1,1,1,1)  
  10.         _OutlineCol("OutlineCol", Color) = (1,0,0,1)  
  11.         _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1  
  12.         _MainTex("Base 2D", 2D) = "white"{}  
  13.     }  
  14.   
  15.     //子着色器    
  16.     SubShader  
  17.     {  
  18.           
  19.         //描边使用两个Pass,第一个pass沿法线挤出一点,只输出描边的颜色  
  20.         Pass  
  21.         {  
  22.             //剔除正面,只渲染背面,对于大多数模型适用,不过如果需要背面的,就有问题了  
  23.             Cull Front  
  24.               
  25.             CGPROGRAM  
  26.             #include "UnityCG.cginc"  
  27.             fixed4 _OutlineCol;  
  28.             float _OutlineFactor;  
  29.               
  30.             struct v2f  
  31.             {  
  32.                 float4 pos : SV_POSITION;  
  33.             };  
  34.               
  35.             v2f vert(appdata_full v)  
  36.             {  
  37.                 v2f o;  
  38.                 //在vertex阶段,每个顶点按照法线的方向偏移一部分,不过这种会造成近大远小的透视问题  
  39.                 //v.vertex.xyz += v.normal * _OutlineFactor;  
  40.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  41.                 //将法线方向转换到视空间  
  42.                 float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
  43.                 //将视空间法线xy坐标转化到投影空间,只有xy需要,z深度不需要了  
  44.                 float2 offset = TransformViewToProjection(vnormal.xy);  
  45.                 //在最终投影阶段输出进行偏移操作  
  46.                 o.pos.xy += offset * _OutlineFactor;  
  47.                 return o;  
  48.             }  
  49.               
  50.             fixed4 frag(v2f i) : SV_Target  
  51.             {  
  52.                 //这个Pass直接输出描边颜色  
  53.                 return _OutlineCol;  
  54.             }  
  55.               
  56.             //使用vert函数和frag函数  
  57.             #pragma vertex vert  
  58.             #pragma fragment frag  
  59.             ENDCG  
  60.         }  
  61.           
  62.         //正常着色的Pass  
  63.         Pass  
  64.         {  
  65.             CGPROGRAM     
  66.           
  67.             //引入头文件  
  68.             #include "Lighting.cginc"  
  69.             //定义Properties中的变量  
  70.             fixed4 _Diffuse;  
  71.             sampler2D _MainTex;  
  72.             //使用了TRANSFROM_TEX宏就需要定义XXX_ST  
  73.             float4 _MainTex_ST;  
  74.   
  75.             //定义结构体:vertex shader阶段输出的内容  
  76.             struct v2f  
  77.             {  
  78.                 float4 pos : SV_POSITION;  
  79.                 float3 worldNormal : TEXCOORD0;  
  80.                 float2 uv : TEXCOORD1;  
  81.             };  
  82.   
  83.             //定义顶点shader,参数直接使用appdata_base(包含position, noramal, texcoord)  
  84.             v2f vert(appdata_base v)  
  85.             {  
  86.                 v2f o;  
  87.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  88.                 //通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;  
  89.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  90.                 o.worldNormal = mul(v.normal, (float3x3)_World2Object);  
  91.                 return o;  
  92.             }  
  93.   
  94.             //定义片元shader  
  95.             fixed4 frag(v2f i) : SV_Target  
  96.             {  
  97.                 //unity自身的diffuse也是带了环境光,这里我们也增加一下环境光  
  98.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;  
  99.                 //归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的  
  100.                 fixed3 worldNormal = normalize(i.worldNormal);  
  101.                 //把光照方向归一化  
  102.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);  
  103.                 //根据半兰伯特模型计算像素的光照信息  
  104.                 fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;  
  105.                 //最终输出颜色为lambert光强*材质diffuse颜色*光颜色  
  106.                 fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;  
  107.                 //进行纹理采样  
  108.                 fixed4 color = tex2D(_MainTex, i.uv);  
  109.                 color.rgb = color.rgb* diffuse;  
  110.                 return fixed4(color);  
  111.             }  
  112.   
  113.             //使用vert函数和frag函数  
  114.             #pragma vertex vert  
  115.             #pragma fragment frag     
  116.   
  117.             ENDCG  
  118.         }  
  119.     }  
  120.     //前面的Shader失效的话,使用默认的Diffuse  
  121.     FallBack "Diffuse"  
  122. }  
开启了描边效果:

原始模型渲染采用了半兰伯特Diffuse进 行渲染,主要是前面多了一个描边的Pass。这个Pass里,我们没有关闭深度写入,主要是开启了模型的正面剔除,这样,在这个Pass渲染的时候,就只 会渲染模型的背面,让背面向外拓展一下,既不会影响什么,并且背面一般都在正面的后面,一般情况下不会遮挡住正面,正好符合我们后面的部分外拓的需求。这 个的主要优点是没有关闭深度写入,因为关闭深度写入,引入的其他问题实在是太多了。
附上一张进行了Cull Front操作的效果,只渲染了我们正常看不到的面,效果比较惊悚:
然 后再来看看转换的部分,我们通过UNITY_MATRIX_IT_MV矩阵将法线转换到视空间,这里可能会比较好奇,为什么不用正常的顶点转化矩阵来转化 法线,其实主要原因是如果按照顶点的转换方式,对于非均匀缩放(scalex, scaley,scalez不一致)时,会导致变换的法线归一化后与面不垂直。如下图所示,左边是变化前的,而中间是沿x轴缩放了0.5倍的情况,显然变 化后就不满足法线的性质了,而最右边的才是我们希望的结果。造成这一现象的主要原因是法线只能保证方向的一致性,而不能保证位置的一致性;顶点可以经过坐 标变换变换到正确的位置,但是法线是一个向量,我们不能直接使用顶点的变换矩阵进行变换。
我们可以推导一个法线的变换矩阵,就能够保证转化后的法线与面垂直,法线的变换矩阵为模型变换矩阵的逆转置矩阵。具体推导过程可以参考这篇文章
在 把法线变换到了视空间后,就可以取出其中只与xy面有关的部分,视空间的z轴近似于深度,我们只需要法线在x,y轴的方向,再通过 TransformViewToProjection方法,将这个方向转化到投影空间,最后用这个方向加上经过MVP变换的坐标,实现轻微外拓的效果。 (从网上和书上看到了不少在这一步计算的时候,又乘上了pos.z的操作,个人感觉没有太大的用处,而且会导致描边效果越远,线条越粗的情况,离远了就会 出现一团黑的问题,所以把这个去掉了)
 
上 面说过,一般情况下背面是在我们看到的后面的部分,但是理想很美好,现实很残酷,具体情况千差万别,比如我之前常用的一个模型,模型的袖子里面,其实用的 就是背面,如果想要渲染,就需要关闭背面剔除(Cull Off),这种情况下,使用Cull Front只渲染背面,就有可能和第二次正常渲染的时候的背面穿插,造成效果不对的情况,比如:

不过,解决问题的方法肯定要比问题多,我们可以用深度操作神器Offset指令,控制深度测试,比如我们可以让渲染描边的Pass深度远离相机一点,这样就不会与正常的Pass穿插了,修改一下描边的Pass,其实只多了一句话Offset 1,1:
[csharp] view plain copy
  1. //描边使用两个Pass,第一个pass沿法线挤出一点,只输出描边的颜色  
  2.         Pass  
  3.         {  
  4.             //剔除正面,只渲染背面,对于大多数模型适用,不过如果需要背面的,就有问题了  
  5.             Cull Front  
  6.             //控制深度偏移,描边pass远离相机一些,防止与正常pass穿插  
  7.             Offset 1,1  
  8.             CGPROGRAM  
  9.             #include "UnityCG.cginc"  
  10.             fixed4 _OutlineCol;  
  11.             float _OutlineFactor;  
  12.               
  13.             unity描边效果

    关于Unity中的模型描边与Shader切换(专题二)

    unity之自制玻璃啤酒瓶shader

    Unity Shader 卡通渲染 基于退化四边形的实时描边

    Unity开发bug记录100例子(第1例)——打包后shader失效或者bug

    Unity开发bug记录100例子(第1例)——打包后shader失效或者bug