第2章 片元着色器的妙用
上一章介绍了顶点着色器的妙用,给出了不少顶点着色器的使用技巧。其实片元着色器在开发中一样可以大显身手,本章将通过几个非常实用的案例来介绍这方面的知识。
2.1 程序纹理技术
《Open GL ES 2.0游戏开发(上卷)》的案例中对物体进行纹理映射时采用的都是预先由美工人员设计好的纹理图,这种模式能满足大部分的需要。但需要满足一些特殊的效果时,也可以使用片元着色器程序来基于一定的规则计算每个片元的颜色,这就是程序纹理技术。
提示
其实《Open GL ES 2.0游戏开发(上卷)》第6章介绍光照时对圆球采用的棋盘着色器就是基于程序纹理技术,本节将再给出两个这方面的案例。
2.1.1 砖块着色器
本小节将介绍本节的第一个程序纹理着色器——砖块着色器,其基本原理如图2-1所示。
▲图2-1 砖块着色器原理图
从图2-1中可以看出,砖块着色器可以实现类似于砖墙的效果,具体的实现细节如下。
(1)先根据需着色片元的某种参数计算出片元位于哪一行(纵向分割),并记录下行号。
(2)再根据片元的参数计算出片元是否位于此行的区域1中(纵向分割),若位于区域1中,则采用砖块缝隙的水泥色着色。
(3)若片元不在区域1中,则根据行号的奇偶性及片元的参数计算出片元是否位于此行的区域3中(横向分割)。若位于区域3中则采用砖块色着色,否则也采用砖块缝隙的水泥色着色。之所以需要依据行号的奇偶性是因为奇数行与偶数行要偏移半个砖块。
提示
进行纵向分割时的依据有很多选择,如本小节案例是对球面进行的,则可以采用纬度。若表面是平面,则可以采用某个轴的坐标。进行横向分割时的依据也有很多选择,如本小节案例是对球面进行的,则可以采用经度。若表面是平面,则也可以采用某个轴的坐标。
了解了砖块着色器的基本原理后,下面请读者了解一下本小节案例Sample2_1的运行效果,如图2-2所示。
▲图2-2 案例Sample2_1的运行效果图
从图2-2中可以看出,本小节案例是针对球面应用的砖块着色器。由于构建球面的知识在本书第一卷中已经详细介绍过,故这里仅给出与砖块着色器相关的部分代码,具体内容如下。
(1)首先在负责绘制球面的Ball类中,需要增加将顶点经纬度数据传入渲染管线的相关代码,此部分代码与将顶点坐标传递进渲染管线的代码套路完全一致。因此这里不再赘述,需要的读者请参考随书光盘中的源代码。
(2)接下来给出的是顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第2章/Sample2_1/assets目录下的vertex.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵 2 attribute vec3 aPosition; //顶点位置 3 attribute vec2 aLongLat; //顶点经纬度 4 varying vec2 mcLongLat; //用于传递给片元着色器的顶点经纬度 5 void main(){ 6 gl_Position = uMVPMatrix * vec4(aPosition,1);//根据总变换矩阵计算此次绘制此顶点的位置 7 mcLongLat=aLongLat; //将顶点的经纬度传递给片元着色器 8 }
说明
与普通的顶点着色器相比,上述顶点着色器中主要是增加了接收从管线传入的顶点经纬度,并将其传递给片元着色器的相关代码。
(3)最后给出的是片元着色器,其代码如下。
代码位置:见随书光盘中源代码/第2章/Sample2_1/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec2 mcLongLat; //从顶点着色器传递过来的经纬度 3 void main(){ 4 vec3 bColor=vec3(0.678,0.231,0.129); //砖块的颜色 5 vec3 mColor=vec3(0.763,0.657,0.614); //水泥的颜色 6 vec3 color; //片元的最终颜色 7 int row=int(mod((mcLongLat.y+90.0)/12.0,2.0));//计算当前片元位于奇数行还是偶数行 8 float ny=mod(mcLongLat.y+90.0,12.0); //计算当前片元是否在此行区域1中的辅助变量 9 float oeoffset=0.0; //每行的砖块偏移值,奇数行偏移半个砖块 10 float nx; //当前片元是否在此行区域3中的辅助变量 11 if(ny>10.0){ //位于此行的区域1中 12 color=mColor; //采用水泥色着色 13 } else { //不位于此行的区域1中 14 if(row==1){ //若为奇数行则偏移半个砖块 15 oeoffset=11.0; 16 } 17 nx=mod(mcLongLat.x+oeoffset,22.0); //计算当前片元是否在此行区域3中的辅助变量 18 if(nx>20.0){ //不位于此行的区域3中 19 color=mColor; //采用水泥色着色 20 } else { //位于此行的区域3中 21 color=bColor; //采用砖块色着色 22 }} 23 gl_FragColor=vec4(color,0); //将片元的最终颜色传递进管线 24 }
说明
上述片元着色器按照前面介绍的砖块着色器的原理实现了针对球面的砖块着色器,分割的依据是片元的经纬度。有兴趣的读者还可以针对其他表面基于不同的分割依据实现砖块着色器,以进一步加深理解。
2.1.2 沙滩球着色器
上一小节介绍了砖块着色器,本小节将介绍另一种基于程序纹理技术的着色器——沙滩球着色器。沙滩球着色器的实现相比于砖块着色器要简单不少,主要思路为,将靠近球面两级的片元用白色着色,其他部分按照经度切分,以不同的颜色着色。
介绍本小节案例Sample2_2所使用的着色器前,首先请读者了解一下本小节案例的运行效果,如图2-3所示。
▲图2-3 案例Sample2_2的运行效果图
了解了案例的运行效果后,下面就可以进行案例的开发了。由于本案例中的大部分代码与上一小节的案例非常相似,主要的区别在于片元着色器。因此这里仅给出本案例中的片元着色器,其代码如下。
代码位置:见随书光盘中源代码/第2章/Sample2_2/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec4 vAmbient; //从顶点着色器传递过来的环境光分量 3 varying vec4 vDiffuse; //从顶点着色器传递过来的散射光分量 4 varying vec4 vSpecular; //从顶点着色器传递过来的镜面光分量 5 varying vec2 mcLongLat; //从顶点着色器传递过来的经纬度 6 void main(){ 7 vec3 color; //片元的最终颜色 8 if(abs(mcLongLat.y)>75.0){ 9 color = vec3(1.0,1.0,1.0); //两极附近是白色 10 }else{ 11 int colorNum = int(mcLongLat.x/45.0); //根据经度计算出颜色编号 12 if(colorNum == 0){ 13 color = vec3(1.0,0.0,0.0); //0号颜色 14 }else if(colorNum == 1){ 15 color = vec3(0.0,1.0,0.0); //1号颜色 16 }else if(colorNum == 2){ 17 color = vec3(0.0,0.0,1.0); //2号颜色 18 }else if(colorNum == 3){ 19 color = vec3(1.0,1.0,0.0); //3号颜色 20 }else if(colorNum == 4){ 21 color = vec3(1.0,0.0,1.0); //4号颜色 22 }else if(colorNum == 5){ 23 color = vec3(0.0,1.0,1.0); //5号颜色 24 }else if(colorNum == 6){ 25 color = vec3(0.3,0.4,0.7); //6号颜色 26 }else if(colorNum == 7){ 27 color = vec3(0.3,0.7,0.2); //7号颜色 28 }} 29 vec4 finalColor = vec4(color,1.0); //将颜色扩充为带Alpha通道的Vec4类型 30 //综合3个通道光的最终强度及片元的颜色计算出最终片元的颜色并传递给管线 31 gl_FragColor=finalColor*vAmbient + finalColor*vDiffuse + finalColor*vSpecular; 32 }
● 第8-9行中首先判断片元是否靠近两极,如果靠近两极,就采用白色对片元着色。
● 第11-27行首先将不靠近两极的片元根据经度确定其位于的颜色区域,然后采用对应区域的既定颜色对片元着色。
提示
相比于上一小节中砖块着色器的案例,本案例中还引入了光照。光照部分的相关代码与本书第一卷第6章介绍光照时案例中的完全一致,需要的读者请参考随书光盘中的源代码。