OpenGL(8)渲染基础
简介
前面内容主要集中在搭建OpenGL 的环境,包括库,窗口创建。接下来的内容就专注学习OpenGL渲染。
让我们放下其它任何OpenGL概念不说,我们使用OpenGL,最终目的是在显示设备上显示出一张图片。而计算机在创建这张图片的过程就叫渲染。我们渲染3D环境是相当复杂的,有物体,光照,阴影,镜像等等一系列场景都需要表现在一张张图片上。这就需要用到OpenGL,它强大的状态机制,让我们在应对各种场景时游刃有余。
这里我们直接使用OpenGL 的 Sharder(着色器) 和 Frame Buffer Object(FBO, 帧缓存),进行渲染过程实现。固定管线慢慢会被淘汰的。
Shader 和 FBO 的简单理解
这里简单介绍一下Sharder 和 FBO 的用途。
- Shader可编程着色器,它是工作在图形硬件上的,一般指的计算机的显卡(GPU)。而Shader中的源码由OpenGL提供的编译连接工具,编译成可以在GPU执行的代码。在OpenGL渲染过程中如下图所示,其中可以进行编程的着色器有:
- Vertex Shader(顶点着色器) , 必须进行编程
- Tessellation, 可选
- Geometry Shader (几何着色器 ) 可选
- Fragment Shader (片段着色器) 必须进行编程
- Frame Buffer Object
顾名思义,就是将渲染的数据放在GPU缓存中,这样可以提高渲染的效率,避免频繁从CPU中拷贝数据过来。
渲染实践
参考实现基本的渲染过程,当中加入了个人的理解。
渲染一个三角形的步骤
- 创建基本的着色器,即顶点着色器 和 片段着色器
- 加载着色器
- 创建顶点数组对象
- 设置顶点属性指针,告诉OpenGL该如何解析顶点数据
- 使能顶点属性
void MyGLWidget::initTriangle()
{
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
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";
m_shaderProgram = LoadShader(vertexShaderSource, fragmentShaderSource);
if(0 == m_shaderProgram)
return;
//设置顶点数据和缓冲区以及配置顶点属性
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
/*
* VAO顶点数组对象
* 保存了顶点状态的集合:所有顶点属性的调用的存储在VAO
* 即顶点着色器属性与VBO的联系
* 包含顶点数据的格式以及顶点数据数据所需的缓存对象的引用。
*/
GLuint VAO;
// 创建顶点数组对象Id
glGenBuffers(1, &VAO);
//绑定顶点数组对象
glBindVertexArray(VAO);
//定义顶点缓冲对象
GLuint VBO;
//创建缓冲区名称,即唯一的ID
glGenBuffers(1, &VBO);
//绑定缓冲区,数组缓冲区,一般用于存储 颜色,位置纹理坐标、自定义属性
glBindBuffer(GL_ARRAY_BUFFER, VBO);
/*
* @ brief 填充缓冲区, 就是将数据传递到缓冲区中
* @ param 相同绑定点
* @ param 上传数的大小
* @ param 上传数据
* @ param 告诉openGL,我们是如何使用缓冲区,帮助openGL驱动在正确位置分配内存。
用途:缓冲区内容作为OpenGL一条命令输出来进行一次设置,并经常绘制或复制到其它图形。
如果不确定时,可以使用GL_DYNAMIC_DRAW是一个比较安全的值
用途:缓冲区将经常由应用程序更新,并经常绘制或复制到其它图形。
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
/*
* brief:设置顶点属性指针,告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
* 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
* 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
* 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
* 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
* 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
* 第六个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
*/
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
/*
* 默认顶点属性是不使能的,需要使能属性0[layout(location = 0)],
* 编号为0的属性将使用VBO的数据
* 这样才能将顶点数据传入顶点着色器
*/
glEnableVertexAttribArray(0);
/*
* 1.将对象0绑定到GL_ARRAY_BUFFER,即解绑。
* 2.上面的过程 编号为0的属性和VBO以及建立的联系,并不会影响当前的顶点属性
* 3.因此,GL_ARRAY_BUFFER决定了OpenGL的一种状态,当将顶点着色器的属性和VBO建立联系后,
* GL_ARRAY_BUFFER的这一次牵线搭桥的使命也就完成了,可以解绑,
* GL_ARRAY_BUFFER就可以为下一次任务做准备,即 (建立着色器属性和VBO的联系)
*/
glBindBuffer(GL_ARRAY_BUFFER, 0);
m_VAO = VAO;
m_VBO = VBO;
}
加载着色器过程
GLint MyGLWidget::LoadShader(const char *vertexShaderSource, const char *fragmentShaderSource)
{
//临时着色器对象
GLuint vertexShader;
GLuint fragmentShader;
//创建着色器对象
vertexShader = glCreateShader(GL_VERTEX_SHADER);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
/*
* brief:着色器源码附加到着色器对象上
* 顶点着色器对象
* 源码字符串数量
* 顶点着色器源码
* ....
*/
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译着色器
glCompileShader(vertexShader);
//重复片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//检查顶点着色器编译错误
GLint testVal;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &testVal);
if(testVal == GLU_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(vertexShader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return 0;
}
//检查片段着色器中编译错误
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &testVal);
if(testVal == GLU_FALSE)
{
char infoLog[1024];
glGetShaderInfoLog(vertexShader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return 0;
}
GLuint shaderProgram = 0;
//创建程序对象Id,并且链接着色器
shaderProgram = glCreateProgram();
//链接着色器
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &testVal);
if (!testVal) {
char infoLog[1024];
glGetProgramInfoLog(shaderProgram, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
return 0;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
在循环中绘制三角形
void MyGLWidget::drawTriangle()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glUseProgram(m_shaderProgram);
glBindVertexArray(m_VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
渲染结果