OpenGL 绘制图形主要概括成以下几个步骤:
创建程序
初始化着色器
将着色器加入程序
链接并使用程序
绘制图形
上述每个步骤还可能会被分解成更细的步骤,对应着多个 api,下面我们来逐个看下。
创建程序使用 glCreateProgram 创建一个 program 对象并返回一个引用 ID,该对象可以附加着色器对象。注意要在OpenGL渲染线程中创建,否则无法渲染。
初始化着色器着色器的初始化可以细分为三个步骤:
创建顶点、片元着色器对象
关联着色器代码与着色器对象
编译着色器代码
上一篇文章我们提到了顶点着色器和片元着色器都是可编程管道,因此着色器的初始化少不了对着色器代码的关联与编译,上面三个步骤对应的 api 为:
glCreateShader(int type)
type:GLES20.GL_VERTEX_SHADER 代表顶点着色器、GLES20.GL_FRAGMENT_SHADER 代表片元着色器
glShaderSource(int shader, String code)
shader:着色器对象 ID
code:着色器代码
glCompileShader(code)
code:着色器对象 ID
着色器代码使用 GLSL 语言编写,那代码要怎么保存并使用呢?我看到过三种方式,列出供大家参考:
字符串变量保存
这种应该是最直观的写法了,直接在对应的类中使用硬编码存储着色器代码,形如:
private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}";这种方式不是很建议,可读性不好。
存放于 assets 目录
assets 文件夹下的文件不会被编译成二进制文件,因此适于存放着色器代码,还可以配合 AndroidStudio 插件 GLSL Support 实现语法高亮:
然后再封装读取 assets 文件的方法:
private fun loadCodeFromAssets(context: Context, fileName: String): String { var result = "" try { val input = context.assets.open(name) val reader = BufferedReader(InputStreamReader(input)) val str = StringBuilder() var line: String? while ((reader.readLine().also { line = it }) != null) { str.append(line) str.append("\n") //注意结尾要添加换行符 } input.close() reader.close() result = str.toString() } catch (e: IOException) { e.stackTrace } return result }需要注意的是要在结尾添加换行符,否则最后输出的只是一行字符串,不符合 GLSL 语法,自然也就无法正常使用。
存放于 raw 目录
存放于 raw 目录和 assets 目录其实异曲同工,但有个好处是 raw 文件会映射到 R 文件,代码中可以通过 R.raw 的方法使用对应的着色器代码,但 raw 目录下不能有目录结构,这点需要做个取舍。
同样的,封装读取 raw 文件的方法:
private fun loadCodeFromRaw(context: Context, fileId: Int): String { var result = "" try { val input = context.resources.openRawResource(fileId) val reader = BufferedReader(InputStreamReader(input)) val str = StringBuilder() var line: String? while ((reader.readLine().also { line = it }) != null) { str.append(line) str.append("\n") } input.close() reader.close() result = str.toString() } catch (e: IOException) { e.stackTrace } return result }着色器程序可能编译失败,可以使用 glGetShaderiv 方法获取着色器编译状况:
var compileStatus = IntArray(1) //获取着色器的编译情况 GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); if (compileStatus[0] == 0) {//若编译失败则显示错误日志并 GLES20.glDeleteShader(shader);//删除此shader shader = 0; } 将着色器加入程序初始化着色器后拿到着色器对象 ID,再使用 glAttachShader 将着色器对象附加到 program 对象上。
GLES20.glAttachShader(mProgram, shader) //将顶点着色器加入到程序 GLES20.glAttachShader(mProgram, fragmentShader) //将片元着色器加入到程序中 链接并使用程序使用 glLinkProgram 为附加在 program 对象上的着色器对象创建可执行文件。链接可能失败,可以通过 glGetProgramiv 查询 program 对象状态:
GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0) // 如果连接失败,删除这程序 if (linkStatus[0] == 0) { GLES20.glDeleteProgram(mProgram) mProgram = 0 }链接成功后,通过 glUseProgram 使用程序,将 program 对象的可执行文件作为当前渲染状态的一部分。
绘制图形终于到最核心的绘制图形了,前面我们初始化了 OpenGL 程序以及着色器,现在需要准备绘制相关的数据,绘制出一个图形最基础的两个数据就是顶点坐标和图形颜色。
定义顶点数据