• 本文简介
  • 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来放置这些图形。比较直接的方式是实现GLSurfaceViewGLSurfaceView.RendererGLSurfaceView是一个可以容纳使用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中。通常你会使用TextViewButton等,在一个使用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 图形的视图容器,它本身并不会太多事情。实际上,图形的绘制是交由你设置到GLSurfaceViewGLSurfaceView.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]是视图框架的左下角,如下图所示。

android opengl立方体 android opengles_OpenGL ES2.0

跟多的坐标信息,请查看介绍。

注意,图形的坐标点被定义为逆时针方向。绘制顺序非常重要,因为这决定了那边是图形的正面,你想绘制的是什么以及那边是背面。

定义正方形

在OpenGL中定义一个三角形很简单,稍复杂的是定义一个正方形。通常的做法是,将两个三角形绘制在一起组成正方形。当然,这只是其中一个正方形的定义方式。

android opengl立方体 android opengles_OpenGL ES2.0_02

和三角形定义一样,你应该以逆时针方向定义两个三角形的所有坐标点,然后将坐标值放入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();
}

程序运行后,三角形长这样:

android opengl立方体 android opengles_android opengl立方体_03

这个Demo还有一些问题,比如,很丑,以及当屏幕方向变化时,图像会挤压变形。图像变形的原因是,图像的顶点没有随着屏幕的变化而变化。怎么解决,请听下回分解。