这是对learnopengl的简单笔记。原教程网址:learnopengl。原教程同时涉及图形学的基本理论与opengl API,本文更多关注API,而简化甚至省略了背后的图形学原理性内容。
渲染任何图形都有相当长的前摇,涉及相当繁琐的东西,大体来说,我们需要:
- 编写并编译着色器(至少顶点着色器与片段着色器)。
- 准备要输入的顶点数据。设置 顶点数组对象,顶点缓冲对象,元素(索引)缓冲对象。
- 链接顶点属性
- 绘图(到这才算真正开始画图)
着色器 Shader
openGL的绘图管线有六个步骤、其中有三个着色器,它们组成一条流水线,大多数工作在硬件显卡中完成。着色器是我们可以自己编写的,用类C的GLSL语言编写。
- 顶点着色器(vertex shader)。将输入的顶点的坐标(五花八门)变换为另一种坐标(位于-1,1的标准化设备坐标。落在外面的被丢弃)。没有默认的顶点着色器,必须自己至少编写一个。
- 形状(图元)装配。将顶点依序和所要连接的图形连起来成几何图元。
- 几何着色器(geometry shader)。有默认的。这里可以通过增删顶点等调整图元,可能网格的变换就在这里。
- 光栅化。
- 片段着色器(fragment shader)。需要自己手动配置至少一个。设置像素的颜色。但这个颜色并非最后的输出。openGL中一个片段是渲染一个像素所需的所有数据。这里大部分是根据自己的算法与需要定义的。
- 测试与混合。光照、消隐等可能在这里。Alpha测试(透明度),多图形混合,深度等等。
顶点着色器
定义着色器
将输入的顶点依照自己的规则转换为标准化设备坐标输出。这里写一个最简单的,原样输出(保证输入就是标准化设备坐标NDC)。
#version 330 core
layout (location = 0) in vec3 aPos;//布局,把对这个着色器的输入放到位置0
// in 表示以三维向量形式输入,输入的命名为aPos。
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);//原样输出
//gl_Position应该就是顶点着色器的(固定?)输出。还有其它输出信息,这里略去。
//这里vec4的最后一个是w,用在后续透视除法。
}
gl_Position: 长 宽 高 for透视除法(最后那个也许是用作齐次坐标的扩展)
其实着色器语言 GLSL 和硬件描述语言 HDL 有点像。后面在片段着色器的地方更像。
OpenGL定义了vecn, 是n维向量结构,最多6维。
着色器是运行在GPU上的小程序,所以编写着色器的语言其实和硬件描述语言有很高的类似。虽然它是类似C的,但我们应当将它和硬件描述语言(如verilog)类比学习。
编译着色器
将着色器的定义用字符串完全存下来,然后用OpenGL函数编译他。
//整个存为一个字符串。
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";
创建一个顶点着色器对象,它有一个唯一的id辨识它,放在unsigned int变量里。glCreateShader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);//GL_VERTEX_SHADER
现在可以通过vertexShader访问将要编译的着色器对象。
下面编译( glShaderSource, glCompileShader ):
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//依次是要编译的着色器对象,原码字符串数量,原码字符串指针的地址,最后一个略。
glCompileShader(vertexShader);//编译
错误信息处理:glGetShaderiv glGetShaderInfoLog
...compile code... shaderID is vertexShader
int success; //成功标志
char infoLog[512]; //存放可能的错误信息
glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success)
if(!success){
glGetShaderInfoLog(vertexShader,512,NULL,infoLog);
printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED\n%s\n",infoLog);
}
总的来说:
int success; //成功标志
char infoLog[512]; //存放可能的错误信息`
//整个原码存为一个字符串。
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";
//创建顶点着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);//GL_VERTEX_SHADER
//设置源码并编译
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//依次是要编译的着色器对象,原码字符串数量,原码字符串指针的地址,最后一个略。
glCompileShader(vertexShader);//编译
//检查错误
glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success)
if(!success){
glGetShaderInfoLog(vertexShader,512,NULL,infoLog);
printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED\n%s\n",infoLog);
}
片段着色器
这里直接输出固定的颜色:
#version 330 core
out vec4 FragColor; //out表示“输出端口”,是不是很像HDL
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
颜色向量vec4 是RGBA,A是透明度。为1表示完全不透明。
编译步骤一样。
着色器程序
把自定义的所有着色器拼接在一起变成完整的管线,用到着色器程序。检查错误的步骤也一样。
glCreateProgram, glAttachShader, glLinkProgram, glGetProgramiv, glGetProgramInfoLog
过程:创建 ShaderProgram,添加着色器,连接着色器,应用着色器,删除之前的着色器…
int success;
char infoLog[512];
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
printf("......");
...
}
最后应用这个着色器程序,并且删除已经没用的着色器对象。在下一次应用新的着色器程序前,都会用这个。
glUseProgram, glDeleteShader
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
(猜测,按照惯例,用glUseProgram(0)单纯解绑,但恐怕不会这么用)。
顶点数据结构
用顶点缓冲对象VBO与显存交流
顶点数据缓存在显存里,它们的识别符号是GL_ARRAY_BUFFER。
一组顶点数据,如三角形三顶点:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
将要存在显存里进行处理。用顶点缓冲对象(vertex buffer object)管理显存。注意,顶点缓存对象才是管理显存中的顶点的本体,其它VAO IBO 都只是便于我们使用的辅助,因而要用VBO绑定GL_ARRAY_BUFFER。
用 glBufferData 将数组中的数据保存到显存中。
unsigned int VBO;
glGenBuffers(1, &VBO); //创建一个缓冲区对象用作VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定VBO为顶点缓存数组
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//目标缓冲:GL_ARRAY_BUFFER,还有许多其它。
//要缓存的数组字节大小,sizeof
//数组指针
//GL_STATIC_DRAW,数据基本不变,GL_DYNAMIC_DRAW数据时而会变,GL_STREAM_DRAW,实时在变。
用 glBindBuffer(GL_ARRAY_BUFFER, VBO)设置Buffer的状态,表示接下来的BufferData等操作用VBO对GL_ARRAY_BUFFER进行,用 glBindBuffer(GL_ARRAY_BUFFER, 0) 表示解除绑定。后面的EBO VAO的绑定、解绑行为都是一样的。
链接顶点属性
告诉硬件你存下的一串数据是怎样的格式与结构。
glVertexAttribPointer glEnableVertexAttribArray
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//属性位置,还记得顶点着色器里的 layout(location=0)吗,这就设置了顶点属性位置为0.
//要输入的顶点属性大小,in vecx 里的x,这里是vec3,即3
//每个参数的数据类型
//是否希望提前将数据标准化。提前映射到标准区间。(0-1 for unsigned, -1-1 for signed)
//步长,连续两项(两个顶点)间的字节数。也是一整个顶点数据所包含的字节数
//缓冲区中起始位置的偏移。后面再说
glEnableVertexAttribArray(0);
//使能**属性位置为0**的缓冲!
openGL根据所绑定的 VBO 来寻找顶点数据。如果有两个VBO,分别绑定、分别传数据,只要在画图的时候,并且在这之前分别用不同的VAO记录,那么只要在画图前绑定了相应VAO,就能找到对应VBO中的点。注意每次处理VBO的过程是完全一样的,都需要 glEnableVertexAttribArray(0),不能因为另一个VBO的处理中已经调用过了就少用一次,而是都需要!
顶点数组对象VAO
openGL要求必须使用VAO。
用顶点数组对象VAO记录、管理所有对它相关的VBO的修改与状态。从而在不同的VBO组之间轻松切换。
VAO会保存:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置(参数)。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象(可以有多个VBO)。
VertexArray
glGenVertexArray, glBindVertexArray,
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 0. 创建一个VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0); //解绑VAO
在绑定VAO后(glBindVertexArray)后,所有对顶点缓冲的操作、相关的参数、VBO、使能与链接属性的函数调用都会记录在VAO中。在下次再次 glBindVertexArray 后,这些操作将被”重现“已回到之前的状态来在这组VBO上继续操作。所以确保对VBO的众多操作全部放到绑定相应VAO之后!
元素(索引)缓冲对象EBO(IBO)
GL_ELEMENT_ARRAY_BUFFER
用顶点数组中每个点的编号(从0开始的伪下标)来指定绘制顺序(防止只能按顺序画顶点下的重复存储许多顶点)。这也是直接和显存相关的对象!因而同样用glBindBuffer, glBufferData,绑定GL_ELEMENT_ARRAY_BUFFER缓冲。
对它的操作和VBO很像。但EBO似乎不需要经历一个类似 VBO glVertexAttribPointer(…) and glEnableVertexAttribArray(…)的过程。对它存入内存中结构的指定在后面绘制中的 Draw_Element() 中指定。
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
/**************************************/
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
EBO也可以记录在VAO中,确保glBindVertexArray(VAO) 在最前面。
注意
VBO 通过location指定了位置,通过 glEnableVertexAttribArray(location) 指定从显存中的哪个地方读取数据,VAO通过记录这个 glEnableVertexAttribArray的调用来让程序知道从哪里找顶点(当然它也会记录 glDisableVertexAttriArray,所以在 glBindVertexArray(0)解绑VAO前是否真的要调用 glDisableVertexAttribArray),而 EBO, Draw_Elemets 函数中从 GL_ELEMENT_VERTEX_ARRAY 所绑定的EBO那里获得索引数组,所以这之前必须要有一个 glBindBuffer(GL_ELEMENT_VERTEX_ARRAY, EBO) 来指定这个EBO。VAO会记录这个过程(当然也会记录解绑的过程),最后一次绑定、解绑EBO的操作被VAO记录下来,所以确定 不要在VAO被解绑之前解绑EBO!
最好:绑定VAO在所有其它VBO、EBO的绑定、操作之前,而解绑VAO同样在所有其它解绑之前。
绘制!!!
走完了前摇终于开始绘制。
glDrawArrays,不用EBO画图。
glDrawArray(GL_TRIANGLES,0,3);
//GL_TRIANGLES,表示要画的是三角形。其它还有许多。
//顶点数组的起始索引
//要绘制多少个顶点
glDrawElements,使用EBO画图。
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//画三角形
//画几个点
//EBO里的数据是什么类型,索引类型
//EBO中的偏移量。
通过指定最后一个参数,不用EBO而用索引数组似乎也可以做到类似的效果。
用之前的顶点与EBO,最后出来是个矩形。
此外,设置状态为线框图、恢复填色用:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
完整例程
关于VAO,VBO,EBO等乱七八糟的一个成功了例子
//vertex info. VAO, VBO, contain an EBO this time.
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f, // 左上角
};
unsigned int indics[] = {
0,1,3,
1,2,3,
};
unsigned int VAO,VBO,EBO;
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glGenBuffers(1,&EBO);
glBindVertexArray(VAO);
//VBO
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),NULL);
glEnableVertexAttribArray(0); //enable location=0 的显存区域(大概)它被VAO记录,所以只要绑了VAO就能用这个VAO
//EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO); //它被VAO记录,所以只要绑了VAO就能找到EBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indics),indics,GL_STATIC_DRAW);
glBindVertexArray(0); //解绑VAO,在后面要用的时候再绑定。VAO最早绑定,最早解绑
glBindBuffer(GL_ARRAY_BUFFER,0); //解绑,对于VAO,数据传完了、设置完了就没必要绑着内存了!
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
//render loop//
glBindVertexArray(VAO);
glDrawElement(...);