1 OpenGL ES简介
谈到OpenGL ES,首先我们应该先去了解一下Android的基本架构,基本架构下图:
这里我们可以找到Libraries里面有我们目前要接触的库,即OpenGL ES。
根据上图可以知道Android 目前是支持使用开放的图形库的,特别是通过OpenGL ES API来支持高性能的2D和3D图形。OpenGL是一个跨平台的图形API。为3D图形处理硬件指定了一个标准的软件接口。OpenGL ES 是适用于嵌入式设备的OpenGL规范。
Android 支持OpenGL ES API版本的详细状态是:
OpenGL ES 1.0 和 1.1 能够被Android 1.0及以上版本支持
OpenGL ES 2.0 能够被Android 2.2及更高版本支持
OpenGL ES 3.0 能够被Android 4.3及更高版本支持
OpenGL ES 3.1 能够被Android 5.0及以上版本支持
2 OpenGL ES使用
在了解OpenGL的使用之前,我们需要了解两个基本类别的Android框架:GLSurfaceView和GLSurfaceView.Renderer。
2.1 GLSurfaceView
GLSurfaceView从名字就可以看出,它是一个SurfaceView。看源码可知,GLSurfaceView继承自SurfaceView,并增加了Renderer,它的作用就是专门为OpenGL显示渲染使用的。
2.2 GLSurfaceView.Renderer
此接口定义了在GLSurfaceView中绘制图形所需的方法。您必须将此接口的实现作为单独的类提供,并使用GLSurfaceView.setRenderer()将其附加到您的GLSurfaceView实例。
GLSurfaceView.Renderer要求实现以下方法:
- onSurfaceCreated():创建GLSurfaceView时,系统调用一次该方法。使用此方法执行只需要执行一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形对象。
- onDrawFrame():系统在每次重画GLSurfaceView时调用这个方法。使用此方法作为绘制(和重新绘制)图形对象的主要执行方法。
- onSurfaceChanged():当GLSurfaceView的发生变化时,系统调用此方法,这些变化包括GLSurfaceView的大小或设备屏幕方向的变化。例如:设备从纵向变为横向时,系统调用此方法。我们应该使用此方法来响应GLSurfaceView容器的改变。
介绍完了GlSurfaceView和GlSurfaceView.renderer之后,接下来说下如何使用GlSurfaceView:
1、创建一个GlSurfaceView
2、为这个GlSurfaceView设置渲染
3、在GlSurfaceView.renderer中绘制处理显示数据
3 OpenGL ES绘制图形
3.1 OpenGL ES环境搭建
为了在Android应用程序中使用OpenGL ES绘制图形,必须要为他们创建一个视图容器。其中最直接或者最常用的方式就是实现一个GLSurfaceView和一个GLSurfaceView.Renderer。GLSurfaceView是用OpenGL绘制图形的视图容器,GLSurfaceView.Renderer控制在该视图内绘制的内容。
3.1.1 在Manifest中声明OpenGL ES使用
了让你的应用程序能够使用OpenGL ES 2.0的API,你必须添加以下声明到manifest:
如果你的应用程序需要使用纹理压缩,你还需要声明你的应用程序需要支持哪种压缩格式,以便他们安装在兼容的设备上。
3.1.2 创建一个Activity 用于展示OpenGL ES 图形
使用OpenGL ES的应用程序的Activity和其他应用程的Activity一样,不同的地方在于你设置的Activity的布局。在许多使用OpenGL ES的app中,你可以添加TextView,Button和ListView,还可以添加GLSurfaceView。
下面的代码展示了使用GLSurfaceView做为主视图的基本实现:
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
3.1.3 创建GLSurfaceView对象
GLSurfaceView是一个特殊的View,通过这个View你可以绘制OpenGL图像。但是View本身没有做太多的事情,主要的绘制是通过设置在View里面的GLSurfaceView.Renderer 来控制的。实际上,创建这个对象的代码是很少的,你能会想尝试跳过extends的操作,只去创建一个没有被修改的GLSurfaceView实例,但是不建议这样去做。因为在某些情况下,你需要扩展这个类来捕获触摸的事件,捕获触摸的事件的方式会在后面的文章里面做介绍。
GLSurfaceView的基本代码很少,为了快速的实现,通常会在使用它的Activity中创建一个内部类来做实现:
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer);
}
}
你可以通过设置GLSurfaceView.RENDERMODE_WHEN_DIRTY来让你的GLSurfaceView监听到数据变化的时候再去刷新,即修改GLSurfaceView的渲染模式。这个设置可以防止重绘GLSurfaceView,直到你调用了requestRender(),这个设置在默写层面上来说,对你的APP是更有好处的。
3.1.4 创建一个GLSurfaceView.Renderer类
实现了GLSurfaceView.Renderer 类才是真正算是开始能够在应用中使用OpenGL ES。这个类控制着与它关联的GLSurfaceView 绘制的内容。在Renderer 里面有三个方法能够被Android系统调用,以便知道在GLSurfaceView绘制什么以及如何绘制:
- onSurfaceCreated() - 在View的OpenGL环境被创建的时候调用。
- onDrawFrame() - 每一次View的重绘都会调用
- onSurfaceChanged() - 如果视图的几何形状发生变化(例如,当设备的屏幕方向改变时),则调用此方法。
下面是使用OpenGL ES 渲染器的基本实现,仅仅做的事情就是在GLSurfaceView绘制一个黑色背景。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
3.2 OpenGL ES定义形状
我们能够配置好基本的Android OpenGL 使用的环境。但是如果我们不了解OpenGL ES如何定义图像的一些基本知识就使用OpenGL ES进行绘图还是有点棘手的。所以能够在OpenGL ES的View里面定义要绘制的形状是进行高端绘图操作的第一步。
下面讲解Android设备屏幕相关的OpenGL ES坐标系统,定义形状,形状面的基础知识,以及定义三角形和正方形。
3.2.1 定义三角形
OpenGL ES允许你使用三维空间坐标系定义绘制的图像,所以你在绘制一个三角形之前必须要先定义它的坐标。在OpenGL中,这样做的典型方法是为坐标定义浮点数的顶点数组。
为了获得最大的效率,可以将这些坐标写入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, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// 设置颜色,分别为red, green, blue 和alpha (opacity)
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// // 为存放形状的坐标,初始化顶点字节缓冲
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4)float占四字节
// 使用设备的本点字节序
bb.order(ByteOrder.nativeOrder());
// 从ByteBuffer创建一个浮点缓冲
vertexBuffer = bb.asFloatBuffer();
//把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
//设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
请注意,此图形的坐标以逆时针顺序定义。 绘图顺序非常重要,因为它定义了哪一面是您通常想要绘制的图形的正面,以及背面。
3.2.2 定义正方形
可以看到,在OpenGL里面定义一个三角形很简单。但是如果你想要得到一个更复杂一点的东西呢?比如一个正方形?能够找到很多办法来作到这一点,但是在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, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; //顶点的绘制顺序
// 设置图形的RGB值和透明度
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4))
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
//为绘制列表初始化字节缓冲
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (对应顺序的坐标数 * 2)short是2字节
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
这个例子让你了解用OpenGL创建更复杂的形状的过程。 一般来说,您使用三角形的集合来绘制对象。
3.3 OpenGL ES绘制形状
3.3.1 初始化形状
在你做任何绘制操作之前,你必须要初始化并加载你准备绘制的形状。除非形状的结构(指原始的坐标)在执行过程中发生改变,你都应该在你的Renderer的方法onSurfaceCreated()中进行内存和效率方面的初始化工作。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
3.3.2 绘制形状
使用OpenGLES 2.0画一个定义好的形状需要比较多的代码,因为你必须为图形渲染管线提供一大堆信息。特别的,你必须定义以下几个东西:
- Vertex Shader - 用于渲染形状的顶点的OpenGLES 图形代码。
- Fragment Shader - 用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。
- Program - 一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。
你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定义一个可以用来绘制形状的基本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们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法:
private int loadShader(int type, String shaderCode) {
//根据type创建顶点着色器或者片元着色器
//创建一个vertex shader类型(GLES20.GL_VERTEX_SHADER)
//或一个fragment shader类型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
//将资源加入到着色器中,并编译
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个program。在renderer对象的构造器中做这些事情,从而只需做一次即可。
注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
//编译shader代码
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// // 创建空的OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// 将vertex shader(顶点着色器)添加到program
GLES20.glAttachShader(mProgram, vertexShader);
// 将fragment shader(片元着色器)添加到program
GLES20.glAttachShader(mProgram, fragmentShader);
// 创建可执行的 OpenGL ES program
GLES20.glLinkProgram(mProgram);
}
}
此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。
创建一个draw()方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和fragmentshader,然后执行绘制功能:
private int mPositionHandle;
private int mColorHandle;
//顶点个数
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
//顶点之间的偏移量
private final int vertexStride = COORDS_PER_VERTEX * 4; // 每个顶点四个字节
public void draw() {
// // 添加program到OpenGL ES环境中
GLES20.glUseProgram(mProgram);
// 获取指向vertex shader(顶点着色器)的成员vPosition的handle
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用一个指向三角形的顶点数组的handle
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 准备三角形的坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 获取指向fragment shader(片元着色器)的成员vColor的handle
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()方法:
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
当你运行程序的时候,你就应该看到以下的内容:
此例子中的代码还有很多问题。首先,它不会打动你和你的朋友。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其顶点们没有跟据屏幕的宽高比进行修正。而且这里展示出来的三角形是静止的,这样的图形是有点无聊的,在“添加动画”的文章中,我们会使用OpenGL ES 的视图管线来旋转此形状。
源码地址:https://github.com/Xiaoben336/OpenGLES20Study
在下一篇文章,我们将使用投影和摄像头视图来修正显示的问题。