OpenGL C++记录(二):基本渲染与着色器
根据LearnOpenGL记录了OpenGL的基本入门,初衷只是想接触一些计算机图形学内容,该系列估计最多只会到坐标变换一节,完整学习可移步原教程。
基本程序
OpenGL的入门打印不再是Hello
World了,其牵扯到比较多的概念,完整的一个基本程序如下,做了很多事情,但是效果上仅仅输出一个空白灰绿框以及打印了OpenGL版本信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
const unsigned int VIEW_WIDTH = 800;
const unsigned int VIEW_HEIGHT = 600;
void framebuffer_size_callback(GLFWwindow* window, int width, int height) //窗口大小变化回调
{
std::cout << "Call frame buffer callback function!" << std::endl;
glViewport(0, 0, width, height); //自动更新视口大小
}
int main()
{
int glfwState = glfwInit(); /// 1. glfw初始化
if (glfwState == GLFW_FALSE)
{
std::cout << "GLFW initialize failed!" << std::endl;
exit(EXIT_FAILURE);
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); /// 2. (Optional)OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Hello OpenGL", NULL, NULL); ///3. 创建窗口对象
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); ///4. 该窗口为当前线程上下文窗口
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) ///5. glad加载所有函数指针
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//需要在加载glfw、glad库之后
const GLubyte* version = glGetString(GL_VERSION); ///6. (Optional)打印当前OpenGL版本
const GLubyte* vendor = glGetString(GL_VENDOR);
const GLubyte* renderer = glGetString(GL_RENDERER);
const GLubyte* glslVersion = glGetString(GL_SHADING_LANGUAGE_VERSION);
std::cout << "OpenGL Version : " << version << std::endl;
std::cout << "GLSL Version : " << glslVersion << std::endl;
std::cout << "Renderer : " << renderer << std::endl;
std::cout << "Vendor : " << vendor << std::endl;
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); /// 7.注册窗口大小调节函数
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); /// 8. 设置windows颜色
while (!glfwWindowShouldClose(window)) /// 9. 监听关闭事件,持续渲染
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){ /// 10. 输入处理:esc时退出循环
glfwSetWindowShouldClose(window, true);
}
glClear(GL_COLOR_BUFFER_BIT); /// 11. 清除上一帧的颜色缓冲区,再使用glClearColor渲染颜色
glfwSwapBuffers(window); /// 12. 双缓冲区渲染
glfwPollEvents(); /// 13. 处理输入和窗口事件
}
glfwTerminate(); /// 14. 终止glfw,释放资源
return 0;
}
glfw初始化
glfwInit
初始化,glfw
负责拿到OpenGL函数底层实现的函数指针,因此首先确保其调用正常;
OpenGL版本管理
既不是glad也不是glfw版本,是OpenGL的版本声明和打印,不同的显卡驱动对OpenGL的支持可能是不同的,为了防止跨平台出现难以排查的bug,首先声明OpenGL版本能确保不满足该版本的环境不能运行程序,查询方法如上述6;
窗口上下文
glfwCreateWindow
创建了窗口,并且设为当前线程上下文的窗口对象;
glad加载函数指针
gladLoadGLLoader
加载函数指针,glad库获取到指针才能调用OpenGL的函数,可见有的glfw初始化函数不需要依赖glad,例如glfwInit
、glfwWindowHint
、glfwCreateWindow
、glfwMakeContextCurrent
,而后续的调用函数都必须在glad拿到函数指针后才能正常实现调用,例如glGetString
、glClearColor
、glClear
等;
回调与视口
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback)
注册了一个回调,当窗口大小变化时会进行调用(MacOS系统第一次时可能会调一次),例如手动改变窗口大小,或者在while渲染时手动调用;glViewport
用于修改OpenGL的视口大小,对OpenGL而言,它并不主动管理窗口大小,窗口的左下角坐标常常定义为(-1,-1)
,右上角为(1,1)
,可见其坐标系定义是一个窗口中心为原点,水平向右为x正方向、竖直向上是y正方向,区别于图像左上角为(0,0)的窗口定义。所以其大小需要用户通过glViewport
告知,当窗口大小变化应该及时更新OpenGL视口,否则窗口上只有部分画面被渲染;
双缓冲区渲染
因为窗口上画面的渲染不是瞬间完成的,是逐个像素渲染的,如果使用单缓冲区,用户就会看到整个渲染过程造成卡顿的直觉,因此OpenGL一般在后台缓存渲染,等整个画面就绪直接swap到前台的缓存进行显示。
渲染
OpenGL的工作有两个,首先是通过图形渲染管线(Graphics Pipeline)将3D坐标变成2D坐标,这个过程是一个复杂的过程;第二部分是将2D坐标变成实际颜色的像素,2D坐标指的是二维空间的一个精确位置,像素指的是该位置的近似值,会受到设备的分辨率影响;
以下主要讨论第一部分。图像渲染管线的流程被划分成高度专门化的若干流程,每一个小流程被称为着色器(Shader),在OpenGL中由着色器语言编写(OpenGL
Shading Language,
GLSL),整个流水线依次为顶点着色器(Vertex
Shader)-几何着色器(Geometry
Shader)-图元装配(Shape
Assembly)-光栅化(Rasterization)-片段着色器(Fragment
Shader)-测试与混合(Tests and
Blending),在现代OpenGL中,要求用户至少提供自定义的顶点着色器和片段着色器,其他着色器均有默认的实现。
顶点着色器
顶点着色器的输入是一个数组数据,被称为顶点数据(Vertex
Data),每个数据单元是一个顶点,其数据被称为顶点属性(Vertex
Attribute),数据可以包含我们想用的任何数据,简化而言是三维坐标信息+颜色信息;顶点着色器主要作用是将顶点信息映射到屏幕空间(Screen-space
Coordinates),而且只有在标准化设备坐标(Normalized Device
Coordinates,NDC)内才会处理,即满足三个维度都在-1到1范围,其他范围的输入会被丢弃或裁剪;在GLSL中,着色器的输出是一个vec4结果,包含x、y、z
三个坐标,以及w
信息,因为对三维物体的投影变换可以统一为四维齐次坐标变换,这个w量会后续被用于透视除法。
另一个问题是程序和计算机如何存储和传递顶点像素。
顶点缓冲对象(Vertex Buffer Objects, VBO)
顶点缓冲对象相当于一块内存,通常CPU向GPU发送数据是较慢的,使用这个对象,可以快速且大批量地将顶点数据发送到显存中,而且显存能够存储这些数据,并且在运行顶点着色器时能够快速访问(访问是极快的)。
使用这样的对象步骤是先创建内存对象,绑定缓冲对象类型(相当于向OpenGL告知这块内存的作用,决定了其如何存储数据),最后将数据放入这个内存,代码:
1
2
3
4
5
6
7
8
9
10float vertices[] = { // veb3类型,三角形3个顶点信息
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int VBO; //创建成功,这块的内存有唯一的id,由系统分配uint
lGenBuffers(1, &VBO); //创建1块顶点缓冲对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定为array类型
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //数据放入内存对象1
2glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO1); //VBO1会替代掉VBO1
2
3
4
5GL_ARRAY_BUFFER ///顶点属性数据(位置、颜色、法线、纹理坐标等)
GL_ELEMENT_ARRAY_BUFFER ///索引数据(用于 glDrawElements)
GL_UNIFORM_BUFFER ///Uniform 缓冲(与 Uniform Blocks 交互)
GL_SHADER_STORAGE_BUFFER ///通用的大数据交互缓冲,实现更加复杂的数据结构
GL_ATOMIC_COUNTER_BUFFER ///原子计数器缓冲区
glBufferData的第四个参数定义了数据的存放类型,根据数据变化的频率决定其在显存上的位置(频繁更改的会在较高速位置):
1
2
3GL_STATIC_DRAW ///数据不会或几乎不会改变。
GL_DYNAMIC_DRAW ///数据会被改变很多。
GL_STREAM_DRAW ///数据每次绘制时都会改变。
片段着色器
屏幕图像是由一个一个格子构成的,光栅化计算了各种位置坐标在像素上的投影,片段着色器会基于该投影计算颜色插值,此外还会受到各种特效影响,例如阴影、光照等。
编译着色器程序
着色器的编译和运行发生在GPU中,所以编写规范和过去我们接触的编程方法都有差异,首先我们会将着色器的定义源码封装成C风格的const char*对象,然后将它们绑定到着色器(Shader)对象,送到GPU编译,在调用时会再通知GPU执行该着色器程序。
顶点/片段着色器编译
最简单一个顶点着色器程序:位置坐标原封不动、w系数为1,其中#version
460对应OpenGL4.6版本,OpenGL 3+都有这种关系,且采用核心编译。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const char* vertexShaderSrc = "#version 460 core\n \
layout(location = 0) in vec3 aPos; \n \
void main(){ \
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n \
}\n\0";
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); //vertex shader
glShaderSource(vertexShader, 1, &vertexShaderSrc, NULL); //送到GPU编译
glCompileShader(vertexShader); //GPU编译
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); //获取编译结果
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); //打印失败信息
cout << "vertex compile failed " << infoLog<< endl;
}
片段着色器与此类似,注意创建shader对象时使用GL_FRAGMENT_SHADER
,这也是最简单的输出片段着色,无需计算,使用橘色作为输出色:
1
2
3
4
5
6
7
8
9
10
11
12
13
14const char* fragmentShaderSrc = "#version 460 core\n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); \n \
}\n\0";
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //fragment shader
glShaderSource(fragmentShader, 1, &fragmentShaderSrc, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
cout << "fragment compile failed " << infoLog<< endl;
}
完整编译程序
完成着色器设计,只需要将两种着色器注册到一个着色器调用程序,在渲染时调用并且绘制即可,以下是一个三角形基本渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
using std::cout;
using std::cerr;
using std::endl;
const GLuint SCR_WIDTH = 800;
const GLuint SCR_HEIGHT = 600;
const char* vertexShaderSrc = "#version 460 core\n \
layout(location = 0) in vec3 aPos; \n \
void main(){ \
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n \
}\n\0";
const char* fragmentShaderSrc = "#version 460 core\n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); \n \
}\n\0";
int main(){
int glfwState = glfwInit();
if(glfwState==GLFW_FALSE){
cout<< "glfw Init failed ! " << endl;
exit(EXIT_FAILURE);
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "OpenGL", NULL, NULL);
if(window == NULL){
cout << "window Null" << endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height){ //无捕获lambda is ok
cout << "Func Called" << endl;
glViewport(0, 0, width, height);
});
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
cout << "load glad failed !" << endl;
return -1;
}
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); //vertex shader
glShaderSource(vertexShader, 1, &vertexShaderSrc, NULL);
glCompileShader(vertexShader);
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
cout << "vertex compile failed " << infoLog<< endl;
}
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //fragment shader
glShaderSource(fragmentShader, 1, &fragmentShaderSrc, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
cout << "fragment compile failed " << infoLog<< endl;
}
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success){
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
cout << "shader attach failed " << infoLog << endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
///
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao); //表明glVertexAttribPointer都是写入vao
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); ///数据放入VBO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (void*)0); //定义location=0着色器解析属性
glEnableVertexAttribArray(0); //启用,否则会出现预期外结果
//optional:防止误操作,可以解绑
// glBindBuffer(GL_ARRAY_BUFFER, 0); ///取消vbo
// glBindVertexArray(0); //取消vao
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(vao); //绘制前必须bind一下
glDrawArrays(GL_TRIANGLES, 0, 3); //三角形绘制,从VBO第0个顶点开始,绘制三个顶点
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vbo);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
顶点数组对象(Vertex Array Object,VAO)
VBO是一块缓存,定义了数据存储的位置和方式,GPU要解析这块缓冲,例如有几个顶点,顶点中有几个数据,数据按什么字节类型读取,都需要信息提示,这个工作由VAO完成,核心模式下VBO绘制必须绑定VAO,否则无法进行图形渲染等操作,基本绑定流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/// 1. 创建和绑定VAO
unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
/// 2. 配置属性前必须创建和绑定VBO
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); ///数据放入VBO
/// 3. 配置VAO属性并且启用
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(0); //启用,否则会出现预期外结果
//optional:4. 防止误操作,可以解绑
// glBindBuffer(GL_ARRAY_BUFFER, 0); ///取消vbo
// glBindVertexArray(0); //取消vao
多个VAO与VBO渲染
如果需要绘制多个三角形,存在若干种方法:
方法一:一个vbo+绘图指定点数
这种方法比较简单: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37///将两个三角形写到同一个vbo数据:
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, //左上角
0.5f, -0.5f, 0.0f, //右下角
0.5f, 0.5f, 0.0f //右上角
};
///VBO、VAO创建绑定逻辑均不改变:
unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao); //表明glVertexAttribPointer都是写入vao
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); ///数据放入VBO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (void*)0); //属性仍然保持为3组数据
glEnableVertexAttribArray(0); //启用,否则会出现预期外结果
///绘制时,指明两个三角形、6个点即可
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 6); //6点
glfwSwapBuffers(window);
glfwPollEvents();
}
方法二:使用多个VAO和VBO
1 | ///增加绑定逻辑: |
方法三:多着色器程序+location属性指定
方法三来自对方法二的思考,既然vao[0]和vao[1]是一致的,那样能不能就用一个VAO绑定两个VBO对象呢,这样一来,使得VBO相对独立,又可以复用相同VAO的属性,于是写下这样的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34///这是一段错误代码
unsigned int vao;
unsigned int vbo[2];
glGenVertexArrays(2, &vao);
glGenBuffers(2, vbo);
glBindVertexArray(vao); //绑定VAO
glBindBuffer(GL_ARRAY_BUFFER,vbo[0]); //装载VBO 0
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); //装载VBO 1
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_2), vertices_2, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), (void*)0); VAO对两个VBO同时生效?
glEnableVertexAttribArray(0);
///甚至你会像切换VAO那样切换VBO:
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(vao); //绘制
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); //切
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}1
2
3
4
5
6
7
8glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);
///参数:
- index: 属性索引,对应每个顶点着色器的 location= ?
- size: 每组数据有几个顶点,例如三角形数据应该有三个顶点;
- type: 数据的数据类型
- normalized: 是否需要标准化
- stride: 顶点属性的数据长度,例如一个三角形的顶点是一个三维浮点数坐标
- pointer: VBO的偏移量,如果多个数据在一个VBO中,根据偏移量读出
所以VAO复用分两种情况:
VBO本身存储了不同属性的数据,例如位置、颜色、纹理等,那么就可以直接和VAO对应了,这种情况下一个着色器程序即可,顶点着色器的写法类似:
1
2
3
4
5
6
7
8
9
10const char* vertexShaderSrc = "#version 460 core\n"
"layout(location = 0) in vec3 aPos; \n"
"layout(location = 1) in vec3 aPos; \n" ///纹理属性
"void main(){"
" ...... \n"
"}\n\0";
///所以VOA必须提高属性1以解析纹理等属性:
glVertexAttribPointer(0, ..., off); //属性0
glVertexAttribPointer(1, ..., off); //属性1VAO如果需要用不同属性位置,去保持相同的属性,例如上述想要用一个VAO去绘制两个VBO的坐标数据,可以将其中一个坐标数据看成是另一个着色器程序的顶点着色器数据,这种方法更加麻烦,但是如果你本来就需要多个着色器程序(例如本来就想要不同的片段着色器),可以使用该方法,以下:
效果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
using std::cout;
using std::cerr;
using std::endl;
const GLuint SCR_WIDTH = 800;
const GLuint SCR_HEIGHT = 600;
const char* vertexShaderSrc = "#version 460 core\n \
layout(location = 0) in vec3 aPos; \n \
void main(){ \
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n \
}\n\0";
const char* fragmentShaderSrc = "#version 460 core\n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); \n \
}\n\0";
const char* vertexShaderSrc_lc1 = "#version 460 core\n \
layout(location = 1) in vec3 aPos; \n \
void main(){ \
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n \
}\n\0";
const char* fragmentShaderSrc_Green = "#version 460 core \n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = vec4(0.0f, 1.0f, 0.0f, 1.0f); \n \
}\n\0";
int main(){
int glfwState = glfwInit();
if(glfwState==GLFW_FALSE){
cout<< "glfw Init failed ! " << endl;
exit(EXIT_FAILURE);
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "OpenGL", NULL, NULL);
if(window == NULL){
cout << "window Null" << endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height){ //无捕获lambda is ok
cout << "Func Called" << endl;
glViewport(0, 0, width, height);
});
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){
cout << "load glad failed !" << endl;
return -1;
}
int success;
char infoLog[512];
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); //vertex shader
glShaderSource(vertexShader, 1, &vertexShaderSrc, NULL);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
cout << "vertex compile failed " << infoLog << endl;
}
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //fragment shader
glShaderSource(fragmentShader, 1, &fragmentShaderSrc, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
cout << "fragment compile failed " << infoLog << endl;
}
unsigned int vertexShader_lc1 = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader_lc1, 1, &vertexShaderSrc_lc1, NULL);
glCompileShader(vertexShader_lc1);
glGetShaderiv(vertexShader_lc1, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(vertexShader_lc1, 512, NULL, infoLog);
cout << "vertex lc1 compile failed " << infoLog << endl;
}
unsigned int fragmentShader_Green = glCreateShader(GL_FRAGMENT_SHADER); //fragment shader
glShaderSource(fragmentShader_Green, 1, &fragmentShaderSrc_Green, NULL);
glCompileShader(fragmentShader_Green);
glGetShaderiv(fragmentShader_Green, GL_COMPILE_STATUS, &success);
if(!success){
glGetShaderInfoLog(fragmentShader_Green, 512, NULL, infoLog);
cout << "fragment Green compile failed " << infoLog << endl;
}
///着色器程序1
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success){
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
cout << "shader attach failed " << infoLog << endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
///着色器程序2
unsigned int shaderProgram_lc1 = glCreateProgram();
glAttachShader(shaderProgram_lc1, vertexShader_lc1);
glAttachShader(shaderProgram_lc1, fragmentShader_Green);
glLinkProgram(shaderProgram_lc1);
glGetProgramiv(shaderProgram_lc1, GL_LINK_STATUS, &success);
if(!success){
glGetProgramInfoLog(shaderProgram_lc1, 512, NULL, infoLog);
cout << "shader attach failed " << infoLog << endl;
}
glDeleteShader(vertexShader_lc1);
glDeleteShader(fragmentShader_Green);
float vertices_1[] = {
//下三角
-0.5f, 0.5f, 0.0f, //左上角
-0.5f, -0.5f, 0.0f, //左下角
0.5f, -0.5f, 0.0f //右下角
};
//上三角
float vertices_2[] = {
-0.5f, 0.5f, 0.0f, //左上角
0.5f, -0.5f, 0.0f, //右下角
0.5f, 0.5f, 0.0f //右上角
};
unsigned int vao;
unsigned int vbo[2];
glGenVertexArrays(1, &vao);
glGenBuffers(2, vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_2), vertices_2, GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GL_FLOAT), (void*)0); ///location==1,使用该位置属性
glEnableVertexAttribArray(1);
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram); ///着色器程序1
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(shaderProgram_lc1); ///着色器程序2
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(2, vbo);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
元素缓冲对象(Element Buffer Object, EBO)
在上述的方法一中,使用了六个坐标去描绘两个三角形,但两个三角形是相邻的,其中两个坐标是重合的,实际上只需要四个坐标即可绘制两个三角形组成的矩形,元素缓冲对象(EBO)允许我们重复利用这些重复对象,而节省了在VBO中存储的数据,因为核心模式要求每个VBO都需要与VAO属性绑定,所以EBO的只需要和VAO绑定即可,每个VAO对象的最后属性后的那块空间,会指向绑定的EBO。
其创建绑定方法与VBO基本类似,只是使用的缓冲名从GL_ARRAY_BUFFER
变成GL_ELEMENT_ARRAY_BUFFER
,绘制时使用glDrawElements
替代 glDrawArrays
: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 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, 1, 2,
0, 1, 3
};
unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//此处增加EBO绑定代码
unsigned int ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(0);
//渲染绘制
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(vao);
//glDrawArrays(GL_TRIANGLES, 0, 3); ///使用glDrawElements 替代 glDrawArrays
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制类型、实际点数、索引类型、EBO偏移量(如果indices数组不再使用可以传入)
glfwSwapBuffers(window);
glfwPollEvents();
}
线框模式
上述代码均基于填充模式绘制,也可以由线框模式绘制,只需要渲染前配置即可:
1
2glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) //默认:填充模式
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) //线框模式
着色器与GLSL
GLSL是一种类C语言,上述程序用这样的语言设计了最简单的顶点和片段着色器,但需要了解更多。
顶点着色器的输入是顶点属性,在一般硬件设备中最大支持16个,具体可通过查询:
1
2
3int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
GLSL数据类型
如下,GLSL使用向量类型作为基本数据类型,其中N代表维度数量,仅支持1到4。 |类型|含义| |:-:|:-:| |vecN|包含N个float类型| |bvecN|包含N个bool类型| |ivecN|包含N个int类型| |uvecN|包含N个unsigned int类型| |dvecN|包含N个double类型|
可以分别使用xyzw
依次访问四个分量,对于颜色数据,也可以使用rgba
,对于纹理数据,还可以使用stpq
;
数据重组
向量之间的分量可以重组和运算成新的向量: 1
2vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
低维度向量能够直接构成高维度向量的构造参数:
1
2
3vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
输入和输出
已知图像渲染管线是多个着色器构成的,着色器之间靠输入和输出关系连接,没有其他连接关系,顶点着色器的输入是顶点数据,大多数情况下最大支持16个属性,上述例子没有定义顶点着色器的输出,而实际上片段着色器接受vec4类型的颜色输入,这个vec4可以来自顶点着色器的输出。
属性组合和偏移量终于可以用上了,定义了含位置和颜色的VBO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37///着色器定义:
const char* vertexShaderSrc = R"(#version 460 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 vColor;
out vec4 fColor;
void main(){
gl_Position = vec4(aPos, 1.0);
fColor = vColor;
})";
const char* fragmentShaderSrc = "#version 460 core\n \
in vec4 fColor; \n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = fColor; \n \
}\n\0";
///位置、颜色
float vertices[] = {
//下三角
-0.5f, 0.5f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f, //颜色
0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.5f, 1.0f,
};
unsigned int vbo, vao;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER,vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 7*sizeof(GLfloat), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 7*sizeof(GLfloat), (void*)(3*sizeof(GLfloat))); //偏移量
glEnableVertexAttribArray(1);
uniform类型
从上面可以体会到,其余着色器的输入都依赖着顶点着色器,意味着所有的变更也要经过顶点着色器,这一定程度上带来不便。因此,OpenGL允许定义一种能被所有着色器访问的变量,这种变量被uniform修饰,这样的变量对于一个着色器程序而言是全局的,只要程序在运行uniform数据会被一直保存,只能被重置或者更新,任何着色器可以在任何阶段对其进行访问。另一方面,如果一个uniform变量在GLSL中从未被使用,可能导致麻烦的错误。
这样的变量最大的意义在于无需VOA或者VOB传递,能在C++程序而非只在GLSL程序中更新各种属性,特别是渲染时,uniform变量的定义和修改基本方法如下:
1
2
3
4
5
6
7
8
9
10
11
out vec4 FragColor;
uniform vec4 ourColor; //GLSL程序中定义
void main(){
FragColor = ourColor;
}
/// C++程序:
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); //获取uniform变量索引
glUseProgram(shaderProgram); //修改uniform变量前必须指定程序
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); ///更新参数
一个示例:片段着色器的颜色输出随时间变化,能输出颜色实时变化的图形:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41const char* vertexShaderSrc = R"(#version 460 core
layout(location = 0) in vec3 aPos;
out vec4 fColor;
void main(){
gl_Position = vec4(aPos, 1.0);
})";
//定义uniform
const char* fragmentShaderSrc = "#version 460 core\n \
uniform vec4 outColor; \n \
out vec4 FragColor; \n \
void main(){ \n \
FragColor = outColor; \n \
}\n\0";
float vertices[] = {
//下三角
-0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0
};
/// ......
///绘制时按时间变化颜色
while(!glfwWindowShouldClose(window)){
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
float timeValue = glfwGetTime();
float greenValue = sin(timeValue)/2.0f + 0.5f;
int uniformIdx = glGetUniformLocation(shaderProgram, "outColor");
glUseProgram(shaderProgram);
glUniform4f(uniformIdx, 0.0f, greenValue, 0.0f, 1.0f); //实时修改颜色
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}