用OpenGL定义了形状之后,下一步是绘制他们。以OpenGL ES 2.0绘制形状要写比你想象要多一些的代码,因为所提供的API提供了许多对图形渲染pipeline的控制。
本节课讲解如何用OpenGL ES 2.0 API绘制上节课所定义的形状。
初始化形状
在做任何绘制之前,需要初始化并加载所要绘制的形状。除非形状的结构(最初的坐标)在程序执行过程中会变化,否则你应该用你的渲染器的onSurfaceCreated()方法来初始化形状,这样做是为了内存效率和处理效率考虑。
public void onSurfaceCreated(GL10 unused, EGLConfig config) { ... // 初始化一个三角形 mTriangle = new Triangle(); // 初始化一个正方形 mSquare = new Square(); }
绘制形状
用OpenGL ES 2.0绘制一个定义好的形状需要大量的代码,因为你必须提供许多图形渲染通道的设置细节。特别的,你应该定义如下:
- 顶点着色器Vertex Shader - OpenGL ES 图形代码,用于渲染一个形状的顶点
- 片段着色器Fragment Shader - OpenGL ES 代码,用于以颜色或者纹理来渲染一个图形的face
- 项目Program - 一个OpenGL ES 对象,包含了你所要使用的shader
你需要至少一个Vertex Shader来绘制一个图形,至少一个fragment shader来染色这个图形。这些shader必须首先被编译,然后添加到OpenGL ES Program中,program用于绘制形状。
private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}"; private final String fragmentShaderCode = "precision mediump float;" + "uniform vec4 vColor;" + "void main() {" + " gl_FragColor = vColor;" + "}";
shader包含OpenGL着色语言(GLSL)代码,代码必须在其被使用之前被编译。为了编译这些代码,在你的渲染器类中创建一个工具方法:
public static int loadShader(int type, String shaderCode){ // 创建一个vertex shader 类型 (GLES20.GL_VERTEX_SHADER) // 或者是一个 a fragment shader 类型 (GLES20.GL_FRAGMENT_SHADER) int shader = GLES20.glCreateShader(type); // 将源代码加入到shader,然后编译 GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); return shader; }
为了绘制形状,需要先编译shader代码,然后加入到一个OpenGL ES Program对象,然后链接这个program对象。将这些操作放在要绘制对象的构造函数中,这样保证只会被执行一次。
注意: 编译OpenGL ES shader然后链接OpenGL ES program操作非常耗费CPU周期,处理时间长,因此应当避免执行此操作超过一次。如果你不知道在运行时shader的内容,你应该构建你的代码这样shader只会被创建一次并且会被缓存以备将来的使用。
public Triangle() { ... int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); mProgram = GLES20.glCreateProgram(); // 创建一个空的OpenGL ES Program GLES20.glAttachShader(mProgram, vertexShader); // 把vertex shader添加到program GLES20.glAttachShader(mProgram, fragmentShader); // 把fragment shader到program GLES20.glLinkProgram(mProgram); // 创建OpenGL ES program可执行文件 }
到这一步,就可以执行实际的绘制形状的调用了。用OpenGL ES 绘制形状需要你指定几个参数以通知渲染通道你想要绘制什么,如何绘制。由于这些参数因形状不同而不同,所以将其包含在具体形状类的绘制逻辑代码中比较合适。
创建一个draw()
方法作为绘制图形的总调方法。在这个方法中,首先将位置值和颜色值设置到形状的vertex shader和fragment shader,然后开始绘制形状。
public void draw() { // 把program添加到OpenGL ES环境 GLES20.glUseProgram(mProgram); // 获取vertex shader的vPosition的句柄 mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); // Enable a handle to the triangle vertices GLES20.glEnableVertexAttribArray(mPositionHandle); // 准备三角形坐标数据 GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); // 获取fragment shader的vColor句柄 mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); // 设置颜色 GLES20.glUniform4fv(mColorHandle, 1, color, 0); // 绘制三角形 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); // Disable vertex array GLES20.glDisableVertexAttribArray(mPositionHandle); }
当你将所有这些代码都准备好,绘制形状只需要在你的渲染器的onDrawFrame()方法中调用draw()。当运行app时,它会看起来是这个样子:
图 1. 无投影和摄像机视角的三角形
这个代码示例中有几个问题。首先,它不会给使用者以深刻的印象。其次,这个三角形有些扁平歪曲,而且当你改变屏幕方向时其形状也随之改变。形状看起来歪曲的原因是形状的顶点并未根据GLSurfaceView显示区域的比例而纠正。下节课中,用投影和摄像机视角来解决此问题。
最后,这个三角形是静止不动的,有点单调乏味。在后面课程中,你可以用OpenGL ES图形通道旋转形状,并且做一些其他有趣的事情。