《OpenGL ES 3.x游戏开发(下卷)》一2.3 风吹椰林场景的开发

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

时间: 2024-11-16 20:31:17

《OpenGL ES 3.x游戏开发(下卷)》一2.3 风吹椰林场景的开发的相关文章

《OpenGL ES 3.x游戏开发(下卷)》一导读

前 言 OpenGL ES 3.x游戏开发(下卷) 为什么要写这样一套书 随着智能手机硬件性能的不断提升,如"水果忍者""极品飞车""狂野飙车8:极速凌云"等一批优秀的3D游戏娱乐应用在广大智能机用户间流行开来.与此同时,也带动了手机游戏产业逐渐从2D走向3D.但目前国内专门介绍3D游戏开发的书籍与资料都非常少,同时3D应用开发的门槛又比较高,使得很多初学者无从下手.根据这种情况,笔者结合多年从事3D游戏应用开发的经验编写了这样一套书. 了解一些

《OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例》——6.4节点法向量和面法向量

6.4 点法向量和面法向量 OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例 本章前面几节的案例都是基于球面开发的,球面属于连续.平滑的曲面,因此面上的每个顶点都有确定的法向量.但现实世界中的物体表面并不都是连续.平滑的,此时对于面上的某些点的法向量计算就不那么直观了,图6-18说明了这个问题. 从图6-18中可以看出,顶点A位于长方体左.上.前3个面的交界处,此处是不光滑的.这种情况下顶点A的法向量有两种处理策略,具体如下所列. 在顶点A的位置放置3个不同的顶点,每个顶点看作是仅

《OpenGL ES 3.x游戏开发(上卷)》一第1章 Android概述

第1章 Android概述 OpenGL ES 3.x游戏开发(上卷)随着移动互联网时代的到来,智能手机逐渐走进了人们的生活, Google公司于2007年11月5日发布了基于Linux平台的开源手机操作系统--Android.由于Android系统的开源性以及其他各个方面的因素,其受到了广大手机厂商的青睐,因此需要大量的Android开发人员来满足日益增长的海量软件开发需求. 提示 有些读者可能会有一点奇怪,不是介绍OpenGL ES 3.0的知识吗,怎么一开始就介绍Android呢?这是因为

《OpenGL ES 3.x游戏开发(上卷)》一第2章 游戏开发相关的 Android基础知识

第2章 游戏开发相关的 Android基础知识 OpenGL ES 3.x游戏开发(上卷)虽然本书主要是介绍OpenGL ES 3.0 3D应用及游戏开发的,但由于很多3D游戏应用中还需要用到目标平台的一些其他应用开发方面的知识,而本书3D基础知识部分主要是借助于Android平台来介绍OpenGL ES 3.0 3D应用及游戏开发的.故本章将向读者简要介绍一些在Android游戏开发中必备的一些基础知识,如音效.文件的读取.游戏信息的存储.Socket网络.蓝牙网络等. 提示 由于介绍Andr

OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例》一6.3 定位光与定向光

6.3 定位光与定向光 OpenGL ES 2.0游戏开发(上卷):基础技术和典型案例上一节中介绍的光照效果都是基于定位光光源的,定位光光源类似于现实生活中的白炽灯灯泡,其在某个固定的位置,发出的光向四周发散.定位光照射的一个明显特点就是,在给定光源位置的情况下,对不同位置的物体产生的光照效果不同. 现实世界中并不都是定位光,例如照射到地面上的阳光,光线之间是平行的,这种光称为定向光.定向光照射的明显特点是,在给定光线方向的情况下,场景中不同位置的物体反映出的光照效果完全一致.图6-16中对定位

《OpenGL ES 3.x游戏开发(下卷)》一1.2 顶点数组对象

1.2 顶点数组对象 使用了顶点缓冲技术后,绘制效率有了较大的提升.但是还有一点不尽如人意,那就是顶点的位置坐标.法向量.纹理坐标等不同方面的数据每次使用时需要单独指定,重复了一些不必要的工作.OpenGL ES 3.0考虑到了这一点,提供了一种专门用于解决此问题的对象--顶点数组对象(VAO).本节将介绍顶点数组对象. 1.2.1 基本知识与案例效果 顶点数组对象的主要功能就是将绘制一个物体时所需的对应于不同方面(如顶点坐标.法向量.纹理坐标)的顶点缓冲及相关设置包装成一个整体,绘制时直接使用

《OpenGL ES 3.x游戏开发(下卷)》一2.7 固定渲染管线与可编程渲染管线实现方案的对比

2.7 固定渲染管线与可编程渲染管线实现方案的对比 本章最开始提到过,在固定渲染管线平台上想高效地实现本章案例中的特效是非常困难的.这是因为在固定渲染管线中,顶点数据一旦送入渲染管线后就不可能对其方便地自定义处理了.因此,在固定渲染管线上想实现本章案例中的特效只能采用以下两种策略之一. 提示 回顾一下,OpenGL ES 1.x(含1.0和1.1)采用的是固定渲染管线,从OpenGL ES 2.0开始采用可编程渲染管线. 1.初始化时预先计算数据此种策略的基本思想非常简单,就是在初始化时将动画中

《OpenGL ES 3.x游戏开发(下卷)》一1.1 顶点缓冲区

1.1 顶点缓冲区 将绘制物体的顶点数据保存在内存中,在调用glDrawArrays或者glDrawElements等绘制方法前需要调用相应的方法将数据送入显存,I/O开销大,性能不够好. 若采用顶点缓冲区对象存放顶点数据,则不需要在每次绘制前都将顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,每次绘制时直接使用显存中的数据,可以大大提高渲染性能. 1.1.1 基本知识 OpenGL ES 3.0中支持两种类型的顶点缓冲区对象,分别为数组缓冲区对象(Array Buff

《OpenGL ES 3.x游戏开发(下卷)》一1.5 其他缓冲区对象操作

1.5 其他缓冲区对象操作 前面已经介绍了顶点缓冲区对象.顶点数组对象.一致缓冲区对象以及映射缓冲区对象,本节将介绍两个其他的缓冲区对象操作,包括复制缓冲区对象(Copying Buffer Objects)以及从颜色缓冲区复制纹理数据(Copying Texture Data from the Color Buffer). 1.5.1 复制缓冲区对象 到目前为止,已经介绍了如何使用glBufferData.glBufferSubData以及 glMapBufferRange方法将数据加载到缓冲