Skip to content

Files

Latest commit

63c79d8 · Dec 26, 2021

History

History
589 lines (392 loc) · 23.6 KB

File metadata and controls

589 lines (392 loc) · 23.6 KB

十、OpenGL 专家系统初探

在本章中,我们将涵盖以下主题:

  • 设置 OpenGL 专家系统环境
  • 在 GLSurfaceView 上绘制形状
  • 绘图时应用投影和相机视图
  • 旋转移动三角形
  • 根据用户输入旋转三角形

简介

正如我们在上一章中看到的,安卓提供了许多处理图形和动画的工具。虽然画布和可绘制对象是为自定义绘制而设计的,但当您需要高性能图形,尤其是 3D 游戏图形时,Android 也支持 OpenGL ES。嵌入式系统开放图形库 ( OpenGL ES ),针对嵌入式系统。(嵌入式系统包括游戏机和手机。)

本章旨在介绍如何在安卓系统上使用 OpenGL ES。像往常一样,我们将提供步骤并解释事情是如何工作的,但我们不会深入研究 OpenGL 的数学或技术细节。如果你已经熟悉了其他平台的 OpenGL ES,比如 iOS,这一章应该可以让你快速上手运行。如果你是 OpenGL 新手,希望这些食谱能帮助你决定这是否是你想要追求的领域。

安卓支持以下版本的 OpenGL:

  • OpenGL 是 1.0 : Android 1.0
  • OpenGL ES 2.0 :安卓 2.2 中引入(API 8)
  • OpenGL ES 3.0 :安卓 4.3 中引入(API 18)
  • OpenGL ES 3.1 :安卓 5.0 中引入(API 21)

本章的食谱是介绍性的,目标是 OpenGL ES 2.0 和更高版本。OpenGL ES 2.0 几乎适用于目前所有可用的设备。与 OpenGL ES 2.0 及更低版本不同,OpenGL 3.0 及更高版本需要硬件制造商提供驱动程序实现。这意味着,即使您的应用运行在 Android 5.0 上,OpenGL 3.0 和更高版本也可能不可用。因此,在运行时检查可用的 OpenGL 版本是一个很好的编程实践。或者,如果您的应用需要 3.0 和更高的功能,您可以在您的安卓清单中添加一个<uses-feature/>元素。(我们将在接下来的第一个食谱中讨论这一点。)

与本书的其他章节不同,这一章更多的是作为一个教程来写的,每一个食谱都建立在从以前的食谱中吸取的经验教训之上。每个食谱的准备部分将阐明先决条件。

设置 OpenGL ES 环境

我们的第一个食谱将首先显示使用 OpenGL GLSurfaceView设置活动的步骤。类似于画布,GLSurfaceView是您执行 OpenGL 绘图的地方。因为这是起点,当其他食谱需要创建GLSurfaceView时,他们会将这个食谱作为基础步骤。

做好准备

在 Android Studio 中创建新项目,并将其称为:SetupOpenGL。使用默认的电话&平板电脑选项,当提示输入活动类型时,选择空活动

怎么做...

我们将首先在安卓清单中指出应用对 OpenGL 的使用,然后我们将 OpenGL 类添加到活动中。以下是步骤:

  1. 打开安卓清单,添加如下 XML:

    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
  2. 打开MainActivity.java并添加以下全局变量:

    private GLSurfaceView mGLSurfaceView;
  3. MainActivity类中增加以下内部类:

    class CustomGLSurfaceView extends GLSurfaceView {
    
        private final GLRenderer mGLRenderer;
    
        public CustomGLSurfaceView(Context context){
            super(context);
    		setEGLContextClientVersion(2);
            mGLRenderer = new GLRenderer();
            setRenderer(mGLRenderer);
        }
    }
  4. MainActivity类中添加另一个内部类:

    class GLRenderer implements GLSurfaceView.Renderer {
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            GLES20.glClearColor(0.5f, 0.5f, 0.5f, 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);}
    }
  5. 将以下代码添加到现有的onCreate()方法中:

    mGLSurfaceView = new CustomGLSurfaceView(this);
    setContentView(mGLSurfaceView);
  6. 您已经准备好在设备或模拟器上运行应用。

它是如何工作的...

如果您运行前面的应用,您会看到创建的活动和设置为灰色的背景。因为这些是设置 OpenGL 的基本步骤,所以您也可以将这段代码用于本章的其他食谱。下面是详细解释的过程:

在安卓清单中声明 OpenGL

我们首先声明我们在安卓清单中使用 OpenGL ES 2.0 版本的需求,如下所示:

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

如果我们使用 3.0 版,我们将使用以下内容:

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

对于 3.1 版本,使用这个:

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

扩展 GLSurfaceView 类

通过扩展GLSurfaceView创建一个自定义 OpenGL SurfaceView类,正如我们在这段代码中所做的:

class CustomGLSurfaceView extends GLSurfaceView {

    private final GLRenderer mGLRenderer;

    public CustomGLSurfaceView(Context context){
        super(context);
        setEGLContextClientVersion(2);
        mGLRenderer = new GLRenderer();
        setRenderer(mGLRenderer);
    }
}

这里,我们实例化一个 OpenGL 渲染的类,并用setRenderer()方法将其传递给GLSurfaceView类。OpenGL SurfaceView为我们的 OpenGL 绘图提供了一个曲面,类似于CanvasSurfaceView对象。实际绘图在Renderer中完成,接下来我们将创建:

创建一个 OpenGL 渲染类

最后一步是创建GLSurfaceView.Renderer类并实现以下三个回调:

  • onSurfaceCreated()
  • onDrawFrame()
  • onSurfaceChanged()

下面是代码:

class GLRenderer implements GLSurfaceView.Renderer {
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 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);
    }
}

现在,我们对这个类所做的就是设置回调,并使用我们用glClearColor()指定的颜色(本例中为灰色)清除屏幕。

还有更多...

设置好 OpenGL 环境后,我们将继续下一个食谱,我们将在视图上实际绘制。

在 GLSurfaceView 上绘制形状

之前的配方设置了使用 OpenGL 的活动。这个食谱将继续展示如何在OpenGLSurfaceView上画画。

首先,我们需要定义形状。使用 OpenGL,重要的是要认识到定义形状顶点的顺序非常重要,因为它们决定了形状的正面(面)和背面。逆时针定义顶点是惯例(也是默认行为)。(虽然这种行为可以改变,但它需要额外的代码,并且不是标准做法。)

了解 OpenGL 屏幕坐标系也很重要,因为它不同于 Android 画布。默认坐标系将(0,0,0)定义为屏幕的中心。四个边缘点如下:

  • 左上角 : (-1.0,1.0,0)
  • 右上角 : (1.0,1.0,0)
  • 左下方 : (-1.0,-1.0,0)
  • 右下角 : (1.0,-1.0,0)

Z 轴直接从屏幕中出来或者直接在后面。

下面是显示 XYZ 轴的插图:

Drawing shapes on GLSurfaceView

我们将创建一个Triangle类,因为它是基础形状。在 OpenGL 中,通常使用三角形的集合来创建对象。要使用 OpenGL 绘制形状,我们需要定义以下内容:

  • 顶点着色器:这是为了绘制形状
  • 片段着色器:这是为了给形状上色
  • 程序:这是前面着色器的 OpenGL ES 对象

着色器使用 OpenGL 着色语言 ( GLSL )定义,然后编译并添加到 OpenGL 程序对象中。

下面是两张截图,显示了纵向和横向的三角形:

Drawing shapes on GLSurfaceView

Drawing shapes on GLSurfaceView

做好准备

在 Android Studio 中创建新的项目,并将其称为:ShapesWithOpenGL。当提示输入活动类型时,使用默认的电话&平板电脑选项并选择空活动

该配方使用在先前配方中创建的 OpenGL 环境来设置开放 GL 环境。如果您尚未完成这些步骤,请参考之前的配方。

怎么做...

如前所述,我们将使用上一个食谱中创建的 OpenGL 环境。接下来的步骤将引导您创建三角形形状的类,并将其绘制在 GLSurfaceView 上:

  1. 创建一个名为Triangle的新 Java 类。

  2. 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;" +"}";
    
    final int COORDS_PER_VERTEX = 3;
    float triangleCoords[] = {
            0.0f,  0.66f, 0.0f,
            -0.5f, -0.33f, 0.0f,
            0.5f, -0.33f, 0.0f
    };
    
    float color[] = { 0.63f, 0.76f, 0.22f, 1.0f };
    
    private final int mProgram;
    private FloatBuffer vertexBuffer;
    private int mPositionHandle;
    private int mColorHandle;
    private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
    private final int vertexStride = COORDS_PER_VERTEX * 4;
  3. 将以下loadShader()方法添加到Triangle类:

    public int loadShader(int type, String shaderCode){
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }
  4. 添加为Triangle构造函数,如所示:

    public Triangle() {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
        mProgram = GLES20.glCreateProgram();
        GLES20.glAttachShader(mProgram, vertexShader);
        GLES20.glAttachShader(mProgram, fragmentShader);
        GLES20.glLinkProgram(mProgram);
    
        ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
    
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(triangleCoords);
        vertexBuffer.position(0);
    }
  5. 增加draw()方法,如下:

    public void draw() {
        GLES20.glUseProgram(mProgram);
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        GLES20.glEnableVertexAttribArray(mPositionHandle);
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,GLES20.GL_FLOAT, false,vertexStride, vertexBuffer);
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
        GLES20.glUniform4fv(mColorHandle, 1, color, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
        GLES20.glDisableVertexAttribArray(mPositionHandle);
    }
  6. 现在打开MainActivity.java,给GLRenderer类添加一个Triangle变量,如下所示:

    private Triangle mTriangle;
  7. onSurfaceCreated()回调中初始化Triangle变量,如下所示:

    mTriangle = new Triangle();
  8. onDrawFrame()回调中调用draw()方法:

    mTriangle.draw();
  9. 您已经准备好在设备或模拟器上运行应用。

它是如何工作的...

正如介绍中提到的,要使用 OpenGL 进行绘制,我们首先必须定义着色器,我们使用以下代码来完成:

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 着色语言 ( OpenGLSL ),下一步是编译并将其附加到我们的 OpenGL 对象上,我们使用以下两种 OpenGL ES 方法来完成:

  • glAttachShader()
  • glLinkProgram()

设置着色器后,我们创建ByteBuffer来存储三角形顶点,这些顶点在triangleCoords中定义。draw()方法是使用从onDrawFrame()回调调用的 GLES20 库调用进行实际绘图的地方。

还有更多...

你可能已经注意到,从介绍中的截图来看,肖像和风景中的三角形看起来确实是一样的。从代码中可以看出,我们在绘图时没有方向上的区别。我们将解释为什么会发生这种情况,并在下一个食谱中展示如何纠正这个问题。

另见

有关 OpenGL 着色语言的更多信息,请参考以下链接:

https://www.opengl.org/documentation/glsl/

绘制时应用投影和相机视图

正如我们在之前的配方中看到的,当我们将形状绘制到屏幕时,形状会因屏幕方向而偏斜。这样做的原因是,默认情况下,OpenGL 假设屏幕是完美的正方形。我们之前提到过,右上角的默认屏幕坐标是(1,1,0),左下角是(-1,-1,0)。

由于大多数设备屏幕都不是完美的正方形,我们需要映射显示坐标以匹配我们的物理设备。在 OpenGL 中,我们用投影来实现。本食谱将展示如何使用投影将 GLSurfaceView 坐标与设备坐标进行匹配。除了投影,我们还将展示如何设置摄像机视图。下面是显示最终结果的截图:

Applying Projection and Camera View while drawing

做好准备

在安卓工作室新建一个项目,并命名为:ProjectionAndCamera。当提示输入活动类型时,使用默认的电话&平板电脑选项并选择空活动

该配方基于之前的配方在 GLSurfaceView 上绘制形状。如果您还没有输入之前的配方,请在开始这些步骤之前输入。

怎么做...

如前所述,此配方将建立在之前的配方基础上,因此在开始之前完成这些步骤。我们将修改之前的代码,将投影和相机视图添加到绘图计算中。以下是步骤:

  1. 打开Triangle类,将以下全局声明添加到现有声明中:

    private int mMVPMatrixHandle;
  2. vertexShaderCode添加一个矩阵变量,并在位置计算中使用。以下是最终结果:

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "uniform mat4 uMVPMatrix;" +
        "void main() {" +
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";
  3. 更改draw()方法,传入矩阵参数,如下所示:

    public void draw(float[] mvpMatrix) {
  4. 要使用转换矩阵,在GLES20.glDrawArrays()方法之前的draw()方法中添加以下代码:

    mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
  5. 打开MainActivity.java并将以下类变量添加到GLRenderer类中:

    private final float[] mMVPMatrix = new float[16];
    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];
  6. 修改onSurfaceChanged()回调计算位置矩阵如下:

    public void onSurfaceChanged(GL10 unused, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
    }
  7. 修改onDrawFrame()回调计算相机视图如下:

    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);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        mTriangle.draw(mMVPMatrix);
    }
  8. 您已经准备好在设备或模拟器上运行应用。

它是如何工作的...

首先,我们修改vertexShaderCode以包含一个矩阵变量。我们在onSurfaceChanged()回调中使用高度和宽度计算矩阵,高度和宽度作为参数传入。我们将变换矩阵传递给draw()方法,在计算要绘制的位置时使用它。

在我们调用draw()方法之前,我们计算相机视图。这两行代码计算相机视图:

Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

没有这段代码,实际上就不会画出三角形,因为相机的视角不会“看到”我们的顶点。(这可以追溯到我们关于顶点的顺序如何决定图像的正面和背面的讨论。)

现在运行程序,会看到介绍中显示的输出。请注意,我们现在有一个统一的三角形,即使当显示器旋转。

还有更多...

在下一个食谱中,我们将通过旋转三角形开始展示 OpenGL 的威力。

旋转移动三角形

到目前为止,我们用 OpenGL 演示的东西可能会比使用传统画布或可绘制对象更容易。这个食谱将通过旋转三角形来展示 OpenGL 的一点威力。不是我们不能用其他的绘图方法创建运动,而是我们可以用 OpenGL 多么容易地做到这一点!

这个食谱将演示如何旋转三角形,如这个截图所示:

Moving the triangle with rotation

做好准备

在 Android Studio 中创建新项目,并将其称为:CreatingMovement。使用默认的电话&平板电脑选项,当提示输入活动类型时,选择空活动

该配方建立在先前配方的基础上,在绘制时应用投影和相机视图。如果您还没有输入之前的配方,请在继续之前输入。

怎么做...

由于我们是从以前的食谱继续,我们几乎没有什么工作要做。打开MainActivity.java并按照以下步骤操作:

  1. GLRendered类添加矩阵:

    private float[] mRotationMatrix = new float[16];
  2. onDrawFrame()回调中,用以下代码替换现有的mTriangle.draw(mMVPMatrix);语句:

    float[] tempMatrix = 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(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0);
    mTriangle.draw(tempMatrix);
  3. 您已经准备好在设备或模拟器上运行应用。

它是如何工作的...

我们使用Matrix.setRotateM()方法根据我们传递的角度计算一个新旋转矩阵。在本例中,我们使用系统正常运行时间来计算角度。我们可以使用任何我们想要导出角度的方法,例如传感器读数或触摸事件。

还有更多...

使用系统时钟提供了创建连续运动的额外好处,这对于演示目的来说看起来更好。下一个食谱将演示如何使用用户输入来推导旋转三角形的角度。

渲染模式

OpenGL 提供了一个setRenderMode()选项,只在视图脏的时候才绘制。这可以通过在setRenderer()调用下方的CustomGLSurfaceView()构造函数中添加以下代码来实现:

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

这将导致显示更新一次,然后等待,直到我们用requestRender()请求更新。

通过用户输入旋转三角形

前面的例子演示了基于系统时钟旋转三角形。这创建了一个连续旋转的三角形,取决于我们使用的渲染模式。但是如果你想回应用户的输入呢?

在本食谱中,我们将展示如何通过覆盖来自GLSurfaceViewonTouchEvent()回调来响应用户输入。我们仍将使用Matrix.setRotateM()方法旋转三角形,但我们将根据触摸位置计算角度,而不是从系统时间中导出角度。

这里有一个截图显示了在物理设备上运行的这个配方(为了突出显示触摸,显示触摸开发者选项被启用):

Rotating the triangle with user input

做好准备

在 Android Studio 中创建新项目,并将其称为:RotateWithUserInput。使用默认的电话&平板电脑选项,当提示输入活动类型时,选择空活动

该配方展示了前一配方的替代方法,因此将基于在绘制时应用投影和相机视图(与前一配方的起点相同)。)

怎么做...

如前所述,我们将继续,不是从之前的配方,而是从应用投影和相机视图绘制配方的。打开MainActivity.java并按照以下步骤操作:

  1. 将以下全局变量添加到MainActivity类中:

    private float mCenterX=0;
    private float mCenterY=0;
  2. 将以下代码添加到GLRendered类中:

    private float[] mRotationMatrix = new float[16];
    public volatile float mAngle;
    public void setAngle(float angle) {
        mAngle = angle;
    }
  3. 在同一类中,修改onDrawFrame()方法,用以下代码替换现有的mTriangle.draw(mMVPMatrix);语句:

    float[] tempMatrix = new float[16];
    Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
    Matrix.multiplyMM(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0);
    mTriangle.draw(tempMatrix);
  4. 将以下代码添加到onSurfaceChanged()回调中:

    mCenterX=width/2;
    mCenterY=height/2;
  5. 将以下代码添加到CustomGLSurfaceView构造函数中,位于setRenderer()下方:

    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  6. 将以下onTouchEvent()添加到CustomGLSurfaceView类:

    @Override
    public boolean onTouchEvent(MotionEvent e) {
      float x = e.getX();
      float y = e.getY();
      switch (e.getAction()) {
          case MotionEvent.ACTION_MOVE:
              double angleRadians = Math.atan2(y-mCenterY,x-mCenterX);
              mGLRenderer.setAngle((float)Math.toDegrees(-angleRadians));
              requestRender();
      }
      return true;
    }
  7. 您已经准备好在设备或模拟器上运行应用。

它是如何工作的...

本例与前一个配方的明显区别在于我们如何推导出传递给Matrix.setRotateM()调用的角度。我们还使用setRenderMode()更改了GLSurfaceView渲染模式,使其仅根据请求进行绘制。在onTouchEvent()回调中计算出新的角度后,我们使用requestRender()发出请求。

我们还展示了衍生我们自己的GLSurfaceView类的重要性。没有我们的CustomGLSurfaceView类,我们就没有办法覆盖onTouchEvent回调,或者来自GLSurfaceView的任何其他回调。

还有更多...

OpenGL 专家系统食谱到此结束,但我们只是触及了 OpenGL 的力量。如果你真的想学习 OpenGL,请查看下一节中的链接,并查看许多关于 OpenGL 的书籍之一。

还值得一提的是众多可用框架中的一个,比如虚幻引擎:

类型

虚幻引擎 4 是游戏开发者为游戏开发者打造的一整套游戏开发工具。

https://www.unrealengine.com/what-is-unreal-engine-4

另见