Android 音视频开发教程——OpenGL基础(一、绘制三角形四边形)

一、OpenGL简介

1.1 OpenGL规范

OpenGL 是一种跨平台的图形 API,用于为 3D 图形处理硬件指定标准的软件接口。OpenGL ES 是 OpenGL 规范的一种形式,适用于嵌入式设备。Android 支持多版 OpenGL ES API(推荐在最新 Android 设备上使用OpenGL ES 2.0 API版本):

  • OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。
  • OpenGL ES 2.0 - 此 API 规范受 Android 2.2(API 级别 8)及更高版本的支持。
  • OpenGL ES 3.0 - 此 API 规范受 Android 4.3(API 级别 18)及更高版本的支持。
  • OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。

1.2 OpenGL框架基本类

Android 框架中,GLSurfaceView 是使用 OpenGL 绘制的图形的视图容器,而 GLSurfaceView.Renderer 可控制该视图中绘制的图形。

1.2.1 GLSurfaceView

此类是一个 View,对于全屏或接近全屏的图形视图,选择GLSurfaceView合理一些。此外,如果希望将 OpenGL ES 图形整合到其布局中的一小部分,也可以考虑使用 TextureView。SurfaceView也可以用于OpenGL的视图容器,但是需要编写的代码比较多,暂不推荐。

1.2.2 GLSurfaceView.Renderer

此接口定义了在GLSurfaceView中绘制图形所需的方法。将此接口的实现类通过 GLSurfaceView.setRenderer()GLSurfaceView 实例关联起来。GLSurfaceView.Renderer 接口要求实现以下方法:

  • onSurfaceCreated():系统会在创建 GLSurfaceView 时调用一次此方法。通常用来设置仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。
  • onDrawFrame():系统会在每次重新绘制 GLSurfaceView 时调用此方法。是将图像绘制到GLSurfaceView的主要方法。
  • onSurfaceChanged():系统会在 GLSurfaceView 几何图形发生变化(包括 GLSurfaceView 大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。

1.3 OpenGL标准化设备坐标

OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。

与通常的屏幕坐标不同,OpenGL假设屏幕采用均匀的方形坐标系,OpenGL采用的是标准化设备坐标(Normalized Device Coordinates, NDC),标准化设备坐标的y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。标准化设备坐标是一个x、y和z值都在-1.0到1.0的一个坐标系,任何落在范围外的坐标都会被丢弃/裁剪,不会绘制到手机屏幕上。
  例如:一个三角形的三个点在OpenGL标准坐标系中表示如下(z轴都是0,暂时先忽略z轴):

二、基础用法

下面通过一个简单的绘制三角形的例子入门OpenGL。

2.1 构建 OpenGL ES 环境

2.1.1 声明清单

manifext.xml中声明了OpenGL的版本glEsVersion是OpenGL ES 2.0。



    


2.1.2 创建GLSurfaceView及Render

GLSurfaceView是使用 OpenGL 绘制的图形的视图容器,而GLSurfaceView.Renderer可控制该视图中绘制的图形。


    


在自定义的MyGLSurfaceView中把GLSurfaceView与Render关联起来:

class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {
    private val renderer: MyGLRenderer

    init {
        // 创建一个OpenGL ES 2.0上下文
        setEGLContextClientVersion(2)
        renderer = MyGLRenderer()
        // GLSurfaceView关联Render
        setRenderer(renderer)
    }
}

GLSurfaceView.Renderer负责实际的绘制工作,这里先只把背景设置为黑色:

class MyGLRenderer : GLSurfaceView.Renderer {
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        // 设置背景色为黑色
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        // 上面提到OpenGL使用的是标准化设备坐标;
        GLES20.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    }
}

至此,打开APP后可以看到一个黑色背景的GLSurfaceView。

2.2 扩展绘制方法

如果需要在OpenGL中绘制其他内容,则在onDrawFrame()方法内扩充即可,在大型的OpenGL项目中,一般采用类似Android系统View体系的模板设计模式(即ViewGroup调用子View的draw()方法,层层调用)。
  下面继续介绍绘制三角形的步骤,完成绘制三角形的主要工作在自定义的Triangle类中,只需要在onDrawFrame()中调用Triangle完成三角形的绘制:

class MyGLRenderer : GLSurfaceView.Renderer {

    private lateinit var triangle: Triangle

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
        // 我们的例子中在Triangle构造函数中就操作了GLES20,所以一定要在onSurfaceCreated中再去创建Triangle对象
        triangle = Triangle()
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        // 绘制三角形
        triangle.draw()
    }
}

2.3 定义形状

下面继续介绍Triangle绘制三角形的主要步骤,OpenGL使用FloatBuffer来管理顶点数据提高效率。一个三角形需要由三个顶点表示,这三个顶点在交给OpenGL时需要使用FloatBuffer格式,下面是三个顶点的定义方式:

class Triangle {
    // 三角形三个点的坐标值(逆时针方向,在3D坐标系中,方向决定了哪面是正面)
    private var triangleCoords = floatArrayOf(
        0.0f, 0.622008459f, 0.0f,      // top
        -0.5f, -0.311004243f, 0.0f,    // bottom left
        0.5f, -0.311004243f, 0.0f      // bottom right
    )

    // 设置颜色(分别代表red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)

    private var vertexBuffer: FloatBuffer =
        // 坐标点的数目 * float所占字节
        ByteBuffer.allocateDirect(triangleCoords.size * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
                // 把坐标添加到FloatBuffer
                put(triangleCoords)
                // 设置buffer的位置为起始点0
                position(0)
            }
}

2.3.1 顶点缓冲对象

这里引入三个名词:

  • 顶点数组对象:Vertex Array Object,VAO,表示存放顶点的数组,即例子中的triangleCoords;
  • 顶点缓冲对象:Vertex Buffer Object,VBO,表示存放顶点缓冲的数据,即例子中的FloatBuffer对象vertexBuffer;
  • 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO,表示存放顶点索引的数组,3.2小节会涉及到,用于描述顶点之间的顺序来重复使用顶点。

OpenGL会在GPU内存中存储大量顶点,使用顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存。使用缓冲对象VBO的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至GPU内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
  顶点数组对象与顶点缓冲对象关系如下,暂时简单了解即可:

2.4 绘制三角形

2.4.1 GLSL 着色器

如果要渲染图形,OpenGL需要我们至少设置一个顶点着色器和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在着色器程序中使用它了。
  着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

2.4.2 顶点着色器

顶点着色器是用于渲染形状的顶点的 OpenGL ES 图形代码。一个GLSL顶点着色器的源代码如下所示:

/**
* 顶点着色器代码
* 我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中
**/
private val vertexShaderCode =
    "attribute vec4 vPosition;" +
            "void main() {" +
            "  gl_Position = vPosition;" +
            "}"

  • attribute是GLSL的关键字表示声明一个属性;
  • vec4是GLSL的数据类型关键字,包含4个float分量的默认向量
  • vPosition是开发者自定义的变量名;
  • gl_Position是GLSL的内建变量:顶点着色器输出向量,这里把我们自定义的vPosition赋值过去,后面我们会在着色器程序中取出来操作顶点着色器中的数据;

2.4.3 片段着色器

片段着色器是用于使用颜色或纹理渲染形状面的 OpenGL ES 代码,主要工作是计算像素最后的颜色输出。一个片段着色器的源码如下:

/**
 * 片段着色器代码
 */
private val fragmentShaderCode =
    "precision mediump float;" +
            "uniform vec4 vColor;" +
            "void main() {" +
            "  gl_FragColor = vColor;" +
            "}"

  • Uniform是GLSL的关键字,是一种从CPU中的应用向GPU中的着色器发送数据的方式;与普通attribute不同的是,uniform是全局的(Global),即uniform变量在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
  • vColor是开发者自定义的变量名;
  • gl_FragColor是GLSL的内建变量:片段着色器对象,这里把我们自定义的vColor赋值过去,后面我们会在着色器程序中取出来并进行操作;

2.4.4 编译着色器代码

为了能够让OpenGL使用上述着色器代码,首先需要在运行时动态编译它的源代码。编译操作只需执行一次,一般放在绘制对象的构造函数中完成。

/**
* 编译着色器
* @param type 表示着色器的类型:GLES20.GL_VERTEX_SHADER GLES20.GL_FRAGMENT_SHADER
* @param shaderCode 着色器源码;即上述硬编码的GLSL代码
**/
private fun loadShader(type: Int, shaderCode: String): Int {
    // glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
    val shader = GLES20.glCreateShader(type)
    // 把着色器和代码关联,然后编译着色器
    GLES20.glShaderSource(shader, shaderCode)
    GLES20.glCompileShader(shader)
    return shader
}

2.4.5 着色器程序

如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

class Triangle {
    init {
        // 编译顶点着色器和片段着色器
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        // glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
        mProgram = GLES20.glCreateProgram().also {
            // 把顶点着色器添加到程序对象
            GLES20.glAttachShader(it, vertexShader)
            // 把片段着色器添加到程序对象
            GLES20.glAttachShader(it, fragmentShader)
            // 连接并创建一个可执行的OpenGL ES程序对象
            GLES20.glLinkProgram(it)
        }
    }
}

2.4.6 总结

下面将上面几个流程串联起来,在实际绘制时执行的方法draw()中激活着色器程序,然后操作顶点着色器和片段着色器。下面是绘制三角形的完整流程代码:

class Triangle {
    // 三角形三个点的坐标值(逆时针方向,在3D坐标系中,方向决定了哪面是正面)
    private var triangleCoords = floatArrayOf(
        0.0f, 0.5f, 0.0f,      // top
        -0.5f, -0.5f, 0.0f,    // bottom left
        0.5f, -0.5f, 0.0f      // bottom right
    )
    // 每个顶点的坐标数
    const val COORDS_PER_VERTEX = 3

    // 设置颜色(分别代表red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)

    private var vertexBuffer: FloatBuffer =
        // 坐标点的数目 * float所占字节
        ByteBuffer.allocateDirect(triangleCoords.size * 4)
            .order(ByteOrder.nativeOrder()).asFloatBuffer().apply {
                // 把坐标添加到FloatBuffer
                put(triangleCoords)
                // 设置buffer的位置为起始点0
                position(0)
            }

    /**
     * 顶点着色器代码;
     */
    private val vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}"

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}"

    /**
     * 着色器程序ID引用
     */
    private var mProgram: Int

    init {
        // 编译顶点着色器和片段着色器
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        // glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
        mProgram = GLES20.glCreateProgram().also {
            // 把顶点着色器添加到程序对象
            GLES20.glAttachShader(it, vertexShader)
            // 把片段着色器添加到程序对象
            GLES20.glAttachShader(it, fragmentShader)
            // 连接并创建一个可执行的OpenGL ES程序对象
            GLES20.glLinkProgram(it)
        }
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        // glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
        val shader = GLES20.glCreateShader(type)
        // 把着色器和代码关联,然后编译着色器
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }

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

    /**
    * 实际绘制时执行的方法
    **/
    fun draw() {
        // 激活着色器程序,把程序添加到OpenGL ES环境
        GLES20.glUseProgram(mProgram)
        // 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // 允许操作顶点对象position
        GLES20.glEnableVertexAttribArray(position)
        // 将顶点数据传递给position指向的vPosition变量;将顶点属性与顶点缓冲对象关联
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer)
        // 获取片段着色器中的vColor变量
        val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
        // 通过colorHandle设置绘制的颜色值
        GLES20.glUniform4fv(colorHandle, 1, color, 0)
        // 绘制顶点数组;
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount)
        // 操作完后,取消允许操作顶点对象position
        GLES20.glDisableVertexAttribArray(position)
    }
}

绘制结果如下:

三、绘制四边形

OpenGL只支持绘制点、线、三角形。对于绘制四边形,OpenGL ES中的典型方式是使用两个绘制在一起的三角形:

3.1 绘制几何图形的方法

OpenGL ES 提供了两类方法来绘制一个空间几何图形:

  • public abstract void glDrawArrays(int mode, int first, int count) 使用VetexBuffer 来绘制,顶点的顺序由vertexBuffer中的顺序指定。
  • public abstract void glDrawElements(int mode, int count, int type, Buffer indices) ,可以重新定义顶点的顺序,顶点的顺序由indices Buffer 指定。

3.2 索引缓冲对象

以上两种方式都可以用来绘制四边形,区别在于glDrawElements方式通过另外一个索引数组表示顶点间的绘制顺序,更加灵活。通过索引数组告诉 OpenGL ES 图形管道按什么顺序绘制这些顶点。
  同样地,索引数组也需要通过FloatBuffer的形式传递给OpenGL:

// 四个顶点的绘制顺序数组
    private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)

    // 四个顶点绘制顺序数组的缓冲数组
    private val drawListBuffer: ShortBuffer =
        ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
            .asShortBuffer().apply {
                put(drawOrder)
                position(0)
            }

3.3 绘制四边形

绘制四边形的代码如下,整体逻辑和绘制三角形类似,不同的是采用了glDrawElements方法进行绘制:

class Square {
    // 每个顶点的坐标数
    private val COORDS_PER_VERTEX = 3
    private var 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
    )

    // 四个顶点的缓冲数组
    private val vertexBuffer: FloatBuffer =
        ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
            .asFloatBuffer().apply {
                put(squareCoords)
                position(0)
            }

    // 四个顶点的绘制顺序数组
    private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)

    // 四个顶点绘制顺序数组的缓冲数组
    private val drawListBuffer: ShortBuffer =
        ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
            .asShortBuffer().apply {
                put(drawOrder)
                position(0)
            }

    /**
     * 顶点着色器代码;
     * 暂时将顶点着色器的源代码硬编码在C风格字符串中
     */
    private val vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}"

    /**
     * 片段着色器代码
     */
    private val fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}"

    // 设置颜色(分别代表red, green, blue and alpha)
    private val color = floatArrayOf(0.63671875f, 0.76953125f, 0.22265625f, 1.0f)

    /**
     * 着色器程序ID引用
     */
    private var mProgram: Int

    init {
        // 编译顶点着色器和片段着色器
        val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
        val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
        // glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
        mProgram = GLES20.glCreateProgram().also {
            // 把顶点着色器添加到程序对象
            GLES20.glAttachShader(it, vertexShader)
            // 把片段着色器添加到程序对象
            GLES20.glAttachShader(it, fragmentShader)
            // 连接并创建一个可执行的OpenGL ES程序对象
            GLES20.glLinkProgram(it)
        }
    }

    private fun loadShader(type: Int, shaderCode: String): Int {
        // glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
        val shader = GLES20.glCreateShader(type)
        // 把着色器和代码关联,然后编译着色器
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
        return shader
    }

    private val vertexStride: Int = COORDS_PER_VERTEX * 4

    fun draw() {
        // 激活着色器程序 Add program to OpenGL ES environment
        GLES20.glUseProgram(mProgram)
        // 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
        val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
        // 允许操作顶点对象position
        GLES20.glEnableVertexAttribArray(position)
        // 将顶点数据传递给position指向的vPosition变量
        GLES20.glVertexAttribPointer(
            position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
            false, vertexStride, vertexBuffer
        )
        // 获取片段着色器中的vColor变量
        val colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
        // 通过colorHandle设置绘制的颜色值
        GLES20.glUniform4fv(colorHandle, 1, color, 0)
        // 按drawListBuffer中指定的顺序绘制四边形
        GLES20.glDrawElements(
            GLES20.GL_TRIANGLES, drawOrder.size,
            GLES20.GL_UNSIGNED_SHORT, drawListBuffer
        )
        // 操作完后,取消允许操作顶点对象position
        GLES20.glDisableVertexAttribArray(position)
    }
}

绘制结果如下:

3.4 其他方式

再介绍下不使用索引数组,而是通过glDrawArrays()方式绘制四边形的方式,这种方式需要指定四个顶点相互连接的方式是GLES20.GL_TRIANGLE_STRIP:

class Square {
    // 顶点数
    private val vertexCount: Int = squareCoords.size / COORDS_PER_VERTEX

    fun draw() {
        // 省略...
        // 按GL_TRIANGLE_FAN方式连接绘制四个顶点
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount)
        // 省略...
    }
}

最后

Android OpenGL开发者文档:developer.android.com/guide/topic…
opengl学习资料:learnopengl-cn.github.io/

你可能感兴趣的:(Android 音视频开发教程——OpenGL基础(一、绘制三角形四边形))