第1章 顶点着色器的妙用
《OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例》(以下简称第一卷)的章节中已经大量地用到了顶点着色器,但并没有能完全体现出顶点着色器的功效。本章将进一步介绍一些顶点着色器的使用技巧,通过应用这些技巧可以开发出很多酷炫的效果,而这些效果在OpenGL ES 1.x时代是很难高效实现的。
提示
由于本书是系列书籍的第二卷,故当在本书正文中提到某些代码与本书前面的案例相同,不再重复给出时,既有可能指的是本书中的案例,也可能指的是第一卷中的案例。不过读者不用担心,虽然在正文中没有给出与第一卷重复的部分代码,但本书所附的光盘中包含了本书案例的完整源代码。因此,有需要时读者既可以去参考第一卷书,也可以参看本书所附光盘中的源代码。
1.1 飘扬的旗帜
飘扬的旗帜是本章要介绍的第一个案例,通过此技术可以实现类似旗帜迎风飘扬的效果,也可以实现类似水面起伏的效果。
1.1.1 基本原理
介绍本案例的具体开发步骤之前首先需要了解一下实现旗帜飘扬的基本原理,如图1-1所示。
▲图1-1 旗帜飘扬的线框图
提示
图1-1中左图为原始情况下旗帜的顶点位置情况,右图为顶点着色器根据参数计算后某一帧画面中旗帜的顶点位置情况。
从图1-1中可以看出,矩形的旗帜与以前案例中绘制的矩形不同,不再是仅有两个三角形组成的整体,而是有大量的小三角形组成的。这样只要在绘制一帧画面时由顶点着色器根据一定的规则变换各个顶点的位置,即可得到旗帜迎风飘动的效果。
为了使旗帜的飘动过程比较平滑,本案例采用的是基于正弦曲线的顶点位置变换规则,具体情况如图1-2所示。
说明
图1-2给出的是旗帜面向z轴正方向,即顶点沿z轴上下振动,形成的波浪沿x轴传播的情况。同时注意,观察的方向是沿y轴的方向。
▲图1-2 X方向波浪原理图
从图1-2中可以看出,传入顶点着色器的原始顶点的z坐标都是相同的(本案例中为0),经过顶点着色器变换后顶点的z坐标是根据正弦曲线分布的。具体的计算方法如下。
● 首先计算出当前处理顶点的x坐标与最左侧顶点x坐标的差值,即X距离。
● 然后根据距离与角度的换算率将X距离换算为当前顶点与最左侧顶点的角度差(tempAngle)。
提示
所谓距离与角度的换算率指的是由开发人员人为设定的一个值,将距离乘以其后就可以换算成角度值。例如可以规定,x方向上距离4等于2π,则换算公式为:x距离×2π/4。
● 接着将tempAngle加上最左侧顶点的对应角度(startAngle)即可得到当前顶点的对应角度(currAngle)。
● 最后通过求currAngle的正弦值即可得到当前顶点变换后的z坐标。
可以想象出,只要在绘制每帧画面时传入不同的startAngle值(例如在0~2π连续变化),即可得到平滑的基于正弦曲线的旗帜飘扬的动画了。
1.1.2 开发步骤
上一小节介绍了飘扬旗帜的基本原理,本小节将基于此原理开发一个旗帜迎风飘扬的案例Sample1_1,其运行效果如图1-3所示。
▲图1-3 案例Sample1_1的运行效果图
提示
图1-3中所示从左到右分别为x方向波浪、斜向下方向波浪和xy双向波浪的效果。由于插图是灰度印刷且是静态的,因此可能看得不是很清楚,建议读者用真机运行本案例进行体会。
了解了案例的运行效果后,接下来对本案例的具体开发过程进行简要介绍。由于本案例中的大部分类和前面章节很多案例中的非常类似,因此在这里只给出本案例中比较有代表性的部分,具体内容如下所列。
(1)首先需要简单说明的是,表示旗帜的纹理矩形类TextureRect,其大部分代码与本书前面的很多案例基本一致。主要的区别是需要增加将起始角度和角度总跨度等数据传入渲染管线的相关代码,有了这些数据后顶点着色器在绘制每帧画面前就可以顺利地对顶点位置进行变换了。
提示
增加的代码非常简单,需要的读者请自行查阅随书光盘。
(2)从案例效果图中可以看出,本案例中的波浪方向有3种选择,因此需要3套着色器来实现不同的波浪方向。首先给出最简单的实现x方向波浪的顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第1章/Sample1_1/ assets目录下的vertex_tex_x.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵 2 uniform float uStartAngle; //本帧起始角度(即最左侧顶点的对应角度) 3 uniform float uWidthSpan; //横向长度总跨度 4 attribute vec3 aPosition; //顶点位置 5 attribute vec2 aTexCoor; //顶点纹理坐标 6 varying vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标 7 void main(){ 8 float angleSpanH=4.0*3.14159265; //横向角度总跨度,用于进行 x距离与角度的换算 9 float startX=-uWidthSpan/2.0; //起始 x坐标(即最左侧顶点的 x坐标) 10 //根据横向角度总跨度、横向长度总跨度及当前点 x坐标折算出当前顶点 x坐标对应的角度 11 float currAngle=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH; 12 float tz=sin(currAngle)*0.1; //通过正弦函数求出当前点的 Z坐标 13 //根据总变换矩阵计算此次绘制此顶点位置 14 gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tz,1); 15 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器 16 }
说明
上述顶点着色器实现了上一小节中所介绍的基于正弦曲线的x方向波浪,其中第8行的变量angleSpanH可以用来控制波浪的密度,其值越大,波浪密度越大。
(3)接着给出的是实现斜向下方向波浪的顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第1章/Sample1_1/ assets目录下的vertex_tex_xie.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵 2 uniform float uStartAngle; //本帧起始角度(即最左侧顶点的对应角度) 3 uniform float uWidthSpan; //横向长度总跨度 4 attribute vec3 aPosition; //顶点位置 5 attribute vec2 aTexCoor; //顶点纹理坐标 6 varying vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标 7 void main(){ 8 float angleSpanH=4.0*3.14159265; //横向角度总跨度,用于进行X距离与角度的换算 9 float startX=-uWidthSpan/2.0; //起始 x坐标(即最左侧顶点的 x坐标) 10 //根据横向角度总跨度、横向长度总跨度及当前点 X坐标折算出当前顶点 x坐标对应的角度 11 float currAngleH=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH; 13 float angleSpanZ=4.0*3.14159265; //纵向角度总跨度,用于进行Y距离与角度的换算 14 float uHeightSpan=0.75*uWidthSpan; //纵向长度总跨度 15 float startY=-uHeightSpan/2.0; //起始 y坐标(即最上侧顶点的 y坐标) 16 //根据纵向角度总跨度、纵向长度总跨度及当前点 y坐标折算出当前顶点 y坐标对应的角度 17 float currAngleZ=((aPosition.y-startY)/uHeightSpan)*angleSpanZ; 18 float tzH=sin(currAngleH-currAngleZ)*0.1; //通过正弦函数求出当前点的 z坐标 19 //根据总变换矩阵计算此次绘制此顶点的位置 20 gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tzH,1); 21 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器 22 }
说明
本质上讲,上述斜向下方向波浪的顶点着色器与前面的x方向波浪的顶点着色器没有本质区别,仅仅是在计算当前顶点的对应角度时增加了y轴方向的计算,不再是仅考虑x轴的坐标。因此,形成的波浪方向就是斜向下的。
(4)最后给出的是沿x、y两个方向各自传播的波浪效果叠加的顶点着色器,其代码如下。
代码位置:见随书光盘中源代码/第1章/Sample1_1/ assets目录下的vertex_tex_xy.sh。
1 uniform mat4 uMVPMatrix; //总变换矩阵 2 uniform float uStartAngle; //本帧起始角度(x、y两个方向都是其) 3 uniform float uWidthSpan; //横向长度总跨度 4 attribute vec3 aPosition; //顶点位置 5 attribute vec2 aTexCoor; //顶点纹理坐标 6 varying vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标 7 void main(){ 8 //首先计算当前顶点X方向波浪对应的 Z坐标 9 float angleSpanH=4.0*3.14159265; //横向角度总跨度,用于进行 x距离与角度的换算 10 float startX=-uWidthSpan/2.0; //起始 x坐标(即最左侧顶点的 x坐标) 11 //根据横向角度总跨度、横向长度总跨度及当前点 X坐标折算出当前顶点 x坐标对应的角度 12 float currAngleH=uStartAngle+((aPosition.x-startX)/uWidthSpan)*angleSpanH; 13 float tzH=sin(currAngleH)*0.1; //x方向波浪对应的 z坐标 14 //接着计算当前顶点Y方向波浪对应的 Z坐标 15 float angleSpanZ=4.0*3.14159265; //纵向角度总跨度,用于进行 y距离与角度的换算 16 float uHeightSpan=0.75*uWidthSpan; //纵向长度总跨度 17 float startY=-uHeightSpan/2.0; //起始 y坐标(即最上侧顶点的 y坐标) 18 //根据纵向角度总跨度、纵向长度总跨度及当前点 y坐标折算出当前顶点 y坐标对应的角度 19 float currAngleZ=uStartAngle+3.14159265/3.0+((aPosition.y-startY)/uHeight Span)*angleSpanZ; 20 float tzZ=sin(currAngleZ)*0.1; //y方向波浪对应的 Z坐标 21 //根据总变换矩阵计算此次绘制此顶点的位置, 22 gl_Position = uMVPMatrix * vec4(aPosition.x,aPosition.y,tzH+tzZ,1); 23 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器 24 }
提示
本质上讲上述 x、y 双向波浪的顶点着色器与前面的 x 方向波浪的顶点着色器没有本质区别,仅仅是首先分别计算了x方向和y方向波浪在当前顶点位置的z坐标,最后将两个z坐标叠加实现了波的叠加。因此,运行案例时看到的波浪就是x、y两个方向的了。
本案例3套着色器中的片元着色器都是一样的,并且都是采用的普通的纹理采样片元着色器,前面很多案例中已经出现过,因此这里不再赘述。