- 本文简介
- OpenGL简介
- 正文
- 构建OpenGL ES 环境
- 在清单文件中申明使用OpenGL ES 版本
- 创建一个支持OpenGL ES的Activity
- 构建渲染器类
- 定义图形
- 定义三角形
- 定义正方形
- 绘制图形
- 初始化图形
- 绘制图形
朋友圈里大神发过的朋友圈:每天进步一小步,一年下来,你会发现你得到的远超你想象。只是极少有人愿意这么做,短时间无法得到积极回馈,足以将大多数人挡在门外。
本文简介
阅读本文,你将能够完成基本的OpenGL应用开发,包括初始化OpenGL,绘制图形,移动图形,触控事件的响应。
OpenGL简介
现有的Android Framework 提供了大量标准工具、接口,来实现丰富多彩的图形界面,但如果你想对这些界面图像,实现更加灵活的控制,又或者你想进入3D图形的大门,你就需要使用完全不同的控件。
Android Framework 提供了一系列的OpenGL ES APIs,用于展示你能想到的所有高端、生动的图像。并且,这些API能被大量Android设备的GPU支持。
正文
构建OpenGL ES 环境
学习如何创建一个基于OpenGL ES 技术的Android应用。
想在你的应用中使用OpenGL绘制图形,首先,你需要创建一个View Container来放置这些图形。比较直接的方式是实现GLSurfaceView 和 GLSurfaceView.Renderer。GLSurfaceView是一个可以容纳使用OpenGL绘制图形的视图容器(View Container),GLSurfaceView.Renderer 用于这些图形的控制。更多信息,请查看OpenGL ES developer guide.
另外,GLSurfaceView只是OpenGL在Android应用中应用的其中一种方式,你还可以使用TextureView和SurfaceView 让OpenGL在android应用中落地。对于全屏或近似全屏的应用来说,GLSurfaceView是一种很好的选择。如果你只是想在一个很小的布局区域内,使用OpenGL,你可以尝试TextureView。当然,SurfaceView也可以创建OpenGL ES图像,但这可能需要增加大量额外的代码。
在清单文件中申明使用OpenGL ES 版本
为了在应用中使用OpenGL,你必须在manifest中添加申明。
1. OpenGL ES 2.0 申明
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
2.OpenGL ES 3.0申明
<!-- Tell the system this app requires OpenGL ES 3.0. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
3.OpenGL ES 3.1申明
<!-- Tell the system this app requires OpenGL ES 3.1. -->
<uses-feature android:glEsVersion="0x00030001" android:required="true" />
如果你想使用纹理压缩,你必须申明纹理压缩格式,这样的话你的应用就只能安装在兼容纹理压缩的设备上了(现在的android设备大多数都支持)。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
更多纹理压缩信息,参见OpenGL developer guide.
创建一个支持OpenGL ES的Activity
使用OpenGL ES的应用和其它正常应用一样,拥有一个用户界面。他们主要的区别在于,你设置了什么样的布局在Activity中。通常你会使用TextView、Button等,在一个使用OpenGL ES的应用中,你可以添加一个GLSurfaceView。
下面是一个最简单的示例代码。
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建一个GLSurfaceViewView,
// 并将它设置为Activity的Content。
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
GLSurfaceView只是一个绘制OpenGL ES 图形的视图容器,它本身并不会太多事情。实际上,图形的绘制是交由你设置到GLSurfaceView的GLSurfaceView.Renderer控制。 因为GLSurfaceView类的代码非常少,你可能跳过继承,自己创建一个不可更改的GLSurfaceView实例。这样做你将无法获取到各种监听事件。
基本的GLSurfaceView代码量很小,为了快速实现,通常只在Activity中创建并使用一个内部内。
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// 创建一个OpenGL ES2.0的上下文。
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// 将绘制用的渲染器设置给GLSurfaceView。
setRenderer(mRenderer);
}
}
你还可以设置渲染模式GLSurfaceView.RENDERMODE_WHEN_DIRTY,表示只有当绘制数据有变化时才重新绘制。
// 只有当绘制数据变化时,才绘制/重绘视图。
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
该设置表明,GLSurfaceView框架只有在调用requestRender()时才会重绘。这对于简单应用来说,会更加高效。
构建渲染器类
GLSurfaceView.Renderer 控制绘制在GLSurfaceView上的所有图形,并且,它们之间是相互联系的。Android 系统会调用如下三个函数,来判断绘制什么以及怎样绘制。
- onSurfaceCreated() - 视图的OpenGL ES环境初始化后调用。
- onDrawFrame() - 视图每次重绘是调用。
- onSurfaceChanged() - 视图的几何属性变化是调用,例如当设备屏幕朝向变化时。
下面是一个OpenGL ES 渲染器的基本实现,除了在GLSurfaceView上绘制了一个黑色的背景,啥也没做。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 设置黑色的背景
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// 重绘背景色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
以上所有代码,创建了一个简单的Android应用,使用OpenGL展示了一个黑色背景。虽然没啥意思,但这正是使用OpenGL绘制图形元素的基础。
定义图形
学习如何定义图形,以及为什么需要知道面部(faces)和曲线(winding)。
定义图形是在OpenGL ES上下文中绘制高端图形的第一步。使用OpenGL ES绘制很智能,不需要知道一OpenGL ES是如何将你的意思传达给图形对象的。
定义三角形
OpenGL ES支持通过三维控件中的坐标绘制实物。所以,在绘制三角形之前,你需要定义一个坐标系。最高效的做法是,将坐标系写入ByteBuffer ,使用该ByteBuffer传入OpenGL ES的图形管道进行处理。
public class Triangle {
private FloatBuffer vertexBuffer;
// 数组中,每个点的坐标数。
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // 逆时针方向:
0.0f, 0.622008459f, 0.0f, // 上
-0.5f, -0.311004243f, 0.0f, // 左下
0.5f, -0.311004243f, 0.0f // 右下
};
// 设置颜色为红,绿,蓝,以及透明度值。
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 初始化三角形在坐标轴的的点,并存放在Byte buffer中。申请一个
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
// 使用设备硬件使用的字节序
bb.order(ByteOrder.nativeOrder());
// 使用浮点buffer视图操作数据
vertexBuffer = bb.asFloatBuffer();
// 在浮点缓冲视图中中添加三角形的坐标点集合。
vertexBuffer.put(triangleCoords);
// 标记缓冲去的位置为第一个
vertexBuffer.position(0);
}
}
在默认情况下,OpenGL ES认为零点[0,0,0](x, y, z)坐标位于GLSurfaceView框架的中心。[1, 1, 0]是视图框架的右上角,[-1, -1, 0]是视图框架的左下角,如下图所示。
跟多的坐标信息,请查看介绍。
注意,图形的坐标点被定义为逆时针方向。绘制顺序非常重要,因为这决定了那边是图形的正面,你想绘制的是什么以及那边是背面。
定义正方形
在OpenGL中定义一个三角形很简单,稍复杂的是定义一个正方形。通常的做法是,将两个三角形绘制在一起组成正方形。当然,这只是其中一个正方形的定义方式。
和三角形定义一样,你应该以逆时针方向定义两个三角形的所有坐标点,然后将坐标值放入ByteBuffer中。为了避免二次绘制两个三角形的共有坐标,使用绘制列表通知OpenGL ES 图形管道如何绘制这些坐标,下面是代码。
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// 数组中,每个坐标点的坐标数。
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // 左上
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.5f, 0.5f, 0.0f }; // 右上
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 坐标点绘制顺序
public Square() {
// 初始化坐标点buffer
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// 初始化绘制顺序列表buffer
ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
通过这个示例,使用OpenGL绘制复杂图形可见一斑。通常,我们使用一系列三角形的集合来绘制事物。
绘制图形
学习如何在你的应用中绘制图形。
当你使用OpenGL定义好图形后,你一定想将它们绘制出来。使用OpenGL ES2.0绘制图形需要的代码可能比你想象的多一些,因为API提供了对绘制管道大量的控制接口。
初始化图形
在绘制任何东西之前,你必须先将你要绘制的图形初始化,并加载。为了高效的内存和处理效率,你应该在渲染器的onSurfaceCreated()方法体中初始化图形,除非你使用的坐标系在程序执行过程中发生变化。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
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 使用颜色和纹理渲染图形面的代码。
- Program - 一个包含了,你想要用来绘制一个或多个图形的渲染器(Shader)的OpenGL ES事物。
你至少需要一个Vertex Shader和一个Fragment Shader来为图形着色。这些Shaders必须被编译并且加入到一个OpenGL ES的程序中,然后才能用于绘制图形。下面是如何在一个三角形class中定义基本的Shader用于绘制图形。
public class Triangle {
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 Shading Lauguage(GLSL)代码,为了在OpenGL ES的环境中使用,这些代码必须提前编译。你可以在你的渲染器中创建一些公共函数来初始化这些代码。
public static int loadShader(int type, String shaderCode){
// 创建一个顶点渲染器类型(GLES20.GL_VERTEX_SHADER)
// 或者碎片渲染器类型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 将渲染器源码加入渲染器并编译
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
为为了绘制图形,你必须编译渲染器代码,将他们添加到OpenGL ES程序中并链接。你只需要在绘制图形构造函数中做这些操作,一次就够了。
注意:编译、链接OpenGL ES渲染器和程序需要消耗CPU时间片,所以应该尽量避免多次执行。如果你不知道你的程序在运行时会有多少渲染器,你应该如下构建代码,让这些操作只执行一次并缓存起来供二次利用。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 创建一个空的OpenGL ES 程序
mProgram = GLES20.glCreateProgram();
// 将顶点渲染器添加到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 将碎片着色器添加到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 执行链接程序
GLES20.glLinkProgram(mProgram);
}
}
现在,你已经为真正绘制图形做好了一切准备。使用OpenGL ES绘制图像,你需要指定一些参数,告知渲染管道你要绘制什么,怎么绘制。因为绘制选项因图像而异,最好让你的图像类包含自己的绘制逻辑。
创建一个draw()函数来绘制图形。下面的代码,向顶点渲染器、碎片渲染器设置了位置和颜色值。
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() {
// 将渲染程序添加到OpenGL ES环境中
GLES20.glUseProgram(mProgram);
// 获取顶点渲染器vPosition成员位置的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用三角形顶点的句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 准备三角形坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 获取碎片渲染器的vColor成员句柄
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 弃用顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
当你准备好这些代码,你只需要在渲染器的onDrawFrame()中调用draw()。
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
程序运行后,三角形长这样:
这个Demo还有一些问题,比如,很丑,以及当屏幕方向变化时,图像会挤压变形。图像变形的原因是,图像的顶点没有随着屏幕的变化而变化。怎么解决,请听下回分解。