Android 官方 Training 笔记之 OpenGL ES

说在前面

本篇是学习 Android 官方 Training 中关于 OpenGL ES 的笔记。
官方地址:https://developer.android.google.cn/training/graphics/opengl/index.html

前言

Android 提供了大量工具用来创建吸引人、功能丰富的界面。然而,如果我们需要对应用在屏幕上绘制的内容进行更多的控制,或者需要建立三维图像,那么这时就能考虑用 OpenGL ES 了。

Android 给我们提供的 OpenGL 接口给予了我们一组可以显示高级动画和图像的工具集,非常强大。同时,在许多 Android 设备上搭载了图形处理单元(GPU)都能为其提供 GPU 加速等性能优化。

通过本篇的学习,我们可以知道如何使用 OpenGL 构建应用的基础知识,包括配置、绘制对象、添加移动以及响应触摸事件。

注意

以下的实例代码使用的 OpenGL ES 2.0 API,它们是当前 Android 设备推荐的 API 版本。
我们要小心,不要混合使用 OpenGL ES 2.0 的 API 和 OpenGL ES 1.x 的 API 调用,这两个版本的 API 是不可以互换的!

构建 OpenGL 环境

为了让绘制的 OpenGL ES 图形显示在我们的应用程序上,我们需要为它创建一个 View 容器。
一种比较直接的方法是实现 GLSurfaceView 类和 GLSurfaceView.Renderer 类,其中 GLSurfaceView 是一个 View 容器,用来存放 OpenGL 绘制的图形,
GLSurfaceView.Renderer 则是控制器,用来控制绘制的内容。

注:对于全屏或接近全屏的图形视图,我们使用 GLSurfaceView 是一个合理的选择。如果我们只想显示在布局中的一小块部分,那么可以尝试使用 TextureView。对于喜欢自己动手实现的开发者来说,还可以通过使用 SurfaceView 搭建一个 OpenGL ES View,但这需要编写更多的代码。

在清单文件中声明

为了让我们的应用程序使用 OpenGL ES 2.0 API,我们必须在清单文件中添加如下声明:

<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" />

创建 activity

我们创建一个 activity,其内容 View 是 一个自定义的 GLSurfaceView,用于来显示 OpenGL ES 绘制的图形:

public class OpenGLES20Activity extends AppCompatActivity {

    private MyGLSurfaceView mGLView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 创建一个 GLSurfaceView 实例,并设置它为此 Activity 的 ContentView。
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
}

注:OpenGL ES 2.0 要求 Android2.2 (API8)或更高。

创建 GLSurfaceView 对象

GLSurfaceView 是一个比较特殊的 View,我们在其中绘制 OpenGL ES 图形,但是它自己却不做太多与绘制相关的任务。
实际绘图的任务是由我们在 GLSurfaceView 中配置的 GLSurfaceView.Renderer 来负责的。
所以,事实上,这个对象的代码非常简短,所以我们可能会希望跳过扩展它,来直接去创建一个未经修改的 GLSurfaceView 实例。
但是在本文中,我们需要捕获触摸事件(最后一段中会讲),所以我们需要去扩展此类来去响应其触摸事件。

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

class MyGLSurfaceView extends GLSurfaceView {

    private MyGLRenderer mRenderer;

    public MyGLSurfaceView(Context context) {
        super(context);
        // 创建 OpenGL ES 2.0 context
        setEGLContextClientVersion(2);
        mRenderer = new MyGLRenderer();
         // 设置绘图器
        setRenderer(mRenderer);

        // 设置渲染模式 (可选设置),此设置可以防止 GLSurfaceView 帧被重新绘制,让应用程序更高效。
        // RENDERMODE_CONTINUOUSLY: 自动循环模式,每隔一段时间自动调用用户实现的 onDrawFrame() 方法进行绘制。
        // GLSurfaceView.RENDERMODE_WHEN_DIRTY: “手动”模式,当用户需要重绘的时候,需要手动调用 requestRender() 方法。
//        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }
}

创建渲染器类

在实现 GLSurfaceView.Renderer 类或者说渲染器类时,我们必须要去实现其三个方法,分别是:

  • onSurfacefaceCreate():调用一次,用来配置 View 的 OpenGL ES 环境。
  • onSurfaceChanged():当 View 的形状发生改变时调用,如屏幕的方向发生了改变。
  • onDrawFrame():每次重新绘制 View 时调用。

下面的实例,是一个非常简单的 OpenGL ES 渲染器实现,仅仅是在 GLSurfaceView 中绘制了一个黑色背景:

class MyGLRenderer implements Renderer{
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // 设置背景颜色
        gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        gl.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // 重绘背景颜色
        gl.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }
}

结合我们以上的代码,运行起来,就可以看到一个背景为黑色的 activity 页了。
通过这个基础的小实例,已经为我们奠定了使用 OpenGL 绘制图形的基础。
下面我们将继续的去深入使用。

注:有同学可能已经发现了,这三个方法的参数都是 GL10,我们明明使用的是 OpenGL ES 2.0 接口,为什么还会有 GL10 的参数呢?
这是因为 Android 为了保持 Android 框架的代码尽量简单,把这些方法的签名 (Method Signature) 在 2.0 接口中被简单的重用了。

定义形状

这一节让我们来学习 OpenGL ES 相对于 Android 设备屏幕坐标系,定义形状和形状表面的基本只是,如定义一个三角形和一个矩形。

定义一个三角形

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

下面的代码,我们创建了一个三角形类,用于来定义绘制三角形需要的数据和逻辑:

public class Triangle {

    private FloatBuffer mVertexBuffer;

    // 每个顶点坐标
    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(){
        // 初始化形状的各坐标顶点的字节缓冲区
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // 参数为新缓冲区的容量 (字节)
                // 个人理解: 一个 float = 4 个字节, 一个 byte = 1 个字节,故这里 * 4。(不对请纠正,谢谢!)
                triangleCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        // 从字节缓冲区创建一个浮点缓冲区
        mVertexBuffer = bb.asFloatBuffer();
        // 将坐标添加到浮点缓冲区
        mVertexBuffer.put(triangleCoords);
        // 设置缓冲区以读取第一个坐标
        mVertexBuffer.position(0);
    }
}

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

注意到这个形状的坐标是以逆时针顺序定义的。绘制的顺序非常关键,因为它定义了哪一面是形状的正面(希望绘制的一面),以及背面(使用OpenGL ES的Cull Face功能可以让背面不要绘制)。

定义一个矩形

有很多方法可以定义一个矩形,但用 OpenGL ES 绘制这样的形状,典型做法是用两个三角形拼在一起。
再一次的,我们需要逆时针地为三角形顶点定义坐标来表现这个图形,并将值放入到 ByteBuffer 中。为了避免两个三角形重合,我们还需要再定义一个绘制顺序的数组,指示 OpenGL ES 图形处理流程应该去按照什么绘制顺序去画这些顶点。示例如下:

public class Square {

    private ShortBuffer mDrawListBuffer;
    private FloatBuffer mVertexBuffer;

    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(){
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        mVertexBuffer = bb.asFloatBuffer();
        mVertexBuffer.put(squareCoords);
        mVertexBuffer.position(0);

        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // short 占 2 个字节,所以这里 * 2
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        mDrawListBuffer = dlb.asShortBuffer();
        mDrawListBuffer.put(drawOrder);
        mDrawListBuffer.position(0);
    }
}

绘制形状

上一节中定义了几个形状,这节让我们将它们绘制出来。

初始化形状

在开始绘制之前,我们需要初始化并加载我们想绘制的形状。除非我们所使用的形状结构(原始坐标)在执行过程中发生了变化,不然的话,我们应该在 onSuffaceCreated() 方法中初始化它们,保证内存的合理使用和执行效率。

class MyGLRenderer implements Renderer{
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        ...
        // 初始化三角形
        mTriangle = new Triangle();
        // 初始化矩形
        mSquare = new Square();
        ...
    }

    ...
}

绘制形状

使用 OpenGL ES 2.0 绘制定义的形状需要大量的代码,因为我们必须提供很多图形渲染流程的细节。具体来说,我们必须定义如下几项:

  • 顶点着色器(Vertex Shader):用于渲染形状顶点的 OpenGL ES 代码。
  • 片段着色器(Fragment Shader):使用颜色或纹理渲染形状表面的 OpenGL ES 代码。
  • 程式(Program):一个 OpenGL 对象,其中包含了我们用于绘制一个或多个图形要用到的着色器。

我们需要至少一个顶点着色器来绘制一个形状,以及一个片段着色器来为该形状上色。
这些着色器必须被编译然后添加到 OpenGL ES 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;" +
                    "}";

   ...
 }

着色器包含了 OpenGL Shading Language(GLSL)代码,它必须先被编译,然后才能在 OpenGL ES 环境中使用。
在我们的渲染器类中加一个静态方法,用于加载着色器并编译:

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...

    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 Program 对象中,然后执行链接。
我们应该在绘制对象的构造中执行此操作,使其只能执行一次。

注:编译 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 Program 对象
        mProgram = GLES20.glCreateProgram();

        // 添加顶点着色器到对象
        GLES20.glAttachShader(mProgram, vertexShader);

        // 添加片段着色器到对象
        GLES20.glAttachShader(mProgram, fragmentShader);

        // 执行链接操作,以创建可被执行的 OpenGL ES Program 对象
        GLES20.glLinkProgram(mProgram);
    }
}

到这里,我们已经准备好调用实际的绘制代码来绘制图形了。
使用 OpenGL ES 绘制图形需要指定几个参数来告诉渲染流程我们想绘制的内容以及如何绘制它们。
既然绘图属性可能会因为要绘制的图形形状而异,因此让图形类包含自己的绘制逻辑是一个好的注意。

在图形类(Triangle)中创建一个 draw() 方法用于来绘制形状,该代码为形状的顶点着色器和形状着色器设置了位置和颜色值,然后执行绘制函数:

public class Triangle {
    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4;
    ...

    private int mPositionHandle;
    private int mColorHandle;
    ...
    public void draw() {
    // 将 Program 添加到 OpenGL ES 环境中
    GLES20.glUseProgram(mProgram);

    // 获取指向顶点着色器的成员 vPosition 的 handle
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    // 启用一个指向三角形的顶点数组的 handle
    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // 准备三角形的坐标数据
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
            GLES20.GL_FLOAT, false,
            vertexStride, mVertexBuffer);

    // 获取指向片段着色器的成员 vColor 的 handle
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

    // 设置绘制三角形的颜色
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);

    // 绘制三角形
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // 禁用指向三角形的顶点数组
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

绘制的代码我们已经准备好了,现在该来绘制看看效果了,在我们渲染器类的 onDrawFragme() 方法中调用 Triangle 图形类的 draw() 方法:

public class MyGLRenderer implements GLSurfaceView.Renderer {

    private Triangle mTriangle;
    private Square mSquare;

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // 初始化三角形
        mTriangle = new Triangle();
        // 初始化正方形
        mSquare = new Square();
        // 设置背景颜色
        gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // 重绘背景颜色
        gl.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }
}

运行效果:
这里写图片描述

至此,我们已经通过 OpenGL ES 绘制了一个三角形。但是这个代码示例还有一些问题,当我们更改屏幕方向使,三角形的形状发生了改变,并且有点压扁的感觉。
这个原因是由于对象顶点没有根据显示 GLSurfaceView 的屏幕区域的长宽来进行修正。我们将在下一节中使用投影(Projection)或者相机视角(Camera View)来修复该问题。

运用投影和相机视角

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

  • 投影(Projection):此变换会根据显示它们的 GLSurfaceView 的宽度和高度来调整绘制对象的坐标。没有这个计算,OpenGL ES 绘制的对象会由于其长宽比例和 View 窗口比例不一致而发生形变。一个投影变换一般仅当 OpenGL View 的比例在渲染器的 onSurfaceChanged() 方法中建立或发生变化时才计算。

  • 相机视角(Camera View):此变换会基于一个虚拟相机位置改变绘图对象的坐标。注意的是,OpenGL ES 没有定义一个实际的相机对象,取而代之的,它提供了一些辅助方法,通过对绘图对象的变化来模拟相机视角。一个相机视角变化可能只在建立 GLSurfaceView 时计算一次,或者也可能会根据用户的行为或应用程序的功能进行动态调整。

本节将介绍如何创建一个投影和相机视角,并将其应用在 GLSurfaceView 中的绘制图形上。

定义投影

投影变换的数据会在 GLSurfaceView.Renderer 类的 onSurfaceChanged() 方法中被计算。
以下示例代码使用 GLSurfaceView 的高度和宽度,并利用它使用 Matrix.frustumM() 方法填充一个投影变化矩阵(Projection Transformation Matrix):

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...

    // mMVPMatrix 是 "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 gl, int width, int height) {
        gl.glViewport(0, 0, width, height);

        float ratio = (float) width / height;

        // 这个投影矩阵应用于对象坐标
        // 在 onDrawFrame() 方法中
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
    }
    ... 
}

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

注:只将投影变换应用于绘图对象上,会导致显示效果非常空旷。一般来说,我们还要实现一个相机视角,使的所有对象出现在屏幕上。

定义一个相机视角

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

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...

    @Override
    public void onDrawFrame(GL10 gl) {
        // 重绘背景颜色
        gl.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        // 设置相机位置 (视图矩阵)
        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);;
    }
}

应用投影和相机变换

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

private final String vertexShaderCode =
        // 这个矩阵成员变量提供了一个钩子来操作
        // 使用该顶点着色器的对象的坐标
        "uniform mat4 uMVPMatrix;" +
                "attribute vec4 vPosition;" +
                "void main() {" +
                // 矩阵必须包含为 gl_Position 的修饰符
                // 注意 uMVPMatrix 因子必须按照顺序排列
                // 用于矩阵乘法乘积正确
                "  gl_Position = uMVPMatrix * vPosition;" +
                "}";

// 用于访问和设置视图转换
private int mMVPMatrixHandle;

接下来,修改图形对象(Triangle)的 draw() 方法,以接收组合后的变换矩阵并将其应用到图形上:

public void draw(float[] mvpMatrix) { // 传递计算转换矩阵
    ...

    // 获得图形转换矩阵的处理
    mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

    // 传递投影并将变换视图到着色器
    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

    // 绘制三角形
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // 禁用顶点数组
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

一旦我们正确的计算并应用了投影变换和相机视角变换,那么我们的图形对象就会按照正确的比例进行绘制,运行后的结果应该是这样的:

这里写图片描述

添加移动

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

在这一节中我们来进一步学习如何通过 OpenGL ES 为我们在之前代码中绘制的三角形添加旋转的动画。

旋转形状

使用 OpenGL ES 2.0 旋转绘图对象相对简单。在我们的渲染器类中,创建另一个变换矩阵(旋转矩阵),然后将其与我们之前的投影变换矩阵和相机视角变换矩阵相组合:

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...

    private float[] mRotationMatrix = new float[16];

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

        // 将旋转矩阵与投影变换矩阵和相机视角变换矩阵结合
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

        // 绘制图形
        mTriangle.draw(scratch);
    }
}

注:请不要设置渲染模式为:

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

运行结果:

这里写图片描述

(录制较卡、真机流畅)

响应触摸事件

在上一节中我们让三角形自动旋转了起来,这一节中我们来让三角形随用户的触摸旋转。

配置触摸监听器

为了使我们的 OpenGL ES 程序响应触摸事件,我们必须实现 GLSurfaceView 类的 onTouchEvent() 方法。
下面的示例演示了如何监听 MotionEvent.ACTION_MOVE 事件,并把它们转换为旋转的角度:

public class MyGLSurfaceView extends GLSurfaceView {

    ...

    private float mPreviousX;
    private float mPreviousY;
    private final float TOUCH_SCALE_FACTOR = 180.0f / 320;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                float dx = x - mPreviousX;
                float dy = y - mPreviousY;
                // 反转方向在中线以上
                if(x > getHeight() / 2){
                    dx = dx * -1;
                }
                // 反向旋转方向到中线左边
                if(y < getWidth() / 2){
                    dy = dy * -1;
                }
                mRenderer.setAngle(mRenderer.getAngle() + ((dx + dy) * TOUCH_SCALE_FACTOR));
                requestRender();
                break;
        }
        mPreviousX = x;
        mPreviousY = y;
        return true;
    }
}

需要注意的是,在我们修改旋转角度之后,马上调用了requestRender() 方法来告诉渲染器,你该渲染了。
这时我们并不需要渲染器去循环不停的渲染,所以渲染模式就需要改为“手动”模式了:

public MyGLSurfaceView(Context context) {
    super(context);
    ...
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

公开旋转角度

上述代码中,需要我们公开旋转的角度,具体来说,就是添加一个 public 的成员变量。由于渲染器代码是运行在一个独立的线程中(非 ui 线程),所以我们同时将该成员变量用 volatile 修饰,保证第一时间写入到主存中,避免出现读取值和写入值不一致情况。

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    // 旋转角度
    public volatile float mAngle;
    ...

    /**
     * 获取旋转角度
     * @return 旋转角度
     */
    public float getAngle(){
        return mAngle;
    }

    /**
     * 设置旋转角度
     * @param angle 旋转角度
     */
    public void setAngle(float angle){
        this.mAngle = angle;
    }
}

应用旋转

修改渲染器类的旋转逻辑,注释掉创建旋转角度的代码,使用 mAngle 变量:

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

        Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

        // 将旋转矩阵与投影和相机视图结合
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

        // 绘制图形
        mTriangle.draw(scratch);
    }

重新运行项目可以发现,现在三角形的旋转是根据我们的触摸来的:

这里写图片描述

示例下载

发布了55 篇原创文章 · 获赞 117 · 访问量 30万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 精致技术 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览