Unity Shader入门精要——第8章 透明效果

Unity Shader入门精要读书笔记系列

第1章 欢迎来到Shader的世界
第2章 渲染流水线
第3章 Unity Shader基础
第4章 学习Shader所需的数学基础
第5章 开始 Unity Shader 学习之旅
第6章 Unity中的基础光照
第7章 基础纹理
第8章 透明效果



前言

上一章中主要学习了,在Unity shader中各种纹理的应用。本章主要学习Unity中的透明效果是如何实现的。包括透明度测试和透明度混合。

一、Unity中的渲染顺序

对于不透明(Opaque)的物体,由于深度缓冲(z-buffer)的存在。即使不考虑他们的渲染顺序也能得到正确的渲染关系。
深度缓冲根据深度缓存中的值来判断该片元距离摄像机的距离,以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡 。

但是渲染不透明的物体,就没有那么简单了。
这是因为当我们使用透明度混合时,关闭了深度写入。

为啥要关闭深度写入呢?
假设我们需要渲染半透明物体 A 和不透明物体 B,并且开启深度写入。

在这里插入图片描述

第一种情况先渲染 B,再渲染 A:
这种情况下 B首先写入颜色缓冲和深度缓冲。当渲染 A时,发现 A离摄像机更近,通过深度测试。
然后使用 A的透明度与 B的颜色进行混合,这种情况得到正确的半透明效果。
第二种情况先渲染 A ,再渲染 B:
这种情况下 A首先写入颜色缓冲和深度缓冲。当渲染 B时,发现 A离摄像机更近,不能通过深度测试。
这时候B的片元就被舍弃了,没有得到正确的半透明效果。

为了防止这种情况,渲染通常遵循以下规则:
1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。
2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
3)由于当两个网格覆盖的时候,无法得到正确的遮挡关系,此时需要进行网格分割。或者将复杂的模型拆分成可以独立排序的多个子模型。
4)如果不想分割网格,可以尝试让透明通道更加柔和,使穿插看起来不那么明显。
在这里插入图片描述
Unity 为了解决渲染顺序的问题提供了渲染队列 (render queue) 这一解决方案。
我们可以使用SubShader Queue 标签来决定我们的模型将归于哪个渲染队列。

									unity提前定义的5个渲染队列
名称 队列索引号 描述
Background 1000 这个渲染队列会在任何其他队列之前被渲染,我们通常使用该队列来渲染那些需要绘制在背景上的物体
Geometry 2000 默认的渲染队列,大多数物体都使用这个队列。不透明物体使用这个队列
AlphaTest 2450 需要透明度测试的物体使用这个队列。在 Unityu5 中它从 Geometry 队列中被单独分出来,这是因为在所有不透明物体渲染之后再渲染它们会更加高效
Transparent 3000 这个队列中的物体会在所有 Geometry 和 Alpha Test 物体涫染后,再按从后往前的顺序进行渲染。任何使用了透明度混合(例如关闭了深度写入的 Shader 的物体都应该使用该队列
Overlay 4000 该队列用于实现 些叠加效果。任何需要在最后渲染的物体都应该使用该队列

如果我们想要通过透明度测试实现透明效果:

SubShader {
    
    
        Tags {
    
      "RenderType"="AlphaTest"  }
        Pass {
    
    ...}
}

如果我们想要通过透明度混合来实现透明效果:

SubShader {
    
    
        Tags {
    
      "RenderType"="Transparent"  }
        Pass 
        {
    
    
        // 也可以写在SubShader中对所有Pass都生效
        ZWri Off
        ...
        }
}

二、透明度测试

透明度测试是一个很极端的做法:通过测试的按照不透明物处理,未通过测试的片元直接舍弃,
不做任何处理,表现为透明。

clip函数通过discard 指令剔除透明度小于某个阈值的片元

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
    
    
	Properties {
    
    
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {
    
    }
		_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
	}
	SubShader {
    
    
		//使用了透明度测试的都应该设置这三个标签
		//RenderType 标签可以让 Unity 把这个 Shader 归入到提前定义的组(这里就是 TransparentCutout 组)中
		//,以指明该 Shader是一个使用了透明度测试的 Shader
		// IgnoreProjector 设置为 True, 这意味着这个 Shader 不会受到投影器 (Projectors) 的影响
		Tags {
    
    "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
		
		Pass {
    
    
			Tags {
    
     "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			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 = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				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);
				// Equal to 
				//if ((texColor.a - _Cutoff) < 0.0) {
    
    
				//	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"
}

三、透明度混合

使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。透明度混合需要关闭深度写入 ,这使得我们要非常小心物体的渲染顺序。
混合是一个逐片元的操作,高度可配置。

Unity为我们提供了混合语义Blend:
Blend Off:关闭混合
Blend SrcFactor DstFactor :开启混合,并设置混合因子。使用源颜色(该片元产生的颜色)乘以SrcFactor加上目标颜色(已经存在于颜色缓存的颜色)乘以DstFactor来更新颜色缓冲区。
Blend SrcFactor DstFactor, SrcFactorA DstFactorA:使用不同的混合因子来混合透明通道。
BlendOp BlendOperation:使用BlendOperation对源颜色和目标色进行混合。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
    
    
	Properties {
    
    
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {
    
    }
		_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
	}
	SubShader {
    
    
		//使用了透明度混合的Shader都应该设置这三个标签
		Tags {
    
    "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		
		Pass {
    
    
			Tags {
    
     "LightMode"="ForwardBase" }

			ZWrite Off
			// 开启透明度混合
			Blend SrcAlpha OneMinusSrcAlpha
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			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 = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				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进行正常的透明度混合,由于上第一个Pass得到了逐像素的正确的深度信息,该 Pass 就可以按照像素级别的深度排序结果进行透明渲染。
但种方法的缺点在于,多使用一个 Pass 会对性能造成一定的影响。
而且模型内部之间不会有任何真正的半透明效果。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
    
    
	Properties {
    
    
		_Color ("Color Tint", 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"}
		
		// Extra pass that renders to depth buffer only
		Pass {
    
    
			ZWrite On
			//ColorMask 设为 0 时,意味着该 Pass 不写入任何颜色通道,即不会输出任何颜色
			ColorMask 0
		}
		
		Pass {
    
    
			Tags {
    
     "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			
			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 = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				
				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"
}

在这里插入图片描述

四、双面渲染的透明效果

在前面实现的透明效果中 无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个。
这是因为默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的渲染图元。

Cull Back I Front I Off // 剔除背面 | 正面 | 关闭剔除

对于透明度测试,我们只需要使用 Cull Off 关闭剔除,就可以得到双面渲染的效果。

对于透明度混合,由于关闭了深度写入,我们无法保证图元是从后往前渲染的。
因此我们需要分别使用两个Pass进行渲染。第一个Pass渲染背面,第二个Pass渲染正面。
这是因为Unity 会顺序执行 SubShader 中的各个 Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。

Pass {
    
    
	Cull Front
	// 和之前一样的代码
}
Pass {
    
    
	Cull Back
	// 和之前一样的代码
}

在这里插入图片描述
可以看到,实现了双面渲染的透明效果可以看到模型的内部结构。

五、补充:常见的混合操作

前面我们提到,使用 Blend 加混合因子的形式进行混合。
在这里插入图片描述
混合的时候会使用两个混合等式分别对源颜色(当前片元颜色)和目标颜色(颜色缓冲区颜色)的RGB通道和A通道进行混合。因此我们需要4个混合因子。
当只指定两个混合因子时,对RGB通道和A通道使用相同的混合因子进行混合。

ShaderLab中的混合因子:

参数 描述
One 因子为1
Zero 因子为0
SrcColor 因子为源颜色值。 当用于混合RGB的混合等式时, 使用SrcColor的RGB分量作为混合因子;当用于混合A的混合等式时,使用SrcColor的A分量作为混合因子。
SrcAlpha 因子为源颜色值的A通道值。
DstColor 因子为目标颜色值。 当用于混合RGB的混合等式时, 使用DstColor 的RGB分量作为混合因子;当用于混合A的混合等式时,使用DstColor 的A分量作为混合因子。
DstAlpha 因子为目标颜色值的A通道值。
OneMinusSrcColor 因子为(1 - 源颜色),当用于混合RGB的混合等式时, 使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子。
OneMinusSrcAlpha 因子为(1 - 源颜色)的A通道值
OneMinusDstColor 因子为(1 - 目标颜色),当用于混合RGB的混合等式时, 使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子。
OneMinusDstAlpha 因子为(1 - 目标颜色)的A通道值

使用混合因子对源颜色和目标颜色混合后,需要进行混合操作。默认为相加。

ShaderLab中的混合操作:

参数 描述
Add 将混合后的源颜色和目标颜色相加。 默认的混合操作。
Sub 用混合后的源颜色减去混合后的目标颜色。
RevSub 用混合后的目标颜色减去混合后的源颜色。
Min 使用源颜色和目标颜色中较小的值, 是逐分量比较的。
Max 使用源颜色和目标颜色中较大的值, 是逐分量比较的。

在这里插入图片描述
得到的效果如下:
在这里插入图片描述

注意:虽然上面使用 Min ax 混合操作时仍然设置了混合因子,但实际上它们不会对结果有任何影响,因为 Min ax 混合操作会忽略混合因子。

猜你喜欢

转载自blog.csdn.net/qq_41044598/article/details/126425662