第一章 创建OpenGL ES的环境
首先需要为OpenGL ES创建一个视图(View)容器,一种实现方式是创建一个类实现GLSurfaceView和GLSurfaceView.Renderer。GLSurfaceView是显示图形的视图(View)容器,GLSurfaceView.Renderer是控制画图的方法。更多的介绍可以看OpenGL ES的开发指南。
GLSurfaceView是多种集成OpenGL ES到应用方法中的一种,对于全屏或者近乎全屏的图形视图(graphics view)来说,应该选择这种方式。当用户只是对一小区域使用OpenGL ES绘制图形,那么可以选择TextureView。
这篇文章只是简单的实现绘制OpenGL ES视图的功能。
1.1 在Manifest中声明所使用到的OpenGL ES
如果你使用到OpenGL ES 2.0API,你需要在manifest中添加以下声明:
如果应用程序使用纹理压缩功能,则还必须声明应用程序支持的压缩格式,以便仅安装在兼容设备上。
1.2 为OpenGL ES图形创建一个Activity
这个Activity与其他类型应用的Activity并无不同,要说不同,也仅仅是放到Activity的layout的view不一样,你需要放入的是一个GLSurfaceView
下面的代码展示了使用GLSurfaceView作为主视图的Activity的最少代码实现:
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建一个GLSurfaceView实例然后设置为activity的ContentView.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
Note: OpenGL ES 2.0要求Android2.2(API Level 8)或者更高,所以需要指定Android工程的目标API必须高于或者等于 API 8.
1.3 创建GLSurfaceView对象
GLSurfaceView是专门用来绘制OpenGL ES图形的,但是它自己没有做多少工作,真正的绘制是由GLSurfaceView.Renderer来完成的。所以GLSurfaceView的代码页不多,你可以直接使用GLSurfaceView,但是不要这样做,因为需要处理触摸事件,我们需要自定义一个类继承GLSurfaceView。
下面是一个简单的自定义类:
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// 创建OpenGL ES 2.0 的上下文
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// 设置Renderer 到 GLSurfaceView
setRenderer(mRenderer);
}
}
除此之外你还需要设置GLSurfaceView绘制的方式,GLSurfaceView有两种绘制方式,Google API:
When renderMode is
RENDERMODE_CONTINUOUSLY
, the renderer is called repeatedly to re-render the scene. When renderMode isRENDERMODE_WHEN_DIRTY
, the renderer only rendered when the surface is created, or when requestRender is called. Defaults toRENDERMODE_CONTINUOUSLY
.
Using RENDERMODE_WHEN_DIRTY
can improve battery life and overall system performance by allowing the GPU and CPU to idle when the view does not need to be updated.
大意是RENDERMODE_CONTINUOUSLY
模式就会一直Render,如果设置成RENDERMODE_WHEN_DIRTY
,就是当有数据时才rendered或者主动调用了GLSurfaceView的requestRender
.默认是连续模式,很显然Camera适合脏模式,一秒30帧,当有数据来时再渲染。
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
这里我们设置成为RENDERMODE_WHEN_DIRTY模式,当有数据时,我们调用requestRender()再进行绘制,这样效率比较高。
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// TODO Auto-generated method stub
this.requestRender();
}
1.4 创建Renderer类
GLSurfaceView.Renderer控制向GLSurfaceView的绘制工作,它有三个方法被Android系统调用来计算在GLSurfaceView上画什么以及如何画:
-
onSurfaceCreated()
:当GLSurfaceView被创建时,会调用一次,用于设置view的OpenGL ES环境。 -
onDrawFrame()
:每次绘制图像的时候都会调用这个方法。 -
onSurfaceChanged()
:当几何图形变化时,会调用此方法,例如,当手机屏幕大小变化时。
下面代码实现了GLSurfaceView.Renderer最基本功能,它仅在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应用,它使用OpengGL 显示了一个黑色的屏幕。单这段代码并没有什么特殊之处,只是为使用OpenGL绘制做好准备。
第二章 定义形状
会定义在OpenGL ES View上所绘制的形状,是你创建高端图形应用杰作的第一步。如果你不懂OpenGL ES定义图形对象的一些基本只是,使用OpenGL ES可能会有点棘手。
本章解释OpenGL ES相对于Android设备屏幕的坐标系统、定义一个形状的基础只是、形状的外观、以及如何定义三角形和正方形。
2.1 定义一个三角形
OpenGL ES允许使用三维坐标系来绘制图像。在绘制图像之前,首先需要定义一个坐标系。然后,在OpenGL中,典型的做法是定义一个浮点类型的顶点数组来指向相应的坐标系。为了高效,将这个数组代表的坐标写入到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, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// 设置颜色RGBA(red green blue alpha)
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 为存放形状的坐标,初始化顶点字节缓冲
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4 )float 占四个字节
triangleCoords.length * 4);
// 使用设备的本点字节序
bb.order(ByteOrder.nativeOrder());
// 从ByteBuffer创建一个浮点缓冲
vertexBuffer = bb.asFloatBuffer();
// 把坐标加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
默认情况下,OpenGL ES将坐标[0,0,0](X,Y,Z)当做坐标轴的中心,[1,1,0]是坐标轴的右上角,[-1,-1,0]是坐标轴的坐下角。
绘制的顺序是非常重要的,绘制定义了哪边是正面的形状,哪边是反面的形状,一般情况下绘制的顺序是逆时针的方向,使用OpenGL ES的cull face特性,你可以只画正面而不画反面。
2.2 绘制正方形
在OpenGL上绘制三角形还是比较容易的,但是绘制正方形,相对来说就有一些麻烦了,绘制一个正方形一般的做法是,将两个三角形绘制在一起,如下图所示:
和绘制三角形一样,你需要定义一个逆时针绘制顶点的数组,并且将这个数组放入到ByteBuffer中。为了避免分别为两个三角形定义两个坐标数组,我们需要使用一个绘制列表列来告诉OpengGL 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 }; // 绘制顶点的顺序
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
这个例子告诉你创建OpenGL更复杂的形状需要什么。通常,使用三角形的集合来绘制对象,下一张,将会描述这些形状怎样绘制到屏幕上。
第三章 绘制形状
当你定义使用OpenGL绘制的形状之后,你可能想把它们绘制到屏幕上。使用OpenGL ES2.0绘制这些图形可能会比较麻烦,因为API提供了大量的功能来控制图形的渲染管道。
3.1初始化图形
在做任何绘制图形之前,你必须初始化形状然后再加载它。除非形状的结构(指原始坐标)在执行过程中发生改变,你需要在Renderer的方法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();
}
...
}
3.2 绘制图形
使用OpenGL ES 2.0绘制一个定义好的图形需要大量的代码,因为需要必须提供图形渲染管道的许多细节,具体来说,你需要定义以下内容:
- vertex Shader - 顶点着色器,用来绘制图形的形状
- Fragment Shader - 片段着色器,用来绘制图形的颜色或者是纹理
- Program - 一个OpenGL ES对象,包含了用来绘制一个或者多个形状的shader。
你需要定义至少一个顶点着色器和片段着色器,这些形状必须被编译然后被添加到一个OpenGL ES program中,program之后会被用来绘制形状。下面代码是绘制三角形时定义的着色器语言:
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 ES环境中,为了编译着色器的代码,需要在你的Renderer类中创建一个工具类方法:
public static int loadShader(int type, String shaderCode){
// 创建一个vertex shader 类型 (GLES20.GL_VERTEX_SHADER)
// 或者一个 fragment shader 类型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 将源码添加到shader并编译
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
为了绘制你的形状,必须编译shader代码,将着色器添加到OpengGL ES program对象中,然后链接到program,在renderer对象的构造函数中做这些事情,只需要定义一次就好。
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 Program
mProgram = GLES20.glCreateProgram();
// 将vertex shader 添加到 program
GLES20.glAttachShader(mProgram, vertexShader);
// 将fragment shader 添加到 program
GLES20.glAttachShader(mProgram, fragmentShader);
// 创建一个可执行的 OpenGL ES program
GLES20.glLinkProgram(mProgram);
}
}
使用OpengGL 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() {
// 将program 添加到 OpenGL ES 环境中
GLES20.glUseProgram(mProgram);
// 获取指向vertex shader的成员vPosition的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用一个指向三角形的顶点数组的句柄
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);
// 禁用指向三角形的定点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
在renderer的onDrawFrame()
方法中调用draw()
就会绘制三角形了。
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
在上面的代码示例中有一些问题:
- 给人留下的印象不深刻。
- 当你改变屏幕的方向时,三角形有点压扁和变形。形状变形的的原因是由于顶点的租表没有根据GLSurfaceView的改变而改变,下一章节中会有解决方法。
- 这个图形没有互动,也就是没有触摸事件。
在OpenGL ES环境中,投影和相机视图的方式展现出来的物体更像人眼中的物体。通过转换矩阵坐标的方式可以模拟出这种物理视图。
- 投影-需要基于
GLSurfaceView
所展示界面的大小来进行坐标的调整。如果不这样,展示出来的图像的比例是不对的。一般情况是是在方法onSurfaceChanged()
中进行计算的。 - 相机视图-需要基于虚拟的相机位置来进行坐标的转换。OpenGL ES没有定义一个真实的相机对象,但是提供了方法来模拟相机。
第四章 应用投影和相机视图
在OpenGL ES环境中,投影和相机视图使你绘制的对象以更接近物理对象的样子显示。这是通过对坐标精确的数学变换实现的。
- 投影-这种变化根据所在的GLSurfaceView的宽和高调整对象的坐标。如果没有此变化,对象会被不规则的视口扭曲。投射变换一般只需要在OpenGL view创建或者发生变化时调用,代码卸载renderer的
onSurfaceChanged()
方法中。 - 相机视图-次变换是基于一个虚拟相机的位置调整对象的坐标。注意OpenGL ES并没有定义一个真的相机对象,而是提供了一些工具方法变换绘制对象的显示来模拟一个相机。一个相机视图的变换只可能在创建GLSurfaceView时调用,或者根据用户动作动态调用。
本章讲解了如何创建一个投影和一个相机视图,然后用刀GLSurfaceView的形状绘制过程。
4.1 定义投影(projection)
投影一般定义在GLSurfaceView.Renderer
的onSurfaceChanged()
方法中。下面的例子就是根据GLSurfaceView
的宽和高来计算转换矩阵,使用了Matrix.frustumM()
方法来计算出了一个投影变化Matrix。
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// 此投影矩阵在onDrawFrame()中将应用到对象的坐标
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
以上代码生成了一个投影矩阵,mProjectionMatrix
在onDrawFrame()
方法中和相机视图结合起来。
只对你的对象应用一个投影变换一般会导致什么也看不到。通常,你必须也对其应用一个视图变换才能看到东西。
4.2 定义相机视图
再定义一个相机视图变换以使对绘制对象的变换处理变的完整。在下面的代码中,使用Matrix.setLookAtM()
来计算相机视图转换,然后结合之前的投影矩阵。结合后的矩阵将之后传给要绘制的对象。
@Override
public void onDrawFrame(GL10 unused) {
...
// 设置相机的位置 (视图矩阵)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// 计算投影和视图变换
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// 绘制形状
mTriangle.draw(mMVPMatrix);
}
4.3 使用投影和相机转换
为了使用前面合并后的投影和相机图像变换矩阵,需要在顶点着色器定义语句中添加以下代码:
public class Triangle {
private final String vertexShaderCode =
//这个矩阵成员变量提供了一个勾子来操控
// 使用这个顶点着色器的对象的坐标
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
...
}
接下来,给draw()
方法添加参数,接受之前的矩阵:
public void draw(float[] mvpMatrix) { // 传递计算出来的变换矩阵
...
// 获得形状的变换矩阵的句柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
做完以上步骤后,就可以得到一个正确的图形了。
现在你拥有了一个正确显示形状的应用了,接下来应该给图形添加触摸事件了。
第五章 添加事件
在屏幕上绘制是OpengGL的基础能力,但是你也可以使用其他的Android图形框架类来做,包括Canvas和Drawable。但是OpenGL ES提供了另外的能力,可以再三维上移动和变换对象。总之它能创造很好的用户体验。在本文中,你将学会如何使用OpenGL ES为形状添加旋转功能。
5.1 旋转图形
在OpengGL ES 2.0中旋转一个图形,相对来说比较简单。在渲染的时候,创建另外一个转换矩阵(旋转矩阵),然后将这个矩阵合并到你的投影和相机图形变换矩阵就行了。
private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// 为三角形创建一个旋转变化
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
// 把旋转矩阵合并到投影和相机矩阵
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 画三角形
mTriangle.draw(scratch);
}
如果你的三角形在此新代码后旋转不起来,则要查看是否把GLSurfaceView.RENDERMODE_WHEN_DIRTY设置注释了。
5.2 持续的渲染
如果你已经跟着例子做到了这一步,确保GLSurfaceView
的绘制模式不是dirty模式,否则,只有在调用requestRender()
时,GLSurfaceView
才会渲染。
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data.
// To allow the triangle to rotate automatically, this line is commented out:
// 注释掉,图形就可以自动旋转了
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
除非你不让对象与用户有交互,否则启用这个设置是一个好做法。要准备接触这句的注释了,因为下一章节会用到它。
第六章 响应触摸事件
使你的OpenGL ES应用能相应触摸的关键是扩展你实现的GLSurfaceView代码,覆写onTouchEvent()方法来监听触摸事件。
本章节向你展示如何监听用户的触摸事件以使用户可以旋转某个OpenGL ES对象。
6.1 设置触摸监听事件
为了使你的OpenGL ES应用响应触摸事件,你必须在GLSurfaceView
类中实现onTouchEvent()
方法。以下代码在MOtionEvent.ACTION_MOVE
事件中计算了图形旋转的角度。
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
从代码中可以看到,在计算完旋转的角度后,调用了requesetRender()
,这是为了告诉renderer要渲染帧了。这样做的好处就是效率高,因为只有在旋转的时候才需要去渲染,其他时候不需要渲染。为了达到这种效果,我们需要设置GLSurfaceView
的渲染模式。
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
6.2 添加旋转角度接口
我们需要在自定义的MyGLRenderer
类中,定义旋转角度的接口,并且暴露出去。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
6.3 旋转图形
将旋转的角度添加到渲染的代码中:
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
至此,OpenGL ES的初步教程就叙述完了。