本篇文章属于 使用 OpenGL ES 进行图形绘制 这个系列的第三篇文章,主要内容是介绍在如何在 Android 应用中利用 OpenGL 绘制图形的形状。文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
在上篇文章:OpenGL ES 定义形状 中我们定义了 OpenGL 绘制的形状之后,下面就一起看看如何使用 OpenGL ES 2.0 接口绘制出在 OpenGL ES 定义形状 文章中定义的形状。
使用 OpenGL ES 2.0 绘制图形可能会腻比想象当中要复杂一些,因为 Android 中保留提供了大量对于图形渲染流程控制的 API ,就像我们在绘制自定义 View 时一样,绘制控制的方法和参数都会很丰富。
其实在前面文章:配置 OpenGL ES 的环境 里面有提到 一个核心的类 GLSurfaceView.Renderer,它是控制 view 绘制过程的渲染器,之前文章中展示了如何使用 GLSurfaceView.Renderer 进行绘制黑色背景的简单试验,所以接下来的关于形状的绘制必然少不了它的参与。
初始化形状
在开始绘制之前,需要对绘制的图形进行初始化并加载。如果这些形状结构(原始坐标)在执行过程不会发生变化,那么应该在 Renderer 的 onSurfaceCreated() 方法中进行初始化和加载,这样可以更省内存以及提升执行效率。
public class MyGLRenderer2 implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
绘制形状
使用 OpenGL ES 2.0 绘制一个定义好的形状需要较多代码,因为你需要提供很多图形渲染流程的细节,比如:
- 顶点着色器(Vertex Shader):用来渲染形状(shape)顶点的 OpenGL ES 代码。
OpenGL ES 2.0 渲染管线中顶点着色器(Vertex Shader)取代了 OpenGL ES 1.x 渲染管线中的“变换和光照”
- 片元着色器(Fragment Shader):使用颜色或纹理(texture)渲染形状表面(face)的 OpenGL ES 代码。
片元着色器取代了 OpenGL ES 1.x 渲染管线中的“纹理环境和颜色求和”、“雾”以及“Alpha测试”
- 程式(Program):一个 OpenGL ES 对象,包含了你希望用来绘制一个或更多图形(shape)所要用到的着色器(shader)。
以上三个,你需要至少一个顶点着色器(Vertex Shader)来绘制一个形状,以及一个片元着色器(Fragment Shader)为该形状上色。这些着色器必须被编译然后再添加到一个OpenGL ES Program当中,并利用这个 progrem 来绘制形状。通过编写顶点及片元着色器程序,来完成一些顶点变换和纹理颜色计算工作,实现更加灵活、精细化的计算与渲染。
下面的代码在 Triangle 类中定义了基本的着色器,我们可以利用它们绘制出一个图形:
public class Triangle {
/**
* 顶点着色器代码
* attribute变量(属性变量)只能用于顶点着色器中,不能用于片元着色器。一般用该变量来表示一些顶点数据,如:顶点坐标、纹理坐标、颜色等
* uniforms变量(一致变量)用来将数据值从应用程其序传递到顶点着色器或者片元着色器。 该变量有点类似C语言中的常量(const),即该变量的值不能被shader程序修改。一般用该变量表示变换矩阵、光照参数、纹理采样器等。
* varying变量(易变变量)是从顶点着色器传递到片元着色器的数据变量。顶点着色器可以使用易变变量来传递需要插值的颜色、法向量、纹理坐标等任意值。 在顶点与片元shader程序间传递数据是很容易的,一般在顶点shader中修改varying变量值,然后片元shader中使用该值,当然,该变量在顶点及片元这两段shader程序中声明必须是一致的。
* gl_Position 为内建变量,表示变换后点的空间位置。 顶点着色器从应用程序中获得原始的顶点位置数据,这些原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。
*/
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;" + // 颜色值传给 gl_FragColor内建变量,完成片元的着色
"}";
...
}
关于着色器和 GLSL 语言推荐几篇文章
OpenGL ES 入门(一)着色器简介
OpenGL Shading language学习总结
着色器(Shader)包含了OpenGL Shading Language(GLSL)代码,它必须先被编译然后才能在 OpenGL 环境中使用。要编译 GLSL 代码需要在渲染器类中创建一个辅助方法:
public class MyGLRenderer2 implements GLSurfaceView.Renderer
...
/**
* 加载并编译着色器代码
* @param type 渲染器类型 {GLES20.GL_VERTEX_SHADER, GLES20.GL_FRAGMENT_SHADER}
* @param shaderCode 渲染器代码 GLSL
* @return
*/
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
}
要绘制图形前,必须先编译着色器代码并将它们添加至一个 OpenGL ES Program 对象中,然后执行链接方法。
Note:编译 OpenGL ES 着色器及链接操作对于 CPU 周期和处理时间而言消耗巨大,所以应该避免重复执行这些事情。这个操作建议在形状类的构造方法中调用,这样只会执行一次。如果在执行期间不知道着色器的内容,可以考虑使用一次后缓存以备后续使用。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
// 加载编译顶点渲染器
int vertexShader = MyGLRenderer2.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
// 加载编译片元渲染器
int fragmentShader = MyGLRenderer2.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
至此,你已经完全准备好添加实际的调用语句来绘制你的图形了。使用 OpenGL ES 需要一些参数来告诉渲染流程(redering pipeline )你要绘制的内容以及如何绘制,由于每个 shape 的 drawing option 都不一样,因此将每个 shape 的绘制逻辑放到自己的类里面是一个比较好的方法。
创建一个 draw() 方法来绘制图形。下面的代码为形状的顶点着色器和形状着色器设置了位置和颜色值,然后执行绘制函数:
public class Triangle {
// 绘制形状的顶点数量
private static final int COORDS_PER_VERTEX = 3;
...
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
一旦完成了上述所有代码,仅需要在渲染器的 onDrawFrame() 方法中调用 draw() 方法就可以画出我们想要画的对象了:
public class MyGLRenderer2 implements GLSurfaceView.Renderer {
@Override
public void onDrawFrame(GL10 gl) {
...
mTriangle.draw();
}
}
运行这个应用时,它看上去会像是这样:
实际操作过程中你发现,这个三角形看上去有一些扁,另外当你改变屏幕方向时,它的形状也会随之改变。发生形变的原因是因为对象的顶点没有根据显示 GLSurfaceView 的屏幕区域的长宽比进行修正。你可以使用投影(Projection)或者相机视角(Camera View)来解决这个问题。
文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
最后,这个三角形是静止的,这看上去有些无聊。在后续文章会让这个形状发生旋转,并使用一些 OpenGL ES 图形处理流程中更加新奇的用法。