Android音视频学习之路(十)】OpenGL ES 简单使用

539 阅读6分钟

【Android音视频学习之路(一)】如何在 Android 平台绘制一张图片

【Android音视频学习之路(二)】AudioRecord API详解及应用

【Android音视频学习之路(三)】AudioTrack 使用与详解

【Android音视频学习之路(四)】Camera 的使用

【Android音视频学习之路(五)】MediaExtractor和MediaMuxer讲解

【Android音视频学习之路(六)】 MediaCodec 简单讲解

【Android音视频学习之路(七)】音视频录制流程实践

【Android音视频学习之路(八)】YUV格式初探

【Android音视频学习之路(九)】OpenGL ES 简单入门

“这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

前言

本文主要讲解Android设备屏幕相关的OpenGL ES坐标系统,定义形状,形状面的基础知识,以及定义三角形和正方形。

一、定义三角形

OpenGL ES允许你使用三维空间坐标系定义绘制的图像,所以你在绘制一个三角形之前必须要先定义它 的坐标。在OpenGL中,这样做的典型方法是为坐标定义浮点数的顶点数组。 为了获得最大的效率,可以将这些坐标写入ByteBuffer,并传递到OpenGL ES图形管道进行处理。

class Triangle {
    private val vertexBuffer: FloatBuffer

    // Set color with red, green, blue and alpha (opacity) values
    var color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)

    init {
        // initialize vertex byte buffer for shape coordinates
        // (number of coordinate values * 4 bytes per float)
        val bb: ByteBuffer = ByteBuffer.allocateDirect(triangleCoords.size * 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)
    }

    companion object {
        // number of coordinates per vertex in this array
        const val COORDS_PER_VERTEX = 3
        var triangleCoords = floatArrayOf( // 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
        )
    }
}

默认情况下,OpenGL ES采用坐标系,[0,0,0](X,Y,Z)指定GLSurfaceView框架的中心,[1,1,0]是框 架的右上角,[ - 1,-1,0]是框架的左下角。

image.png

请注意,此图形的坐标以逆时针顺序定义。 绘图顺序非常重要,因为它定义了哪一面是您通常想要绘制的图形的正面,以及背面。

二、定义正方形

可以看到,在OpenGL里面定义一个三角形很简单。但是如果你想要得到一个更复杂一点的东西呢?比如一个正方形?能够找到很多办法来作到这一点,但是在OpenGL里面绘制这个图形的方式是将两个三角形画在一起

image.png

同样,你应该以逆时针的顺序为这两个代表这个形状的三角形定义顶点,并将这些值放在一个 ByteBuffer中。 为避免定义每个三角形共享的两个坐标两次,请使用图纸列表告诉OpenGL ES图形管道 如何绘制这些顶点。 这是这个形状的代码:

class Square {
    private val vertexBuffer: FloatBuffer
    private val drawListBuffer: ShortBuffer
    private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw vertices

    init {
        // initialize vertex byte buffer for shape coordinates
        // (# of coordinate values * 4 bytes per float)
        val bb: ByteBuffer = ByteBuffer.allocateDirect(squareCoords.size * 4)
        bb.order(ByteOrder.nativeOrder())
        vertexBuffer = bb.asFloatBuffer()
        vertexBuffer.put(squareCoords)
        vertexBuffer.position(0)
        // initialize byte buffer for the draw list
        // (# of coordinate values * 2 bytes per short)
        val dlb: ByteBuffer = ByteBuffer.allocateDirect(drawOrder.size * 2)
        dlb.order(ByteOrder.nativeOrder())
        drawListBuffer = dlb.asShortBuffer()
        drawListBuffer.put(drawOrder)
        drawListBuffer.position(0)
    }

    companion object {
        // number of coordinates per vertex in this array
        const val COORDS_PER_VERTEX = 3
        val squareCoords = floatArrayOf(
            -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
    }
}

这个例子让你了解用OpenGL创建更复杂的形状的过程。 一般来说,您使用三角形的集合来绘制对象。 现在就将讲述如何使用OpenGL ES 2.0 API来绘制出来我们上节定义的形状。

三、初始化形状

在你做任何绘制操作之前,你必须要初始化并加载你准备绘制的形状。除非形状的结构(指原始的坐标)在执行过程中发生改变,你都应该在你的Renderer的方法onSurfaceCreated()中进行内存和效率方面的初始化工作。

private lateinit var triangle: Triangle
private lateinit var square: Square
override fun onSurfaceCreated(gl10: GL10?, config: EGLConfig?) {
    triangle = Triangle()
    square = Square()
}

四、绘制形状

使用OpenGLES 2.0画一个定义好的形状需要比较多的代码,因为你必须为图形渲染管线提供一大堆信息。特别的,你必须定义以下几个东西:

  • Vertex Shader - 用于渲染形状的顶点的OpenGLES 图形代码。
  • Fragment Shader - 用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。
  • Program - 一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。

你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被 编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定 义一个可以用来绘制形状的基本shader的例子:

class Triangle {
    private val vertexShaderCode =
        "attribute v4 vPosition;" +
                "void main() {" +
                "   gl_position = vPosition" +
                "}"

    private val fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                " gl_FragColor = vColor;" +
                "}"

    ...
}

Shader们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法:

private class MyGLRenderer : Renderer {
    ...
    companion object {
        fun loadShader(type: Int, shaderCode: String): Int {
            // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
            // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER
            val shader = GLES20.glCreateShader(type)

            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode)
            GLES20.glCompileShader(shader)

            return shader
        }
    }

}

为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个 program。在renderer对象的构造器中做这些事情,从而只需做一次即可。

注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。 如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。

class Triangle {

    ...
    private val program: Int

    init {
     
        val vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)

        // create empty OpenGL ES Program
        program = GLES20.glCreateProgram()

        // add the vertex shader to program
        GLES20.glAttachShader(program, vertexShader)

        // add the fragment shader to program
        GLES20.glAttachShader(program, fragmentShader)

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(program)
    }
}

此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如 何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。 创建一个draw()方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和 fragmentshader,然后执行绘制功能:

...
private val vertexCount = triangleCoords.size / COORDS_PER_VERTEX
private val vertexStride = COORDS_PER_VERTEX * 4 // 4 bytes per vertex

fun draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(program)

    // get handle to vertex shader's vPosition member
    val positionHandle = GLES20.glGetAttribLocation(program, "vPosition")

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(positionHandle)

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer)

    // get handle to fragment shader's vColor member
    val colorHandle = GLES20.glGetUniformLocation(program, "vColor")

    // Set color for drawing the triangle
    GLES20.glUniform4fv(colorHandle, 1, color, 0)

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

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(positionHandle)
}

一旦完成了所有这些代码,绘制该对象只需要在渲染器的onDrawFrame()方法中调用draw()方法:

override fun onDrawFrame(gl10: GL10?) {
    triangle.draw()
}

当你运行程序的时候,你就应该看到以下的内容:

image.png