2.3 风吹椰林场景的开发
前两节分别给出了两个单一的用顶点着色器实现软体的案例,本节将给出一个综合性的软体案例。此案例为风吹海滩上椰子林的场景,场景中海浪拍打沙滩,椰子树在风的吹动下摇摆,伴随着海浪的声音,非常吸引人。
提示
本案例中的海浪实际就是放平的、采用了海水纹理的飘扬的旗帜,天空采用的是天空穹,海岛采用的是灰度图地形,海浪的声音采用的是声音池。这些技术在前面的章节中都已经详细介绍过,因此本节就不再赘述。而椰子树随风摆动是本案例的重点,下面将详细进行介绍。
2.3.1 椰子树随风摇摆的基本原理
介绍椰子树的具体开发之前首先需要了解沙滩椰子树随风摆动的基本原理。本案例中椰子树的树干会随着风力的大小、方向产生对应的弯曲,图2-9给出了如何计算某一帧中树干上指定顶点弯曲后位置的策略。
从图2-9中可以看出,为了简化计算,本案例中采用的风向是与XOZ平面平行的。设当前风向与z轴正方向的夹角为α,树干原始状态下与y轴重合。点A为树干模型中的任一顶点,在风的吹动下偏转到A'点。
顶点着色器需要计算的问题为:已知A点坐标(X0,Y0,Z0)、当前风向与z轴正方向的夹角α以及弧OA'所在圆的半径OO',求A点偏转到A'点后的坐标。
提示
本案例采用的计算模型中,半径OO'的大小与风力的大小成反比,风力越大,半径OO'越小。这样就非常容易地实现了风越大,树干弯曲得越厉害。
下面给出具体的计算步骤。
(1)由于OA'为半径为OO'的一段圆弧,那么可以得出OA'=OA,且O'O=O'A'。
(2)根据弧长公式,可得出树干弯曲后的弧对应的圆心角θ的弧度计算公式如下。
θ= OA'/ OO'= OA/ OO'
(3)从图2-9以及根据三角函数的知识可以得出如下结论。
A'D= O'A'×sin(θ)= O'O'×sin(OA/ OO')
OD=OO'- O'A'×cos(θ)= OO'- O'O'×cos(OA/ OO')
(4)接着可以得出如下结论。
OX'=OD×sin(α)=( OO'- O'O'×cos(OA/ OO'))×sin(α)
OZ'= OD×cos(α)= (OO'- O'O'×cos(OA/ OO'))×cos(α)
(5)设顶点A的坐标为(X0,Y0,Z0),偏移后A'的坐标为(X1,Y1,Z1)。则可以用Y0替换上面的OA,那么有如下结论。
OX'=(OO'- OO'×cos(Y0/ OO'))×sin(α)
OZ'= (OO'- OO'×cos(Y0/ OO'))×cos(α)
(6)最后可以得到A'点的坐标。
X1= X0+ OX'= X0+(OO'- OO'×cos(Y0/ OO'))×sin(α)
Y1= A'D= OO'×sin(Y0/ OO')
Z1= Z0+ OZ'= Z0+(OO'- OO'×cos(Y0/ OO'))×cos(α)
从上述得出的顶点位置变换公式中可以看出,只需要改变风向角度α,就可以使椰子树向不同的方向摆动。同时,只需要根据风力大小改变弯曲半径OO'的大小,就可以改变椰子树树干的弯曲程度。
2.3.2 开发步骤
上一小节介绍了树干弯曲的基本原理,本小节将基于此原理开发一个呈现风吹椰林场景的案例Sample2_3,其运行效果如图2-10所示。
本案例运行时可以通过手指在屏幕上左右滑动使摄像机绕场景转动,上下滑动使摄像机推近或远离场景。通过单击手机上的菜单键,程序会弹出菜单。选择菜单中的风向选项可以设置风向,选择菜单中的风力选项可以设置风力,如图2-11所示。
了解了案例的运行效果后,接下来简要介绍本案例的具体开发过程。由于本案例中的大部分类和前面章节很多案例中的类非常相似,因此,这里只给出本案例中比较有代表性的与椰子树相关的部分,具体内容如下。
(1)首先给出用于生成椰子树树干原始位置顶点坐标的initVertexData方法,其来自于表示椰子树树干的TreeTrunk类,具体代码如下。
1 public void initVertexData(float bottom_radius,float joint_Height,int jointNum,int
availableNum){
2 List<Float> vertex_List=new ArrayList<Float>(); //顶点坐标列表
3 List<float[]> texture_List=new ArrayList<float[]>(); //顶点纹理坐标列表
4 for(int num=0;num<availableNum;num++){ //循环计算出每节树干中的各个顶点
5 float temp_bottom_radius=bottom_radius*(jointNum-num)/(float)jointNum;
//此节树干底端半径
6 float temp_top_radius=bottom_radius*(jointNum-(num+1))/(float)jointNum;
//此节树干顶端半径
7 float temp_bottom_height=num*joint_Height; //此节树干底端的y坐标
8 float temp_top_height=(num+1)*joint_Height; //此节树干顶端的y坐标
9 //循环一周,生成组成此节树干各个四边形的顶点坐标,并卷绕成三角形
10 for(float hAngle=0;hAngle<360;hAngle=hAngle+longitude_span) {
11 //当前四边形左上点的x、y、z坐标
12 float x0=(float) (temp_top_radius*Math.cos(Math.toRadians(hAngle)));
13 float y0=temp_top_height;
14 float z0=(float) (temp_top_radius*Math.sin(Math.toRadians(hAngle)));
15 //当前四边形左下点的x、y、z坐标
16 float x1=(float) (temp_bottom_radius*Math.cos(Math.toRadians(hAngle)));
17 float y1=temp_bottom_height;
18 float z1=(float) (temp_bottom_radius*Math.sin(Math.toRadians(hAngle)));
19 //当前四边形右上点的x、y、z坐标
20 float x2=(float) (temp_top_radius*Math.cos(Math.toRadians(hAngle+
longitude_span)));
21 float y2=temp_top_height;
22 float z2=(float) (temp_top_radius*Math.sin(Math.toRadians(hAngle+
alongitude_span)));
23 //当前四边形右下点的x、y、z坐标
24 float x3=(float) (temp_bottom_radius*Math.cos(Math.toRadians (hAngle+
longitude_span)));
25 float y3=temp_bottom_height;
26 float z3=(float) (temp_bottom_radius*Math.sin(Math.toRadians(hAngle+ longitude_
span)));
27 //将顶点坐标按照卷绕成两个三角形的顺序依次放入顶点坐标列表
28 vertex_List.add(x0);vertex_List.add(y0);vertex_List.add(z0);
29 vertex_List.add(x1);vertex_List.add(y1);vertex_List.add(z1);
30 vertex_List.add(x2);vertex_List.add(y2);vertex_List.add(z2);
31 vertex_List.add(x2);vertex_List.add(y2);vertex_List.add(z2);
32 vertex_List.add(x1);vertex_List.add(y1);vertex_List.add(z1);
33 vertex_List.add(x3);vertex_List.add(y3);vertex_List.add(z3);
34 }
35 ……//此处省略了计算纹理坐标以及将顶点坐标与纹理坐标送入缓冲的代码
36 }
提示
从上述代码中可以看出,椰子树的树干是由一节一节的圆形梯台组合而成的。每一节都是下面的半径大,上面的半径小,这也符合现实世界椰子树树干的情况。
(2)为了使树干能够根据风向与风力摆动,还需要在TreeTrunk类中增加将当前风向以及风力对应的树干曲率半径数据传入渲染管线的相关代码。由于将这两项数据传入渲染管线的代码与传递其他数据的代码没有本质区别,故这里不再赘述,需要的读者可以自行查看随书中的源代码。
(3)接着给出的是根据风力、风向对树干顶点位置进行变换的顶点着色器,其代码如下。
1 #version 300 es
2 uniform mat4 uMVPMatrix; //总变换矩阵
3 uniform float bend_R; //这里指的是树的弯曲半径
4 uniform float direction_degree; //用角度表示的风向,沿Z轴正方向逆时针旋转
5 in vec3 aPosition; //顶点位置
6 in vec2 aTexCoor; //顶点纹理坐标
7 out vec2 vTextureCoord; //用于传递给片元着色器的纹理坐标
8 void main(){
9 float curr_radian=aPosition.y/bend_R; //计算当前的弧度
10 float result_height=bend_R*sin(curr_radian); //计算当前点变换后的y坐标
11 float increase=bend_R-bend_R*cos(curr_radian); //计算当前点增加的长度
12 float result_X=aPosition.x+increase*sin(radians(direction_degree));
//计算当前点最后的x坐标
13 float result_Z=aPosition.z+increase*cos(radians(direction_degree));
//计算当前点最后的z坐标
14 vec4 result_point=vec4(result_X,result_height,result_Z,1.0);//最后结果顶点的坐标
15 gl_Position = uMVPMatrix * result_point;//根据总变换矩阵计算此次绘制此顶点的位置
16 vTextureCoord = aTexCoor; //将接收的纹理坐标传递给片元着色器
17 }
说明
上述顶点着色器实现了上一小节介绍的顶点随风力、风向变换的算法。读者要想彻底掌握该算法,最好比对上一小节介绍的原理研读代码,直接看代码可能难于理解。
(4)介绍完树干部分后,下面介绍树叶随风摆动的相关代码。本案例中的树叶采用纹理矩形来实现,每棵椰子树有6片树叶(6个纹理矩形)。树叶会根据风向、风力改变位置姿态,本身不会发生形变。首先给出用于绘制树叶的纹理矩形的顶点及纹理坐标生成方法initVertexData,其来自于TreeLeaves类,具体代码如下。
1 public void initVertexData(float width,float height,float absolute_height,int index) {
2 vCount=6;
3 float vertices[]=null; //顶点坐标数组
4 float texCoor[]=null; //纹理坐标数组
5 switch(index) { //根据情况编号生成对应角度树叶纹理矩形的顶点数据
6 case 0: //第一种情况,树叶纹理矩形的边与X轴重合,对应旋转角度为0
7 vertices=new float[]{
8 0,height+absolute_height,0, 0,absolute_height,0,
9 width,height+absolute_height,0, width,height+absolute_height,0,
10 0,absolute_height,0, width,absolute_height,0,
11 };
12 texCoor=new float[]{ 1,0, 1,1, 0,0, 0,0, 1,1, 0,1 };//纹理坐标
13 terX=width/2; enterZ=0; //确定中心点坐标
14 break;
15 case 1: //第二种情况,与X轴夹角60的树叶纹理矩形
16 ……//此处省略了后面5种不同情况的代码,与第一种情况套路完全相同
17 }
提示
上述initVertexData方法的主要功能为根据情况编号生成对应角度树叶纹理矩形的顶点数据,情况编号为0~5,分别对应0、60、120、180、240、3006种情况。
(5)接下来给出的是根据当前帧对应的风向、风力计算树叶纹理矩形位置与姿态数据的resultPoint方法,其来自TreeLeavesControl类,具体代码如下。
1 public float[] resultPoint(float direction_degree,float currBend_R,float
pointX,float pointY,float pointZ){
2 float []position=new float[6]; //记录位置、姿态数据的数组
3 float curr_radian=pointY/currBend_R; //计算当前的弧度
4 float result_Y=(float) (currBend_R*Math.sin(curr_radian)); //计算结果的Y分量
1 //计算结果相对于中心点的偏移距离
5 float increase=(float) (currBend_R-currBend_R*Math.cos(curr_radian));
6 //计算结果的X、Z分量
7 float result_X=(float) (pointX+increase*Math.sin(Math.toRadians(direction_degree)));
8 float result_Z=(float) (pointZ+increase*Math.cos(Math.toRadians(direction_degree)));
9 position[0]=result_X; //将计算出的位置数据存入结果数组
10 position[1]=result_Y;
11 position[2]=result_Z;
12 position[3]=(float) Math.cos(Math.toRadians(direction_degree));//计算旋转轴的X分量
13 position[4]=(float) Math.sin(Math.toRadians(direction_degree)); /计算旋转轴的Z分量
14 position[5]= (float) Math.toDegrees(curr_radian); //计算旋转的角度
15 return position; //返回结果数组
16 }
第3行利用弧长公式计算当前弯曲半径对应的弧度。
- 第4~第8行根据计算出的弧度及风向计算树叶位置偏移的X、Y、Z分量。
- 第12~第14行根据当前的风力、风向计算树叶的旋转轴X、Z分量以及旋转角度。
(6)最后给出绘制树叶的drawSelf方法,其来自TreeLeavesControl类,具体代码如下。
1 public void drawSelf(int tex_leavesId,float bend_R,float wind_direction){//绘制树叶
2 MatrixState.pushMatrix();
3 MatrixState.translate(positionX, positionY, positionZ); //移动到指定的位置
4 float curr_height=Constant.leaves_absolute_height; //当前叶子矩形的绝对高度
5 float result[]=resultPoint(wind_direction,bend_R,0,curr_height,0); //计算偏移量和旋转角
6 MatrixState.translate(result[0], result[1], result[2]); //进行偏移
7 MatrixState.rotate(result[5], result[3],0,-result[4]); //进行旋转
8 treeLeaves.drawSelf(tex_leavesId); //绘制
9 MatrixState.popMatrix();
10 }
提示
此方法根据前面resultPoint方法计算出来的位置偏移数据以及旋转轴、旋转角度数据,在绘制树叶前首先对坐标系进行对应的平移,然后再对坐标系进行对应的旋转,最后绘制树叶。