androidandroidAndroid开发经验谈

使用OpenGL ES来显示图像

2016-06-17  本文已影响3824人  keith666

Android framework提供了很多的标准工具来创建有吸引力的,功能强大的图形用户界面.但是如果你想要拥有更多的控制权来控制你的app在屏幕上画的东西或是画三维图像,你就需要使用一个不同的工具,这就是OpenGL ES. Android framework中的OpenGL ES API带有一堆用于显示高端动画的工具,只要你能想象的到的效果,它就有工具来让你实现,并且使用这些工具还可以利用大多数Android设备的GPU来加速.

下面的代码使用的是OpenGL ES 2.0的API,这个版本的API也是推荐使用的,更多的信息可参考OpenGL Developer Guide.

注意: 不要将OpenGL ES 1.x API 和 2.0 API的方法弄混,这两个API是不能相互替换的.

1. 建立一个OpenGL ES环境

要在你的应用中使用OpenGL ES来画图,你要先有一个view container.创建view container的最直接的一个方法是实现GLSurfaceViewGLSurfaceView.Renderer这两个类.其中:

注: GLSurfaceView只是一个存放OpenGL ES绘制的图像的view container,适用于全屏或近乎全屏的view. 对于只占据整个布局一部分的view来说,可以使用TextureView.你还可以直接自定义一个view继承SurfaceView,这样你就能控制更多同时也需要写更多的代码.

1.1 在Manifest中声明OpenGL ES的使用

OpenGL ES 2.0 API的声明如下,注意每个版本android:glEsVersion的值都不同:

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

如果你的应用使用了Texture compression,你也要在manife中通过<supports-gl-texture>声明你的应用所支持的格式,然后用户在Google Play上下载安装时就会过滤不支持的应用.

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

1.2 给OpenGL ES绘制的图像创建Activity

使用OpenGL ES的应用的activity和其他的应用一样,主要的不同在于content view中放的是什么.其他app放的是Button,TextView等等,而用OpenGL ES的应用的Activity不仅可以放其他标准的View,还可以放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);
    }
}

注意: OpenGL ES 2.0需要Android 2.2(API 8)及以上版本才支持.

1.3 创建一个GLSurfaceView对象

GLSurfaceView是一个特殊的View,可以用来绘制OpenGL ES图像. 实际图像的绘制是由GLSurfaceView中设置的GLSurfaceView.Renderer来控制的,因此GLSurfaceView中要做的事情不多,所以代码会很少,但是不要去直接去用匿名类来实现它,因为在后续的事件处理上还有需要.如下示例:

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

有一个可选的设置是setRenderMode(),将模式设置为RENDERMODE_WHEN_DIRTY,这样可以减少渲染次数,也就可以减少电量的使用以及更少的使用系统的GPU和CPU资源.

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

1.4 创建一个Renderer类

Renderer这个类中有个三个方法会被系统回调:

如下绘制黑色背景示例:

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 ES的使用,运行起来会是一个黑色的背景.

2. 定义Shape

定义多边形是实现各种复杂的图形的基础,下面将介绍相关基础知识.

2.1 定义三角形

OpenGL ES 允许你使用三维空间坐标来定义绘制的对象,在你绘制三角形之前,你需要先定义坐标. 最典型的做法是定义一个float类型的数组来存放三角形各顶点坐标,为了效率最大化,你可以使用ByteBuffer,这会被传到OpenGL ES的pipeline来处理.如下示例:

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设定GLSurfaceView的frame的中心为坐标的原点[0,0,0](X,Y,Z),frame的右上角的坐标为[1,1,0],左下角的坐标为[-1,-1,0],如下图(来自官网).

coordinates.png

注意:上述图形坐标是按逆时针来排序的,

2.2 定义正方形

正方形的定义方式有很多种,有一种常用的方法就是画两个三角形,如下图(来自官网):

ccw-square.png

为了避免两次定义被两个三角形公用的两个点的坐标,使用一个drawing list来告诉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);
    }
}

3. 绘制Shape

在前面定义完形状之后,现在开始将它们画出来.

3.1 初始化shape

在绘制之前,需要先将各shape初始化并加载,如果这些shape的坐标不会在执行的时候变化,那么可以在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.2 绘制一个Shape

使用OpenGL ES 2.0来绘制图像需要你提供一些细节给它,如下:

以上三个,你至少需要一个vertex shader来绘制shape和一个fragment shader来绘制颜色和texture,这些shader必须要被编译然后再添加到一个OpenGL ES program中,然后这个progrem会被用来绘制shape.如下示例:

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 Language(GLSL)代码,这些代码必须在使用之前编译,如下面的编译方法:

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 shader和把它们绑定到program上是非常消耗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);
    }
}

现在就可以来绘制shape了.使用OpenGL ES需要一些参数来告诉redering pipeline你要绘制什么并且要怎么绘制,由于每个shape的drawing option都不一样,因此将每个shape的绘制逻辑放到自己的类里面是一个比较好的方法.如下代码,将绘制逻辑方法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);
}

现在就可以调用draw()方法来绘制了,在onDrawFrame()中调用:

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

效果如下:

triangle.png

上面的示例有些不足的地方:

4. 应用Projection和Camera Views

在OpenGL ES环境中,projection和camera views允许你将图像以现实中人眼所看到的物理物体的形式显示出来,这种物理视角的模拟是通过对绘制对象的坐标进行相应的数学转换来实现:

4.1 定义一个Projection

projection转换的数据是在GLSurfaceView.RendereronSurfaceChanged()方法中计算出来的,如下:

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

注:应用projection转换到绘制对象上时会导致empty display,通常你在projection转换时也要应用camera view转换来让屏幕有东西显示.

4.2 定义一个Camera View

通过Matrix.setLookAtM()方法来计算camera view转换,然后与之前计算的projection的矩阵进行combine,如下:

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

4.3 应用Projection和Camera转换

为了让combine后的projection和camera view的转换矩阵在预览的时候显示,需要先在vertex shader中添加一个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);
}

跑起来,效果如下:

projection_camera.png

5. 添加动作

前面主要一个基本的OpenGL的绘制图像功能,实际上你还可以将其与CanvasDrawable配合一起使用.OpenGL ES还提供了其他的功能,你可以使用它们来在三维空间中移动和转换绘制对象.

5.1 旋转shape

要旋转一个图像,只需要创建一个变换矩阵(a rotation matrix)然后将其与projection和camera view的转换矩阵combine:

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

效果如下:

rotate.gif

注: 若图像没有旋转,你可能是设将GLSurfaceview的模式设置为GLSurfaceView.RENDERMODE_WHEN_DIRTY,将其注释掉即可.

5.2 允许持续渲染

如前面所说,若要运行绘制对象持续渲染,如下将渲染模式设置为RENDERMODE_CONTINUOUSLY(也是默认的),或者直接将之前设置为其他的方法注释掉:

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

6. 响应用户操作

6.1 设置Touch监听

为了响应用户的touch事件,你必须要在你的GLSurfaceView实现类中实现onTouchEvent()方法,如下:

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 ()来告诉渲染器去渲染,这是本例中最有效的方法,因为这可以减少渲染器的无用渲染次数,当然该这也要将渲染模式设置为GLSurfaceView.RENDERMODE_WHEN_DIRTY才有效:

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

6.2 暴露旋转角度

由于渲染器是在其他的线程上运行的,你需要将角度变量声明为volatile:

public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...

    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

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

6.3 应用旋转

将之前生成角度的方法注释掉,取而代之的是touch事件产生的角度:

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

最终效果如下:

touch_rotate.gif

总结

本篇只是通过一个例子简单介绍了Android中OpenGL ES的非常基本的使用, 只是熟悉一下流程,来发现该功能的强大,背后的原理还是需要去其他的地方学习.

Reference

  1. Displaying Graphics with OpenGL ES
  2. Building an OpenGL ES Environment
  3. Defining Shapes
  4. Drawing Shapes
  5. Applying Projection and Camera Views
  6. Adding Motion
  7. Responding to Touch Events
上一篇下一篇

猜你喜欢

热点阅读