透明效果
透明是游戏中经常要使用的一种效果。在实时渲染中要实现透明效果,通常会在渲染模型时 控制它的透明通道 (Alpha Channel) 。当开启透明混合后,当一个物体被渲染到屏幕上时,每个 片元除了颜色值和深度值之外,它还有另 个属性一透明度 。当透明度为1 时,表示该像素是 完全不透明的,而当其为 0时,则表示该像素完全不会显示。 Unity 中,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试(Alpha Test), 这种方法其实无法得到真正的半透明效果;另一种是透明度混合 (Alpha Blending )
对 于不透明 (opaque) 物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度 缓冲 depth buffer, 也被称为 z-buffer) 的存在。在实时渲染中 ,深度缓冲是用千解决可见性 visibility 问题的,它可以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡 。它的基本思想是 根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染 一个片元时, 需要把它的 深度值和已经存在千深度缓冲中的值进行比较(如果开启了深度测试),如果它的值距离摄像机更 远,那么说明这个片元不应该被渲染到屏幕上(有物体挡住了它);否则,这个片元应该覆盖掉此 时颜色缓冲中的像素值,并把它的深度值更新到深度缓冲中(如果开启了深度写入)
简单来说,透明度测试和透明度混合的基本原理如下。
透明度测试:
只要 一个片元的透明度不满足条件(通常 是小于某个阙值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理, 也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它, 即进行深度测试、深度写入等。也就是说,透明度测试是不需要关闭深度写入的,它和其 他不透明物体最大的不同就是它会根据透明度来舍弃 些片元。虽然简单,但是它产生的 效果也很极端,要么完全透明,即看不到,要么完全不透明,就像不透明物体那样。
透明度混合:
这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合 因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需 要关闭深度写入(我们下面会讲为什么需要关闭),这使得我们要非常小心物体的渲染顺 序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当 使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如 果它的深度值距离摄像机更远,那么就不会再进行混合操作。这一点决定了,当 个不透 明物体出现在 个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常地遮挡 住透明物体。也就是说,对千透明度混合来说,深度缓冲是只读的。
渲染顺序
假设场景里有两个物体A和B, 如图8.1所示, 其中A是半透明 物体, 而B是不透明物体
我们来考虑不同的渲染顺序会有什么结果。
第一种情况
我们先渲染B, 再渲染A。那么由于不透明物体开启了深度测试和深度写入, 而此时深度缓冲中没有任何有效数据, 因此B首先会写入颜色缓冲和深度缓冲。 随后我 们渲染A, 透明物体仍然会进行深度测试, 因此我们发现和B相比,A距离摄像机更近, 因此, 我们会使用A的透明度来和颜色缓冲中的B的颜色进行混合, 得到正确的半透明 效果。
第二种情况
我们先渲染A, 再渲染B。 渲染A时, 深度缓冲区中没有任何有效数据, 因此A直接写入颜色缓冲, 但由于对半透明物体关闭了深度写入, 因此A不会修改深度 缓冲。 等到渲染B时,B会进行深度测试, 它发现,“咦, 深度缓存中还没有人来过, 那 我就放心地写入颜色缓冲了! "' 结果就是B会直接覆盖A的颜色。 从视觉上来看, B就 出现在了A的前面, 而这是错误的。
从这个例子可以看出, 当关闭了深度写入后, 渲染顺序是多么重要。 由此我们知道, 我们应 该在不透明物体渲染完之后再渲染半透明物体。那么,如果都是半透明物体,渲染顺序还重要吗? 答案是肯定的。还是假设场景里有两个物体A和B, 如图8.2所示,其中A和B都是半透明物体。
我们还是考虑不同的渲染顺序有什么不同的结果
第一种情况
我们先渲染B, 再渲染A。 那么B会正常写入颜色缓冲, 然后A会和颜色 缓冲中的B颜色进行混合, 得到正确的半透明效果。
第二种情况
我们先渲染A, 再渲染B。 那么A会先写入颜色缓冲, 随后B会和颜色缓 冲中的A进行混合, 这样混合结果会完全反过来, 看起来就好像B在A的前面, 得到的 就是错误的半透明结构。 从这个例子可以看出, 半透明物体之间也是要符合一定的渲染顺序的
渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是。
先渲染所有不透明物体,并开启它们的深度测试和深度写入。
把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半 透明物体,并开启它们的深度测试,但关闭深度写入。
UnityShader的渲染顺序
Unity使用渲染队列(render queue)解决渲染顺序问题。通过使用SubShader的Queue标签决定渲染的模型属于哪一个渲染队列。Unity内部使用一系列索引号表示渲染顺序,索引号越小渲染越早。
若是使用透明度测试实现透明效果:
SubShader{ Tags{"Queue"="AlphaTest"} Pass{ } }
若是使用透明度混合实现透明效果:
SubShader{ Tags{"Queue"="Transparent"} Pass{ Zwrite Off//用于关闭深度写入 } }
使用透明度混合是需要关闭深度写入的。
透明度测试(Alpha Test)实例
透明度测试: 只要一个片元的透明度不满足条件(通常是小于某个阅值) 那么它对应的片元 就会被舍弃。被舍弃的片元将不会再进行任何处理 也不会对颜色缓冲产生任何影响 否则 会按照普通的不透明物体的处理方式来处理它。
透明剪切常用于复杂轮廓,明确边缘的物体表现 ,如:镂空金属,裙摆边缘,特定风格下的头发啊啊,树叶等等。
也常用于卡通渲染的特效表现。
透明剪切有 没有排序的问题,但是边缘效果太实,移动端性能较差。
AlphaBlend中,树叶的边缘比较柔和透明,并且中心部位的排序比较混乱。而AlphaCutout中,边缘清晰并且镇中心排序整正确。
AlphaCutout的边缘类似于1和0,1是开,0是关。而AlphaBlend则是边缘是1-0。是从0到1的一个范围,有类似于渐变的效果。
通常在片元着色器中使用clip函数进行透明度测试。函数定义:
void clip(float x){ if(any(x<0)) discard; }
代码
Shader "Unlit/VS_Shader_AlphaTest"{
Properties{
_Color("Color",Color) = (1,1,1,1)
_MainTex("MainTex", 2d) = "white"{}
_Cutoff("Cutoff", range(0.0, 1.0)) = 0.5
}
SubShader{ //SubSgader是可以有多个的,你可以在一个Sahder里面写多个SubShader
Tags {
"Queue" = "AlphaTest"
"RenderType" = "TransparentCutout" //unity内置专门的透明裁剪AC渲染类型。
"IgnoreProjector" = "True" //把投射器的相应关闭
//通常,使用透明度测试的Shader都应该在SubShader中设置这几个标签
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
// 输入参数
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;//声明一个贴图的_XXX_ST来让贴图支持缩放与位移TilingOffset
uniform half _Cutoff;
uniform fixed4 _Color;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
float3 normal:NORMAL;
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //顶点信息
float3 nDirWS:TEXCOORD0;
float3 posWS:TEXCOORD1;
float2 uv0:TEXCOORD2;
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.nDirWS = UnityObjectToWorldNormal(v.normal);
o.posWS = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv0 = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
fixed3 nDirWS = normalize(i.nDirWS);
fixed3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));
fixed4 texColor = tex2D(_MainTex, i.uv0);// 采样贴图 RGB颜色 A透贴
// 透明贴图就是输入颜色贴图的A通道
clip(texColor.a - _Cutoff); // 透明剪切就是透明贴图减去输入可控制的透明裁切阈值
fixed3 albedo = texColor.rgb * _Color;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(nDirWS, lDirWS));
return fixed4(ambient+ diffuse, 1.0); //输出 返回,返回一个值输出
}
ENDCG
}
}
FallBack "Diffuse"
}
采样贴图
自上到下,CutOff值分别为0.65,0.75
漫反射贴图自带透明通道a,使用clip函数根据传入的透明度与阈值的插值判断片元舍弃还是按照不透明处理,可以看出透明度测试效果要么为全透明,要么为完全不透明。
透明度混合
透明度混合:这种方法可以得到真正的半透明效果 。它会使用当前片元的透明度作为混合因 子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深 度写入 ,这使得我们要非常小心物体的渲染顺序
透明混合常用于复杂的轮廓,无明显边缘的物体表现,常用于半透明的物体表现。也用于一般的特效表现。
透明混合的移动端性能较好,边缘过度效果比较好。但是透明混合有透明排序问题,会有后方物体渲染在前列的缺点,需要手动设置透明排序。
Shader "Unlit/AlphaBlender"
{
Properties{
_MainTex("_MainTex", 2d) = "white"{}
_AlphaScale("AlphaScale",Range(0,1)) = 1
}
SubShader{ //SubSgader是可以有多个的,你可以在一个Sahder里面写多个SubShader
Tags {
"Queue" = "Transparent" //调整渲染顺序,设置到透明的渲染队列
"RenderType" = "Transparent" //"RenderType"="Transparent"指明该shader为使用了透明度混合的shader
"IgnoreProjector" = "True" //把投射器的相应关闭
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
Zwrite Off //关闭深度写入
Blend One OneMinusSrcAlpha //修改混合方式One/SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
// 输入参数
uniform sampler2D _MainTex; uniform float4 _MainTex_ST;//声明一个贴图的_XXX_ST来让贴图支持缩放与位移TilingOffset
fixed _AlphaScale;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex); // 顶点位置 OS>CS
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // UV信息 支持面板上的缩放与位移(TilingOffset)
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
half4 var_MainTex = tex2D(_MainTex, i.uv); // 采样贴图 RGB颜色 A透贴
return (var_MainTex* _AlphaScale); //输出 返回,返回一个值输出
}
ENDCG
}
}
FallBack "Diffuse"
}
只是移除了透明度测试的 代码,并设置了该片元着色 器返回值中的透明通道 它是纹理像素的透明通道和 质参数 AlphaScale 的乘积
Alpha Scale 不同数值下的效果
开启深度写入的半透明效果
当模型本身具有复杂的遮挡关系或者包含复杂非凸网格时,未开启深度写入,会有因为排序错误产生的错误透明效果。一种解决办法是使用两个Pass来对模型进行渲染,第一个Pass开启深度写入,不输出颜色,第二个进行正常透明度混合。
Shader "AlphaBlendZwrite" {
Properties{
_MainTex("MainTex",2D) = "white"{}
_AlphaScale("AlphaScale",Range(0,1)) = 1
}
SubShader{
Tags{"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"}//"RenderType"="Transparent"指明该shader为使用了透明度混合的shader
//开启深度写入的Pass是为了将模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元
Pass{
ZWrite On
ColorMask 0//ColorMask RGB | A | 0 ,设置为0时,表示该Pass不写入任何颜色通道,不会输出任何颜色。
}
Pass{
Tags{"LightMode" = "ForwardBase"}
Zwrite Off //关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子为源的透明度
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct VertexInput {
float4 vertex:POSITION;
float4 uv:TEXCOORD0;
};
struct VertexOutput {
float4 pos:SV_POSITION;
float3 posWS:TEXCOORD0;
float2 uv:TEXCOORD2;
};
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld,v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
return o;
}
float4 frag(VertexOutput i) : COLOR{
fixed4 var_MainTex = tex2D(_MainTex,i.uv);
return fixed4(var_MainTex * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
右侧为添加深度写入的Pass后,模型内部的遮挡效果正常。
透明重叠Addtive·AD
透明重叠常用于发光体,做辉光的表现,一般是特效使用,用作提亮道具等等。
透明重叠也有排序问题,多层叠加会容易堆爆性能(OverDraw),作为辉光效果但是可以用后处理来做代替。
Shader "AlphaBlendZwrite" {
Properties{
_MainTex("MainTex",2D) = "white"{}
_AlphaScale("AlphaScale",Range(0,1)) = 1
}
SubShader{
Tags{"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"}//"RenderType"="Transparent"指明该shader为使用了透明度混合的shader
//开启深度写入的Pass是为了将模型的深度信息写入深度缓冲中,从而剔除模型中被自身遮挡的片元
Pass{
ZWrite On
ColorMask 0
}
Pass{
Tags{"LightMode" = "ForwardBase"}
Zwrite Off //关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子为源的透明度
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct VertexInput {
float4 vertex:POSITION;
float4 uv:TEXCOORD0;
};
struct VertexOutput {
float4 pos:SV_POSITION;
float3 posWS:TEXCOORD0;
float2 uv:TEXCOORD2;
};
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld,v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
return o;
}
float4 frag(VertexOutput i) : COLOR{
fixed4 var_MainTex = tex2D(_MainTex,i.uv);
return fixed4(var_MainTex * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
shaderlab 的混合命令
混合如何实现
例
混合原理
常见的混合类型
Shader "Unlit/VS_Shader_Alpha"
{
Properties {
_MainTex ("RGB:颜色 A:透贴", 2d) = "gray"{}
_Opacity ("透明度", range(0, 1)) = 0.5
[Enum(UnityEngine.Rendering.BlendMode)] //unity自带的源乘子
_BlendSrc ("混合源乘子", int) = 0
[Enum(UnityEngine.Rendering.BlendMode)] //unity自带的目标乘子
_BlendDst ("混合目标乘子", int) = 0
[Enum(UnityEngine.Rendering.BlendOp)] //unity自带的运算符
_BlendOp ("混合算符", int) = 0
}
SubShader { //SubSgader是可以有多个的,你可以在一个Sahder里面写多个SubShader
Tags {
"Queue"="Transparent" //调整渲染顺序,设置到透明的渲染队列
"RenderType"="Transparent" //unity内置的渲染类型。
"ForceNoShadowCasting"="True" //强制关闭阴影投射(特效一般不用投影,如果特殊可以打开)
"IgnoreProjector"="True" //把投射器的相应关闭
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
BlendOp [_BlendOp] // 可自定义混合算符
Blend [_BlendSrc] [_BlendDst] // 可自定义混合模式
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
// 输入参数
uniform sampler2D _MainTex; uniform float4 _MainTex_ST;//声明一个贴图的_XXX_ST来让贴图支持缩放与位移TilingOffset
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//顶点Shader
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos( v.vertex ); // 顶点位置 OS>CS
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // UV信息 支持面板上的缩放与位移(TilingOffset)
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
half4 var_MainTex = tex2D(_MainTex, i.uv); // 采样贴图 RGB颜色 A透贴
return var_MainTex; //输出 返回,返回一个值输出
}
ENDCG
}
}
FallBack "Diffuse"
}
双面渲染的透明效果
默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的 面。如果我们想要得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的渲染图元。
Cull | Back | Front | Off
设置为Back 背对摄像机的图元不会被渲染,设置为Front朝向摄像机的图元不会被渲染,设置为Off,双面渲染。
透明度测试双面渲染
Pass{ Tags{"LightMode"="ForwardBase"} Cull Off //开启双面渲染效果 }
这行代码的作用是关闭剔除功能 使得该物体的所有的渲染图元都会被渲染。
透明度混合的双面渲染
Shader "Unlit/AlphaBlender"
{
Properties{
_MainTex("_MainTex", 2d) = "white"{}
_AlphaScale("AlphaScale",Range(0,1)) = 1
}
SubShader{ //SubSgader是可以有多个的,你可以在一个Sahder里面写多个SubShader
Tags {
"Queue" = "Transparent" //调整渲染顺序,设置到透明的渲染队列
"RenderType" = "Transparent" //"RenderType"="Transparent"指明该shader为使用了透明度混合的shader
"IgnoreProjector" = "True" //把投射器的相应关闭
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
Zwrite Off //关闭深度写入
Cull Front //先渲染背面
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子为源的透明度
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
// 输入参数
uniform sampler2D _MainTex; uniform float4 _MainTex_ST;//声明一个贴图的_XXX_ST来让贴图支持缩放与位移TilingOffset
fixed _AlphaScale;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex); // 顶点位置 OS>CS
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // UV信息 支持面板上的缩放与位移(TilingOffset)
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
half4 var_MainTex = tex2D(_MainTex, i.uv); // 采样贴图 RGB颜色 A透贴
return (var_MainTex * _AlphaScale); //输出 返回,返回一个值输出
}
ENDCG
}
Pass{
Name "FORWARD"
Tags {"LightMode" = "ForwardBase"}
Zwrite Off //关闭深度写入
Cull Back //先渲染背面
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子为源的透明度
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
// 输入参数
uniform sampler2D _MainTex; uniform float4 _MainTex_ST;//声明一个贴图的_XXX_ST来让贴图支持缩放与位移TilingOffset
fixed _AlphaScale;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //顶点信息
float2 uv : TEXCOORD0; // UV信息 采样贴图用
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex); // 顶点位置 OS>CS
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // UV信息 支持面板上的缩放与位移(TilingOffset)
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
half4 var_MainTex = tex2D(_MainTex, i.uv); // 采样贴图 RGB颜色 A透贴
return (var_MainTex * _AlphaScale); //输出 返回,返回一个值输出
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}