前言
新的知识学习都是循序渐进的,从基础到复杂。前面OpenGL ES概念
已经介绍了OpenGL ES的相关概念,这篇文章开始我们就正式开始OpenGL ES渲染系列第一站---绘制三角形。绘制三角形不涉及复杂的矩阵变换和纹理采样。渲染时OpenGL ES上下文并不是采用原生GLSurfaceView,而是自己参考GLSurfaceView流程在NDK层实现了一套EGL渲染的上下文逻辑,好处是自己更加清楚的了解了OpenGL ES上下文切换逻辑的实现。有兴趣可以参考文章OpenGL ES升级打怪之 GLSurfaceView源码分析的分析和实现。
在用法上和GLSurfaceView.Renderer用法基本一致,只需要关心以下三个方法实现:
- onSurfaceCreated(ANativeWindow *nativeWindow)
- onSurfaceChanged(ANativeWindow *nativeWindow, int format, int width, int height)
- draw() //对应Render的onDrawFrame(gl: GL10?)
OpenGL ES渲染原理
OpenGL渲染原理涉及计算机图形学知识,有兴趣的同学可以研究一下。
OpenGL ES概念复习
OpenGL ES概念也可以参考本人的另一文章NDK OpenGL ES 渲染系列之 OpenGL ES概念
着色器
着色器(Shader)是运行在GPU上的小程序,可以处理顶点和片元相关计算。我们只关心顶点着色器和片元着色器代码,着色器语言是GLSL。 主要用到的是顶点着色器和片元着色器
GLSL简介
OpenGL ES着色语言GLSL是一种高级图形化编程语言,其源自C语言,同时它提供了更加丰富的针对图像处理的原生类型,诸如向量、矩阵之类。
OpenGLES主要包含以下特性:
- GLSL是一种高级面向过程的语言,(并不是面向对象);
- GLSL的基本语法与C/C++基本相同;
- 完美支持向量与矩阵的各种操作。
- 通过类型限定符操作来管理输入输出类型的
- GLSL提供了大量的内置函数来提供丰富的拓展功能。
- 总之、OpenGL ES着色语言是一种易于实现、功能强大、便于使用,并且可以高度并行处理、性能优良的高级图形编程语言。
更加详细GLSL语法可以参考OpenGL之GLSL
顶点数组缓冲区(Vertex Array Object)
简称VAO, VAO是保存了所有顶点属性信息VBO的引用,本身不存储任何顶点信息。
顶点缓冲区(Vertex Buffer Object)
简称VBO, VBO是在GPU显存开辟一个内存空间,用于存放顶点各类属性信息,包括顶点坐标,顶点法向量,顶点颜色数据等。好处是渲染时可以直接从显存读取顶点属性信息,不需要从CPU传入,效率更高。
索引缓冲区(Element Buffer Object)
简称EBO,EBO是在GPU显存开辟一个内存空间,用于存放所有顶点位置索引indices。
绘制方式
OpenGL ES中支持的绘制方式大致分3类,包括点、线段、三角形,每类中包含一种或者多种绘制方式,各种具体绘制方式如下:
- 点绘制方式:GL_POINTS;
- 线段绘制方式:GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP;
- 三角形绘制方式GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_F
如何实现OpenGL ES渲染
复习一下如何实现OpenGL ES渲染流程,实现渲染有以下几个步骤:
- 加载顶点着色器和片元着色器代码
- 编译着色器代码
- 创建program编译链接已编译着色器
- 加载顶点坐标和颜色坐标或者纹理坐标
-
根据绘制方式画出相应的图形
绘制三角形
根据上述渲染流程,开始我们的渲染旅程吧,我们先创建一个TriangleFilter
对象,没错,我把它理解为滤镜,其实和GLSurfaceView.Renderer功能是一样的。我自己实现的Demo里面使用了VBO和EBO分别存放顶点坐标数据和索引坐标数据。VBO和EBO的概念可以参考OpenGL ES概念
的高级概念,
本文会介绍VBO和EBO如何使用。
初始化顶点坐标
DEMO使用了VBO存放顶点坐标数据和颜色数据。
GLfloat *vertex_color_coords = new GLfloat[] {
// Positions // Colors
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, // Top Right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // Bottom Right
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Bottom Left
};
初始化索引坐标
DEMO使用了EBO存放绘制索引数据。
GLshort *vertex_indexs = new GLshort[] {0, 1, 2};
初始化着色器代码
DEMO中顶点着色器代码:
const char *vShaderStr =
"#version 300 es \n"
"layout(location = 0) in vec3 aPosition; \n"
"layout(location = 1) in vec4 aColor; \n"
"out vec4 vColor; \n"
"void main() \n"
"{ \n"
" gl_Position = vec4(aPosition, 1.0); \n"
" vColor = aColor; \n"
"} \n";
DEMO中片元着色器代码:
const char *fShaderStr =
"#version 300 es \n"
"precision mediump float; \n"
"in vec4 vColor; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vColor; \n"
"} \n";
上述代码中version 300 es
是由于在OpenGL ES 3.0对应GLSL版本是3.0
编译和链接着色器
由于着色器代码是运行在GPU上的代码,所以我们不能直接使用的,那如何才能使用呢?我们需要编译着色器代码并链接到program实例上,可以理解这个program是一个特色程序指针
,当program链接到Shader程序后,就可以拿program进行操作。
编译着色器
根据不同着色器类型进行编译,并返回着色器的特殊指针
,这个特色指针
只有大于0时才认为是可用的。
GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
顶点着色器使用GL_VERTEX_SHADER
类型,片元着色器使用GL_FRAGMENT_SHADER
。
具体编译着色器代码如下:
GLuint GLShaderUtil::compileShader(GLenum type, const char *shaderSource) {
DLOGD(GLShaderUtil_TAG, "~~~compileShader~~~\n");
GLuint shader = glCreateShader(type);
checkGlError("glCreateShader");
if (shader == 0) {
DLOGE(GLShaderUtil_TAG, "Could not create new shader.\n");
}
glShaderSource(shader, 1, &shaderSource, NULL);
checkGlError("glShaderSource");
glCompileShader(shader);
checkGlError("glCompileShader");
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen) {
char *buf = (char *) malloc(sizeof(char) * infoLen);
if (buf) {
glGetShaderInfoLog(shader, infoLen, NULL, buf);
DFLOGE(GLShaderUtil_TAG, "Could not compile shader %d:\n%s\n", shader, buf);
}
free(buf);
}
glDeleteShader(shader);
shader = 0;
} else {
DFLOGD(GLShaderUtil_TAG, "Create type = 0x%x, shader = %d Success! \n", type, shader);
}
return shader;
}
这里需要和大家同步一点,就是OpenGL只是一个标准API, 实现是各个产商实现的,所以在我们定位问题时发现不能debug进入gpu进行调试,当然有些工具可以,比如RenderDoc(我并没使用这个工具,只是知道它的存在,有兴趣同学可以研究一下,研究后可以告知一下我)。可能有人会问如果在没有工具debug的时候我们怎么定位问题呢?这就是我要告诉大家的内容,我是使用glGetError
和glGetShaderInfoLog
获取错误码和Shader错误日志。
下面这代码就是在执行完一个OpenGL API后检测错误的方法,返回的错误码都可以在
头文件中,根据错误码你们就可以查阅相关资料定位问题,我的经验告诉我,错误码很重要,只要按照流程写代码,这些错误码是不会出现的,如果出现了要好好review一下你们自己的实现。(可能大家觉得是废话,当你们对比正常的代码review了好几遍自然就明白了)
static void checkGlError(const char* op) {
GLint error;
for (error = glGetError(); error; error = glGetError()) {
if (DebugEnable && GL_ERROR_DEBUG) {
DFLOGE(GL_ERROR_TAG, "error::after %s() glError (0x%x)\n", op, error);
}
}
}
链接着色器
接下来就是创建program和链接着色器程序,program只有大于0才有意义。
GLuint program = linkProgram(vertexShader, fragmentShader);
具体链接着色器代码如下:
GLuint GLShaderUtil::linkProgram(GLuint vertexShader, GLuint fragmentShader) {
DLOGD(GLShaderUtil_TAG, "~~~linkProgram~~~\n");
GLuint program = glCreateProgram();
if (program) {
glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
glAttachShader(program, fragmentShader);
checkGlError("glAttachShader");
glLinkProgram(program);
checkGlError("glLinkProgram");
glDetachShader(program, vertexShader);
glDetachShader(program, fragmentShader);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
GLint linkStatus = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (!linkStatus) {
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen) {
char *buf = (char *) malloc(sizeof(char) * infoLen);
if (buf) {
glGetProgramInfoLog(program, infoLen, NULL, buf);
DFLOGE(GLShaderUtil_TAG, "Could not link program %d:\n%s\n", program, buf);
}
free(buf);
}
glDeleteProgram(program);
program = 0;
}
} else {
DLOGE(GLShaderUtil_TAG, "Could not create program.\n");
}
return program;
}
加载顶点坐标和颜色数据
本Demo使用VAO, VBO存储顶点坐标信息和颜色数据。
VAO和VBO初始化:
//generate vao vbo
glGenVertexArrays(1, &vao); //生成VAO对象
checkGlError("glGenVertexArrays");
glGenVertexArrays(1, &vbo); //生成VBO对象
checkGlError("glGenVertexArrays")
加载顶点数据和颜色数据到VBO:
// Bind VAO
glBindVertexArray(vao); //绑定VAO对象
checkGlError("glBindVertexArray");
glBindBuffer(GL_ARRAY_BUFFER, vbo);//绑定VBO对象
checkGlError("glBindBuffer vbo");
glBufferData(GL_ARRAY_BUFFER, VERTEX_COLOR_COORDS_LENGTH * BYTES_PER_FLOAT, vertex_color_coords, GL_STATIC_DRAW);//填充缓冲对象管理的内存,分配了一块显存空间,然后把vertex_color_coords存入其中
checkGlError("glBufferData vbo");
// Position attribute
glEnableVertexAttribArray(0);//使能Shader中location = 0的对象
checkGlError("glEnableVertexAttribArray position");
glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, STRIDE_PRE_COORD * BYTES_PER_FLOAT,(const void *) nullptr); //指定顶点属性数组的数据格式和位置
checkGlError("glVertexAttribPointer position");
// Color attribute
glEnableVertexAttribArray(1);//使能Shader中location = 1的对象
checkGlError("glEnableVertexAttribArray color");
glVertexAttribPointer(1, COORDS_PER_COLOR, GL_FLOAT, false, STRIDE_PRE_COORD * BYTES_PER_FLOAT,(const void *) (COORDS_PER_VERTEX * BYTES_PER_FLOAT));//指定颜色数组的数据格式和位置
checkGlError("glVertexAttribPointer color");
加载索引数据
本DEMO使用EBO存放索引数据
EBO初始化:
//generate ebo
glGenVertexArrays(1, &ebo); //生成EBO对象
checkGlError("glGenVertexArrays");
加载索引数据到EBO:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
checkGlError("glBindBuffer ebo");
glBufferData(GL_ELEMENT_ARRAY_BUFFER, VERTEX_INDICES_LENGTH * BYTES_PER_SHORT, vertex_indexs, GL_STATIC_DRAW);
checkGlError("glBufferData ebo");
绘制
OpenGL ES提供两种API进行绘制,分别是glDrawArrays
和glDrawElements
,两者的区别是glDrawElements
可以根据指定的顶点索引进行渲染。这里就渲染了glDrawElements
。
// 使用EBO
glDrawElements(GL_TRIANGLES, VERTEX_INDICES_LENGTH, GL_UNSIGNED_SHORT, (const void *) nullptr);
// 不使用EBO
glDrawElements(GL_TRIANGLES, VERTEX_INDICES_LENGTH, GL_UNSIGNED_SHORT, vertex_indexs);
可以看到使用EBO和不使用EBO的传入数据不同,使用EBO则传入指针偏移值,不使用EBO则传入指针。
效果图
源码传送门
参考文章
OpenGL之GLSL