透明渲染顺序及透明效果的处理
不透明物体的渲染顺序
在Unity中为避免被遮挡看不见的部分重复Overdraw浪费性能,需要进行深度测试来剔除隐藏面(消隐)。为此不透明物体采用由近到远的绘制顺序一边写入更新深度缓存一边做深度测试(近的物体深度缓存中的值小),在绘制较远物体时,如果某个片段比已有深度缓存中的值大,说明前面有物体遮挡,需要丢弃这个被遮挡的片段。
半透明物体的渲染顺序
如果同样采用不透明物体的渲染方式会产生第一个问题:半透明和不透明物体的渲染顺序。
- 半透明物体在不透明物体前,由不透明物体的重叠区域会被当作是被遮挡的无效区域进行剔除,正常应该保留进行混合。
- 不透明物体在半透明物体前,不透明物体后的透明物体会被剔除,正常。
第一种情况的本质原因是不透明物体的重叠区域因为深度测试到前方有透明物体于是把自己剔除了,解决错误剔除有几种可能的办法可以解决:
- 可以关闭不透明物体B的深度测试或者让其深度测试总为通过,但这种方式仅适用于B前面没有别的不透明物体,否则B会遮盖住前方的物体,不可取,那么是否可以让B检测前面的物体是不透明就不关闭深度测试呢,实际上不可以,只有在渲染的最后Blending阶段才能知道一个物体是不是半透明的,在深度测试阶段无法知道。
- 正确的方式应该是从半透明物体A入手,不将半透明物体B的深度值写入深度缓存,这样B也就不会知道自己前方存在A,而并不干扰B对其他不透明物体的正常深度测试。
这里需要注意,半透明物体关闭了深度写入但并没有关闭深度测试,当其被不透明物体遮住时还是要正常剔除。
- 现在关闭了半透明物体A的深度写入,B的重叠部分不会被剔除了,但如果还是从近到远的绘制顺序一起绘制不透明和半透明物体,如图不透明物体B会把A盖住(B不知道自己前面有A),这时如果由远到近进行绘制,就可以解决半透明物体在不透明物体前面的情况。
到这里透明物体A在前,不透明B在后的情况已经正确,但回过头来看不透明B在前的情况:
在对A进行深度测试的时候,B还没有写入深度缓存,所以A不知道自己前面有B,自己不会被剔除,虽然最后效果是正确的,但本该剔除的片元错误的保留会造成浪费。所以还是应该从近到远进行绘制,但这就造成了没有一个统一的绘制顺序。
实际上是可以归为一种顺序的,Unity指定所有不透明物体绘制完毕后,再绘制半透明物体(深度测试信息不受顺序影响)。这样就确保了透明物体A在不透明物体B之前时,由远到近正常叠加,相反时由近到远正常剔除。
另一方面,对于多个半透明物体重叠绘制时,先近后远会导致近处的不透明物体被远处的盖在上面叠加。因此对于半透明物体的渲染顺序还是要指定由远到近进行渲染。
有关不透明之间和半透明物体之间绘制顺序相反可以理解为:不透明物体相当于剔除,要从近到远扣除重叠部分,而透明物体是叠加,要从远到近进行一层一层的叠加,符合正常画画的逻辑。
总结来说,半透明物体之间渲染从远往近,不透明物体之间从近往远,半透明和不透明物体之间先不透明后透明。具体流程如下:
1、先由近到远渲染所有不透明物体,并开启它们的深度测试和写入。
2、把半透明物体按它们离摄像机远近排序,然后按照从后往前的顺序渲染,开启深度测试,关闭深度写入。
透明效果的处理
目前处理透明效果的常见方式有透明测试(适用于完全透明/不透明,非0即1)和透明混合(适用于颜色的混合叠加)
透明测试
和深度测试的思路类似,剔除某些小于透明阈值的像素,实现某些位置完全透明,我们设定一个alpha阈值,当偏于不满足条件(通常是小于阈值)就直接舍弃当作全透明,满足条件的片元就直接当作不透明物体进行处理。所以注意AlphaTest并没有实现真正的半透明效果。 一般透明测试的Shader包含以下指令:
SubShader
{
//设置渲染标签
Tags
{
"Queue" = "AlphaTest" ////透明测试的渲染队列
"RenderType" = "TransparentCutout"
"IgnoreProjector" = "True" //透明物体不接受投影仪的投射
}
Pass
{
CGPROGRAM
clip(textureColor.a - alphaCutoffValue);
ENDCG
}
}
实例Shader(就与Lambert光照模型)
Shader "Custom/AlphaTest Transparent"
{
Properties
{
_MainTex("MainTex",2D) = "white"{}
_AlphaTest("Alpha Test",Range(0, 1)) = 0
}
SubShader
{
//设置渲染标签
Tags
{
"Queue" = "AlphaTest"
"RenderType" = "TrannsparentCutout"
"IgnoreProjector" = "True"
}
//渲染背面
Pass
{
Tags{"LightMode" = "ForwardBase"}
//关闭几何体剔除
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD0;
float2 texcoord : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaTest;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = normalize(worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//灯光法线 等同于_WorldSpaceLightPos0
float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
worldLight = normalize(worldLight);
//按照公式计算漫反射
fixed ndotl = dot(i.worldNormal,worldLight);
fixed4 color = tex2D(_MainTex, i.texcoord);
//开启AlphaTest
clip(color.a - _AlphaTest);//丢弃小于阈值的像素
color.rgb *= _LightColor0 * saturate(ndotl);
color.rgb += unity_AmbientSky;
return color;
}
ENDCG
}
}
}
效果:
开启透明测试:
混合透明效果
将新渲染出来的图像和已存在的进行合并。
常用的混合模式有:
- Blend Off 关闭混合处理。
- Blend SrcFactor DstFactor 开启混合处理,自定义混合模式,将目标图像(新渲染出的图像)和源图像(已存在的图像)按照混合系数进行rgba四通道的加权混合。
- Blend SrcFactor DstFactor,SrcFactorA DstFactorA 与上条类似,只不过分别计算rgb通道和alpha通道的混合。
- BlendOp Op:使用逻辑操作指令指定加权后的图像像素值具体如何混合,比如Add,Sub等(默认不写为Add)。
- BlendOp OpColor,OpAlpha: 使用逻辑操作,分别计算rgb通道和alpha通道。
注意,先用BlendOp声明逻辑操作类型,在用Blend声明混合系数。
常用混合逻辑操作:
名称 | 说明 |
---|---|
Add | 相加 |
Sub | 源图像-目标图像 |
RevSub | 目标图像 - 源图像 |
Min | 取最小值 |
Max | 取最大值 |
混合系数:
名称 | 说明 |
---|---|
Zero | 数值为0,目标图像或源图像完全不混合 |
One | 数值为1,目标图像或源图像完全混合 |
SrcColor | 用源图像的像素颜色做混合系数 |
DstColor | 用目标图像的像素颜色做混合系数 |
SrcAlpha | 用源图像的Alpha值做混合系数 |
DstAlpha | 用目标图像的Alpha值做混合系数 |
OneMinusSrcColor | 用源图像的像素颜色反相(1-)之后做混合系数 |
OneMinusDstColor | 用目标图像的像素颜色反相(1-)之后做混合系数 |
OneMinusSrcAlpha | 用源图像的Alpha值反相(1-)之后做混合系数 |
OneMinusDstAlpha | 用目标图像的Alpha值反相(1-)之后做混合系数 |
常用混合指令(操作为Add)
名称 | 说明 |
---|---|
Blend SrcAlpha OneMinusSrcAlpha | 普通的透明叠加 |
Blend One OneMinusSrcAlpha | 预乘透明 |
Blend One One | 相加 |
Blend OneMinusDstColor One | 柔和相加 |
Blend DstColor Zero | 相乘 |
Blend DstColor SrcColor | 2倍相乘 |
混合透明的使用办法
一般混合透明Shader包含以下指令:
SubShader
{
Tags
{
"Queue" = "Transparent" // 透明物体的单独渲染队列,使得透明物体在不透明物体之后被渲染
"RenderType" = "Transparent" //透明物体的渲染类型,使得透明物体在不透明物体之后被渲染
"IgnoreProjector" = "True" //透明物体不接受投影仪的投射
}
Pass
{
ZwriteOff //关闭透明物体深度写入
Blend SrcAlpha OneMinusSrcAlpha //透明叠加
}
}
混合透明实例
对Lambert光照模型的代码进行修改。
Shader "Custom/Blending Transparent"
{
Properties
{
_MainTex("MainTex",2D) = "white"{}
_MainColor("MainColor",Color) = (1, 1, 1, 1)
}
SubShader
{
//设置渲染标签
Tags
{
"Queue" = "Transparent" // 透明物体的单独渲染队列,使得透明物体在不透明物体之后被渲染
"RenderType" = "Transparent" //透明物体的渲染类型,使得透明物体在不透明物体之后被渲染
"IgnoreProjector" = "True" //透明物体不接受投影仪的投射
}
Pass
{
Tags{"LightMode" = "ForwardBase"}
//设置渲染状态
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD0;
float2 texcoord : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _MainColor;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = normalize(worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//灯光法线 等同于_WorldSpaceLightPos0
float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
worldLight = normalize(worldLight);
//按照公式计算漫反射
fixed ndotl = dot(i.worldNormal,worldLight);
fixed4 color;
color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
color.rgb += unity_AmbientSky;
//通过_MainColor属性的a分量控制透明度
color.a = _MainColor.a;
return color;
}
ENDCG
}
}
FallBack "Diffuse"
}
效果
半透明物体的双面渲染
上节实现的透明混合效果实际上是没有背面(物体的内表面,背朝摄像机)的,这是因为渲染状态默认是Cull Back,将背面进行了剔除。
可以使用Cull Off关闭背面剔除,但由于关闭了深度写入,深度缓存中没有物体的深度信息,导致背面和正面绘制顺序错误,出现了背面的图像叠加到正面的现象(背面的图片反而更亮,经过了一次透明叠加,而正面反而经过了两次)。
为此,需要分两个Pass,先渲染背面再渲染正面。
Pass
{
Tags{"LightMode" = "ForwardBase"}
//开启正面剔除
Cull front
//设置渲染状态
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD0;
float2 texcoord : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _MainColor;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = normalize(worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//灯光法线 等同于_WorldSpaceLightPos0
float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
worldLight = normalize(worldLight);
//按照公式计算漫反射
fixed ndotl = dot(i.worldNormal,worldLight);
fixed4 color;
color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
color.rgb += unity_AmbientSky;
//通过_MainColor属性的a分量控制透明度
color.a = _MainColor.a;
return color;
}
ENDCG
}
//渲染正面
Pass
{
Tags{"LightMode" = "ForwardBase"}
//开启背面剔除
Cull Back
//设置渲染状态
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 worldPos : TEXCOORD0;
float2 texcoord : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _MainColor;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldNormal = normalize(worldNormal);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//灯光法线 等同于_WorldSpaceLightPos0
float3 worldLight = UnityWorldSpaceLightDir(i.worldPos.xyz);
worldLight = normalize(worldLight);
//按照公式计算漫反射
fixed ndotl = dot(i.worldNormal,worldLight);
fixed4 color;
color.rgb = _LightColor0 * tex2D(_MainTex, i.texcoord) * _MainColor.rgb * saturate(ndotl);
color.rgb += unity_AmbientSky;
//通过_MainColor属性的a分量控制透明度
color.a = _MainColor.a;
return color;
}
正常的双面渲染:
半透明物体的交叉重叠问题也可以用类似的想法解决,用两个Pass,第一个Pass写入深度信息不做渲染,第二个Pass再做渲染。