unity 描边之stencil篇

Unity描边所使用的方式何其多,每种都有其两面性,有些效果不太好,但实现简单效率也高;有些效果棒棒哒,但耗性能。我们需要在了解每种方式的厉害后,再根据项目的情况来择优用之方为上上策。本篇就先从比较简单说起:利用模板测试的方式来实现描边效果。

本篇必备技能:模板测试的基础知识

一、原理

  1. 先用一个pass把模板缓冲区的值刷至特定值(比如 2),并在该pass里进行正常的模型渲染(纹理采样、高光、漫反等等)。
  2. 再用一个pass进行描边。首先需要把模型的顶点延法线方向向外延伸一定值,也就是描边的厚度/宽度(众多描边实现方法中,超过一大半都是用来膨胀模型的方式,所以膨胀模型本身所带来的弊端也会体现在这些实现方法中,比如对于凹多边形的mesh,由于它们的法线方向可能是相向的,就容易造成穿模;还有像立方体这种低模的,描边就会断掉,不连续;如果你的模型有镂空的地方,那也会被描上边,尽管你不愿意。所以描边厚度不要太大,以免在某些模型中加剧上述问题)。然后模板的Ref值设置成第1步的值,comp一般取NotEqual,Pass时保持模板缓冲区的值。这样得到的效果就是只有多余膨胀的部分会被渲染,渲染的这部分就是我们看到的描边了。此时在我们的片元着色器里输出描边颜色就可以了。
  3. 需要注意的是我们需要保证第1步的pass先执行,第2步的pass后执行,所有需要控制一下两个pass的 Queue值,比如本篇案第1个pass的Queue = “Gemetry + 1”,第2个pass的Queue = “Gemetry + 2”

二、案例

以下代码只是为了验证上述原理,不能直接用于项目中

Shader "Unlit/StencilOutline"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_OutlineColor("outline Color",Color) = (0,0,0,0)
		_OutlineWidth("outline Width",Range(0,0.05)) = 0.01
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			Tags{"Queue" = "Gemetry + 1"}
			Stencil
			{
				Ref 1
				Comp Always
				Pass Replace
			}

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
		pass
		{
			Tags{"Queue" = "Gemetry + 2"}
			Stencil
			{
				Ref 1
				Comp NotEqual
				Pass keep
			}

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
			};

			fixed4 _OutlineColor;
			float _OutlineWidth;

			v2f vert (appdata v)
			{
				v2f o;
				//在模型空间里直接膨胀模型,也可以在别的空间里做
				//需要注意法线的变换,还要注意在别的空间里膨胀的话,顶点变换到裁剪空间时不是MVP了
				v.vertex.xyz += v.normal * _OutlineWidth;
				o.vertex = UnityObjectToClipPos(v.vertex);

				//// 在裁剪空间里膨胀,可以解决描边近大远小的问题
				//// 先把法线从模型空间转到相机空间
				//// 这里直接使用unity给我们提供的宏,等价于 vNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal))
				//// 为什么是(float3x3)UNITY_MATRIX_IT_MV,而不是 (float3x3)UNITY_MATRIX_MV,冯乐乐美女的4.7节是有这个解释的
				// fixed3 vNormal = COMPUTE_VIEW_NORMAL;
				//// 再把 vNormal 的xy转化到裁剪空间里去,z不要了
				// fixed2 pNormal = TransformViewToProjection(vNormal.xy);
				//// 开始膨胀模型
				// o.vertex.xy += pNormal * _OutlineWidth;

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				return _OutlineColor;
			}
			ENDCG
		}
	}
}


unity 描边之stencil篇_第1张图片

三、优缺点

优点

正如前文所说,实现简单,效率也高,只用未开启混合的两个pass就能做到。

缺点

除了第2个步骤里提到的,还有一些
1、同样厚度的描边存在近大远小的问题,也有人说这不算缺点,因为透视相机的原因,属于正常现象;如果需要解决也很简单,直接在把膨胀步骤放到裁剪空间里做,只是要多些把法线的转换到相机空间步骤。(我觉得没必要,真没多大区别)
2、多个单位有重叠时,描边断了,如下图
unity 描边之stencil篇_第2张图片
这是因为当有重叠时,重叠部分的模板测试未通过(我们设置的是不等于时通过,而此时是等于的关系,所以未通过),所以描边没有被渲染出来。

你可能感兴趣的:(unity之着色器)