一.需要知道的概念
1.深度缓存
它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在于深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)。
2.颜色缓存
需要渲染的场景的每一个像素都最终写入该缓冲区,然后由他渲染到屏幕上显示。
3.透明实现
实时渲染中,深度缓存(depth buffer,也称z-buffer)是用于解决可见性问题的,可以决定哪个问题的的哪个部分会被渲染到前面。
unity有两种实现透明效果的方式:
1.透明度测试(Alpha Test)
2.透明度混合(Alpha Blend)
4.unity shader的渲染顺序
为了解决渲染顺序的问题提供了渲染队列。可以使用SubShader的Queue标签决定模型归于哪个渲染队列。 Unity内部使用一些整数索引来表示每个渲染队列,索引号越小越早被渲染。
5.为什么渲染顺序很重要?
第一种情况:先渲染B,再渲染A。结果:能得到正确的渲染效果。
第二种情况:先渲染A,再渲染B。渲染A时,深度缓冲区中没有任何有效数据,因此A直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此A不会修改深度缓冲。等到渲染B时,B会进行深度测试,它发现“咦,深度缓冲中还没有人来过,那我就放心地写入颜色缓冲了!”,结果就是B会直接覆盖A的颜色。从视觉上来看,B就出现了A的前面,这是错误的。
渲染常用顺序:
a.先渲染所有不透明物体,开启深度测试和深度写入。
b,半透明物体按离摄像机距离远近排序,从后往前渲染,开启深度测试,关闭深度写入。
c.以上两种是unity渲染顺序的基本常识,为了解决更多更复杂的渲染顺序问题,我们应该使用unity为我们提供的渲染队列
部分相互重叠的物体不适用,解决方法是分割网格。
二.透明度测试(Alpha Test)
概念:只要一个片元的透明度不满足条件(通常是小于某个阈值),对应的片元就会舍弃,不会对颜色缓冲产生影响。因此,透明度测试的效果比较极端: 要么完全透明,要么完全不透明。
一般在片元着色器中使用clip函数进行透明度测试。定义如下:
函数:void clip(float)
参数:裁剪时使用的标量或者矢量条件
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。
实现如下:
Shader "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 {
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"
}
效果如下图
三.透明度混合(Alpha Blend)
1概念
使用当前的片元的透明度作为混合因子,与存储在颜色缓存中的颜色值进行混合,得到新的颜色。透明度混合需要关闭深度写入(ZWrite),不关闭深度测试。即,如果深度测试不通过,就不会再进行混合操作。对于透明度混合来说,深度缓冲是只读的。
2.为什么要关闭深度写入 ?
如果不关闭深度写入,一个半透明表面背后的表面本来可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面会被剔除,我们也就无法透过半透明表面看到后面的物体了。
4.混合命令---Blend
为了进行混合,需要使用unity提供的混合命令---Blend(设置混合模式的命令)。
设置混合因子的时候,也同时开启了混合模式。
对于Blend SrcFactor DstFactor混合命令来说,经过混合后的颜色是:
DstColornew = SrcAlpha * SrcColor + (1 - ScrcAlpha) * DstColorold
a.关闭深度写入的透明度混合实现:
Shader "Alpha Blend" {
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" }
// 关闭深度写入,设置混合模式
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"
}
效果如下:
当模型网格具有交叉结构时往往会得到错误的半透明效果,如下图
这是由于我们关闭了深度写入造成的,因为这样我们就无法对模型进行像素级别的深度排序。
b.开启深度写入的透明度混合实现:
思路:使用2个pass来渲染模型,第一个pass开启深度写入,但不输出颜色,目的仅仅是为了通过深度缓存的规则,把该模型的深度值写入深度缓存中,这样就可以将模型自身被遮挡住的片元剔除掉,第二个pass进行正常的透明度混合。
因为这种做法多使用了一个Pass,因此会对性能造成一定的影响。
Shader "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
//这一句用来设置颜色通道的写入掩码(write mask)
//它的语义如下:
//ColorMask RGB|A|0|其它任何R、G、B、A的组合
//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;
};
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里,我们开启了深度写入,因此可以把模型的深度信息写入到深度缓冲当中,从而剔除模型中被自身遮挡的片元;然后我们使用了ColorMask来设置颜色通道的写掩码(write mask),语法如下:
ColorMask RGB | A | 0 | 其他R,G,B,A的组合
ColorMask R,输出颜色中只有R通道会被写入
ColorMask 0,不会输出任何颜色
默认值为RGBA,即四个通道都写入
效果如下
四.ShaderLab的混合命令
a.基本定义
源颜色——Src:片元着色器产生的颜色值
目标颜色——Dst:从颜色缓冲区取到的颜色值
使用混合的前提是开启混合,Untiy中我们使用了Blend命令(除了Blend Off)时,会设置混合状态并开启混合。
混合的时候需要一个混合等式来计算输出颜色。当进行混合时,我们需要两个混合等式,一个计算RGB值,一个计算A的值。默认情况混合等式使用加操作
使用混合公式Blend SrcFactor DstFactor,SrcFactorA DstFactorA
比如:
Orgb = SrcFactor * Srgb + DstFactor * Dfgb
Oa = SrcFactorA * Sa + DstFactorA * Da
b.混合因子
上面的命令都是RGB通道的混合因子和A通道的混合因子都是一样的,如果希望使用不同的参数来混合Alpha通道,就可以利用:
Blend SrcFactor DstFactor,SrcFactorA DstFactorA
例:
Blend SrcAlpha OnMinusSrcAlpha,One Zero
c.透明度混合操作
Blend命令默认使用加运算来计算输出颜色,我们可以使用ShaderLab的BlendOp BlendOperation命令来使用不同的运混合操作
需要注意的是,当使用Min或者Max混合操作时,混合因子实际上是不起任何作用的,它们仅会判断源颜色和目标颜色之间的比较结果。
透明度混合示例
例如: 正常(Normal),即透明度混合 Blend SrcAlpha OneMinusSrcAlpha
NewColor = SrcColor ×SrcAlpha + DstColor * (1-SrcAlpha)
变亮(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))
五.双面渲染的透明度效果
前面的透明度测试和透明度混合都无法将物体的背面显示出来,因为在unity中默认是剔除物体背面的渲染图元的,只渲染物体的正面图元;如果一个物体是透明的,我们不仅仅可以透过它看到其他物体,我们也应该可以看到它自己的内部结构。
在Unity中,使用Cull指令来控制需要剔除哪个面的渲染图元:
Cull Back | Front | Off
Cull Back,剔除背对着相机的图元;
Cull Front,剔除正面;
Cull Off,关闭剔除功能;
a.透明度测试的双面渲染代码
思路:只需要在pass中的渲染设置中使用Cull指令关闭剔除即可。
Shader "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" }
// Turn off culling
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"
}
效果如下
b.透明度混合的双面渲染代码
把双面渲染分为两个pass,第一个渲染背面,第二个渲染正面,由于shader是顺序执行各个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" }
// First pass renders only back faces
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" }
// Second pass renders only front faces
Cull Back
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"
}