当前位置:   article > 正文

unity 代码边缘发光_Unity法线外扩外描边Shader

pass offset

bc5c0aca4f1d9e4f4fc064deec63af9b.png

效果一览

外描边是许多游戏的画面需求,通常大体分为法线外扩和后处理边缘检测两种,法线外扩通常用于特殊需求,如外描边高亮关键物体,选中外描边高亮等,后处理边缘检测画面表现力更强一点,通常用于全屏的风格化描边,如卡通渲染,素描风格画面等(其实我也不清楚,凭感觉应该有这样的使用趋向区别),本篇文章主要讲解法线外扩外描边效果。

e232d7aba6ee4e9f1e75fc01742344fb.png
法线外扩外描边效果

基本原理

一个shader两个pass,第一个pass绘制法线外扩后的纯色,第二个pass正常绘制

34751ed901778f34033ef5b90eacc5d3.png
第一个pass绘制法线外扩后的纯色

具体细节

UnityShader代码如下

  1. Shader "Custom/Outline"
  2. {
  3. Properties{
  4. _MainTex("Texture", 2D) = "white"{}
  5. _Diffuse("DiffuseColor", Color) = (1,1,1,1)
  6. _Specular("SpecularColor",Color)=(1,1,1,1)
  7. _Gloss("Gloss",Range(8,256))=32
  8. _OutlineColor("OutlineColor", Color) = (1,0,0,1)
  9. _OutlineLength("OutlineLength", Range(0,1)) = 0.1
  10. }
  11. SubShader
  12. {
  13. //第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
  14. Pass
  15. {
  16. //剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
  17. Cull Front
  18. //深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
  19. //即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
  20. Offset 20,20
  21. //Zwrite Off
  22. CGPROGRAM
  23. #include "UnityCG.cginc"
  24. fixed4 _OutlineColor;
  25. float _OutlineLength;
  26. struct v2f
  27. {
  28. float4 pos : SV_POSITION;
  29. };
  30. v2f vert(appdata_full v)
  31. {
  32. v2f o;
  33. //在物体空间下,每个顶点沿法线位移,这种描边会造成近大远小的透视问题
  34. //v.vertex.xyz += v.normal * _OutlineLength;
  35. o.pos = UnityObjectToClipPos(v.vertex);
  36. //将法线方向转换到视空间,为接下来转换到投影空间做准备
  37. float3 normalView = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
  38. //将视空间法线xy坐标转换到投影空间,z深度不转换的原因是尽量避免垂直于视平面的顶点位移
  39. //防止描边pass与正常渲染pass的模型交叉
  40. float2 offset = TransformViewToProjection(normalView.xy);
  41. //最终在投影空间进行顶点沿法线位移操作
  42. o.pos.xy += offset * _OutlineLength;
  43. return o;
  44. }
  45. fixed4 frag(v2f i) : SV_Target
  46. {
  47. //这个Pass直接输出描边颜色
  48. return _OutlineColor;
  49. }
  50. #pragma vertex vert
  51. #pragma fragment frag
  52. ENDCG
  53. }
  54. //第二个pass利用Blinn-Phong着色模型正常渲染
  55. Pass
  56. {
  57. CGPROGRAM
  58. #include "Lighting.cginc"
  59. fixed4 _Diffuse;
  60. sampler2D _MainTex;
  61. //使用了TRANSFROM_TEX宏就需要定义XXX_ST
  62. float4 _MainTex_ST;
  63. fixed4 _Specular;
  64. float _Gloss;
  65. struct v2f
  66. {
  67. float4 pos : SV_POSITION;
  68. float3 worldNormal : TEXCOORD0;
  69. float2 uv : TEXCOORD1;
  70. float3 worldPos : TEXCOORD2;
  71. };
  72. v2f vert(appdata_base v)
  73. {
  74. v2f o;
  75. o.pos = UnityObjectToClipPos(v.vertex);
  76. o.worldPos = mul((float3x3)unity_ObjectToWorld, v.vertex);
  77. //通过TRANSFORM_TEX转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
  78. o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
  79. o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
  80. return o;
  81. }
  82. fixed4 frag(v2f i) : SV_Target
  83. {
  84. fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
  85. fixed3 worldNormal = normalize(i.worldNormal);
  86. fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
  87. fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
  88. fixed3 halfDir = normalize(viewDir + worldLightDir);
  89. fixed3 specular = _Specular * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
  90. fixed3 diffuse =_LightColor0.xyz * _Diffuse * saturate(dot(worldNormal, worldLightDir));
  91. fixed4 color = tex2D(_MainTex, i.uv);
  92. color.rgb = color.rgb * diffuse + ambient;
  93. return color;
  94. }
  95. #pragma vertex vert
  96. #pragma fragment frag
  97. ENDCG
  98. }
  99. }
  100. FallBack "Diffuse"
  101. }

其中关于第一个pass中的Offset深度便宜操作有以下解释:

格式为Offset factor , units,默认不设置时是Offset 0 , 0

每一个Fragment的深度值都会增加如下所示的偏移量:

offset = (m * factor) + (r * units)

m是多边形的深度的斜率(在光栅化阶段计算得出)中的最大值。这句话难以理解,你只需知道,一个多边形越是与近裁剪面(near clipping plan)平行,m就越接近0。

r是能产生在窗口坐标系的深度值中可分辨的差异的最小值,r是由具体实现OpenGL的平台指定的一个常量。

一个大于0的offset 会把模型推到离你(摄像机)更远一点的位置,相应地,一个小于0的offset 会把模型拉近。

Offset在这里的作用为防止描边pass与正常渲染pass的模型交叉,例子如下:

9d240ec4e0df507161c35230c9478793.png
设置为Offset 0 , 0即为默认状态时的描边效果,可看到在模型内部,描边pass覆盖部分正常渲染pass

c98ef59540092a71bbf7a71319034f69.png
设置为Offset 20 , 20,意味着描边pass距离相机更远,描边集中在模型外边缘,内部少有覆盖正常渲染pass

若描边pass开启Zwrite Off,则会解决pass模型交叉的问题

  1. Pass
  2. {
  3. //剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
  4. Cull Front
  5. //深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
  6. //即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
  7. Offset 0,0
  8. Zwrite Off
  9. ......

ccd83bb6acda5f2af6abbd49e5ae6ee1.png
设置为Offset 0 ,0 , Zwrite On

db9fae6a54fdc1398029a1660fb44fe4.png
设置为Offset 0 ,0 , Zwrite Off

可以看到Zwrite Off解决了内部模型交叉问题,同时也带来了一个新特性,多个物体重叠时只会描外边,内部重叠的地方没有描边,适用于多选重叠的物体时高亮提醒的外部描边。

Zwrite Off同时也会带来诸多其他不稳定因数,如下例

d14cb0c6e49eb8b38510f1fc6226cc84.png
在天空盒绘制的地方,外描边消失

通过分析Frame Debug,发现天空盒的绘制顺序在描线物体之后,因为描边没有写入深度而被天空盒覆盖

e274c9a129d91cf38812a1541129aeb9.png

20bac2b1581a8519ff08ad1cb67b590e.png

Unity的geometry类型的渲染顺序是从前往后的,而Transparent类型是从后往前的。天空盒的渲染顺序位于geometry之后,Transparent之前。因此我们把描边pass放在Transparent的渲染顺序就不会被geometry类型遮挡了。(此处解释摘自https://zhuanlan.zhihu.com/p/50450545,但我有疑问,天空盒不是有默认的渲染顺序background=1000吗?怎么这里会变成geometry之后,Transparent之前?是否是unity为了early-Z而做的内部优化?)

解决方案

在描边shader中更改绘制顺序为Queue=Transparent

  1. SubShader
  2. {
  3. Tags{"Queue"="Transparent"}
  4. //第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
  5. Pass
  6. {......

1850367702fb1be9f52dc6fd37bc2282.png

虽然解决了当前的问题,但关闭深度写入,调整渲染顺序并不是很稳妥的做法,随着工程继续不知道还会出现什么坑,在此不鼓励这么做,对于教程,it just work就OK啦

缺点

法线外面边对于边缘平滑,法线变化不大的模型效果比较好,对于形如立方体这类多边形较明显,法线突变状况比较多的模型下效果较差,会出现描边断裂的情况,如:

3d54597a089aa92ccaacf5c867ea7f7a.png
立方体描边效果差劲

956d2a1897f5b0fb03d5ed86c8e0ba75.png
人物模型的平滑角度阈值设置为0,人物将变成lowpoly风格

9a87fe2dfcbbcd48ab47172f377874ae.png
lowpoly风格人物描边效果差劲
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/110158
推荐阅读
相关标签
  

闽ICP备14008679号