顶点着色器能让手机游戏画面实现过去想都不敢想的效果,比如让一面静态的旗帜变得像被风吹动一样自然,或者让水面产生逼真的波浪起伏。这种技术在ES 1.x时代几乎无法高效实现,而现在只需要几段巧妙的代码就能完成。
波浪运动的核心原理
要实现旗帜飘扬的效果,首先得把旗帜的模型做细。不能像以前那样只用两个三角形拼成一个矩形,而是要用大量的小三角形组成网格。这样顶点数量多了,才能通过改变每个顶点的位置来模拟飘动。
具体计算时采用正弦曲线来控制顶点位置。假设旗帜面向z轴正方向,原始状态下所有顶点的z坐标都是0。顶点着色器会根据每个顶点的x坐标计算出新的z值,让顶点在z轴方向上下振动,从而形成波浪。
计算过程其实不复杂。先算出当前顶点离最左边顶点的水平距离,把这个距离乘上一个转换系数变成角度值,再加上一个随时间变化的基础角度,最后对这个角度求正弦就得到了z坐标的偏移量。
三种不同方向的波浪效果
案例中实现了三种不同风格的波浪效果,让旗帜的飘动方式更加多样化。第一种是沿x轴方向传播的波浪,看起来就像风吹过旗帜时产生的横向波纹,这是最基础也最直观的效果。
第二种是斜向下方向的波浪,计算时不仅考虑x坐标,还把y轴方向的因素也加了进去。这样波浪就会沿着斜线方向传播,视觉效果更加丰富,有点像旗帜被风吹得斜着飘起来。
第三种是x和y两个方向的波浪叠加效果。先分别算出x方向波浪和y方向波浪在当前顶点产生的z坐标,然后把两个值加在一起。这样旗帜表面就会出现纵横交错的波纹,看起来更加自然真实。
密度参数控制波浪形态
顶点着色器里有一个变量专门用来控制波浪的密度。这个值调得越大,波浪的数量就越多,旗帜表面的起伏也就越频繁。开发者可以根据需要灵活调整,实现从微波荡漾到大浪翻涌的各种效果。
比如在模拟旗帜飘扬时,密度值可以设置得小一些,让波浪看起来舒展自然。如果是模拟水面效果,密度值就可以适当加大,表现出水面的细碎波纹。这种参数化的设计大大提高了技术的适用性。
纹理矩形的数据传递
为了能让顶点着色器正常工作,需要在Java代码中把起始角度和角度总跨度这些数据传进去。这些数据每帧都会变化,比如让起始角度在0到2π之间连续循环,这样旗帜就能一直飘动下去。
纹理矩形类的代码大部分和之前的案例相同,主要就是增加了传递这些参数的部分。具体的实现细节在光盘的源代码里都有,读者可以直接参考,不需要重新编写重复的代码。
片元着色器的纹理采样
三套顶点着色器对应的片元着色器是完全一样的,都是采用普通的纹理采样方式。这部分代码在很多前面的案例中都已经出现过,就是根据纹理坐标从旗帜贴图上取出对应的颜色值。
这样设计的好处是顶点着色器专注于处理位置变化,片元着色器负责表面颜色,分工明确。开发者只需要换不同的顶点着色器组合,就能快速实现多种不同的波浪效果。
真机运行的实际体验
由于书中的插图是灰度印刷,而且画面是静态的,很难完全展示出波浪的动态效果。强烈建议读者把案例代码放到真机上运行,这样才能真正感受到旗帜飘扬的流畅动画。
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方向波浪的规整、斜向下波浪的变化以及双向波浪的复杂。这种动态效果在ES 1.x时代需要大量CPU计算才能勉强实现,现在用顶点着色器轻松搞定。
看了这三种波浪效果,你觉得在实际游戏开发中,哪种更适合用来表现水面,哪种更适合表现旗帜?欢迎在评论区分享你的看法,点赞转发让更多开发者看到这个实用的技术。
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 }


