赞
踩
目录
在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel)。开启透明混合后,每个片元除了颜色值和深度值之外,还有透明度属性。透明度为1时表示该像素是完全不透明的,为0时,表示完全透明,也就不会显示。
在Unity我们有两种方法来实现透明效果:1、透明度测试,这种方法无法得到真正的半透明效果;2、透明度混合。
下面讲讲关于渲染顺序的问题,也就是场景中包含很多模型的时候,应该按什么顺序来渲染。对于不透明的物体,不用考虑渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲(depth buffer,也叫z-buffer)的存在。深度缓冲是用于解决可见性问题的,它可以决定物体的哪些部分渲染在前面,哪些部分被其他物体遮挡。其基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,当渲染一个片元时,需要把它的深度值和已经存在深度缓存中的值进行比较(前提是开启了深度测试),如果它的值距离摄像机更远,那么说明它不应该被渲染到屏幕上(被物体挡住了);如果它的值距离摄像机更近,这个片元应该覆盖掉此时颜色缓冲中的像素值,并把它的深度更新到深度缓冲中(前提是开启了深度写入(ZWrite On))。但如果要实现透明效果,就要使用透明度混合,这个时候要关闭深度写入。
前面说到,对于透明度混合技术,需要关闭深度写入,所以我们就需要小心处理透明物体的渲染顺序。如果不关闭深度写入,一个半透明物体的背后的物体本来是可以看到的,但是由于深度测试时判断该半透明表面距离摄像机更近,会导致后面的物体被剔除,也就无法透过半透明表面看到后面的物体了。但也因此就破坏了深度缓存的工作机制,关闭深度写入也就导致渲染顺序变得非常重要。
如图8.1所示,A半透明,B不透明,我们来讨论渲染顺序对结果的影响
同理8.2所示,两个物体都是半透明物体的话,先渲染A再渲染B也会出现错误,得到一个B在A前面的半透明结构。所以半透明物体之间也要符合一定的渲染顺序。
基于上面两个例子,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:
Unity提供了渲染队列的解决方案。我们可以使用SubShader的Queue标签来决定我们的模型将归于哪个队列。
名称 | 队列索引号 | 描述 |
Background | 1000 | 这个渲染队列会在其他队列之前被渲染,绘制需要绘制在背景上的物体 |
Geometry | 2000 | 默认的渲染队列,大多数物体可使用这个队列。不透明物体使用这个序列 |
AlphaTest | 2450 | 需要透明度测试的物体。在不透明物体渲染后再渲染这个队列会更高效 |
Transparent | 3000 | 队列中的物体会在所有Geometry和AlphaTest物体渲染后再从后往前的顺序渲染。任何使用了透明度混合(例如关闭了深度写入的Shader)的物体都应使用该队列 |
Overlay | 4000 | 用于实现一些叠加效果。最后渲染的物体。 |
通过透明度测试或者透明度混合实现透明效果的应该包含类似下面的代码:
- //透明度测试
- SubShader{
- Tags{"Queue" = "AlphaTest"}
- Pass{...}
- }
-
- //透明度混合
- SubShader{
- Tags{"Queue" = "Transparent"}
- Pass{
- ZWrite Off //用于关闭深度写入,也可以放在SubShader里面,这意味着该SubShader下的所有Pass都会关闭深度写入
- }
- }
透明度测试:只要有一个片元的透明度不满足条件(通常是小于某个阈值),那么其对应的片元将会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响,否则就会按照普通的不透明物体的处理方式来处理它。根据描述,我们发现透明度测试是不需要关闭深度写入,但产生的效果也很极端,要么完全透明,要么完全不透明。
我们一般使用clip函数来进行透明度测试。clip是CG当中的一个函数:void clip(float4 x),里面的参数还可以是float3、float2、float2、float1、float。如果给定参数的任何一个分量是负数,那么就会舍弃当前像素的输出颜色,相当于下面的代码:
- void clip(float4 x)
- {
- if(any(x<0))
- discard;
- }
本节我们要实现下面的效果:
- Shader "MyShader/Chapter 8/Alpha Test" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- _MainTex ("Main Tex", 2D) = "white" {}
- //_Cutoff 参数用于决定我们调用 clip 进行透明度测试时使用的判断条件,它的范围是[O, 1],这是因为纹理像素的透明度就是在此范围内。
- _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
- }
- SubShader {
- //把Queue标签设置为AlphaTest
- //把IgnoreProjector设置为true,意味着这个Shader不会受到投影器(Properties)的影响
- //RenderType标签可以让Unity把这个Shader归入到提前定义的组(TransparentCutout组),以指明该Shader是一个使用了透明度测试的Shader
- //使用了透明度测试的Shader都应该在SubShader设置三个标签
- Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
-
- Pass {
- //LightMode用于定义该Pass在Unity的光照流水线中的角色。只有正确定义LightMode,才能正确得到一些Unity的内置光照,如_LightColor0
- Tags { "LightMode"="ForwardBase" }
-
- CGPROGRAM
-
- #pragma vertex vert
- #pragma fragment frag
-
- #include "Lighting.cginc"
-
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- fixed _Cutoff;//由于_Cutoff 范围在[O I], 因此我们可以使用 fixed精度来存储它。
-
- 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);//如果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"//设置回调,这个回调在后面第九章节会讲
- }

这部分的代码大部分前面的章节都学过,就多了_Cutoff变量的声明、SubShader里标签的设置、片元着色器中的透明度测试(clip函数)。
我们可以通过调节材质面板的Alpha cutoff的值(也就是透明度测试的阈值),达到不同程度的剔除。
透明度混合:这种方法可以得到真正的半透明效果 。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深 度写入 ,这使得我们要非常小心物体的渲染顺序。
为了进行混合,我们需要使用Unity提供的混合命令——Blend。Blend是Unity提供的设置混合模式的命令。下面是一些Blend命令的语义。
语义 | 描述 |
---|---|
Blend off | 关闭混合 |
Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色(该片元产生的颜色) 会乘以SrcFactor,而且目标颜色 (已经存在于颜色缓冲的颜色)会乘以DstFactor,任何把两者相加后再存入颜色缓冲中 |
Blend SrcFactor DstFactor, SrcFactorA DstFactorA | 和上面几乎一样,只是使用不同的因子来混合透明通道 |
BlendOp BlendOperation | 不只是把源颜色和目标颜色简单相加后混合,而是使用BlendOperation对他们进行其他操作 |
下面的实践就使用了第二个语义。
实现的效果如下:
- Shader "MyShader/Chapter 8/Alpha Blend" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- _MainTex ("Main Tex", 2D) = "white" {}
- //我们使用一个新的属性_AlphaScale 来替代原先的_Cutoff 属性。_AlphaScale用于在透明纹理的基础上控制整体的透明度。
- _AlphaScale ("Alpha Scale", Range(0, 1)) = 1
- }
- SubShader {
- //把Queue标签设置为Alphaparent
- //把IgnoreProjector设置为true,意味着这个Shader不会受到投影器(Properties)的影响
- //RenderType标签可以让Unity把这个Shader归入到提前定义的组(这里就是Transparent组),以指明该Shader是一个使用了透明度混合的Shader
- //使用了透明度测试的Shader都应该在SubShader设置这三个标签
- Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
-
- Pass {
- Tags { "LightMode"="ForwardBase" }
- //为透明度混合进行合适的混合状态设置
- //关闭深度写入
- ZWrite Off
- //使用了Blend SrcFactor DstFactor语义
- //把源颜色(该片元着色器产生的颜色)的混合因子SrcFactor设为SrcAlpha, 而目标颜色(已经存在于颜色缓冲中的颜色)的混合因子 DstFactor设为 OneMinusSrcAlpha 。
- 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));
-
- //设置了该片元着色器返回值中的透明通道,它是纹理像素的透明通道和材质参数_AlphaScale的乘积。
- return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
- }
-
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }

下图给出了不同 Alpha Scale 参数下的半透明效果。
但是网格之间有互相交叉的结构时,往往会得到错误的半透明效果(如下图)。下面我们来解决这个问题
在上面我们给出了一种由于关闭深度写入造成的错误排序的情况。一种解决方法就是使用两个Pass来渲染模型:
这种方法的缺点就是多使用一个Pass会对性能造成一定的影响。使用了这种方法就可以得到下面的效果。可以看出仍然可以实现模型与后面的背景混合的效果,但是模型内部之间不会有任何的半透明效果。
实现起来也非常简单,我们只要在前面透明度混合的代码的基础上,在原有的Pass前面加一个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 0
- }
-
- Pass {
- ......//和前面的代码内容一样
- }
-
- ENDCG
- }
- }
- FallBack "Transparent/VertexLit"
- }

这个新加的Pass的目的只是为了把模型的深度信息写入深度缓冲,从而剔除模型中被自身遮挡的片元。ZWrite On用来开启深度写入。ColorMask用来设置颜色通道的掩码(write mask)。语义如下:
ColorMask RGB | A | 0 | 其他任何R、G、B、A的组合
当ColorMask设为0时,意味着该Pass不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的——该Pass只需写入深度缓存即可。
混合和两个参数有关:源颜色(source color)和目标颜色(destination color)。源颜色我们用S表示,指的是有片元着色器产生的颜色值;目标颜色我们用D来表示,值的是从颜色缓冲中读取到的颜色值。它们两者进行混合后得到的输出颜色,我们用O来表示,这个O会重新写入到颜色缓冲中。这四种颜色都包含了RGBA四个通道(RGB+Alpha透明通道)。
混合是一个逐片元的操作,而且它不是可编程的,但却是高度可配置的。我们可以提供设置混合时使用的运算操作、混合因子等来影响混合。
我们要将S和D混合得到O,需要一个等式来计算,这个等式就称为混合等式(blend equation)。混合时我们需要两个混合等式,一个混合RGB通道,一个混合A通道。设置混合状态时,实际上就是设置混合等式的操作和因子。默认情况下混合等式使用的操作都是加操作,所有一般只需要设置一下混合因子就可以了。一个等式需要两个因子(一个用于和源颜色相乘,一个用于和目标颜色相乘),所有两个混合等式就要4各因子。ShaderLab有关设置混合因子的命令如下:
语义 | 描述 |
---|---|
Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色(该片元产生的颜色) 会乘以SrcFactor,而且目标颜色 (已经存在于颜色缓冲的颜色)会乘以DstFactor,任何把两者相加后再存入颜色缓冲中 |
Blend SrcFactor DstFactor, SrcFactorA DstFactorA | 和上面几乎一样,只是使用不同的因子来混合透明通道 |
第二个命令Blend后面有四个参数SrcFactor DstFactor,SrcFactorA DstFactorA,分别代表与S的RGB通道相乘的因子、与D的RGB相乘的因子、与S的A通道相乘的因子、与D的A通道相乘的因子。第一个命令只提供了两个因子,意味着使用相同的混合因子来混合RGB通道和A通道,也就是SrcFactorA = SrcFactor、DstFactorA = DstFactor。混合公式如下:
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-源颜色的透明度值) |
OneMinusDstColor | 因子为(1-目标颜色)。当用于混合RGB的混合等式时,使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子 |
OneMinusSrcAlpha | 因子为(1-目标颜色的透明度值) |
前面的默认的混合公式都是加法,当然我们也可以使用其他的一些混合操作命令(BlendOp BlendOperation)
- Blend SrcAlpha OneMinusSrcAlpha //正常(Normal),即透明度混合
-
- Blend OneMinusDstColor One //柔和相加(Soft Additive)
-
- Blend DstColor Zero //正片叠底(Multiply),即相乘
-
- Blend DstColor SrcColor //两倍相乘(2x Multiply)
-
- BlendOp Min
- Blend One One //变暗(Darken)虽然使用了混合因子,但对结果没有任何影响,因为使用了Min
-
- BlendOp Max
- Blend One One //变亮(Lighten)虽然使用了混合因子,但对结果没有任何影响,因为使用了Max
-
- Blend OneMinusDstColor One //滤色(Screen)等同于 Blend One OneMinusSrcColor
-
- Blend One One //线性减淡(Linear Dodge)

与前三.2的透明度测试代码基本一致,只多了一行代码:
- Pass {
- Tags { "LightMode" = "ForwardBase" }
-
- //使用了Cull语句,关闭了剔除功能,使该物体的所有渲染图元都会被渲染。
- Cull Off
下面是使用了双面渲染和没使用双面渲染的区别,我们发现使用了双面渲染可以透过镂空区域看到内部渲染结果。
因为透明度混合关闭了深度写入,所以我们要保证图元是从后往前渲染的。但我们直接关闭剔除功能无法保证一个物体的渲染顺序。
为此我们把双面渲染的工作分成两个Pass——第一个Pass只渲染背面,第二个Pass只渲染正面,Unity会执行SubShader中的各个Pass,所以我们可以保证背面总是在正面被渲染之前渲染。
实现起来,其实就是在四、2中的代码中把Pass语句复制了一份,然后在两个Pass中分别使用Cull指令剔除不同朝向的渲染图元:
- Shader "MyShader/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 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"
- }

最后我们会得到下面的效果,左边使用了双面渲染,右边没有使用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。