我们分析样例的目的是希望了解样例中各部分代码的作用以及它们之间的关系。而实现这一目的的有效方法是对样例进行简化,解析出其中最基本的语句,并分析它们的用法。
从样例的运行结果我们知道OpenGLDemo显示了一个转动的立方体,在立方体的表面还有黑莓图标作为纹理帖图。为了简化该样例,我们可以从最简单的形状——三角形开始,同时去掉旋转、纹理帖图等相对复杂的元素。所以简化样例的第一个目标是在屏幕上显示一个静止的三角形。
为了显示一个静止的三角形,我们需要了解样例中的绘制过程是如何完成的,从而进行简化。
从上一小节的分析知道,类OpenGLScreen完成了EGL的初始化工作,同时开启了一个线程不断更新和显示3D图像。其中3D模型的更新是由OpenGLScreen调用CubeRenderer的update方法完成的,调用之后OpenGLScreen再调用CubeRenderer的render方法绘绘制3D图像,最后OpenGLScreen通过eglCopyBuffers方法或者是eglSwapBuffers方法将图像显示出来。我们希望绘制的三角形是静态的,不需要运动,所以我们暂时不分析CubeRenderer的update方法。而显示3D图像的方法我们也不需要关注,可以将它当作现成的框架使用。因而,我们需要分析CubeRenderer的render方法,进而修改render方法让它显示我们希望显示的三角形。
从开发环境中打开CubeRender的render()方法,我们可以看见该方法的详细实现。先看第一句:
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
这一句的作用是清空屏幕,其中的参数是指定清空的方式,读者可以暂时不去解它的详细作用。
之后两句是矩阵变换的设置和当前矩阵的设置,有关这两句我们在下一节讨论,读者可以暂时认为这两句是开始绘制3D图像之前必须的设置。注意,这两句是必须的,虽然我们现在可以不去理解它的作用。语句如下:
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
设置之后通过gLTranslatef()方法移动模型,因为最初的时候观察点在原点上,模型也在原点上,不移动模型的话可能会看不到需要绘制的模型,有关观察点的移动我们也在下一节进行讨论。
gl.glTranslatef(0, 0, -3.5f);
移动模型之后再通过glRotatef()方法旋转模型,之所以对模型进行旋转是希望达到立方体转动的效果,我们这一节不讨论模型的运动,所以这一句在我们的简化代码里可以删除。
gl.glRotatef(_angle, 1.0f, 1.0f, 0.0f);
下面进一步要讨论的就是有关3D模型的建立。在OpenGL中,一个3D模型的建立是通过不同的面实现的,如实例中的立方体就是由六个平面组成。而一个面是通过指定面的项点进行定义的,比如,通过指定一个正方形的四个项点可以定义一个正方形的平面。当然,一个正方形的平面可以通过两个等腰三角形平面组成,这样就可以通过指定六个项点对一个平面进行定义。实例中的一个平面就是由两个等腰三角形组成的,有关实例中的平台在下面会进一步详细讨论。在3D世界中,一个点需要通过三个浮点数来指定,分别指定它的X坐标、Y坐标和Z坐标。如{0.5f,0.25f,-0.5f}就指定了一个X坐标为0.5,Y坐标为0.25,Z坐标为-0.5的一个点。通过以上简单的介绍,读者可以知道要定义一个立方体需要指定一系列的项点。不过,如果我们只希望绘制一个三角形平面的话,只需要指定3个项点,也就是9个浮点数。
在标准的OpenGL中,一个顶点可以通过方法glVertex3f()指定,如方法glVertex3f(0.5f,0.25f,-05.f)就指定了一个顶点。通过以下方法可以指定一个三角形:
glVertex3f(0.5f,0.25f,-05.f);
glVertex3f(0.0f,0.0f,-05.f);
glVertex3f(0.25f,0.5f,-05.f);
以上定义的三角形在Z坐标为-0.5的一个平面上,三个顶点的X坐标和Y坐标分别是:0.5,0.25 0,0 0.25,0.5。
不过,遗憾的是BlackBerry所支持的OpenGL ES并没有提供方法glVertex3f()。之所以不提供glVertex3f()的支持是因为可以通过顶点数组来代替一个一个的顶点,而使用顶点数组的效率更高一些。所谓顶点数组就是将一个模型的所有顶点放在一个数组里,通过这个数组来定义对应的模型。
为了在OpenGL中启用顶点数组,开发人员需要在使用顶点数组之前启用该功能。启用顶点数组功能可以通过方法glEnableClientState()完成,其中的参数是希望启动的功能。OpenGLDemo中的代码如下:
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
其中第一句用于启动顶点数组支持,第二句用于启动法向量数组的支持,第三句用于启动纹理数组的支持。有关纹理我们在下面章节进行讨论,在绘制三角形的简化代码中我们可以将第三句删除。
有关法向量数组的概念和顶点数组的概念相同,就是通过一个数组来指定不同顶点的法向量,从而避免一个一个地指定法向量。那么,法向量是什么呢?简单来讲一个顶点的法向量就是用于指定这个顶点朝向的向量。
对于刚接触3D图形的读者而言,指定一个点的朝向可能会显得有点奇怪。对于一个单纯的点而言,指定它的朝向确定没有多大意义,不过我们这里的点是用于指定平台的顶点,而一个平面是有它的朝向的。通过指定平面的朝向,可以让系统决定是否绘制这个平面,还可以让系统决定它的光照情况,总之,指定一个平面的朝向对于3D系统来讲非常重要,而一个平面的朝向是通过顶点的朝向决定的。
对于一个绝对的平面来讲,它所有顶点的朝向和平面的朝向是一致的,所有顶点的朝向也都相同。不过,如果所绘制的是一个曲面,则不同顶点的朝向一般都是不同的。对于曲面这种复杂的情况这里不做讨论。对于一个平面来讲,法向量是垂直于这个平面的方向向量。
在OpenGL中,法向量通过三个浮点数指定,分别代表X轴、Y轴和Z轴上的值,通过这三个值的所指定的点和原点连成的线就是法线,就是垂直于所定义平面的线。另外,法向量是一个向量,所以它的长度是不重要的,重要的是它的方向。求一个平面的法向量可能会涉及复杂的数学计算,不过读者可以先了解一些简单的向量,如{0,0,-1}就是一个指向Z轴负方向的向量,它对应的平面就是一个垂直于Z轴,面向Z轴负方向的面。
在启用顶点数组和法向量数组后,需要调用对应用方法指定顶点数组和法向量数组。指定顶点数组的方法为glVertexPointer,带四个参数,主要注意第一个参数和第四个参数。第一个参数为size,用于指定一个顶点由几个坐标值构成,参数必须是2、3或者是4。在样例中使用的是3,表示由3个坐标值构成一个顶点。假设有数组{0.1f,0.1f,0.1f,0.2f,0.2f,0.2f}传入的话,系统会认为{0.1f,0.1f,0.1f }是一个顶点,而{0.2f,0.2f,0.2f }是第二个顶点。第四个参数为需要传入的数组,样例中为_cubeVertices。
指定法向量数组通过方法glNormalPointer完成,带三个参数,只需要关注第三个参数即可。第三个参数是需要传入的法向量数组。传入之后系统会将每三个值作为一个法向量。样例中传入的法向量数组为_cubeNormals。
样例中指定纹理数组的方法暂不讨论。
代码如下:
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _cubeVertices);
gl.glNormalPointer(GL10.GL_FLOAT, 0, _cubeNormals);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, _cubeTexCoords);
在指定了所有数组之后,需要调用glDrawArrays开始绘图。该方法的最后一个参数指定了顶点的个数,在使用过程中需要通过对顶点数组的计算得出顶点的个数。代码如下:
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, _vertexCount);
要进一步分析样例代码的话,我们需要知道变量_cubeVertices和_cubeNormals是如何指定的。打开CubeRenderer的initialize方法,可以看到以下两行初始化_cubeVertices和_cubeNormals变量的语句:
_cubeVertices = Cube.createVertexBuffer();
_cubeNormals = Cube.createNormalBuffer();
从中可以看到CubeRenderer调用了Cube类的静态方法createVertexBuffer 和createNormalBuffer得到了顶点数组和法向量数组。由此跟踪到Cube类,可以看到Cube类中对于顶点数组_vertices和法向量数组_normals的定义,方法createVertexBuffer 和createNormalBuffer将数组转变成对应的FloatBuffer。
通过以上的分析可以大致了解OpenGLDemo样例是如何绘制3D图像的。为了强化对这个过程了理解。读者可以考虑修改render方法,用于绘制自己希望绘制的模型。为了简单化,本文通过修改render方法绘制一个三角形。
打开CubeRenderer的render方法,将原有代码注释掉,开始编写自己的代码。
首先是清空屏幕,然后进行矩阵变换设置,这三行可以直接拷贝原有代码:
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
然后通过方法glTranslatef移动模型。有关模型的移动我们在下一节进行讨论,在进行详细讨论之前请读者暂时不要修改这一句,仍使用{0,0,-3.5f}。
gl.glTranslatef(0, 0, -3.5f);
然后开始启用顶点数组和法向量数组,可以从原来的代码中拷贝以下两行:
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
进一步开始定义顶点数组,样例中是通过Cube类获得顶点数组,并保存在一个变量中。为了更直观、更简单的实现简化代码,我们可以在render方法中直接定义这个数组。注意,render方法是每次绘制都会调用的,在这里定义数组并不是好的做法,在真实项目的代码中请按样例的结构在其它地方预先定义这些数组。
首先定义一个浮点数组,数组中有9个浮点数,对应3个顶点。这里定义的是在Z坐标为0.5的平面上的一个三角形,在这个平面上的XY坐标分别是:{-0.5,0.5},{-0.5,-0.5},{0.5,0.5},可以看出这是一个等腰三角形。
float[] _vertices = { -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f,
0.5f, 0.5f, };
然后通过对应方法将以上浮点数组转换成FloatBuffer:
FloatBuffer bufferVertices = ByteBuffer.allocateDirect(
_vertices.length * 4).asFloatBuffer();
bufferVertices.put(_vertices);
bufferVertices.rewind();
接着需要定义法向量数组,因为以上定义的三角形垂直于Z轴,所以这个三角形的法线应该和Z轴平行,或者可以说Z轴就是这个三角形的法线。我们设定这个三角形朝向Z轴的正方向,所以三个项点的法向量都是{0,0,1},对应的法向量数组为:
float[] _normals = { 0, 0, 1, 0, 0, 1, 0, 0, 1, };
定义法向量数组后需要将它转换成FloatBuffer,方法和顶点数组的转换方法相同。
接着需要计算顶点的个数,本例中顶点是固定的3个,不需要进行计算,不过作为一个好习惯,可以通过顶点数组计算出来,这样修改顶点数组之后就不需要在这里修改顶点个数了。
int vertexNumber = bufferVertices.remaining() / 3;
最后调用对应的方法设置顶点数组和法向量数组,并通过方法glDrawArrays将它绘制出来。
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices);
gl.glNormalPointer(GL10.GL_FLOAT, 0, bufferNormals);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertexNumber);
保留其它地方的代码,运行该样例,可以看到运行结果如图18-6:
图18-6 简化的样例,显示静止的三角形
代码清单18-5中列出的是修改后的render()方法的完整代码,在完整的OpenGLDemo样例中打开CubeRenderer的render()方法,将清单中的代码覆盖原来的代码,其它代码保持不变,编译运行它就可以得到图18-6中的结果。
代码清单18-5
public void render(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, -3.5f);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
float[] _vertices = { -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f,
0.5f, 0.5f, };
FloatBuffer bufferVertices = ByteBuffer.allocateDirect(
_vertices.length * 4).asFloatBuffer();
bufferVertices.put(_vertices);
bufferVertices.rewind();
float[] _normals = { 0, 0, 1, 0, 0, 1, 0, 0, 1, };
FloatBuffer bufferNormals = ByteBuffer.allocateDirect(
_normals.length * 4).asFloatBuffer();
bufferNormals.put(_normals);
bufferNormals.rewind();
int vertexNumber = bufferVertices.remaining() / 3;
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, bufferVertices);
gl.glNormalPointer(GL10.GL_FLOAT, 0, bufferNormals);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, vertexNumber);
}
如果需要进一步修改3D模型,只需要对数组_vertices和_normals进行操作,增加对应的顶点和法向量。要注意这里项点以三个为一组,表示一个平面,所以,每增加一个平面需要增加三个顶点和三个法向量。每个顶点由X,Y,Z三个元素组成,每个法向量也由X,Y,Z三个元素组成,所以每增加一个平面需要在_vertices数组中增加9个浮点数,同时需要在_normals数组中增加9个浮点数。
代码清单18-6中的代码就增加了两个平面,一个是垂直于X轴的三角形平面,一个是垂直于Y轴的三角形平面。
代码清单18-6
float[] _vertices = {
-0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f };
float[] _normals = {
0, 0, 1, 0, 0, 1, 0, 0, 1,
-1, 0, 0, -1, 0, 0, -1, 0, 0,
0, 1, 0, 0, 1, 0, 0, 1, 0 };
读者可以将此样例作为参考了解如何在现有的3D模型中添加平面。不过读者运行这段程序后会发现运行结果和上面的例子没有差别,原因是新增加的两个平面一个垂直于X轴,一个垂直于Y轴,从观察点看不见这两个平面。要看到新增加的两个平面需要转动模型,或者是移动观察点,有关观察点和模型的运动我们在下面章节进行讨论。