1.3 风吹椰林场景的开发

前两节分别给出了两个单一的用顶点着色器实现软体的案例,本节将给出一个综合性的软体案例。此案例为风吹海滩上椰子林的场景,场景中海浪拍打沙滩,椰子树在风的吹动下摇摆,伴随着海浪的声音,非常吸引人。

提示

本案例中的海浪实际就是放平的、采用了海水纹理的飘扬的旗帜,天空采用的是天空穹,海岛采用的是灰度图地形,海浪的声音采用的是声音池。这些技术在前面的章节中都已经详细介绍过,因此本节就不再赘述。而椰子树随风摆动是本案例的重点,下面将详细进行介绍。

1.3.1 椰子树随风摇摆的基本原理

介绍椰子树的具体开发之前首先需要了解一下沙滩椰子树随风摆动的基本原理。本案例中椰子树的树干会随着风力的大小、方向产生对应的弯曲,下面的图1-9给出了如何计算某一帧中树干上指定顶点弯曲后位置的策略。

▲图1-9 椰子树树干弯曲原理图

从图1-9中可以看出,为了简化计算,本案例中采用的风向是与 xoz 平面平行的。设当前风向与 z 轴正方向的夹角为α,树干原始状态下与y轴重合。点A为树干模型中的任一顶点,在风的吹动下偏转到A'点。

则顶点着色器需要计算的问题为:已知A点坐标(x0,y0,z0)、当前风向与 z 轴正方向的夹角α以及弧OA'所在圆的半径OO',求A点偏转到A'点后的坐标。

提示

本案例采用的计算模型中,半径OO'的大小与风力的大小是成反比例的,风力越大,半径OO'越小。这样就非常容易地实现了风越大,树干弯曲得越厉害。

下面给出了具体的计算步骤。

(1)由于OA'为半径为OO'的一段圆弧,那么可以得出OA=OA,且OO=OA'。

(2)根据弧长公式,可得出树干弯曲后的弧对应的圆心角θ的弧度计算公式如下。

θ= OA'/ OO'= OA/ OO'

(3)从图1-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'- O '×cos(Y0/ OO'))×sin(α)

y1= A'D= OO'×sin(Y0/ OO')

z1= z0+ Oz'= z0+(OO'- OO'×cos(y0/ OO'))×cos(α)

从上述得出的顶点位置变换公式中可以看出,只需要改变风向角度α,就可以使椰子树向不同的方向摆动。同时,只需要根据风力大小改变弯曲半径OO'的大小,就可以改变椰子树树干的弯曲程度。

1.3.2 开发步骤

上一小节介绍了树干弯曲的基本原理,本小节将基于此原理开发一个呈现风吹椰林场景的案例Sample1_3,其运行效果如图1-10所示。

▲图1-10 案例Sample1_3的运行效果图

本案例运行时可以通过手指在屏幕上左右滑动使摄像机绕场景转动,上下滑动使摄像机推近或远离场景。通过单击手机上的菜单键,程序会弹出菜单。选择菜单中的风向选项可以设置风向,选择菜单中的风力选项可以设置风力,如图1-11所示。

了解了案例的运行效果后,接下来将对本案例的具体开发过程进行简要介绍。由于本案例中的大部分类和前面章节很多案例中的非常类似,因此,在这里只给出本案例中比较有代表性的与椰子树相关的部分,具体内容如下所列。

▲图1-11 设置风向和风力的界面

(1)首先给出的是用于生成椰子树树干原始位置顶点坐标的initVertexData方法,其来自于表示椰子树树干的TreeTrunk类,具体代码如下。

代码位置:见随书光盘中源代码/第1章/Sample1_3/com/bn/Sample1_3目录下的TreeTrunk.java。

      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              //当前四边形左上点 xyz坐标
      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              //当前四边形左下点 xyz坐标
      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              //当前四边形右上点 xyz坐标
      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+
                            longitude_span)));
      23              //当前四边形右下点 xyz坐标
      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章/Sample1_3/ assets目录下的vertex_tree.sh。

      1    uniform mat4 uMVPMatrix;                     //总变换矩阵
      2    uniform float bend_R;                        //这里指的是树干弯曲的半径
      3    uniform float direction_degree;              //用角度表示的风向,沿 z轴正方向逆时针旋转
      4    attribute vec3 aPosition;                    //顶点位置
      5    attribute vec2 aTexCoor;                     //顶点纹理坐标
      6    varying vec2 vTextureCoord;                  //用于传递给片元着色器的纹理坐标
      7    void main(){
      8          float curr_radian=aPosition.y/bend_R;           //计算当前的弧度
      9          float result_height=bend_R*sin(curr_radian);    //计算当前点变换后的 y坐标
      10         float increase=bend_R-bend_R*cos(curr_radian);
      11         float result_X=aPosition.x+increase*sin(radians(direction_degree));
                                                                    //计算当前点最后的 x坐标
      12         float result_Z=aPosition.z+increase*cos(radians(direction_degree));
                                                                    //计算当前点最后的 z坐标
      13         vec4 result_point=vec4(result_x,result_height,result_z,1.0); //最后结果顶点的坐标
      14         gl_Position = uMVPMatrix * result_point;//根据总变换矩阵计算此次绘制此顶点的位置
      15         vTextureCoord = aTexCoor;                //将接收的纹理坐标传递给片元着色器
      16   }

说明

上述顶点着色器实现了上一小节介绍的顶点随风力、风向变换的算法。读者要想彻底掌握最好比对上一小节介绍的原理研读代码,直接看代码可能难于理解。

(4)介绍完树干部分后,下面介绍一下树叶随风摆动的相关代码。本案例中的树叶采用的是纹理矩形实现的,每棵椰子树有6片树叶(6个纹理矩形)。树叶会根据风向、风力改变位置姿态,本身不会发生形变。首先给出用于绘制树叶的纹理矩形的顶点及纹理坐标生成方法initVertexData,其来自于TreeLeaves类,具体代码如下。

代码位置:见随书光盘中源代码/第1章/Sample1_3/com/bn/Sample1_3目录下的TreeLeaves.java。

      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[]{   0,height+absolute_height,0, 0,absolute_height,0,
      8                width,height+absolute_height,0, width,height+absolute_height,0,
      9                     0,absolute_height,0,  width,absolute_height,0,
      10           };
      11           texCoor=new float[]{ 1,0, 1,1, 0,0,  0,0, 1,1, 0,1 };  //纹理坐标
      12           terX=width/2;  enterZ=0;                                //确定中心点坐标
      13          break;
      14          case 1:       //第一种情况,与 x轴夹角60°的树叶纹理矩形
      11           ……//此处省略了后面5种不同情况的代码,与第一种情况套路完全相同
      15       }

提示

上述initVertexData方法的主要功能为根据情况编号生成对应角度树叶纹理矩形的顶点数据,情况编号从0~5,分别对应0°、60°、120°、180°、240°、300°6种情况。

(5)接下来给出的是根据当前帧对应的风向、风力计算出树叶纹理矩形位置与姿态数据的resultPoint方法,其来自TreeLeavesControl类,具体代码如下。

代码位置:见随书光盘中源代码/第1章/Sample1_3/com/bn/Sample1_3目录下的TreeLeaves Control.java。

      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    //计算结果的 xz分量
      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行根据计算出的弧度及风向计算出树叶位置偏移的xyz分量。

● 第12-14行是根据当前的风力、风向计算出树叶的旋转轴xz分量以及旋转角度。

(6)最后给出的是绘制树叶的drawSelf方法,其来自TreeLeavesControl类,具体代码如下。

代码位置:见随书光盘中源代码/第1章/Sample1_3/com/bn/Sample1_3目录下的TreeLeaves Control.java。

      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方法计算出来的位置偏移数据,旋转轴、旋转角度数据,在绘制树叶前首先对坐标系进行对应的平移,然后再对坐标系进行对应的旋转,最后绘制树叶。