【Unity Shader入门精要学习笔记】透明效果

透明度测试丶透明度混合和渲染顺序介绍

在Unity中,通常使用两种方法来实现透明效果,一种是透明度测试(Alpha Test),但这种方法无法得到真正的半透明效果; 另一种是透明度混合(Alpha Blend)
对于不透明的物体,不考虑它们的渲染顺序也能得到正确的排序效果,因为有着强大的深度缓冲(z-buffer)的存在。但如果要实现透明度混合时,事情会变得复杂,因为透明度混合会关闭深度写入(ZWrite).

基本原理:
1.透明度测试:只要一个片元的透明度不满足条件(比如小于某个阈值),那么对应的片元就被舍弃掉,被舍弃了的片元不会再进行任何处理,也不会对颜色缓冲产生影响;否则,就按照普通的不透明物体的处理方式来处理它,也就是进行深度测试,深度写入等。 透明度测试不需要关闭深度写入。因此,透明度测试的效果比较极端: 要么完全透明,要么完全不透明
2.透明度混合:透明度混合可以得到真正的半透明效果。 透明度混合会使用当前片元的透明度作为混合因子,与已经存在颜色缓冲中的颜色值进行混合,来得到新的颜色。透明度混合需要关闭深度写入,但不会关闭深度测试,也就是说,如果深度测试不通过,就不会再进行混合操作。对于透明度混合来说,深度缓冲是只读的。

Unity Shader的渲染顺序

Unity提供了渲染队列(Render Queue)来解决渲染顺序。可以使用SubShader的Queue标签来决定模型归于哪一个渲染队列。 Unity内部使用一些整数索引来表示每个渲染队列,索引号越小越早被渲染。

【Unity Shader入门精要学习笔记】透明效果_第1张图片

透明度测试

通常我们在片元着色器中使用clip函数来进行透明度测试。
它的定义为:void clip(float4 x);void clip(float3 x);void clip(float2 x);void clip(float x);
参数中任何一个分量是负数,就会舍弃当前像素的输出颜色。
它的实现大概如下:

void clip(float4 x)
{
	if (any(x < 0))
	{
		discard;
	}
}

透明度测试的shader代码如下:

Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
	Properties
	{
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white"{}
		_CutOff ("Alpha CutOff", Range(0, 1)) = 0.5
	}
	SubShader
	{
		Tags { "Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
		Pass
		{
			Tags {"LightMode" = "ForwardBase"}
			CGPROGRAM
			#include "Lighting.cginc"

			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _CutOff;

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				// 纹理采样
				fixed4 texColor = tex2D(_MainTex, i.uv);
				// alpha test
				clip(texColor.a - _CutOff);
				if ((texColor.a - _CutOff) < 0.0f)
				{
					discard;
				}

				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse, 1.0);
			}

			ENDCG
		}
	}
	Fallback "Transparent/Cutout/VertexLit"
}

我们在SubShader中使用了AlphaTest的队列,而RenderType标签可以让Unity把这个shader归入到提前定义的组(TransparentCutout组),它通常被用于着色器替换功能。 把IgnoreProjector设置为True,表示这个shader不会受到投射器的影响。
我们使用clip来对texColor.a-_Cutoff做判断,如果不通过,就舍弃该片元的输出。

透明度混合

透明度混合,会使用Unity提供的混合命令:Blend.
ShaderLab的Blend命令:
【Unity Shader入门精要学习笔记】透明效果_第2张图片
我们在设置混合因子的时候,也同时开启了混合模式
对于Blend SrcFactor DstFactor混合命令来说,经过混合后的颜色是:
DstColornew = SrcAlpha * SrcColor + (1 - ScrcAlpha) * DstColorold

透明度混合的shader例子如下:

Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
	Properties
	{
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white"{}
		_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
	}
	SubShader
	{
		Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
		Pass
		{
			Tags {"LightMode" = "ForwardBase"}
			// 关闭深度写入,设置混合模式
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#include "Lighting.cginc"
			#pragma vertex vert
			#pragma fragment frag
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _AlphaScale;

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				// 纹理采样
				fixed4 texColor = tex2D(_MainTex, i.uv);				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
			}

			ENDCG
		}
	}
	Fallback "Transparent/VertexLit"
}

我们使用了Transparent的渲染队列来进行透明度混合。然后在Pass中,我们需要关闭深度写入ZWrite Off,然后开始混合模式并设置混合因子Blend SrcAlpha OneMinusSrcAlpha。

开启深度写入的半透明效果

由于关闭了深度写入,我们无法对模型进行像素级别的深度排序。因此会导致一些错误的情况。一种解决方法是,使用两个Pass来渲染模型,第一个Pass开启深度写入,但不输出颜色,仅仅为了把模型的深度值写入到深度缓冲中;第二个Pass进行正常的透明度混合,因为第一个Pass已经得到了逐像素的正确深度信息,第二个Pass就可以按照像素的深度排序而进行透明渲染。因为这种做法多使用了一个Pass,因此会对性能造成一定的影响。

Shader "Unity Shaders Book/Chapter 8/Alpha Blend ZWrite"
{
	Properties
	{
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white"{}
		_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
	}
	SubShader
	{
		Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
		Pass
		{
			ZWrite On
			ColorMask 0
		}

		Pass
		{
			Tags {"LightMode" = "ForwardBase"}
			// 关闭深度写入,设置混合模式
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#include "Lighting.cginc"
			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _AlphaScale;

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				// 纹理采样
				fixed4 texColor = tex2D(_MainTex, i.uv);
				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
			}
			ENDCG
		}
	}
	Fallback "Transparent/VertexLit"
}

第一个Pass里,我们开启了深度写入,因此可以把模型的深度信息写入到深度缓冲当中,从而剔除模型中被自身遮挡的片元;然后我们使用了ColorMask来设置颜色通道的写掩码(write mask),语法如下:
ColorMask RGB | A | 0 | 其他R,G,B,A的组合
ColorMask R,输出颜色中只有R通道会被写入
ColorMask 0,不会输出任何颜色
默认值为RGBA,即四个通道都写入

ShaderLab混合命令

混合的实现就是,当片元产生一个颜色的时候,可以选择与颜色缓冲中的颜色进行混合。 因此,混合就和两个操作数有关:源颜色目标颜色源颜色S表示片元产生的颜色值,目标颜色D表示颜色缓冲中读到的颜色值。将它们混合后的输出颜色O再重新写入颜色缓冲当中。S,D和O都包含了RGBA四个通道。
使用混合的前提是开启混合,Untiy中我们使用了Blend命令(除了Blend Off)时,会设置混合状态并开启混合。
混合的时候需要一个混合等式来计算输出颜色。当进行混合时,我们需要两个混合等式,一个计算RGB值,一个计算A的值。我们使用Blend命令时,实际上就是设置混合等式里的操作因子默认情况混合等式使用加操作,我们只需要再设置一下混合因子就可以。

【Unity Shader入门精要学习笔记】透明效果_第3张图片
图里的两个混合命令,可以看到,第一个命令提供了两个混合因子,因此Unity会使用同样的混合因子来混合RGB通道和A通道,相对于第二个命令,第一个命令此时SrcFactorA等于SrcFactor,以此类推。
混合公式就比如:
Orgb = SrcFactor * Srgb + DstFactor * Dfgb
Oa = SrcFactorA * Sa + DstFactorA * Da

介绍了命令后,我们可以看看具体的混合因子有哪些:
【Unity Shader入门精要学习笔记】透明效果_第4张图片
举个例子来说,使用Blend SrcFactor DstFactor, SrcFactorA DstFactorA命令,带入上述的因子:
Blend SrcAlpha OneMinusSrcAlpha, One Zero
这时候,输出颜色的透明度值就是源颜色的透明度值。

刚才说Blend命令默认使用加运算来计算输出颜色,我们可以使用ShaderLab的BlendOp BlendOperation命令来使用不同的运混合操作:
【Unity Shader入门精要学习笔记】透明效果_第5张图片
【Unity Shader入门精要学习笔记】透明效果_第6张图片
需要注意的是,当使用Min或者Max混合操作时,混合因子实际上是不起任何作用的,它们仅会判断源颜色和目标颜色之间的比较结果。

通过混合操作和混合因子的组合,可以得到一些类似PhotoShop混合模式中的混合效果:

Blend SrcAlpha OneMinusSrcAlpha
// 柔和相加
Blend OneMinusDstColor One
// 正片叠底,相乘
Blend DstColor Zero
// 两倍相乘
Blend DstColor SrcColor
// 变暗
BlendOp Min
Blend One One

// 变亮
BlendOp Max
Blend One One

// 滤色
Blend OneMinusDstColor One
Blend One OneMinusSrcColor

// 线性减淡
Blend One One

双面渲染的透明效果

由于默认情况下渲染引擎剔除了物体背面,只渲染了物体正面,但如果一个物体是透明的,我们不仅仅可以透过它看到其他物体,我们也应该可以看到它自己的内部结构。
在Unity中,使用Cull指令来控制需要剔除哪个面的渲染图元:
Cull Back | Front | Off
设置了Back,就会剔除背对着相机的图元,设置了Front,会剔除正面,设置了Off,就会关闭剔除功能。

Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side"
{
	Properties
	{
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white"{}
		_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
	}
	SubShader
	{
		Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
		Pass
		{
			Tags {"LightMode" = "ForwardBase"}
			Cull Front
			// 关闭深度写入,设置混合模式
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#include "Lighting.cginc"

			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _AlphaScale;

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				// 纹理采样
				fixed4 texColor = tex2D(_MainTex, i.uv);
				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
			}

			ENDCG
		}

		Pass
		{
			Tags {"LightMode" = "ForwardBase"}
			Cull Back
			// 关闭深度写入,设置混合模式
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha

			CGPROGRAM
			#include "Lighting.cginc"

			#pragma vertex vert
			#pragma fragment frag

			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed _AlphaScale;

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float2 uv : TEXCOORD2;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				// 纹理采样
				fixed4 texColor = tex2D(_MainTex, i.uv);
				
				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
			}

			ENDCG
		}
	}
	Fallback "Transparent/VertexLit"
}

我们使用了两个Pass,第一个Pass只渲染背面,第二个Pass只渲染正面。这样做可以保证背面总是在正面渲染之前渲染,从而可以得到正确的深度渲染关系。

你可能感兴趣的:(Unity3D学习&优化)