Unity3D学习(八):《Unity Shader入门精要》——透明效果

前言
在实时渲染中要实现透明效果,一般会在渲染模型时控制它的透明通道。
Unity中一般使用两种方法来实现透明 :(1)透明度测试(AlphaTest)(2)透明度混合(AlphaBlend)。前者每每没法实现真正的半透明效果。

深度缓冲(Depth Buffer)html

深度缓冲是用于解决可见性问题的,它能够决定物体的哪些部分渲染在前面,哪些部分被其余物体遮挡。其基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,须要把它的深度值和已经存在深度缓存中的值进行比较(前提是开启了深度测试),若是它的值距离摄像机更远,那么说明它不该该被渲染到屏幕上(被挡住了);不然,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度更新到深度缓冲中(前提是开启了深度写入,Unity中为ZWrite On)。

透明度混合时应关闭深度写入(ZWrite Off)缓存

若是不关闭深度写入,一个半透明表面背后的表面本就是透过它被咱们看到的,但因为深度测试时判断结果是该半透明表面)距离摄像机更近,致使后面的表面会被剔除掉,也就没法经过半透明面观察到后面的物体。app

另外注意关闭深度写入后要考虑物体的渲染顺序。以下图,一个半透明物体A和一个不透明物体B,B在A后方,若是先渲染A再渲染B会出现A被B遮挡的状况,这是错误的。所以渲染顺序在关闭深度写入的状况下极为重要。函数

 

 为了保证渲染顺序正确,渲染引擎通常会对物体进行排序,再渲染,经常使用的方法是测试

  • 1)先渲染全部不透明物体,并开启它们的深度测试和深度写入。
  • 2)把半透明物体按他们离摄像机的远近进行排序,而后按照从后往前的顺序渲染透明物体,并开启它们的深度测试,但关闭深度写入。

但这种方法没法解决物体重叠的状况,所以须要额外的解决方案,好比分割网格等。可是也能够试着让透明通道更柔和,是穿插重叠看起来不那么明显。spa

Unity的渲染顺序3d

Unity经过一组Queue标签来决定模型归于哪一个渲染队列,队列由整数索引表示,索引号越小越先被渲染。code

渲染队列 渲染队列描述 渲染队列值
Background 这个队列被最早渲染。它被用于skyboxes等。 1000
Geometry 这是默认的渲染队列。它被用于绝大多数对象。不透明几何体使用该队列。 2000
AlphaTest 通道检查的几何体使用该队列。它和Geometry队列不一样,对于在全部立体物体绘制后渲染的通道检查的对象,它更有效。 2450
Transparent 该渲染队列在Geometry和AlphaTest队列后被渲染。任何通道混合的(也就是说,那些不写入深度缓存的Shaders)对象使用该队列,例如玻璃和粒子效果。 3000
Overlay 该渲染队列是为叠加效果服务的。任何最后被渲染的对象使用该队列,例如镜头光晕。 4000

透明度测试orm

只要有一个片元的透明度不知足条件(一般是小于某个阈值),那么它对应的片元便会被舍弃,不作任何处理。htm

在Unity中是用以下函数来进行透明度测试:

clip(texColor.a - _Cutoff);

texColor.a为纹理的alpha值,_Cutoff为阈值,该函数等价于

if(texColor.a - _Cutoff < 0)
    discard;

完整的代码 

Shader "Unity Shader Book/Chapter 8/AlphaTest"
{
	Properties
	{
	    _Color("Color Tint",Color) = (1,1,1,1)
		_MainTex ("Texture", 2D) = "white" {}
		_Cutoff("Alpha Cutoff",Range(0,1)) = 0.5
	}
	SubShader
	{
		Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "IgnoreProjector"="True" "LightMode"="ForwardBase"}
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal:NORMAL;
				float4 texcoord : TEXCOORD0;
			};

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

			fixed4 _Color;
			fixed _Cutoff;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);

				fixed4 texColor = tex2D(_MainTex,i.uv);

				//Alpha Test
				clip(texColor.a - _Cutoff);
				//equal to 
				//if(texColor.a - _Cutoff < 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(diffuse + ambient,1.0);
			}
			ENDCG
		}
	}
}

效果以下,能够看到只是单纯的颜色剔除,并不算正常的透明效果。

为了获得正确的透明效果,可使用透明度混合。

透明度混合

透明度混合即将透明物体的源颜色与其表面后方的目标颜色混合,其基本公式为:

DstColor-new(混合后的颜色) = SrcAlpha * SrcColor + (1- SrcAlpha) * DstColor。(SrcAlpha为混合因子)

该公式能够在Unity的ShaderLab语义中表示为

Blend SrcAlpha OneMinusSrcAlpha

更多语义能够参考官方文档

完整代码实现

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unity/Chapter 8/AlphaBlend"
{
	Properties
	{
	    _Color("Color Tint",Color) = (1,1,1,1)
		_MainTex ("Texture", 2D) = "white" {}
		_AlphaScale("Alpha Scale",Range(0,1)) = 0.5
	}
	SubShader
	{
		Tags { "RenderType"="Transparent" "IgnoreProjector"="True" "Queue"="Transparent" }
		LOD 100
		Blend SrcAlpha OneMinusSrcAlpha
		ZWrite On
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			struct appdata
			{
			    float3 normal:NORMAL;
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};

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

			fixed4 _Color;
			fixed _AlphaScale;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
				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"
}

该代码获得的效果以下

嗯,这才是正常的透明效果

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

上述的透明混合并无开启深度写入(ZWrite On),所以若是物体有重叠,那么会产生错误的效果,以下

当模型网格之间有互相交叉的结构时,每每会获得错误的半透明效果

这时候须要在上文的透明度混合代码中的Pass代码块以前,插入一个新的Pass代码块

Pass
{
    ZWrite On
    ColorMask 0  //用于设置颜色通道的写掩码,0表示不写入任何通道
}

这样就会获得正确的效果

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

双面渲染的透明效果

仔细观察上文的透明效果能够发现,咱们并不能透过半透明物体观察它们的内部状况,这是不合乎常理的,所以咱们须要进行双面渲染。

在Unity中,咱们能够经过Cull指令来控制剔除哪一面的渲染图元

Cull Back  //剔除背面,只渲染前面,引擎的默认设置
Cull Front //剔除前面,仅渲染背面
Cull Off    //关闭剔除

透明度测试的双面渲染

仅须要在Tags标签后插入一行

Cull Off

效果对比

透明度混合的双面渲染

将Pass代码块复制一份,一个Pass负责渲染前面,一个Pass负责渲染后面就行,格式以下

Shader "........."{
propeties
{
     /*.....*/
}
SubShader{
     Pass
   {
         Tags{.......}
         Cull Front  //剔除前面
         /*其他代码保持不变
          ....
         */
    }

     Pass
    {
         Tags{.......}
         Cull Back  //剔除后面
         /*其他代码保持不变
           ....
         */
     }

   }  
}

实现的效果以下

相关文章
相关标签/搜索