《Unity Shader入门精要》笔记(九)
本文为《Unity Shader入门精要》第八章内容《透明效果》的上半部分笔记,主要涉及透明度测试、透明度混合及其代码实践。本文相关代码,详见:原书代码,详见原作者github:
0. 初步了解
实时渲染中,通过控制渲染模型的透明通道来实现透明效果。透明通道值在之间,1表示完全不透明,0表示完全透明。
实时渲染中,深度缓冲用于解决可见性的问题,它决定哪个物体的哪些部分会被渲染在前面,哪些部分被其他物体遮挡。
基本思想:根据深度缓冲中的值来判断该片元距离摄像机的距离,当渲染一个片元时,将它的深度值和已存在于深度缓冲中的值进行比较(如果开启深度测试),如果它的值距离摄像机更近,则将它的深度值更新到深度缓冲中,否则不更新深度缓冲,这个面片不被渲染到屏幕上。
Unity中使用透明度测试和透明度混合实现透明效果。
透明度测试:只要片元的透明度不满足条件(通常是小于某个阈值),则该片元被舍弃。(要么看不见,要么完全不透明)
透明度混合:使用当前片元透明度作为混合因子,与颜色缓冲中的颜色值进行混合,得到新颜色。(是真正的半透明效果,关闭了深度写入,但深度测试不关闭)
深度写入:往深度缓冲中写入数据;
深度测试:将当前片元的深度值与深度缓冲中的深度值进行比较。
为什么透明度混合要关闭深度写入?
如果不关闭深度写入,一个半透明物体背后的物体不会被看到。因为透明物体在深度测试后,它的颜色和深度会写入缓冲中,导致后面的物体在缓冲中的数据被覆盖,所以拿不到后面物体的颜色值,无法产生半透明的效果。
1. 渲染顺序很重要
进行透明度混合需要关闭深度写入,而关闭深度写入会让物体的渲染顺序变得非常重要,渲染顺序的不同会导致渲染的结果差别很大。
1.1 例子:半透明A和不透明B
A是透明的,它关闭了深度写入,但是深度测试是开启的;
B是不透明的,它是开启了深度写入和深度测试的。
情况1:先渲染B,再渲染A
[*]深度缓冲中没有任何数据;
[*]渲染B,B首先写入颜色缓冲和深度缓冲;
[*]渲染A,A距离相机更近,使用A的透明度来和颜色缓冲中的B颜色进行混合;
[*]得到正常的半透明效果。
情况2:先渲染A,再渲染B
[*]深度缓冲中没有任何数据;
[*]渲染A,A直接写入颜色缓冲,但是无法写入深度缓冲;
[*]渲染B,深度缓冲中没有数据,B写入深度缓冲,同时写入颜色缓冲,B颜色覆盖了A的颜色数据;
[*]得到错误的效果:看起来A被B挡住了。
由此得出结论:
应该先渲染不透明物体,再渲染半透明物体。
1.2 例子:半透明A和半透明B
A是透明的,它关闭了深度写入,但是深度测试是开启的;
B是透明的,它关闭了深度写入,但是深度测试是开启的。
情况1:先渲染B,再渲染A
[*]渲染B,B正常写入颜色缓冲;
[*]渲染A,A和颜色缓冲中的B颜色进行混合;
[*]得到正确的半透明效果。
情况2:先渲染A,再渲染B
[*]渲染A,A正常写入颜色缓冲;
[*]渲染B,B和颜色缓冲中的A颜色进行混合;
[*]得到完全相反的混合结果,看起来好像B在A的前面。
由此得出结论:
半透明物体之间也要符合一定的渲染顺序。
基于上面两个例子,渲染引擎一般会先对物体进行排序,再渲染。
常用的方法是:
[*]先渲染所有不透明物体,并开启它们的深度测试和深度写入;
[*]把透明物体按照它们距离摄像机的远近进行排序;
[*]按照从后往前的顺序渲染这些半透明物体,开启它们的深度测试,但关闭深度写入。
其实依然有一些情况是上述方法无法解决的,但是因为上述方法能解决绝大多数的情况且容易实现,所以大多数游戏引擎都是用了这个方法。
接下来看看两种上述方法无法解决的情况,第一种是多个物体循环重叠,我们没办法以模型物体作为颗粒度进行渲染优先级的排序:
第二种是相比两个物体各自相对应的点(左上点、中心点、右下点),都是A离相机更近,计算会得出A在B的前面,而实际却是B挡住了A:
上述两种情况,我们仅作了解即可,因为在以物体作为颗粒度的排序下,不论怎么排序都会出现问题。所以为了减少类似问题的出现,要尽可能让模型是凸面体,或考虑将复杂的模型拆分成可以独立排序的多个子模块。
2. Unity Shader的渲染顺序
Unity使用渲染队列,通过设置SubShader的Queue标签来决定模型归于哪个渲染队列。Unity在内部使用一系列整数索引来表示每个渲染队列,索引号越小表示越早被渲染。
Unity提前定义的5个渲染队列:
[*]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
...
}
}
3. 透明度测试
3.1 概念
只要一个片元的透明度不满足条件(通常是小于某个阈值),则这个片元就会被舍弃,不进行任何处理,否则按照普通的不透明物体的处理方式进行处理。
通常在片元着色器中使用clip函数进行透明度测试,clip是CG中的一个函数。
// 函数的参数是裁剪时使用的标量或矢量条件
void clip(float4 x);
void clip(float3 x);
void clip(float2 x);
void clip(float1 x);
void clip(float x);
// 如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色
void clip(float4 x)
{
if (any(x < 0))
discard;
}
接下来通过实践了解透明度测试。
3.2 实践:使用半透明纹理实现透明度测试
3.2.1 准备工作
完成如下准备工作:
[*]新建名为Scene_8_3的场景,并去掉天空盒子;
[*]新建名为AlphaTestMat的材质;
[*]新建名为Chapter8-AlphaTest的Shader,并赋给上一步创建的材质;
[*]在场景中创建一个立方体,并把第二步的材质赋给它;
[*]保存场景。
详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。
3.2.2 编写Shader代码
打开Chapter8-AlphaTest,删除里面所有代码,编写如下代码,新的代码逻辑已用注释说明:
// 为Shader命名
Shader &#34;Unity Shaders Book/Chapter 8/Alpha Test&#34;
{
Properties
{
_Color (&#34;Main Tint&#34;, Color) = (1, 1, 1, 1)
_MainTex (&#34;Main Tex&#34;, 2D) = &#34;white&#34; {}
// 透明度测试的阈值
_Cutoff (&#34;Alpha Cutoff&#34;, Range(0, 1)) = 0.5
}
SubShader
{
// 透明度测试常用的三个标签
Tags {
// 设置渲染队列为透明度测试队列
&#34;Queue&#34; = &#34;AlphaTest&#34;
// 让当前Shader不会受到投射器的影响
&#34;IgnoreProjector&#34; = &#34;True&#34;
// 让Unity把当前Shader归入到提前定义的TransparentCutout组
&#34;RenderType&#34; = &#34;TransparentCutout&#34;
}
Pass
{
// 设置LightMode为ForwardBase,可以让我们得到
// 一些正确的Unity内置光照变量,如:_LightColor0
Tags { &#34;LightMode&#34; = &#34;ForwardBase&#34; }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 引入内置光照变量
#include &#34;Lighting.cginc&#34;
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);
// 透明度测试:透明度小于_Cutoff的值,则当前片元会被丢弃
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
}
}
// 使用内置的VertexLit Shader兜底
Fallback &#34;Transparent/Cutout/VertexLit&#34;
}
3.2.3 配置材质参数
给材质设置包含不同透明度的纹理贴图,可使用文章开头Git仓库里的素材——transparent_texure.psd,在/Assets/Textures/Chapter8文件夹下:
分别给_Cutoff值设置0.6、0.7、0.8,可以看到纹理中部分区域不再可见。因为纹理中的四块颜色透明度不同,当透明度值达不到_Cutoff定义的阈值,则被丢弃,不会渲染到屏幕上:
4. 透明度混合
4.1 概念
使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合得到新的颜色。但是需要关闭深度写入,所以我们需要非常小心物体的渲染顺序。
Unity为我们提供了混合命令——Blend,用来控制是否开启混合、以怎样的方式混合。
ShaderLab的Blend命令:
[*]Blend Off
关闭混合。
[*]Blend SrcFactor DstFactor
开启混合。源颜色(当前片元的颜色)x SrcFactor + 目标颜色(颜色缓冲中的颜色) x DstFactor,得到的最终颜色存入颜色缓冲中。
[*]Blend SrcFactor DstFactor, SrcFactorA DstFactorA
与上面几乎一样,使用不同的因子来混合透明通道。
[*]BlendOp BlendOperation
并非源颜色和目标颜色的简单相加后混合,而使用BlendOperation对它们进行其他操作。
我们使用Blend指令设置混合因子的同时,Unity会自动帮我们开启混合模式。如果我们遇到模型没有任何透明效果时,不妨看一下Pass中有没有忘记使用Blend命令,或是忘记设置混合因子(本质是开启混合模式)。
我们把源颜色的混合因子SrcFactor设为SrcAlpha,目标颜色的混合因子DstFactor设为OneMinusSrcAlpha,那么混合后的新颜色是:
https://www.zhihu.com/equation?tex=DstColorx_%7Bnew%7D+%3D+SrcAlpha+%5Ctimes+SrcColor+%2B+%281+-+SrcAlpha%29+%5Ctimes+DstColor_%7Bold%7D
4.2 实践:使用半透明纹理实现透明度混合
4.2.1 准备工作
完成如下准备工作:
[*]新建名为Scene_8_4的场景,并去掉天空盒子;
[*]新建名为AlphaBlendMat的材质;
[*]新建名为Chapter8-AlphaBlend的Shader,并赋给上一步创建的材质;
[*]在场景中创建一个立方体,将第二步创建的材质赋给它;
[*]保存场景。
4.2.2 编写Shader代码
打开Chapter8-AlphaBlend,删除里面所有的代码,并赋值上一节Chapter8-AlphaTest的代码,做部分修改,修改部分已用注释说明:
// 修改Shader命名
Shader &#34;Unity Shaders Book/Chapter 8/Alpha Blend&#34;
{
Properties
{
_Color (&#34;Main Tint&#34;, Color) = (1, 1, 1, 1)
_MainTex (&#34;Main Tex&#34;, 2D) = &#34;white&#34; {}
// 用于在透明纹理的基础上控制整体的透明度
_AlphaScale (&#34;Alpha Scale&#34;, Range(0, 1)) = 1
}
SubShader
{
// 透明度混合常用的3个标签
Tags {
// 将渲染队列修改为透明混合队列
&#34;Queue&#34; = &#34;Transparent&#34;
&#34;IgnoreProjector&#34; = &#34;True&#34;
// 将当前Shader归入Transparent组
&#34;RenderType&#34; = &#34;Transparent&#34;
}
Pass
{
Tags { &#34;LightMode&#34; = &#34;ForwardBase&#34; }
// 关闭深度写入
ZWrite Off
// 将源颜色(该片元着色器产生的颜色)的混合因子设为SrcAlpha
// 将目标颜色(已存在颜色缓冲中的颜色)的混合因子
// 设为OneMinusSrcAlpha
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include &#34;Lighting.cginc&#34;
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
}
}
// 修改兜底的Shader
Fallback &#34;Transparent/VertexLit&#34;
}
4.2.3 配置材质参数
使用上一节的纹理赋给当前的材质,并设置_AlphaBlend数值,可得到不同的透明度效果:
原书给出了之前提到的异常透明度混合的情况:就是如果一个物体自身如果存在互相交叉的结构,由于透明度混合关闭了深度写入,会导致我们无法得到正确的半透明遮挡效果。
这里仅作了解回顾。
写在最后
本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页:
[1]