在上一篇教程中我们讨论了关于初始化一个GLSurfaceView。在我们开始新的内容之前,请确保你已经读了它。
在这篇教程中,我们将要渲染我们的第一个多边形。
3D模型是由许多可以被单独操作的小元素(顶点,边,面和多边形)组成的;
一个顶点vertex(复数vertices)是3D模型的最小单元。顶点是由两条或者多条边相交形成的点。在3D模型中,顶点可以被所有相连的边,面,多边形共享。一个顶点也可以代表相机或者光源的位置。你可以在下图中看到一个标记为黄色的顶点。
我们通过定义一个float数组,并将其放进一个bytebuffer中以提高性能的方式来定义一组顶点。如下图,我们标记的顶点对应到下面的代码中:
private float vertices[] = { -1.0f, 1.0f, 0.0f, // 0, 左上 -1.0f, -1.0f, 0.0f, // 1, 左下 1.0f, -1.0f, 0.0f, // 2, 右下 1.0f, 1.0f, 0.0f, // 3, 右上 }; // 一个float占用4字节,所以我们申请大小时要把顶点数组的长度乘以4. ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); vbb.order(ByteOrder.nativeOrder()); FloatBuffer vertexBuffer = vbb.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0);
当你通知opengl ES去渲染的时候,它会调用一些管道方法来实现。大多数管道方法是不开启的,所以你必须记得去开启你所需要的那些。你也需要去告诉这些方法去处理什么。所以,在这个例子中,我们需要告诉opengl ES我们已经创建好了顶点缓冲区,并告诉它缓冲区的位置。
// 为渲染过程中开启顶点数组的读写 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 指定渲染过程中顶点数据的位置和格式 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
当你处理完这些buffer的时候,别忘了禁用顶点数组。
// 禁用顶点数组 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
边是两个顶点之间的连线。他们是面和多边形的边框。在3D模型中,一条边可以被两个面或者多边形共有。对一个边进行变换,会影响所有的相接的点,面,多边形。在opengl ES中,你不必定义边,而应该通过给定一组顶点来定义一个面,从而建立三条边。如果你想要修改一条边,你应该修改组成这条边的两个顶点。如下图,你可以看到一条被标记为黄色的边。
面是一个三角形。面是由三个角的顶点和他们的边围成的区域。对一个面的变换,影响所有相接的顶点,边和多边形。
绘制平面的方向是很重要的,因为这个方向决定了哪一面是“前面”,哪一面是“背面”。之所以这点很重要,是因为出于性能考虑,我们不需要去同时绘制两个面,我们需要背面裁剪。所以使用同样的解析是个不错的办法。这样就可能使得通过glFrontFace定义的“前面”的方向可以被改变。
gl.glFrontFace(GL10.GL_CCW);
当然也可以改变哪个面被绘制或者不被绘制。
gl.glCullFace(GL10.GL_BACK);
是时候解析平面了,记住我们使用默认的方向——逆时针。看下图和代码解释了如何去建立一个四边形。
private short[] indices = { 0, 1, 2, 0, 2, 3 };
为了提升一点性能,我们把这个数组放到一个byteBuffer中:
// short是两个字节,所以应该是数组长度乘以2 ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2); ibb.order(ByteOrder.nativeOrder()); ShortBuffer indexBuffer = ibb.asShortBuffer(); indexBuffer.put(indices); indexBuffer.position(0);
现在我们可以在屏幕上绘制些东西了。这有两个函数可以用来绘制,我们需要决定一下使用哪一个。
这两个函数分别是:
public abstract void glDrawArrays(int mode, int first, int count)
glDrawArrays会按照我们在顶点缓冲里指定的顺序绘制顶点们。
public abstract void glDrawElements(int mode, int count, int type, Buffer indices)
glDrawElements需要多一点的信息才能绘制。他需要知道顶点的绘制顺序,还需要知道顶点的顺序缓冲indicesbuffer。
由于我们已经定义了indicesBuffer,所以我想你应该知道我们要使用哪个方法了。
这两个方法的相同之处在于他们都需要知道要绘制什么,即最原始的东西(图元)。由于绘制这些顶点的方式也有好几种,而出于我们对程序的调试需要,最好也了解一下。下面我将会介绍几种。
在屏幕上绘制独立的点
一系列连起来的线段
和上面的类似,但是多了一条连起起点和终点的线(封闭)
每两个点连接成线段,互相独立。
每三个点组成一个三角形。
按照顶点先v0,v1,v2,然后v2,v1,v3(注意顺序),接着v2,v3,v4。。。,绘制一系列的三角形。这个顺序是保证所有的三角形都是按照同一个方向绘制出来的,这样这些三角形就能正确的组成一个平面的一部分。
和上面类似,除了绘制顺序变成v0,v1,v2,然后是v0,v2,v3,接下来是v0,v3,v4。。。。
我认为GL_TRIANGLES是最容易的,所以接下来我们就使用它了。
现在让我们把四边形的代码放到一个类中。
package se.jayway.opengl.tutorial; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.ShortBuffer; import javax.microedition.khronos.opengles.GL10; public class Square { // 顶点. private float vertices[] = { -1.0f, 1.0f, 0.0f, // 0, Top Left -1.0f, -1.0f, 0.0f, // 1, Bottom Left 1.0f, -1.0f, 0.0f, // 2, Bottom Right 1.0f, 1.0f, 0.0f, // 3, Top Right }; // 顶点连接的顺序 private short[] indices = { 0, 1, 2, 0, 2, 3 }; // 顶点缓冲. private FloatBuffer vertexBuffer; // 顺序缓冲. private ShortBuffer indexBuffer; public Square() { // 一个float是4字节,所以我们申请缓冲的大小为数组长度乘以4 ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4); vbb.order(ByteOrder.nativeOrder()); vertexBuffer = vbb.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0); // 一个short是两个字节,申请缓冲大小为数组长度乘以2 ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2); ibb.order(ByteOrder.nativeOrder()); indexBuffer = ibb.asShortBuffer(); indexBuffer.put(indices); indexBuffer.position(0); } /** * 绘制我们的四边形到屏幕上 * @param gl */ public void draw(GL10 gl) { // 逆时针 gl.glFrontFace(GL10.GL_CCW); // 开启面裁剪. gl.glEnable(GL10.GL_CULL_FACE); // 指定要被裁剪的面——背面 gl.glCullFace(GL10.GL_BACK); // 开启顶点数组状态,以便我们读写顶点信息 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 指定顶点坐标数据的位置和格式 gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indexBuffer); // 禁用顶点数组 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); // 禁用裁剪 gl.glDisable(GL10.GL_CULL_FACE); } }
我们需要在Renderer中初始化我们的square类。
// 初始化square. Square square = new Square();
然后在绘制方法中调用square的绘制方法:
public void onDrawFrame(GL10 gl) { // 清除平面和深度缓存 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // 绘制四边形 square.draw(gl); // ( NEW ) }
如果你现在运行这个程序,会发现平面还是一片漆黑。为什么?因为opengl ES渲染时默认从当前位置渲染,默认是(0,0,0),和视口(这里为眼睛或者镜头最好-译者注)位置一致。而且opengl ES不会渲染离视口很近的东西。解决方法是在绘制之前,先把绘图位置向屏幕里面移动一点。
// 向屏幕里移动4个单位 gl.glTranslatef(0, 0, -4);
在下一篇教程中,我会介绍不同的平移方式。
再次运行程序,你会看到这个四边形,但是很快就向远处移动直到消失。opengl ES不会重置两帧之间的绘制点,所以你必须自己来做这事。
// 替换当前的矩阵为初始矩阵 gl.glLoadIdentity();
现在你运行这个应用可以看到四边形在固定的位置上了。
这篇教程引用如下文献:
Android Developers
OpenGL ES 1.1 Reference Pages
你可以下载教程的源码:Tutorial_Part_II
你也可以检出代码:code.google.com
前一篇教程:安卓opengl ES教程之一——初始化view
后一篇教程:安卓opengl ES教程之三——变换