Unity透明效果原理与实现
一、前提知识
(1)深度缓存
它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。
(2)颜色缓存
需要渲染的场景的每一个像素都最终写入该缓冲区,然后由他渲染到屏幕上显示。
(3)渲染队列
Unity为了解决渲染顺序的问题提供了渲染顺序这一解决方案。我们可以使用SubShader的Queue标签来决定我们的模型属于哪一个渲染队列。Unity内部使用一系列的整数索引来表示每一个渲染队列。且索引号越小表示越早被渲染。
unity内置的渲染队列.png
二、透明度测试
(1)概念
它采用一种“霸道极端”的机制,只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等
(2)效果
透明度测试.png(3)实现
Shader "Unity Shaders Book/Chapter 8/Alpha Test"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Main Tint",Color) = (1,1,1,1)
//透明度参考值
_Cutoff ("Alpha Cutoff",Range(0,1)) = 0.5
}
SubShader
{
// Queue 设置渲染队列为透明度测试队列 IgnoreProjector 表示是否受投影器(Projectors)的影响 TransparentCutout提前定义的组
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 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);
//透明度测试
clip(texColor.a - _Cutoff);
//函数实现
//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"
}
三、透明度混合
(1)概念
这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。注意事项:渲染顺序,关闭深度写入
(2)为什么要关闭深度写入?
如果不关闭深度写入,一个半透明表面背后的表面本来可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面会被剔除,我们也就无法透过半透明表面看到后面的物体了。
(2)为什么渲染顺序很重要?
A为透明物体B为不透明物体.pngAB都为透明物体.png
第一种情况:先渲染B,再渲染A。那么由于不透明物体开启了深度测试和深度检验,而此时深度缓冲中没有任何有效数据,因此B首先会写入颜色缓冲和深度缓冲。随后我们渲染A,透明物体仍然会进行深度测试,因此我们发现和B相比A距离摄像机更近,因此,我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合,得到正确的半透明效果。
第二种情况:先渲染A,再渲染B。渲染A时,深度缓冲区中没有任何有效数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,它发现“咦,深度缓冲中还没有人来过,那我就放心地写入颜色缓冲了!”,结果就是B会直接覆盖A的颜色。从视觉上来看,B就出现了A的前面,这是错误的。
(3)渲染顺序结论
(1)先渲染所有不透明的物体,并开启它们的深度测试和深度写入。
(2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
(3)以上两种是unity渲染顺序的基本常识,为了解决更多更复杂的渲染顺序问题,我们应该使用unity为我们提供的渲染队列(一、前提知识-(3)渲染队列)。
(4)透明度混合命令,混合因子,混合操作介绍
(1)透明度混合命令( Blend )
在设置混合因子的同时也开启了混合模式。这是因为,只有开启了混合之后,设置片元的透明通道才有意义。
透明度混合命令.png
(2)透明度混合因子
透明度混合因子.png
(3)透明度混合操作
透明度混合操作.png
(4)透明度混合示例
例如: 正常(Normal),即透明度混合 Blend SrcAlpha OneMinusSrcAlpha
NewColor = SrcColor ×SrcAlpha + DstColor * (1-SrcAlpha)
正常(Normal).png
变亮(Lighten) BlendOp Max Blend One One
NewColor =Color( max(SrcColor.r,DstColor.r),max(SrcColor.g,DstColor.g),max(SrcColor.b,DstColor.b),max(SrcColor.a,DstColor.a))
(5)透明度混合效果
透明度混合效果.png(6)透明度混合实现
Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Main Tint",Color) = (1,1,1,1)
//透明度参考值
_AlphaScale ("Alpha Cutoff",Range(0,1)) = 1
}
SubShader
{
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Pass
{
Tags {"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 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/Cutout/VertexLit"
}
(7)开启深度写入的透明度混合效果
在一些情况下,由于我们关闭了深度写入,会给我们带来各种复杂的问题。例如:当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样的因为排序错误而产生的错误的透明效果。如下图: 错误的透明效果.png这是因为我们模型无法进行像素级别的深度排序。于是我们可以使用两个pass来渲染这个模型:第1个pass开启深度写入,但是不向颜色缓存中输入任何颜色,目的就是为了通过深度缓存的规则,把该模型的深度值写入深度缓存中,这样就可以将模型自身被遮挡住的片元剔除掉。第2个pass就进行正常的透明度混合就行了。
代码实现:
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"}
// 第一个pass的作用仅仅是开启深度写入
Pass {
ZWrite On
//然后我们使用了一个新的渲染命令——ColorMask。在ShaderLab 中,ColorMask用于设置颜色通道的写掩码,为0时表示不写入任何颜色
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;
};
//顶点着色器,主要是坐标转换以及UV值
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"
}
(8)双面渲染的透明度混合效果
在正常情况下,我们观察一个透明物体的时候,不仅仅可以看到透明物体的正面,也可以透过他的正面看到物体的背面,在上面的实例中不管是透明度测试还是透明度混合,我们都只能看到物体的一面,看起来物体就像只有半个。这是因为在unity中默认是剔除物体背面的渲染图元的,只渲染物体的正面图元。所以想要得到双面渲染的效果,需要我们在shader中手动的设置哪一面需要被渲染。
指令:Cull
Cull Back :背对着摄像机的渲染图元不会被渲染
Cull Front :面朝着摄像机的渲染图元不会被渲染
Cull Off : 关闭剔除功能,所有的图元都将被渲染
透明度测试的双面渲染代码:
Shader "Unity Shaders Book/Chapter 8/Alpha Test With Both Side" {
Properties {
_Color ("Color Tint", 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" }
// 关闭剔除功能 双面渲染
Cull Off
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);
//透明度测试
clip (texColor.a - _Cutoff);
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"
}
透明度混合的双面渲染
对于透明度混合也是直接关闭剔除功能吗?显然是不行的,因为在透明度混合时需要关闭深度写入,所以直接关闭剔除功能,我们不能保证同一个物体正面与背面的图元的渲染顺序,有可能得到错误的半透明效果。为了确保同一个物体的正面与背面的图元以正确的渲染顺序渲染,我们选择把双面渲染的工作分成两个pass,第一个只渲染背面,第二个只渲染正面。因为在SubShader中,pass是顺序执行的。这样可以确保背面在正面之前被渲染。
代码实现:
Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
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"}
Pass {
Tags { "LightMode"="ForwardBase" }
// 不渲染正面图元
Cull Front
// 关闭深度写入
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
}
Pass {
Tags { "LightMode"="ForwardBase" }
// 不渲染背面图元
Cull Front
// 关闭深度写入
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"
}