
·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找

一、渲染顺序
在之前的学习中,我们其实并不会关注哪个模型先被渲染哪个模型后被渲染的顺序关系,这是因为在渲染流水线的逐片元阶段中有深度测试+深度写入这个操作,它可以根据模型最终变换得到的深度值(Z值)去与当前在深度缓冲中的深度值进行比对,如果比深度缓冲区中的值相比离摄像机更近,那这个片元的颜色就会覆盖掉之前这个位置颜色缓冲区中的颜色,反之则不会把这个片元该处的颜色加入颜色缓冲区中。因此,我们并不在意模型的渲染顺序
但是当有半透明的物体在场的时候,我们知道即使不透明的物体在半透明物体后面,那不透明的物体的颜色依然是应该可以被看到的,所以此时提前用深度测试去决定舍弃片元就不太合理了。
1.不透明物体与不透明物体之间
- 它们之间是完全遮挡的关系,因此对它们只要开启深度测试+深度写入即可
- 不需要关心它们的渲染顺序
- 颜色缓冲区中的颜色都采用覆盖的方式更新
2.半透明物体与不透明物体之间
- 需要先渲染不透明物体,再渲染半透明物体
- 需要关闭半透明物体的深度写入(如果半透明物体采用的是透明度测试就不用)
- 如果不透明的物体在前面,颜色缓冲区中的颜色采用覆盖的方式更新
- 如果半透明的物体在前面,颜色缓冲区中的颜色采用混合的方式更新
3.半透明物体与半透明物体之间
- 需要先渲染离摄像机更远的半透明物体,再渲染离摄像机更近的半透明物体
- 需要关闭半透明物体的深度写入功能(如果半透明物体采用的是透明度测试就不用)
- 颜色缓冲区中的颜色采用混合的方式更新(混合顺序与透明效果有关)
因此从上面我们可以知道,只要渲染的模型当中存在半透明的物体,那我们就需要考虑渲染顺序的问题。而且最好不透明的模型先渲染,离摄像机最远的模型先渲染
4.UnityShader定义的渲染顺序
Unity提供了渲染队列(render queue)来解决模型的渲染顺序的问题。在SubShader中使用Queue标签来决定模型归属于哪一个渲染队列
Tags {"Queue" = "Transparent"}
| 名称 | 队列索引号 | 描述 |
|---|---|---|
| BackGround | 1000 | 这个渲染队列会在任何队列之前被渲染,通常用于渲染需要绘制在背景上的图片 |
| Geometry | 2000 | 默认渲染队列,大多数模型都用这个队列。不透明模型使用这个队列 |
| AlphaTest | 2450 | 需要透明度测试的物体使用这个队列。在Unity5中被从Geometry队列中分离出来,因为在渲染所有不透明物体之后再渲染它们会更高效 |
| Transparent | 3000 | 这个队列的物体在Geometry和AlphaTest队列中的物体被渲染完后,再按从后往前的顺序进行渲染。任何使用了透明度混合的(关闭了深度写入的Shader)都应该使用这个队列 |
| Overlay | 4000 | 该队列用于实现一些叠加效果。任何需要最后进行渲染的物体都使用这个队列 |
其中索引越小,越先被渲染
二、两种渲染方法
1.透明度测试:
是通过对该处颜色的Alpha值进行测试,当Alpha值不满足条件的时候直接舍弃的渲染方法。非常的暴力
在片元着色器中使用Clip()函数
当传入的参数中任一分量为负数,就会立刻舍弃当前片元的颜色
2.透明度混合:
是通过对通过深度测试的该处颜色与已经在颜色缓冲区中的颜色进行某种混合操作生成新的颜色并再写入的渲染方法。
在Pass的开头处使用Blend语义
①Pass中关闭深度写入
②开启混合模式
③设置混合操作与参数(Unity中这一步会自动帮我们开启混合模式)
三、透明度测试的使用与效果
1.属性添加与声明
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Cutoff ("Alpha CutOff", Range(0, 1)) = 0.5 //用于作为透明度测试的阈值
}
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
2.SubShader中添加标签
//因为要进行的是透明度测试,所以渲染队列选择AlphaTest;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
这一步非常关键和重要,尤其是设定它的渲染队列为AlphaTest
3.公式化写输入输出结构体和顶点着色器
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
这里我们应该已经很熟悉了,反正就是正常把顶点坐标进行转化。然后计算一些用于世界空间下计算光照的值。
感觉不熟悉了可以去回顾之前的笔记(这里只计算环境光+漫反射的):《Unity Shader入门精要》学习--初级篇--宝宝都能学会的基础光照模型
4.片元着色器中采样纹理计算后进行透明度测试
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
//对纹理进行采样
fixed4 texColor = tex2D(_MainTex, i.uv);
//采样得到的颜色值的alpha通道进行透明度测试
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 * saturate(dot(worldLightDir, worldNormal));
return fixed4(ambient+diffuse, 0.0);
}
这个部分最关键的地方在这里:
这两个语句达到的效果是一样的。都是当前Alpha的值(经过属性_Cutoff的偏移控制后)比0小的片元直接被舍弃,不会参与后续的深度测试之类的了(更不会考虑写入颜色缓冲区的)
5.完整代码与具体效果如下
Shader "Unlit/AlphaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Cutoff ("Alpha CutOff", Range(0, 1)) = 0.5 //用于作为透明度测试的阈值
}
SubShader
{
//因为要进行的是透明度测试,所以渲染队列选择AlphaTest;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc" //在这个库中用于获取灯光方向和颜色等信息
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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通道进行透明度测试
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 * saturate(dot(worldLightDir, worldNormal));
return fixed4(ambient+diffuse, 0.0);
}
ENDCG
}
}
Fallback "Transparent/CutOut/VertexLit"
}
老实说其实效果不是很好
6.双面渲染的透明效果
我们上面透明渲染的矩形直接就透过去看到它后面的物体了,但是它本身后面还有的结构却完全没有渲染出来还。这是因为我们在渲染的时候都会提前剔除一个物体看不到的背面。当然我们也可以手动开启它。
开启的指令语法是:Cull Back | Front | Off (剔除背面,剔除前面,不剔除)
那对于透明度测试,我们采用的方法非常简单粗暴,就是直接关闭剔除就好了
Shader "Unlit/AlphaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_Cutoff ("Alpha CutOff", Range(0, 1)) = 0.5 //用于作为透明度测试的阈值
}
SubShader
{
//因为要进行的是透明度测试,所以渲染队列选择AlphaTest;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
Pass
{
Tags {"LightMode" = "ForwardBase"}
//在透明度测试中要看到背面,那直接把背面剔除的设置关掉就好了
Cull Off
//..后面的和之前一样的
}
}
Fallback "Transparent/CutOut/VertexLit"
}
四、透明度混合的使用与效果
1.属性添加
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0,1)) = 1 //作为采样得到的颜色的Alpha的缩放系数
}
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _AlphaScale;
2.SubShader中添加标签
//因为要进行的是透明度混合,所以渲染队列选择Transparent;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
这一步非常关键和重要,尤其是设定它的渲染队列为Transparent
3.Pass中关闭深度测试并配置混合参数
Pass
{
Tags {"LightMode" = "ForwardBase"}
//该Pass中关闭深度写入,并设置好混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha //它们之间不用逗号隔开
Blend SrcAlpha OneMinusSrcAlpha的意思是:
- 用来源颜色的Alpha值作为原颜色混合因子,
- 1-原颜色Alpha值作为当前在颜色缓冲区的颜色的混合因子
4.正常进行光照计算
完整代码和效果如下:
Shader "Unlit/AlphaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0,1)) = 1 //作为采样得到的颜色的Alpha的缩放系数
}
SubShader
{
//因为要进行的是透明度混合,所以渲染队列选择Transparent;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
Pass
{
Tags {"LightMode" = "ForwardBase"}
//该Pass中关闭深度写入,并设置好混合模式
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha //它们之间不用逗号隔开
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc" //在这个库中用于获取灯光方向和颜色等信息
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _AlphaScale;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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 * saturate(dot(worldLightDir, worldNormal));
//返回的时候需要同时返回该片元的Alpha值
return fixed4(ambient+diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
Fallback "Transparent/CutOut/VertexLit"
}

感觉效果好多了!
5.开启深度写入的半透明效果
但是有的时候如果模型本身有复杂的遮挡关系的时候,就会很容易因为排序错误而得到错误的透明效果。就像下面这样:
这主要是因为我们关闭了深度写入,所以没办法对他进行逐像素级别的深度排序。
因此,是时候再把深度写入重新利用起来了!
5.1.使用两个Pass
- 在第一个Pass中,我们只进行深度写入,找出在最外侧的片元舍弃掉在内侧的片元,但是还不进行对颜色缓冲区的修改
- 在第二个Pass中,我们保持和之前一样即可,正常进行着色计算
//这里是半透明度混合,用两个Pass来对模型片元进行渲染
//第一个Pass用于把片元的深度值测试并写入,让只有在当前像素位置上最接近摄像机的像素才能够进行混合
Pass{
ZWrite On //开启深度写入
ColorMask 0
}
Pass{
//...和之前一样
}
ColorMask语义:ColorMask RGB | A | 0 | 其他任何R、G、B、A的组合
它是用于设置颜色通道的写掩码(write mask),设置为0说明不写入任何颜色通道的值
可以看到它渲染了最外层的像素和后方不透明物体进行混合的颜色,舍弃了内层的像素。同时使用两个Pass也会加大对性能的消耗。
可能会有人想,那和本来一开始就开着深入写入有什么区别嘛?
区别主要是对这个模型进行渲染的时候,在一个Pass中同时开启深度写入与颜色写入会让这个一个像素点位置的颜色被混合多次;而用两个Pass先完全所有的深度写入,那就只会是最外层的颜色进行一次混合。
6.双面渲染的透明效果
我们上面透明渲染的矩形直接就透过去看到它后面的物体了,但是它本身后面还有的结构却完全没有渲染出来还。这是因为我们在渲染的时候都会提前剔除一个物体看不到的背面。当然我们也可以手动开启它。
开启的指令语法是:Cull Back | Front | Off (剔除背面,剔除前面,不剔除)
那对于透明度混合,我们要进行双面渲染的方式就是用两个Pass,一个Pass渲染正面、一个渲染背面,其中的代码和上面进行透明度混合是一样的
Shader "Unlit/AlphaTest"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0,1)) = 1 //作为采样得到的颜色的Alpha的缩放系数
}
SubShader
{
//因为要进行的是透明度混合,所以渲染队列选择Transparent;然后忽略投影器的影响;同时把这个Shader归类为TransparentCutout
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType"="TransparentCutout"} //注意这里面不用加逗号
//透明度混合要进行双面渲染,可以让一个Pass渲染证明,另一个Pass渲染背面,反正深度测试关掉了的
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Front
//..和之前一样的
}
Pass
{
Tags {"LightMode" = "ForwardBase"}
Cull Back
//..和之前一样的
}
Fallback "Transparent/CutOut/VertexLit"
}
五、ShaderLab的混合命令
1.混合操作与混合因子
混合是逐片元操作,是不可编程但是高度可配置的
混合命令 = 混合操作+混合因子
| 命令 | 描述 |
|---|---|
| Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色(该片元产生的颜色)×SrcFactor,而目标颜色(已经存在颜色缓冲中的颜色)×DstFactor,然后两者相加进入混合区。(同一个颜色的RGB和Aplha通道用同一个因子来相乘) |
| Blend SrcFactor DstFactor, SrcFactorA DstFactorA | 和上面一样,只是用不同的因子来混合透明通道 |

总之就是为
- 0,1
- 目标rgb,目标a,源rgb,源a
- 1-目标rgb,1-目标a,1-源rgb,1-源a

总之就是
- 相加、相减、倒过来相减
- 取最小分量(不用因子参与)、取最大分量(不用因子参与)
2.常见的混合类型
//正常(Nromal),即透明度混合(正宗原皮)
Blend SrcAlpha OneMinusSrcAlpha
//柔和相加(Soft Additive)
Blend OneMinusDstColor One
//正片叠底(Multiply),即相乘
Blend DstColor Zero
//两倍相乘(2x Mutiply)
Blend DstColor SrcColor
//变暗(Draken)
BlendOp Min
Blend One One
//变亮(Lighten)
BlendOp Max
Blend One One
//滤色(Screen)
Blend OneMinusDstColor One
//等同于
Blend One OneMinusSrcColor
//线性减淡(Linear Dodge)
Blend One One

虽然听起来和PS中的图层混合设置很像,事实上也确实如此
六、总结
①对于透明效果而言,主要是在半透明物体和不透明物体,或者半透明物体与半透明物体之间才应该要产生的渲染效果,而这两种都需要考虑渲染顺序的问题。
②对于渲染顺序的问题,Unity采用渲染队列来定义模型的渲染顺序,通常按背景图上模型、不透明模型、进行透明度测试的模型、进行透明度混合的模型、额外覆盖在前面的模型的顺序来进行渲染。我们只要根据需要把模型放入对应的队列中即可
③透明度测试可以不关掉深度写入,因为它是直接很暴力地去舍弃掉了透明度达到某个阈值的片元的颜色值。
④透明度混合最基础的需要关掉深度写入,同时在Pass一开始就设置好混合的操作与因子。当然要是想要去改善一些如复杂遮挡类模型的片元颜色渲染顺序可以通过两个Pass来实现。一个Pass只写入深度值,一个Pass只进行颜色值的混合(ZWrite Off | On ; ColorMask RGB | A | 任意RGBA组合)
⑤要想看到实现透明效果的物体的背面,可以考虑双面渲染。透明度测试的双面渲染直接关掉剔除模式就好了(Cull Off),透明度混合的则也是用两个Pass,分别渲染正面(Cull Back)和背面(Cull Front)
⑥对于混合模式的配置,通过搭配不同的混合操作和混合因子,可以得到不同的颜色混合效果


1756

被折叠的 条评论
为什么被折叠?



