Shader编程学习笔记(六)—— Fixed Function Shader 2
Fixed Function Shader
在上一小节中我们已经了解到,Fixed Function Shader是固定功能的着色器,它的功能有限,但是编写比较简单,因为它总是通过一系列的命令达到我们对图形着色的目的。其中我们已经了解了Properties(属性)、Material(材质)、Lighting(光照),接下来来了解一下最为重要的一个命令settexture。
很多时候我们对三维物体的着色并不是简单地去设置它的光照和颜色,我们还希望给它画上贴图,这个功能应该是最为常见的,接着来实现这个功能。
在上一小节固定管线着色器的基础上来进一步编写,创建一个新的shader,命名为FixedFunctionShader2,同时创建一个材质mat_fixed_function_shader_2,并将shader关联到材质上:
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 } 11 12 SubShader { 13 pass{ 14 // color(1,0,0,1) // 分别代表了 r,g,b,a 15 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 16 material{ 17 diffuse[_Color] // 漫反射 18 ambient[_Amnient] // 环境光 19 specular[_Specular] // 高光 20 shininess[_Shininess] // 述specular强度 21 emission[_Emission] // 自发光 22 } 23 lighting on // 光照开关 24 separatespecular on // 镜面高光开关 25 26 settexture[_MainTex]{ 27 combine texture 28 } 29 } 30 } 31 }
要使用贴图采样,我们需要命令settexture,在固定管线着色器中settexture是最重要的也是最复杂的一个命令。settexture需要一个纹理属性参数“_MainTex("MainTex",2d)=""”,其中的值就是纹理的名称,默认可以是空字符串,接着在settexture结构体中添加命令“combine texture”。回到Unity工程中,创建一个新的球体,将新建的材质拖放在球体上,在该球体材质的检视面板中拖放一张贴图,效果如下:
这时就可以看到在球体上的砖块样式的贴图,但是使用这个贴图以后,球体上原有的灯光照明和高光效果已经没有了。原因是在着色器中的combine命令指的是合并,它的参数目前只是用了texture纹理,这个texture指的就是属性中的_MainTex设置的贴图,而这里只有贴图而没有应用先前已经计算好的光照数据,因此在这里我们需要为当前贴图texture乘上primary,primary是Fixed Function Shader的关键字,代表了前面所有计算材质和光照后的颜色值,将贴图和这个值相乘,就会得到一个混合的新的颜色值。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 } 11 12 SubShader { 13 pass{ 14 // color(1,0,0,1) // 分别代表了 r,g,b,a 15 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 16 material{ 17 diffuse[_Color] // 漫反射 18 ambient[_Amnient] // 环境光 19 specular[_Specular] // 高光 20 shininess[_Shininess] // 述specular强度 21 emission[_Emission] // 自发光 22 } 23 lighting on // 光照开关 24 separatespecular on // 镜面高光开关 25 26 settexture[_MainTex]{ 27 combine texture * primary 28 } 29 } 30 } 31 }
需要注意的是texture纹理的颜色值rgba每一个分量都是0到1,primary值的每一个分量也是0到1,当一个小于1的浮点数与另一个小于1的浮点数相乘,就会得到一个更小的浮点数,因此得到的颜色会变深,效果如下:
考虑到这个问题,Fixed Function Shader当中有一个关键字“double”,表示对某个结果乘以2的运算,用法为“combine texture * primary double”。修改完shader,回到工程中,可以观察到球体变亮了。
以上就是一个最基本的settexture的运用,当然在某些情况下需要为这个物体不只混合一张图,如果要混合两张或者两张以上的纹理该怎么处理呢?这里我需要注意的是,一个settexture只能带上一个参数,不能再添加第二个参数,因此需要再编写一个settexture和一个纹理属性参数_SecTex。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 pass{ 15 // color(1,0,0,1) // 分别代表了 r,g,b,a 16 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 17 material{ 18 diffuse[_Color] // 漫反射 19 ambient[_Amnient] // 环境光 20 specular[_Specular] // 高光 21 shininess[_Shininess] // 述specular强度 22 emission[_Emission] // 自发光 23 } 24 lighting on // 光照开关 25 separatespecular on // 镜面高光开关 26 27 settexture[_MainTex]{ 28 combine texture * primary double 29 } 30 31 settexture[_SecTex]{ 32 combine texture * primary double 33 } 34 } 35 } 36 }
回到Unity场景中,给_SecTex拖上一张纹理,效果如下:
可以观察到仅仅显示了第二张贴图,第一张贴图没有和第二张贴图进行混合,原因这里虽然进行了两次settexture计算,但是如果想让第一次settexture和第二次settexture的结果进行混合就不能使用“combine texture * primary”语句,因为primary总是代表前面顶点光照计算的颜色值,这里还需要加上第一次settexture的结果。在Fixed Function Shader当中除了primary外,还有一个previous关键字,previous指的是先前的数据,将代码改成“combine texture * previous”,这里的意思就是用当前纹理的值去乘上当前settexture操作之前所有计算和采样过后的结果。回到Unity,可以看到效果如下:
可以观察到已经有了需要的结果,球体已经被混合了两张纹理贴图。按照这个思路可以继续去编写多个settexture,但是纹理混合的次数不是无限的,Fixed Function Shader是基本对照于显卡硬件的固定渲染部分,所以显卡有一个混合纹理的最大个数,一般来讲越好的硬件可以混合的纹理越多,越差的硬件可以混合的纹理越少,基本上两张纹理的混合是目前所有显卡都能够支持的。
接下来来了解一下固定管线着色器透明的处理,首先把其中一个球体遮挡在另一个球体之前,如下图:
希望前面这个球体能够半透明显示,要实现物体的透明化,需要借助alpha值,alpha值介于0到1之间,0为完全透明,1位不透明。
在当前的shader程序中,最后一个settexture使用了第二张纹理与先前所有计算的颜色值相乘,如果我们去改变先前计算过程中的alpha值,那么应该可以去影响这个球体的半透明度。在当前material材质计算当中,使用了四种颜色值,分别是diffuse、ambient、specular和emission,这四种颜色的alpha值基本都是1,我们可以在检视面板中把它们的alpha值都相对降低,按理来说这些颜色的alpha值都很低了,这四个颜色去影响贴图的时候,即使贴图的alpha值为1,相乘以那些颜色的alpha后,也会得到一个比较低的alpha值,然后第二个settexture又去相乘比较低的alpha值,最后应该会得到一个很低的alpha值,但是为什么不能观察到球体的半透明化呢?这个时候就需要去了解一下Unity Shader当中一个比较重要的命令,这个命令是ShaderLab当中的Blending。如图:
在着色器渲染过后,会进入到alpha测试,alpha测试以后就会进入到Blending阶段,Blending指的是混合,它可以用SrcFactor(源元素)和DstFactor(目标元素)去进行混合运算。其中,当我们正在渲染当前这个球体时,渲染这个球体得到的颜色值就是SrcFactor,而球体以外其他的物体包括天空盒等已经在这个球体渲染之前被渲染完成的值就是DstFactor。
查看Unity Manual中的ShaderLab: Blending,在Blend factors下可以看到很多关键词,其中我们可以先关注"SrcAlpha"和“OneMinusSrcAlpha”,"SrcAlpha"指的就是当前已经渲染得到的alpha值,而“OneMinusSrcAlpha”是指用1减去"SrcAlpha"的值,我们可以将命令“Blend SrcAlpha OneMinusSrcAlpha”放在shader程序当前的渲染通道当中。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 pass{ 15 Blend SrcAlpha OneMinusSrcAlpha 16 // color(1,0,0,1) // 分别代表了 r,g,b,a 17 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 18 material{ 19 diffuse[_Color] // 漫反射 20 ambient[_Amnient] // 环境光 21 specular[_Specular] // 高光 22 shininess[_Shininess] // 述specular强度 23 emission[_Emission] // 自发光 24 } 25 lighting on // 光照开关 26 separatespecular on // 镜面高光开关 27 28 settexture[_MainTex]{ 29 combine texture * primary double 30 } 31 32 settexture[_SecTex]{ 33 combine texture * previous double 34 } 35 } 36 } 37 }
这句指令的意义是用当前渲染的alpha值与1减去当前渲染的alpha值的比例去混合之前已经被渲染好的场景颜色值。回到工程中,可以查看到球体发生了变化。如图,
虽然看起来不明显,但是我们可以发现球体整体变蓝,不过它没有真正达到透明,原因是在Unity引擎当中,存在一种渲染顺序的问题。图形显卡渲染某一帧图像的时候,当场景中有很多物体,到先渲染哪一个,后渲染哪一个,就取决于渲染顺序。在当前场景中,如果先渲染后面的球体,再渲染前面的球体,那么在渲染后面球体的时候是看不到前面这个球体的,因此在第一次渲染后面的球体时是可以看到整个球体的,如下图:
当第二次渲染前面这个球体时,该球体由于深度(z次序)被放在了靠近摄像机的地方,它会被稍后渲染,因此它就被叠加到先前生成的图像上,并且遮住了后面的物体,在这种情况下,我们就需要稍微去改变一下前面这个球体的渲染顺序,要改变渲染顺序,我们就需要了解使用ShaderLab: SubShader Tags。
在SubShader当中允许添加Tags标签,语法格式为:
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
查看“Rendering Order - Queue tag”,这是渲染队列的标签,它包括以下这些值,分别是Background、Geometry、AlphaTest、Transparent、和Overlay。一个实实在在的物体,它不透明的时候是默认使用Geometry进行渲染的,如果要去渲染半透明的物体,要使用在默认值后的渲染队列,其中我们可以使用Transparent,将“Tags { "Queue" = "Transparent" }”加到SubShader中。
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _MainTex("MainTex",2d)="" 10 _SecTex("MainTex",2d)="" 11 } 12 13 SubShader { 14 Tags { "Queue" = "Transparent" } 15 pass{ 16 Blend SrcAlpha OneMinusSrcAlpha 17 // color(1,0,0,1) // 分别代表了 r,g,b,a 18 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 19 material{ 20 diffuse[_Color] // 漫反射 21 ambient[_Amnient] // 环境光 22 specular[_Specular] // 高光 23 shininess[_Shininess] // 述specular强度 24 emission[_Emission] // 自发光 25 } 26 lighting on // 光照开关 27 separatespecular on // 镜面高光开关 28 29 settexture[_MainTex]{ 30 combine texture * primary double 31 } 32 33 settexture[_SecTex]{ 34 combine texture * previous double 35 } 36 } 37 } 38 }
回到工程中,可以观察到球体已经变半透明了,如图:
假设一张贴图里已经含有alpha通道,我们希望使用这张图本身的alpha通道去进行透明化处理,如果贴图的一部分alpha为0,一部分alpha为1,那么就会形成一种镂空的效果,怎么去设置贴图的alpha通道处理呢?我们可以在最后的settexture的combine命令中加入第二个参数texture,代码如下:
combine texture * previous double,texture
其中,逗号后面的参数只是取了alpha通道值,我们期望用当前纹理的alpha通道去做当前alpha通道的设置。
回到场景中,发现球体已经不透明了,这是因为如果在settexture的combine命令的第二个位置填写了alpha的参数,那么它只能针对这个部分去取alpha值运算,而之前所有的颜色alpha值都失效了,要想看到透明效果,我们可以把这张贴图的灰度值编程alpha值,如图:
回到场景中,可以发现球体已经半透明了,如图:
另外,还有一个命令constantColor,它可带一个参数,在properties中声明一个参数"_ConstantColor("ConstantColor",color)=(1,1,1,0.3)",将其放在第二个settexture中,然后在combine命令中参数texture乘上constant,用constant的alpha值0.3与纹理颜色中的alpha值再相乘,那么运算结果的alpha值会变得更小。回到工程场景中后会发现一个编译错误,但是没发现代码中有什么问题,这里需要注意的是先前在编写“_MainTex”和“_SecTex”纹理属性时直接用了一个空字符串,而在官方Shader教程中,纹理的值往往是有值的,比如“_MainTex("MainTex",2d)="white"()”,如果不愿意去添加这些值,可以将语句“_ConstantColor("ConstantColor",color)=(1,1,1,0.3)”添加在纹理属性之前,如:
1 Shader "Lesson/FixedFunctionShader2" { 2 3 properties{ 4 _Color("Main Color",color)=(1,1,1,1) 5 _Amnient("Ambient",color)=(0.3,0.3,0.3,0) 6 _Specular("Specular",color)=(1,1,1,1) 7 _Shininess("Shininess",range(0,8))=4 8 _Emission("Emission",color)=(1,1,1,1) 9 _ConstantColor("ConstantColor",color)=(1,1,1,1) 10 _MainTex("MainTex",2d)="" 11 _SecTex("MainTex",2d)="" 12 } 13 14 SubShader { 15 Tags { "Queue" = "Transparent" } 16 pass{ 17 Blend SrcAlpha OneMinusSrcAlpha 18 // color(1,0,0,1) // 分别代表了 r,g,b,a 19 // color[_Color] // 小括号内容表示固定值,中括号内容表示可变参数值 20 material{ 21 diffuse[_Color] // 漫反射 22 ambient[_Amnient] // 环境光 23 specular[_Specular] // 高光 24 shininess[_Shininess] // 述specular强度 25 emission[_Emission] // 自发光 26 } 27 lighting on // 光照开关 28 separatespecular on // 镜面高光开关 29 30 settexture[_MainTex]{ 31 combine texture * primary double 32 } 33 34 settexture[_SecTex]{ 35 constantColor[_ConstantColor] 36 combine texture * previous double,texture*constant 37 } 38 } 39 } 40 }
回到场景中,着色器已经可以正常工作了,这个时候可以托送检视面板中的“ContantColor”值来改变球体的透明程度,但是可以发现当“ContantColor”的值为1时,球体也是半透明的,原因就是这里使用纹理的灰度值来当透明度。
以上就是ShaderLab中的Fixed Function Shader的简单应用,比较重要的命令就是“settexture”,并且有一点复杂,而“settexture”中比较重要的命令是“combine”,“combine”命令可以配合“constantColor”使用来控制透明度,使用“blend”指令来控制最后的混合,并且使用tags标签来控制渲染顺序。