OpenGL 绘制图形步骤
上一篇介绍了 OpenGL 的相关概念,今天来实际操作,使用 OpenGL 绘制出图形,对其过程有一个初步的了解。
OpenGL 绘制图形主要概括成以下几个步骤:
- 创建程序
- 初始化着色器
- 将着色器加入程序
- 链接并使用程序
- 绘制图形
上述每个步骤还可能会被分解成更细的步骤,对应着多个 api,下面我们来逐个看下。
创建程序
使用 glCreateProgram 创建一个 program 对象并返回一个引用 ID,该对象可以附加着色器对象。注意要在OpenGL渲染线程中创建,否则无法渲染。
初始化着色器
着色器的初始化可以细分为三个步骤:
- 创建顶点、片元着色器对象
- 关联着色器代码与着色器对象
- 编译着色器代码
上一篇文章我们提到了顶点着色器和片元着色器都是可编程管道,因此着色器的初始化少不了对着色器代码的关联与编译,上面三个步骤对应的 api 为:
- glCreateShader(int type)
- type:
GLES20.GL_VERTEX_SHADER
代表顶点着色器、GLES20.GL_FRAGMENT_SHADER
代表片元着色器
- type:
- glShaderSource(int shader, String code)
- shader:着色器对象 ID
- code:着色器代码
- glCompileShader(code)
- code:着色器对象 ID
着色器代码使用 GLSL 语言编写,那代码要怎么保存并使用呢?我看到过三种方式,列出供大家参考:
- 字符串变量保存
这种应该是最直观的写法了,直接在对应的类中使用硬编码存储着色器代码,形如:
1 | private final String vertexShaderCode = |
这种方式不是很建议,可读性不好。
- 存放于 assets 目录
assets 文件夹下的文件不会被编译成二进制文件,因此适于存放着色器代码,还可以配合 AndroidStudio 插件 GLSL Support 实现语法高亮:
然后再封装读取 assets 文件的方法:
1 | private fun loadCodeFromAssets(context: Context, fileName: String): String { |
需要注意的是要在结尾添加换行符,否则最后输出的只是一行字符串,不符合 GLSL 语法,自然也就无法正常使用。
- 存放于 raw 目录
存放于 raw 目录和 assets 目录其实异曲同工,但有个好处是 raw 文件会映射到 R 文件,代码中可以通过 R.raw 的方法使用对应的着色器代码,但 raw 目录下不能有目录结构,这点需要做个取舍。
同样的,封装读取 raw 文件的方法:
1 | private fun loadCodeFromRaw(context: Context, fileId: Int): String { |
着色器程序可能编译失败,可以使用 glGetShaderiv
方法获取着色器编译状况:
1 | var compileStatus = IntArray(1) |
将着色器加入程序
初始化着色器后拿到着色器对象 ID,再使用 glAttachShader 将着色器对象附加到 program 对象上。
1 | GLES20.glAttachShader(mProgram, shader) //将顶点着色器加入到程序 |
链接并使用程序
使用 glLinkProgram 为附加在 program 对象上的着色器对象创建可执行文件。链接可能失败,可以通过 glGetProgramiv
查询 program 对象状态:
1 | GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0) |
链接成功后,通过 glUseProgram
使用程序,将 program 对象的可执行文件作为当前渲染状态的一部分。
绘制图形
终于到最核心的绘制图形了,前面我们初始化了 OpenGL 程序以及着色器,现在需要准备绘制相关的数据,绘制出一个图形最基础的两个数据就是顶点坐标和图形颜色。
定义顶点数据
尝试画一个三角定,定义三个顶点,每个顶点包含三个坐标 x,y,z。手机屏幕中心坐标系(0,0,0),左上角坐标(-1, 1, 0)。
1 | private val points = floatArrayOf( |
OpenGL 修改顶点属性时接受的数据类型为缓冲区类型 Buffer,因此还需要将数组类型转为 Buffer:
1 | fun createFloatBuffer(array: FloatArray): FloatBuffer { |
为顶点属性赋值
顶点着色器代码:
1 | attribute vec4 vPosition; |
顶点着色器的每个输入变量叫顶点属性,着色器中定义了 vPosition 用于存放顶点数据,先使用 GLES20.glGetAttribLocation
获取 vPosition 句柄,再使用 GLES20.glVertexAttribPointer
为 vPosition 添加我们定义好的顶点数据。
1 | public static void glVertexAttribPointer( |
该方法接收六个参数,分别代表:
- indx:要修改的顶点属性的句柄
- size:每个顶点的坐标数,如果只有 x、y 两个坐标值就传 2
- type:坐标数据类型
- normalized:指定在访问定点数据值时是应将其标准化(true)还是直接转换为定点值(false)
- stride:每个顶点之间的字节偏移量
- ptr:顶点坐标 Buffer
1 | val vPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition") //获取 vPosition 句柄 |
如果 glGetAttribLocation 返回值为 -1 代表获取失败,可能 program 对象或着色器对象里没有对应的属性。
还需要注意的是,为顶点属性赋值时,glVertexAttribPointer
建立了 CPU 和 GPU 之前的逻辑连接,实现了 CPU 数据上传到 GPU。但 GPU 数据是否可见,也就是顶点着色器能否读到数据,则由是否启用了对应的属性决定。默认情况下顶点属性都是关闭的,可以通过 glEnableVertexAttribArray
启用属性,允许着色器读取 GPU 数据。
定义片元颜色
OpenGL 定义色值使用 float 数组,可以使用色值转换在线工具将十六进制色值转换为 float 值
1 | private val colors = floatArrayOf( |
为颜色属性赋值
片元着色器代码:
1 | precision mediump float; |
颜色属性定义为 uniform 变量,为颜色属性赋值一样需要先获取属性句柄,再向属性添加数据:
1 | mColorHandle = GLES20.glGetUniformLocation(mProgram, "zColor"); //获取 zColor 句柄 |
绘制
1 | GLES20.glEnableVertexAttribArray(vPositionHandle) //启用顶点句柄 |
当当当当,三角形出现了。上次只是绘制了背景色,今天又向前迈一步绘制出图形。但是显而易见这并不是一个等边三角形,和我们定义的坐标有所出入,这是因为 OpenGL 屏幕坐标系是一个正方形并且分布均匀的坐标系,因此将图形绘制到非正方形屏幕上时图形会被压缩或者拉伸。下一篇文章我们会使用投影变换来解决这个问题。
Comming soon :P