本文档主要使用glfw和glad进行开发,主要是对文档简介 - LearnOpenGL CN (learnopengl-cn.github.io)的学习笔记
1、OpenGL图形渲染原理
OpenGL中要素都为三位坐标,需要通过渲染将其呈现在二维屏幕上。渲染主要分为两步:①三维坐标转二维坐标;②二维坐标转为屏幕像素,为图形渲染。下面主要讲讲图形渲染。
图形渲染分为多个阶段,在opengl中用特定的程序处理每个阶段,这种程序名为着色器(Shader)。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)。
下面是一个图形渲染管线的每个阶段的抽象展示。注意蓝色部分代表的是可以注入自定义的着色器的部分。(图片来自你好,三角形 - LearnOpenGL CN (learnopengl-cn.github.io))
①顶点着色器:顶点处理
顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
②几何着色器(可选):图元处理
几何着色器将一组将顶点着色器(或几何着色器)输出的所有顶点作为输入,这些顶点形成图元,并且能够通过发出新的顶点来形成新的(或其他)图元来生成其他形状。
顶点着色器主要处理单个顶点的数据和变换,而几何着色器则更专注于处理几何图元级别的数据操作和生成。
③图元装配:图元装配
将顶点着色器(或几何着色器)输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。
④光栅化阶段:确定所使用的像素块
把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
⑤片段着色器:确定在屏幕上渲染的每个像素的最终颜色
计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
⑥Alpha测试和混合(Blending)阶段:图层和透明度判断
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器。
2、三角形的渲染
(1)顶点输入
OpenGL是一个3D图形库,所以在OpenGL中我们指定的所有坐标都是3D坐标(x、y和z)。同时opengl的渲染范围为[-1.0,1.0]。为了渲染一个在2d平面上的三角形,我们创建了一个点数组。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
z表示深度,通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。下面是定义在标准化设备中的坐标系(忽略z轴)。
图片来自:你好,三角形 - LearnOpenGL CN (learnopengl-cn.github.io)
顶点缓冲对象(Vertex Buffer Objects, VBO)允许将顶点数据(如位置、颜色、法线等)存储在显存中,以便图形硬件能够直接访问这些数据,从而提高渲染效率。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次,从而提高渲染效率。
就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数生成一个带有缓冲ID的顶点缓冲对象:
unsigned int VBO;
glGenBuffers(1, &VBO); //生成一个带有缓冲ID的顶点缓冲对象,id为VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO); //顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER,把新创建的缓冲绑定到目标上
glBufferData(GL_ARRAY_BUFFER/*目标缓冲的类型*/, sizeof(vertices)/*参数指定传输数据的大小*/, vertices/*实际数据*/, GL_STATIC_DRAW/*管理形式*/); //把之前定义的顶点数据复制到缓冲的内存
glBufferData函数的管理形式共有以下三种:
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
(2)顶点着色器
着色器是用C语言风格的着色器语言GLSL(OpenGL Shading Language)编写的,为了能够让OpenGL使用它,将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中。
const char* vertexShaderSource = "#version 330 core\n" //版本声明
"layout (location = 0) in vec3 aPos;\n" //指定了输入顶点数据的格式和位置,即输入的是三维坐标,第一个顶点数据在位置0
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" //gl_Position变量是四维的,此处把w分量设置为1.0f
"}\0";
然后动态编译着色器。
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建顶点着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //设置顶点着色器的源代码
glCompileShader(vertexShader); //编译顶点着色器
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); //检查是否编译成功
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
(3)片段着色器
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器。按照同一形式的程序,编写一个片段着色器并编译。
片段着色器:
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
编译:
// fragment shader
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
(4)着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
// link shaders
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader); //链接完成后删除着色器
glDeleteShader(fragmentShader);
(5)顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
创建顶点数组对象的过程与创建顶点缓冲对象相似。
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO); //生成一个VAO
glGenBuffers(1, &VBO); //生成一个带有缓冲ID的顶点缓冲对象,id为VBO
glBindVertexArray(VAO); //绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); //顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER,把新创建的缓冲绑定到目标上
glBufferData(GL_ARRAY_BUFFER/*目标缓冲的类型*/, sizeof(vertices)/*参数指定传输数据的大小*/, vertices/*实际数据*/, GL_STATIC_DRAW/*管理形式*/); //把之前定义的顶点数据复制到缓冲的内存
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 设置顶点属性指针
glEnableVertexAttribArray(0); //启用顶点属性数组,第一个参数为顶点属性的索引,这里只有一个顶点属性,所以索引为0
glBindVertexArray(0); //解绑当前的顶点数组对象
(6)三角形渲染
完成以上工作,在窗口循环中就可以开始渲染三角形了。
glUseProgram(shaderProgram); //使用着色器程序
glBindVertexArray(VAO); //绑定指定VAO
glDrawArrays(GL_TRIANGLES, 0, 3); //渲染三角形
渲染结果如下:
源文档作者所展示的代码如下: