Cowboy Tech

使用OpenGL_ES显示图像

转载
原文

Android框架提供了大量的标准工具,用来创建吸引人的,功能丰富的图形界面。然而,如果我们希望应用在屏幕上所绘制的内容进行更多的控制,或者正在尝试建立三维图像,那么我们就需要一个不同的工具了。由Android框架提供的OpenGL ES接口给予我们一组可以显示高级动画和图形的工具集,它的功能仅仅受限于我们自身的想象力。同时,在许多Android设备上搭载的图形处理单元(GPU)都能为其提供GPU加速等性能优化。

这系列课程将展示如何使用OpenGL构建应用的基础知识,包括配置,绘制对象,移动图形元素以及响应点击事件。

这系列课程所涉及的样例代码使用的是OpenGL ES 2.0接口,这是当前Android设备所推荐的接口版本。关于更多OpenGL ES的版本信息,可以阅读:OpenGL开发手册

Note:注意不要把OpenGL ES 1.x版本的接口和OpenGL ES 2.0的接口混合调用。这两种版本的接口不是通用的。如果尝试混用它们可能会让你感到无奈和沮丧。

建立OpenGL ES的环境

要在应用中使用OpenGL ES绘制图像,我们必须为它们创建一个View容器。一种比较直接的方法是实现GLSurfaceView类和GLSurfaceView.Renderer类。其中,GLSurfaceView是一个View容器,它用来存放使用OpenGL绘制的图形,而GLSurfaceView.Renderer则用来控制在该View中绘制的内容。关于这两个类的更多信息,你可以阅读:OpenGL ES开发手册

使用GLSurfaceView是一种将OpenGL ES集成到应用中的方法之一。对于一个全屏的或者接近全屏的图形View,使用它是一个理想的选择。开发者如果希望把OpenGL ES的图形集成在布局的一小部分里面,那么可以考虑使用TextureView。对于喜欢自己动手实现的开发者来说,还可以通过使用SurfaceView搭建一个OpenGL ES View,但这将需要编写更多的代码。

在这节课中,我们将展示如何在一个的Activity中完成GLSurfaceViewGLSurfaceView.Renderer的最简单的实现

在Manifest配置文件中声明使用OpenGL ES

为了让应用能够使用OpenGL ES 2.0接口,我们必须将下列声明添加到Manifest配置文件当中:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果我们的应用使用纹理压缩(Texture Compression),那么我们必须对支持的压缩格式也进行声明,确保应用仅安装在可以兼容的设备上:

<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

更多关于纹理压缩的内容,可以阅读:OpenGL开发手册

为OpenGL ES图形创建一个activity

使用OpenGL ES的安卓应用就像其它类型的应用一样有自己的用户接口,即也拥有多个Activity。主要的区别体现在Acitivity布局内容上的差异。在许多应用中你可能会使用TextViewButtonListView等,而在使用OpenGL ES的应用中,我们还可以添加一个GLSurfaceView

下面的代码展示了一个使用GLSurfaceView作为其主View的Activity:

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);
}
}

Note:OpenGL ES 2.0需要Android 2.2(API Level 8)或更高版本的系统,所以确保你的Android项目的API版本满足该要求。

构建一个GLSurfaceView对象

GLSurfaceView是一种比较特殊的View,我们可以在该View中绘制OpenGL ES图形,不过它自己并不做太多和绘制图形相关的任务。绘制对象的任务是由你在该View中配置的GLSurfaceView.Renderer所控制的。事实上,这个对象的代码非常简短,你可能会希望不要继承它,直接创建一个未经修改的GLSurfaceView实例,不过请不要这么做,因为我们需要继承该类来捕捉触控事件,这方面知识会在响应触摸事件(该系列课程的最后一节课)中做进一步的介绍。

GLSurfaceView的核心代码非常简短,所以对于一个快速的实现而言,我们通常可以在Acitvity中创建一个内部类并使用它:

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实现的可选选项,是将渲染模式设置为:GLSurfaceView.RENDERMODE_WHEN_DIRTY,其含义是:仅在你的绘制数据发生变化时才在视图中进行绘制操作:

// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

如果选用这一配置选项,那么除非调用了requestRender(),否则GLSurfaceView不会被重新绘制,这样做可以让应用的性能及效率得到提高。

构建一个渲染类

在一个使用OpenGL ES的应用中,一个GLSurfaceView.Renderer类的实现(或者我们将其称之为渲染器),正是事情变得有趣的地方。该类会控制和其相关联的GLSurfaceView,具体而言,它会控制在GLSurfaceView上绘制的内容。在渲染器中,一共有三个方法会被Android系统调用,以此来明确要在GLSurfaceView上绘制的内容以及如何绘制:

  1. onSurfaceCreated():调用一次,用来配置View的OpenGL ES环境。
  2. onDrawFrame():每次重新绘制View时被调用。
  3. onSurfaceChanged():如果View的几何形态发生变化时会被调用,例如当设备的屏幕方向发生改变时。

下面是一个非常基本的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);
}
}

就是这样!上面的代码创建了一个简单地应用程序,它使用OpenGL让屏幕呈现为黑色。虽然它的代码看上去并没有做什么非常有意思的事情,但是通过创建这些类,我们已经对使用OpenGL绘制图形有了基本的认识和铺垫。

Note:你可能想知道,自己明明使用的是OpenGL ES 2.0接口,为什么这些方法会有一个GL10的参数。这是因为这些方法的签名(Method Signature)在2.0接口中被简>单地重用了,以此来保持Android框架的代码尽量简单。

如果你对OpenGL ES接口很熟悉,那么你现在就可以在你的应用中构建一个OpenGL ES的环境并绘制图形了。当然, 如果你希望获取更多的帮助来学会使用OpenGL,那么请继续学习下一节课程获取更多的知识。

定义形状

在一个OpenGL ES View的上下文(Context)中定义形状,是创建你的杰作所需要的第一步。在了解关于OpenGL ES如何定义图形对象的基本知识之前,通过OpenGL ES 绘图可能会有些困难。

这节课将讲解OpenGL ES相对于Android设备屏幕的坐标系,定义形状和形状表面的基本知识,如定义一个三角形和一个矩形。

定义一个三角形

OpenGL ES允许我们使用三维空间的坐标来定义绘画对象。所以在我们能画三角形之前,必须先定义它的坐标。在OpenGL 中,典型的办法是为坐标定义一个浮点型的顶点数组。为了高效起见,我们可以将坐标写入一个ByteBuffer,它将会传入OpenGl ES的图形处理流程中:

public class Triangle {

private FloatBuffer vertexBuffer;

// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = {   // in counterclockwise order:
         0.0f,  0.622008459f, 0.0f, // top
        -0.5f, -0.311004243f, 0.0f, // bottom left
         0.5f, -0.311004243f, 0.0f  // bottom right
};

// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

public Triangle() {
    // initialize vertex byte buffer for shape coordinates
    ByteBuffer bb = ByteBuffer.allocateDirect(
            // (number of coordinate values * 4 bytes per float)
            triangleCoords.length * 4);
    // use the device hardware's native byte order
    bb.order(ByteOrder.nativeOrder());

    // create a floating point buffer from the ByteBuffer
    vertexBuffer = bb.asFloatBuffer();
    // add the coordinates to the FloatBuffer
    vertexBuffer.put(triangleCoords);
    // set the buffer to read the first coordinate
    vertexBuffer.position(0);
}
}

默认情况下,OpenGL ES会假定一个坐标系,在这个坐标系中,[0, 0, 0](分别对应X轴坐标, Y轴坐标, Z轴坐标)对应的是GLSurfaceView的中心。[1, 1, 0]对应的是右上角,[-1, -1, 0]对应的则是左下角。如果想要看此坐标系的插图说明,可以阅读OpenGL ES开发手册

定义一个矩形

在OpenGL中定义三角形非常简单,那么你是否想要来点更复杂的呢?比如,定义一个矩形?有很多方法可以用来定义矩形,不过在OpenGL ES中最典型的办法是使用两个三角形拼接在一起:

定义一个矩形

再一次地,我们需要逆时针地为三角形顶点定义坐标来表现这个图形,并将值放入一个ByteBuffer中。为了避免由两个三角形重合的那条边的顶点被重复定义,可以使用一个绘制列表来告诉OpenGL ES图形处理流程应该如何画这些顶点。下面是代码样例:

public class Square {

private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;

// number of coordinates per vertex in this array
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 }; // order to draw vertices

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 ES 2.0绘制图形可能会比你想象当中更复杂一些,因为API中提供了大量对于图形渲染流程的控制。

这节课将解释如何使用OpenGL ES 2.0接口画出在上一节课中定义的形状。

初始化形状

在你开始绘画之前,你需要初始化并加载你期望绘制的图形。除非你所使用的形状结构(原始坐标)在执行过程中发生了变化,不然的话你应该在渲染器的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();
}
...
}

画一个形状

使用OpenGL ES 2.0画一个定义好的形状需要较多代码,因为你需要提供很多图形渲染流程的细节。具体而言,你必须定义如下几项:

  1. 顶点着色器(Vertex Shader):用来渲染形状顶点的OpenGL ES代码。
  2. 片段着色器(Fragment Shader):使用颜色或纹理渲染形状表面的OpenGL ES代码。
  3. 程式(Program):一个OpenGL ES对象,包含了你希望用来绘制一个或更多图形所要用到的着色器。

你需要至少一个顶点着色器来绘制一个形状,以及一个片段着色器为该形状上色。这些着色器必须被编译然后添加到一个OpenGL ES Program当中,并利用它来绘制形状。下面的代码在Triangle类中定义了基本的着色器,我们可以利用它们绘制出一个图形:

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;" +
    "}";

...
}

着色器包含了OpenGL Shading Language(GLSL)代码,它必须先被编译然后才能在OpenGL环境中使用。要编译这些代码,需要在你的渲染器类中创建一个辅助方法:

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 = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                    vertexShaderCode);
    int fragmentShader = MyGLRenderer.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绘制图形需要你定义一些变量来告诉渲染流程你需要绘制的内容以及如何绘制。既然绘制属性会根据形状的不同而发生变化,把绘制逻辑包含在形状类里面将是一个不错的主意。

创建一个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() {
// 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 void onDrawFrame(GL10 unused) {
...

mTriangle.draw();
}

当你运行这个应用时,它看上去会像是这样:

openGL2

在这个代码样例中,还存在一些问题。首先,它无法给用户带来什么深刻的印象。其次,这个三角形看上去有一些扁,另外当你改变屏幕方向时,它的形状也会随之改变。发生形变的原因是因为对象的顶点没有根据显示GLSurfaceView的屏幕区域的长宽比进行修正。你可以在下一节课中使用投影(Projection)或者相机视角(Camera View)来解决这个问题。

最后,这个三角形是静止的,这看上去有些无聊。在添加移动课程当中(后续课程),你会让这个形状发生旋转,并使用一些OpenGL ES图形处理流程中更加新奇的用法。

运用投影与相机视角

在OpenGL ES环境中,利用投影和相机视角可以让显示的绘图对象更加酷似于我们用肉眼看到的真实物体。该物理视角的仿真是对绘制对象坐标的进行数学变换实现的:

  1. 投影(Projection):这个变换会基于显示它们的GLSurfaceView的长和宽,来调整绘图对象的坐标。如果没有该计算,那么用OpenGL ES绘制的对象会由于其长宽比例和View窗口比例的不一致而发生形变。一个投影变换一般仅当OpenGL View的比例在渲染器的onSurfaceChanged()方法中建立或发生变化时才被计算。关于更多OpenGL ES投影和坐标映射的知识,可以阅读Mapping Coordinates for Drawn Objects
  2. 相机视角(Camera View):这个变换会基于一个虚拟相机位置改变绘图对象的坐标。注意到OpenGL ES并没有定义一个实际的相机对象,取而代之的,它提供了一些辅助方法,通过对绘图对象的变换来模拟相机视角。一个相机视角变换可能仅在建立你的GLSurfaceView时计算一次,也可能根据用户的行为或者你的应用的功能进行动态调整。

这节课将解释如何创建一个投影和一个相机视角,并应用它们到GLSurfaceView中的绘制图像上。

定义一个投影

投影变换的数据会在GLSurfaceView.Renderer类的onSurfaceChanged()方法中被计算。下面的代码首先接收GLSurfaceView的高和宽,然后利用它并使用Matrix.frustumM()方法来填充一个投影变换矩阵(Projection Transformation 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;

// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}

该代码填充了一个投影矩阵:mProjectionMatrix,在下一节中,我们可以在onDrawFrame()方法中将它和一个相机视角变换结合起来。

Note:在绘图对象上只应用一个投影变换会导致显示效果看上去很空旷。一般而言,我们还要实现一个相机视角,使得所有对象出现在屏幕上。

定义一个相机视角

在渲染器中添加一个相机视角变换作为绘图过程的一部分,以此完成我们的绘图对象所需变换的所有步骤。在下面的代码中,使用Matrix.setLookAtM()方法来计算相机视角变换,然后与之前计算的投影矩阵结合起来,结合后的变换矩阵传递给绘制图像:

@Override
public void onDrawFrame(GL10 unused) {
...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

// Draw shape
mTriangle.draw(mMVPMatrix);
}

应用投影和相机变换

为了使用在之前章节中结合了的相机视角变换和投影变换,我们首先为之前在Triangle类中定义的顶点着色器添加一个Matrix变量:

public class Triangle {

private final String vertexShaderCode =
    // This matrix member variable provides a hook to manipulate
    // the coordinates of the objects that use this vertex shader
    "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) { // pass in the calculated transformation matrix
...

// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}

一旦我们正确地计算并应用了投影变换和相机视角变换,我们的图形就会以正确的比例绘制出来,它看上去会像是这样:
OPENGL3

现在,应用已经可以通过正确的比例显示图形了,下面就为图形添加一些动画效果吧!

添加移动

在屏幕上绘制图形是OpenGL的一个基本特性,当然我们也可以通过其它的Android图形框架类做这些事情,包括CanvasDrawable对象。OpenGL ES的特别之处在于,它还提供了其它的一些功能,比如在三维空间中对绘制图形进行移动和变换操作,或者通过其它独有的方法创建出引人入胜的用户体验。

在这节课中,我们会更深入地学习OpenGL ES的知识:对一个图形添加旋转动画。

旋转一个形状

使用OpenGL ES 2.0 旋转一个绘制图形是比较简单的。在渲染器中,创建另一个变换矩阵(一个旋转矩阵),并且将它和我们的投影变换矩阵以及相机视角变换矩阵结合在一起:

private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];

...

// Create a rotation transformation for the triangle
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 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);
}

如果完成了这些变更以后,你的三角形还是没有旋转的话,确认一下你是否将启用GLSurfaceView.RENDERMODE_WHEN_DIRTY的这一配置所对应的代码注释掉了,有关该方面的知识会在下一节中展开。

启用连续渲染

如果严格按照这节课的样例代码走到了现在这一步,那么请确认一下是否将设置渲染模式为RENDERMODE_WHEN_DIRTY的那行代码注释了,不然的话OpenGL只会对这个形状执行一次旋转,然后就等待GLSurfaceView容器的requestRender()方法被调用后才会继续执行渲染操作。

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的图形对象与用户交互呢?让我们的OpenGL ES应用可以支持触控交互的关键点在于,拓展GLSurfaceView的实现,重写onTouchEvent()方法来监听触摸事件。

这节课将会向你展示如何监听触控事件,让用户旋转一个OpenGL ES对象。

配置触摸监听器

为了让我们的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;
}

注意在计算旋转角度后,该方法会调用requestRender()来告诉渲染器现在可以进行渲染了。这种办法对于这个例子来说是最有效的,因为图形并不需要重新绘制,除非有一个旋转角度的变化。当然,为了能够真正实现执行效率的提高,记得使用setRenderMode()方法以保证渲染器仅在数据发生变化时才会重新绘制图形,所以请确保这一行代码没有被注释掉:

public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

公开旋转角度

上述样例代码需要我们公开旋转的角度,具体来说,是在渲染器中添加一个public成员变量。由于渲染器代码运行在一个独立的线程中(非主UI线程),我们必须同时将该变量声明为volatile。注意下面声明该变量的代码,另外对应的get和set方法也被声明为了public成员函数:

public class MyGLRenderer implements GLSurfaceView.Renderer {
...

public volatile float mAngle;

public float getAngle() {
    return mAngle;
}

public void setAngle(float angle) {
    mAngle = angle;
}
}

应用旋转

为了应用触控输入所生成的旋转,注释掉创建旋转角度的代码,然后添加mAngle,该变量包含了触控输入所生成的角度:

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);
}

当完成了上述步骤,我们就可以运行这个程序,并通过手指在屏幕上的滑动旋转三角形了:
opengl4