表面着色器可以被若干的标签(tags)所修饰,硬件将通过判定这些标签来决定什么时候调用该着色器。例如Tags { "RenderType"="Opaque" },告诉了系统应该在渲染非透明物体时调用我们。Unity定义了一些列这样的渲染过程,与RenderType是Opaque相对应的显而易见的是"RenderType" = "Transparent"
,表示渲染含有透明效果的物体时调用。在这里Tags其实暗示了你的Shader输出的是什么,如果输出中都是非透明物体,那写在Opaque里;如果想渲染透明或者半透明的像素,那应该写在Transparent中。
另外比较有用的标签还有"IgnoreProjector
"=
"True
"(不被投影(Projectors)影响),"ForceNoShadowCasting
"=
"True
"(从不产生阴影)以及"Queue
"=
"xxx
"(指定渲染顺序队列),如
Tags
{"Queue" = "Transparent""IgnoreProjector" = "True""RenderType" = "Transparent"
}
这里想要着重说一下的是Queue这个标签,如果你使用Unity做过一些透明和不透明物体的混合的话,很可能已经遇到过不透明物体无法呈现在透明物体之后的情况。这种情况很可能是由于Shader的渲染顺序不正确导致的。Queue指定了物体的渲染顺序(数值越小,渲染的越早),预定义的Queue有:
渲染队列 | 渲染队列描述 | 渲染队列值 |
Background | 这个队列被最先渲染。它被用于skyboxes等。 | 1000 |
Geometry | 这是默认的渲染队列。它被用于绝大多数对象。不透明几何体使用该队列。 | 2000 |
AlphaTest | 通道检查的几何体使用该队列。它和Geometry队列不同,对于在所有立体物体绘制后渲染的通道检查的对象,它更有效。 | 2450 |
Transparent | 该渲染队列在Geometry和AlphaTest队列后被渲染。任何通道混合的(也就是说,那些不写入深度缓存的Shaders)对象使用该队列,例如玻璃和粒子效果。 | 3000 |
Overlay | 该渲染队列是为覆盖物效果服务的。任何最后被渲染的对象使用该队列,例如镜头光晕。 | 4000 |
在我们实际设置Queue值时,不仅能使用上面的几个预定义值,我们也可以指定自己的Queue值,写成类似这样:"Queue"="Transparent+100"
,表示一个在Transparent之后100的Queue上进行调用。通过调整Queue值,我们可以确保某些物体一定在另一些物体之前或者之后渲染,这个技巧有时候很有用处。
LOD(Level of Detail),如LOD 200,即指定了其为200(其实这是Unity的内建Diffuse着色器的设定值)。这个数值决定了我们能用什么样的Shader。在Unity的Quality Settings中我们可以设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。Unity内建Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,这样在之后调整根据设备图形性能来调整画质时可以进行比较精确的控制。
名称 | 值 |
VertexLit及其系列 | 100 |
Decal, Reflective VertexLit | 150 |
Diffuse | 200 |
Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit | 250 |
Bumped, Specular | 300 |
Bumped Specular | 400 |
Parallax | 500 |
Parallax Specular | 600 |
通过这个设置,我们可以剔除用户看不见的面,用在不同的情况中,以优化性能。有如下三个属性
Cull Back(默认) | 不渲染背面 |
Cull Front | 不渲染正面 |
Cull Off | 关闭剔除效果,两面都渲染 |
例如在场景中创建一个Plane(默认的Cull Back),可以在Scene视图中正常看见该Plane,但是当你将Scene中的视角移到Plane的背面的时候,会发现该Plane消失了,这就是Cull Back的效果。由此可推出,Cull Front则反之,Cull off则两面都可以看见(大家可多动手试试加深印象)
ColorMask
通过这个设置可以设置输出的颜色通道,(R,G,B,A四个参数可以任意搭配)
RGBA | 四个通道的颜色都输出 |
RG | 只输出R,G两个通道的颜色 |
R | 只输出R通道的颜色 |
... | RGBA搭配组合 |
0 | 不输出颜色 |
我们还可以在Properties中设置参数,类型为float,然后通过参数给ColorMask赋值,RGBA用四位的二进制表示,二进制1111即为15,表示RGBA全部输出,二进制1010即为10,表示只输出RB两个通道。
Properties {_ColorMask("Color Mask", Float) = 14
}SubShader {ColorMask [_ColorMask]
}
注:若直接填写 ColorMask 15 是会报错的。
ZWrite(深度写入) / ZTest(深度测试)
ZWrite决定片元的深度值是否写入深度缓冲,可配置(ZWrite On(默认) / ZWrite Off)
ZTest开启后测试结果决定片元是否被舍弃,有如下配置
ZTest Less | 深度小于当前缓存则通过 |
ZTest Greater | 深度大于当前缓存则通过 |
ZTest LEqual(默认) | 深度小于等于当前缓存则通过 |
ZTest GEqual | 深度大于等于当前缓存则通过 |
ZTest Equal | 深度等于当前缓存则通过 |
ZTest NotEqual | 深度不等于当前缓存则通过 |
ZTest Always | 不论如何都通过(ZTest Off 等同于 ZTest Always) |
系统中存在一个颜色缓冲区和一个深度缓冲区,分别存储颜色值和深度值,来决定画面上应该显示什么颜色。深度值是物体在世界空间中距离摄像机的远近。距离越近,深度值越小;距离越远,深度值越大。
主要流程如下:首先判断是否开启了深度测试,若开启了,则比较该片元的深度值和已经存在于深度缓冲区的深度值,根据配置得到结果。如果通过了深度测试,则再去判断是否开启深度写入,若开启了深度写入则将深度值写入深度缓冲区(未开启即不写入),最后结束流程,写入颜色缓冲区,若没有通过深度测试则直接舍弃该片元,结束流程。若没有开启深度测试,则直接去判断是否开启了深度写入,并执行后面的相关流程。
可以归纳为
1.深度测试通过,深度写入开启:写入深度缓冲区,写入颜色缓冲区
2.深度测试通过,深度写入关闭:不写深度缓冲区,写入颜色缓冲区
3.深度测试失败,深度写入开启或关闭:不写深度缓冲区,不写颜色缓冲区
blend(混合)
Blend 主要作用是颜色混合,颜色混合是在所有渲染完成图像都成型后进行的,这时像素都已经被绘制好了,并且屏幕上也已经有成像的图形,但还没有完全完成,因为是物体各自画各自的已经画完了放入了缓存中,但alpha还没有发挥出效果,这时就需要混合来做补充,这之前的屏幕上所有的图像都是实体的,没有半透明和透明部分。总之Blend最重要的作用就是让alpha起到半透明的作用,当然也可以做到颜色加强,改变,混合,削弱等作用。
详见:《Unity3D高级编程之进阶主程》第七章,Shader(六) - Pass(三)Blend
我们将片段着色器中计算出来的颜色称之为 “源颜色”,帧缓存中对应的像素已经存在的颜色叫做“目标颜色”(背景色)。混合操作就是将源颜色与目标颜色以一些选项进行结合。格式如下:
Blend SrcFactor DstFactor
我们选择混合的选项的过程是通过以下面的等式来进行RGBA颜色的计算的:
float4 result = SrcFactor * fragment_output + DstFactor * pixel_color;
其中SrcFactor和DstFactor可以取下面这些值(假设原颜色为A, 原通道为A_alpha,目标颜色为B,目标通道为B_alpha)
One | 值为1,使用此因子来让帧缓冲区源颜色或是目标颜色完全的通过。* 1 |
Zero | 值为0,使用此因子来删除帧缓冲区源颜色或目标颜色的值。 * 0 |
SrcColor | 使用此因子为将当前值乘以帧缓冲区源颜色的值 * A |
SrcAlpha | 使用此因子为将当前值乘以帧缓冲区源颜色Alpha的值。 * A_alpha |
DstColor | 使用此因子为将当前值乘以帧缓冲区目标颜色的值。* B |
DstAlpha | 使用此因子为将当前值乘以帧缓冲区目标颜色Alpha分量的值。 * B_alpha |
OneMinusSrcColor | 使用此因子为将当前值乘以(1 -帧缓冲区源颜色值) * (1 - A) |
OneMinusSrcAlpha | 使用此因子为将当前值乘以(1 -帧缓冲区源颜色Alpha分量的值) * (1 - A_alpha) |
OneMinusDstColor | 使用此因子为将当前值乘以(1 –目标颜色值) * (1 - B) |
OneMinusDstAlpha | 使用此因子为将当前值乘以(1 –目标Alpha分量的值) * (1 - B_alpha) |
例如:
Blend SrcAlpha OneMinusSrcAlpha | 源颜色 x 源通道 + 目标颜色 x(1 - 源通道) | A * A_alpha + B * (1 - A_alpha) |
Blend One One | 源颜色 + 目标颜色(叠加模式) | A + B |
Blend SrcAlpha One | 源颜色 x 源通道 + 目标颜色(带通道的叠加模式) | A * A_alpha + B |
Blend DstColor SrcColor | 源颜色 x 目标颜色 + 目标颜色 x 源颜色(正片叠底的效果) | A * B + A * B |
Blend DstColor Zero | 源颜色 x 目标颜色 + 0(目标颜色 x 0) | A * B + 0 |
颜色的相加与相乘:
1,颜色乘法,可以视为颜色(或者说图像)的叠加。
为什么乘法颜色节点能够让颜色叠加。 我们把两个RGBA值转换成为0-1之间的范围就可以明白了。 比如白色(1,1,1,1),灰色(0.5,0.5,0.5,1),两个颜色相乘得到(0.5,0.5,0.5,1) 还是灰色,也就是说在白色的底板上,画上灰色的颜色,颜色就叠加了,这就是颜色的叠加方法。
颜色相乘的公式是 Color a, Color b, a*b = new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); 从此公式可以看出,因为所有数字都小于等于1,所以相乘只会让颜色更加深,也就是更加接近黑色,不断的叠加各种各样的颜色,最后都会无限接近黑色(0,0,0,1)或者无限接近黑色透明(0,0,0,0)。
2,颜色加法,可以视为颜色(或者说图像)加白(或者说亮)。
为什么说加法可以视为颜色的加白操作。因为颜色相加后,不能超过最大值1(或者另一种表达方式的数字255),超过部分都会被截断。加法的公式是:Color a, Color b, a+b = new Color(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); 从此公式可以看出,所有的数字都小于等于1,所以r,g,b,a,随着不断加各种颜色,最终会到达最大值1。也就是不透明白色(1,1,1,1)。
实例演示
举几个简单的例子来对上面的一些知识点进行深入理解,小伙伴们自己也可以动手试试举一反三。
首先我们先创建两个最简单的shader,Custom/ShaderDemo1和Custom/ShaderDemo2,内容相同如下(名称不一样),并分别挂在两个新的material下。同时光源的颜色设置为白色,去除光源摄像机的阴影效果。
Shader "Custom/ShaderDemo1" {Properties {_Color ("Color", Color) = (1,1,1,1)}SubShader {Tags { "RenderType"="Opaque"}CGPROGRAM#pragma surface surf Lambert//使用Lambert光照色差较小fixed4 _Color;struct Input {float3 viewDir;};void surf (Input IN, inout SurfaceOutput o) {o.Albedo = _Color.rgb;o.Alpha = _Color.a;}ENDCG}
}
然后我们在场景中创建两个Cube,挂上新的material,color值自行设置(例子中为蓝色对应shader1和绿色对应shader2)。将摄像机的Clear Flags设置为Solid Color,Background自行设置即可(例子中为红色,方便看效果)。如下。
在这里我们做个假设,由于蓝色方块离摄像机较近,我们暂定为蓝色方块的深度值为0.3,绿色方块的深度值为0.6,摄像机的深度值为1。
打开Window->Frame Debugger可以查看渲染的顺序&#xff0c;我们可以看到系统先渲染了蓝色方块&#xff0c;然后渲染绿色方块&#xff08;修改Queue值可以修改渲染顺序&#xff0c;例子中Queue值相同&#xff09;&#xff0c;由于默认的Zwrite On与ZTest LEqual&#xff08;深度小于等于当前缓存则通过&#xff09;。所以系统会先渲染蓝色方块&#xff0c;现在缓存中对比屏幕的深度值1。0.3<1即全部通过。显示蓝色方块并将0.3的深度值写入缓存中。接着渲染绿色方块的时候。被遮挡的部分深度值0.6与缓存中的0.3对比&#xff0c;0.6>0.3不通过则不渲染&#xff0c;未遮挡的部分与1比较&#xff0c;0.6<1&#xff0c;渲染。得到上面的画面。
这个时候如果我们将蓝色方块的shader1添加一句ZWrite Off&#xff0c;即渲染完蓝色方块后不会将蓝色方块的深度值写入深度缓存。所以再之后渲染绿色方块的时候&#xff0c;将和深度1相比较&#xff0c;全部通过。
假设我们shader1继续为ZWrite Off&#xff0c;但是将绿色的shader2修改为ZTest GEqual&#xff08;深度大于等于当前缓存则通过&#xff09;。即渲染绿色的时候被遮挡部分0.6>0.3&#xff0c;通过渲染&#xff0c;未被遮挡部分0.6<1&#xff0c;未通过不渲染&#xff0c;如下&#xff1a;
有关ZWrite 和 ZTest 的暂时就举例这么多&#xff0c;小伙伴们可以自己试试其他参数其他情况。
假如现在我们要让蓝色的小方块变为透明&#xff0c;但是只修改蓝色material的color的布尔值并不会起效果&#xff0c;根据第一篇的内容&#xff0c;我们需要将fullforwardshadows改为alpha。效果如下
可以发现&#xff0c;虽然蓝色方块确实变透明了&#xff0c;但是绿色方块的层级并不对。但是在shader中我们并没有关闭ZWrite和修改ZTest。通过Frame Debug我们可以发现&#xff0c;渲染顺序依旧是先蓝色后绿色&#xff0c;但是蓝色的ZWrite却被关闭了&#xff08;透明物体不写入深度缓存&#xff09;。根据上面的知识&#xff0c;我们应该将蓝色方块的material的Render Queue值设为Transparent&#xff08;先渲染绿色方块再渲染蓝色方块&#xff09;&#xff0c;即可得到正确的效果
接下来我们试试Blend。新建一个Shader如下&#xff1a;
Shader "Custom/BlendDemo" {Properties {_Color("Color", Color) &#61; (1,1,1,1)}SubShader {Tags{ "RenderType" &#61; "Transparent" "Quene" &#61; "Transparent" }Pass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata {float4 vertex : POSITION;};struct v2f {float4 vertex : SV_POSITION;};v2f vert (appdata v) {v2f o;o.vertex &#61; UnityObjectToClipPos(v.vertex);return o;}fixed4 _Color;fixed4 frag (v2f i) : SV_Target {return _Color;}ENDCG}}
}
同样新建一个material关联此shader并绑定到cube_blue上&#xff0c;cube_green不变&#xff0c;此时不管怎么改变新的shader上颜色的透明度&#xff0c;发现都不会有效果。
此时假设我们蓝色方块的RGB值为(0,255,255)即(0,1,1)&#xff0c;透明度为0.7。绿色方块的RGB值为(0,255,0)即(0,1,0)&#xff0c;透明度为1&#xff0c;背景红色RGB值为(255,0,0)即(1,0,0)&#xff0c;透明度为1。
想要实现透明&#xff0c;我们可以加一句Blend SrcAlpha OneMinusSrcAlpha即可。效果如下&#xff1a;
通过上面的知识&#xff0c;我们知道SrcAlpha是自己的透明通道即值为0.7&#xff0c;OneMinusSrcAlpha即为1 - 0.7 &#61; 0.3。
蓝色和红色的部分&#xff0c;原颜色即为蓝色(0,1,1)&#xff0c;目标颜色即为红色(1,0,0)得
最终颜色 &#61; (0,1,1) * 0.7 &#43; (1,0,0) * 0.3 &#61; (0,0.7,0.7) &#43; (0.3,0,0) &#61; (0.3,0.7,0.7) &#61; (77,179,179)
蓝色和绿色的部分&#xff0c;原颜色即为蓝色(0,1,1)&#xff0c;目标颜色即为绿色(0,1,0)得
最终颜色 &#61; (0,1,1) * 0.7 &#43; (0,1,0) * 0.3 &#61; (0,0.7,0.7) &#43; (0,0.3,0) &#61; (0,1,0.7) &#61; (0,255,179)
我们通过颜色选择工具获取透明蓝色方块的两种颜色可以发现正确无误。