文章目录
本人才疏学浅,如果什么错误,望不吝指出。
VAO,VBO,EBO/IBO 几个重要的数据对象
在OpenGL中,我们可以看到很多相关的博客,或是开源项目中都可以看到,VAO,VBO,EBO/IBO,到底是什么鬼。
这几个玩意都是一些英文的缩写:
- VAO : Vertex Array Object,顶点数组对象
- VBO : Vertex Buffer Object, 顶点缓存对象
- EBO/IBO : Element/Index Buffer Object, 索引缓存对象
这篇只是用 VBO,以后学习使用到 VAO、EBO/IBO 再说吧:
- VBO 出了本篇,还有下一篇:
- EBO/IBO 的文章:
- VAO 相关:
在学习 使用 OpenGL 绘制内容前,最好线了解基本的 OpenGL 渲染管线,References 文章 你好,三角形 也有讲到。要想更详细讲解,大家可以搜索了解:图形渲染管线 或是 光栅化图形渲染管线。或是查看之前的一篇 LearnGL - 01.1 - OpenGL 概述 & 管线概述
在现代的GPU设计中,必须要有一个VS(Vertex Shader,顶点着色器)和一个FS(Fragment Shader,片段着色器,也叫片元着色器),所以在我们绘制一个三角形时,我们需要编写最简单的VS和FS。
绘制三角形之前,我们需要有三角形的三个点的坐标
float vertices[] = {
// x,y,z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
渲染管线可以简单的分为三大阶段:
- 应用程序阶段 : 主要负责几何、光栅化阶段需要的渲染数据,顶点、索引、着色器、还有一些绘制状态配置。
- 几何阶段 : 主要对应用程序阶段输入的顶点变换处理。
- 光栅化阶段 : 主要对几何阶段处理后的点作插值生成片段后的处理。
其中几何阶段中,比较重要的是,顶点的变换过程。
顶点变换处理
我们输入的都是3D坐标,而最终显示的是在2D的平面屏幕上的。
所以需要将:3D坐标变换为2D屏幕坐标:OpenGL Transformation 几何变换的顺序概要(MVP,NDC,Window坐标变换过程)
变换顺序:Object Space -> World Space -> View Space -> Projection Space -> NDC Space -> Screen Space
其中 Projection Space -> NDC Space -> Screen Space 底层硬件会处理。
了解的基本的变换过程后,为了简单起见,我们尝试上面三个坐标点都当做NDC坐标点来使用,后续在对 Object Space -> World Space -> View Space 做一些介绍。
上面三个坐标点的z值为0,在OpenGL的NDC中,x,y,z在[-1~1]之间都是可视的NDC范围内的坐标,超出NDC Space的坐标点都当做是看不见的。
(Drirect的x,y都是[-1~1],但是z是[0~1]之间的,这点与OpenGL的NDC是不一样的)
所以上面三个点是可以看到的。
基本上在NDC下这三点组成的三角形如下:
顶点缓存创建、绑定、设置数据
使用顶点缓存对象,需要使用 glGenBuffers 来创建缓存对象。如下:
GLuint vertex_buffer;
glGenBuffers(1, &vertex_buffer);
生成缓存的vertex_buffer
就相当于在显卡的内存(显存)中生成了一个空指针。这也 vertex_buffer 也就是我们的之前说的 VBO。
这是 vertex_buffer
都是默认的数据,直到调用:glBindBuffer 后才是我们自己想要的数据,而绑定的缓存类型要设置好,这里我们绑定到:GL_ARRAY_BUFFER
的类型缓存。
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
绑定后,后续对GL_ARRAY_BUFFER 类型的缓存对象都会对 vertex_buffer
数据操作。
下面可以是用 glBufferData 来对上面我们绑定的 GL_ARRAY_BUFFER
的缓存对象设置(将 CPU 内存 数据复制到 GPU 显存)数据,即:对GPU显存中的 vertex_buffer
指针设置数据。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData 的 uage 参数我们使用的是 GL_STATIC_DRAW
,因为缓存数据几乎不改变,这样性能大大提升。
现在有顶点数据了,也创建了顶点缓存对象了,也设置了顶顶啊缓存对象的数据。
那么就可以开始配置好 至少需要的两个着色器:顶点着色器 和 片段着色器 就可以开始绘制了。
着色器
想要详细的了解 GLSL 4.5 版本的 可以查看 OpenGL Shading Language Specification 4.5
OpenGL 管线中的各个着色器的指定的阶段、位置。
在 OpenGL 着色器的使用可以简单的分为几个步骤:
- 取得编写的 GLSL 着色器脚本
- 创建着色器程序
- 创建着色器子程序,设置好子程序类型,设置好对应脚本
- 编译着色器子程序
- 将着色器附加到程序
- 将程序链接
- 设置当前管线使用的程序
着色器语言,GLSL 也就是OpenGL Shading Language,是一门 CLike 语言(与C语言很类似的)。
GLSL是在OpenGL 2.0 发布的(也就说之前的版本是没有的,只能用固定管线)。
OpenGL 兼容模式(compatibility profile)还是可以使用固定功能的管线(fixed-function pipeline)的,在OpenGL 核心模式(core profile)是没有固定功能管线的。 这里使用的是核心模式的。 其实后面我改用 compatibility profile 了,因为使用的API比较久的话,对应 core profile 下的话,会有过时API导致无法正常绘制。因为我参考的学习资料是3.3的API资料。
顶点着色器
下面我们列出一个最简单的顶点着色器:
#version 450 core
layout (location = 0) in vec3 vPos;
void main() {
gl_Position = vec4(vPos, 1.0);
}
第一行中的 #version 450 core
,就是指定使用的版本,指定这个Shader 是运行在 450 版本的 GLSL,这个 450 其实与我们的 OpenGL 4.5 是对应的,450 == 4.5 * 100
。
core
对应我们之前说的OpenGL 核心模式。(后面我改用 compatibility profile 了)
但一般我们会按照这个着色器使用的API特性来决定它的 version 的,想我们上面这个这么简单的着色器,使用的都是非常基础的特性,可以用 110 版本就可以了。这样它就可以兼容在比较低版本的 OpenGL 或是GLSL比较低的版本中运行。但这里图方便,我直接写了个 450 版本的。
第二行 layout (location = 0) in vec3 pos;
,分几个部分来讲解吧。
- layout(location = 0) : 是布局限定符(layout qualifier),location是指定这类shader变量类型属性所处的索引值。它可以用于
uniform
、attribute
。布局限定符不是必须的,对于attribute
的话,如果布局限定符不手动指定location索引值的话,GLSL 编译时会自动分配对应的索引值,可以使用对应的 API来获取。对于uniform
的话,是在着色器程序链接时确定的,后面的正文有讲到。 - in : 表示这时着色器的输入数据,在这个顶点着色器中会将应用程序设置的数据复制到该变量上,也可以查看下面的GLSL 的类型修饰符。
- vec3 : 包含3个分量的浮点型向量类型,可以通过(x,y,z或是r,g,b来访问,更详细的分量访问可以查看下表GLSL 向量访问分量符,也可以去搜索了解:swizzle)
- vPos: 这是变量的名称,这里用"v"开头的用意是指:在顶点着色器中的变量。(后面我们将片元着色器fragment shader时,是以"f"开头作,当然这不是必须的,都是个人喜欢的命名方式,怎么样可以让代码可读性高一些就好。)
GLSL 的类型修饰符
类型修饰符 | 描述 |
---|---|
const | 将一个变量定义为只读形式。如果它初始化时用的是一个编译时常量,那么它本身也会成为编译时常量 |
in | 设置这个变量为着色器阶段的输入变量 |
out | 设置这个变量为着色器阶段的输出变量 |
uniform | 设置这个变量为用于应用程序传递给着色器的数据,它对于给定的图元而言是一个常量 |
buffer | 设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存(storage buffer)使用 |
shared | 设置变量是本地工作组(local work group)中共享的。它只能用于计算着色器中 |
上面对 uniform 特别讲解一下:uniform是对该所有着色器阶段都是共享使用的,它必须定义为全局变量。
GLSL 向量访问分量符
分量访问符 | 符号描述 |
---|---|
(x,y,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,q) | 与纹理坐标相关的分量 |
第三行 void main() {
就是顶点着色器的入口函数,不需要参数,也不需要返回值。
第四行 gl_Position = vec4(vPos, 1.0);
中 gl_Position
是顶点着色器的内置输出变量,这个变量的值会传递到下一个阶段的着色器作为输入数据使用。这里我们将应用程序阶段设置的ndc space下的顶点数据直接作为gl_Position
来输出,只不过,输出时,我们不全了 vec4
的分量,最后一个分量为1(作为齐次坐标)。可以这句代码也可以写成:gl_Position = vec4(vPos.x, vPos.y, vPos.z, 1.0);
,这是swizzle语法的特点,就这一句的话,可以还很多种写法。
然后我们将这段 顶点着色器 的源代码设置到我们的一个变量中。
你也可以放在一个文件中,如:xxx.vert
文件中,但这里为了方便DEMO内聚的可读性,我就先放到一个变量中。
如下:
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"attribute vec3 vPos;\n"
"void main() {\n"
" gl_Position = vec4(vPos, 1.0);\n"
"}\n";
之前说过,顶点着色器 和 片元着色器 都是必须的要的。
那么下面我简单的来段 片元着色器 的介绍。
片元着色器
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"void main() {\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
可以看到这段 片元着色器 的脚本比 顶点着色器 的还要简短。
第一行 和顶点着色器一样,都是脚本使用的GLSL 版本450,这点基本GLSL中所有着色器都是需要的。
第二行 片元着色器的main函数,也基本是着色器需要的。
第三行 对GLSL内置输出变量为红色值:gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
,这里的vec4(1.0, 0.0, 0.0, 1.0); 的 vec4 类型向量的分量可以解读为RGBA,R=1.0,G和B都是=0.0,A是1.0,所以可以理解为就是一个不透明的红色。
使用着色器
顶点着色器 和 片元着色器 的脚本都有了,那么就可以应用程序代码中使用它们,还记得之前将的使用步骤吧:(那么下面再列一次)
- 取得编写的 GLSL 着色器脚本
- 创建着色器程序
- 创建着色器子程序,设置好子程序类型,设置好对应脚本
- 编译着色器子程序
- 将着色器附加到程序
- 将程序链接
- 设置当前管线使用的程序
取得编写的 GLSL 着色器脚本
再之前的两个 shader 脚本 复制过来,如下:
顶点着色器
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"attribute vec3 vPos;\n"
"void main() {\n"
" gl_Position = vec4(vPos, 1.0);\n"
"}\n";
片元着色器
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"void main() {\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
创建着色器程序
用到 glCreateShader API
unsigned int vertex_shader;
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
这样就创建了一个 顶点着色器 对象,ID为:vertex_shader
。
有了ID,需要给这个着色器指定源码
创建着色器子程序,设置好子程序类型,设置好对应脚本
用到 glShaderSource API
glShaderSource(vertex_shader, 1, &vertexShaderSource, NULL);
编译着色器子程序
用到 glCompileShader API
glCompileShader(vertex_shader);
编译失败诊断日志
也许编译不一定会成功,这时想要查看是什么原因导致编译失败,可以通过 glGetShaderiv,glGetShaderInfoLog 组合使用来查看编译日志。
// 获取编译状态值储存到:success 变量
GLint success, infoLogLen;
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(vertex_shader, infoLogLen, NULL, infoLog);
std::cout << "Vertex Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
当 顶点着色器 编译成功后,我们同样的方式变以好 片段着色器。
unsigned int fragment_shader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragment_shader);
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(fragment_shader, infoLogLen, NULL, infoLog);
std::cout << "Fragment Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
将子程序附加到着色器程序
着色器都准备好后,就可以开始将着色器 附加 到 着色器程序 中。
而在这之前我们需要先有 着色器程序 对象。
那么创建 着色器程序 如下。
通过 glCreateProgram API 创建程序对象:
unsigned int program;
program = glCreateProgram();
将着色器附加到程序
创建好程序对象后,再将之前创建好、设置好源码、编译好的着色器附加到此程序对象中。
通过 glAttachShader API 来将 shader 对象附加到程序对象:
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
将程序链接
通过 glLinkProgram API 来将链接程序对象:
glLinkProgram(shaderProgram);
链接失败诊断日志
之前着色器编译失败的话,是可以通过:glGetShaderiv,glGetShaderInfoLog 查看编译日志信息来诊断问题的。
着色器程序的链接失败的话,同样可以查看日志信息来诊断,可以通过:glGetProgramiv,glGetProgramInfoLog API:
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果链接失败,则将链接日志储存到:infoLog 中
glGetProgramInfoLog(program, infoLogLen, NULL, infoLog);
std::cout << "Program Linking Error Status: " // 输出链接错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
设置当前管线使用的程序
通过 glUseProgram API 设置当前使用的程序对象:
glUseProgram(program);
当前渲染上下文对象使用的程序,包含了所有需要执行的着色器,都将执行该程序对象内的着色器。
当着色器程序都附加上需要的编译好的着色器,并且着色器程序 glLinkProgram 链接成功后,那么相关的着色器都可以删除了。因为他们在 Program 对象都链接合并了。使用的是 glDeleteShader API:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
设置顶点属性格式
虽然现在着色器程序对象都设置好了。
但是用于渲染的着色器中的数据如何识别出我们设置之前设置的数据呢?
如,之前设置的顶点缓存数据,在顶点着色器中如何识别呢?
如果你忘记了?别担心我把它们代码都列出来:
float vertices[] = {
// x,y,z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
...
int main() {
GLuint vertex_buffer;
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
...
return 0;
}
如上面的代码中的 vertices 数据如果识别在顶点着色器呢?
可以使用:glVertexAttribPointer,glEnableVertexAttribArray API:
GLint vpos_location;
...
vpos_location = glGetAttribLocation(program, "vPos");
glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void*)0);
glEnableVertexAttribArray(vpos_location);
glVertexAttribPointer 参数简单说明:
- 第一个参数 -
index
就是着色器中顶点属性(attribute)的索引,在着色器脚本中可以通过layout qualifier(布局限定符)来指定:layout(localtion = 0)
,那么这个索引就是0。如果脚本中没有通过布局限定符来指定索引值,GLSL 编译器会在编译时分配对应的索引值,那么外部如何获取呢?可以使用 glGetAttribLocation API。使用layout(location = 0)
在性能上稍微好一些,但是使用后者不在着色器中指定顶点属性的索引,那么用 glGetAttribLocation 的方式会更容易维护,因为这样就不用因为着色器修改了location而通知应用程序层要修改对应的location索引值。location 的有效数值范围在:0
到GL_MAX_VERTEX_ATTRIBS - 1
。 - 第二个参数 -
size
指定这个属性会有多少个分量,只能设置为1,2,3,4中的其中一个值。这个例子中我在着色器中使用的是vec3
类型的,也就是3个分量的,所以这里填入3
。 - 第三个参数 -
type
指定每个分量的数据类型,因为我们的顶点位置是需要小数的,这里我们使用GL_FLOAT
的浮点型。 - 第四个参数 -
normalized
指定没个分量值是否需要归一化到[-1,1]
(有符号)或是[0,1]
(无符号)。这里设置为GL_FALSE
,因为我们本身当做NDC space下的坐标来使用,本身就是归一化的。 - 第五个参数 -
stride
指定一个 顶点 需要的字节数。一个 顶点 可以包含 若干个属性,具体自己而定,这个例子中我们只有一个属性,就是 顶点坐标,而一个3D坐标我们在顶点着色器中使用了3个float来表达的,所以这里我们填入:sizeof(float) * 3
,其实就是12
,因为每个 float需要4个 byte。 - 第六个参数 -
pointers
指定这个属性在取顶点数组字节数据时,要从偏移多少个字节,它是void*
类型的,所以我们需要转换类型,我们不需要偏移,因为每个stride块的顶点属性0偏移开始的数据就是3个顶点坐标的数值,所以这里填入(void*)0
即可。
如果顶点着色器有设置 layout(location = 0)
,那么调用可以简化为:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
vertex attribute 即:顶点属性,而一个 attribute 块是属于某个顶点内的属性,一个顶点占用的字节数量通过 glVertexAttribPointer 中 stride 参数设置的,可参考下图:
上图只有一个 attribute: x,y,z 三个float分量的。
后面我测试为了理解更容易,我会再添加 attribute:r,g,b 也是三个float分量的。
开始绘制
- 顶点数据 OK
- 着色程序对象 OK
那么就可以开始绘制了
视口大小
但是在绘制之前,我们需要先设置好绘制的视口尺寸,视口大小我们想设置为窗口的大小,而要获取窗口大小,需要使用到 glfw 的Introduction to the API 在里面可以查找glfwGetFramebufferSize API。
有了窗口大小,再通过 glViewport API 设置视口大小:
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
清理缓存
绘制前需要清理上次绘制的颜色缓存,用到 glClear API:
glClear(GL_COLOR_BUFFER_BIT);
这样就清理了颜色缓存。
在 glViewport、glClear 上一篇也有用到,但由于篇幅原因,当时没有说明。
绘制
我们可以通过:glDrawArrays API
glDrawArrays(GL_TRIANGLES, 0, 3);
- 第一个参数 -
mode
绘制图元类型,可以有多种类型,这里使用GL_TRIANGLES
绘制三角形。 - 第二个参数 -
first
指定从当前绑定的缓存对象的数组数据中的第几个索引开始构建图元。这里从0
开始。 - 第三个参数 -
mode
要绘制的顶点索引的数量。这里绘制3
个点。
最后要记得调用 glfw 的API:glfwSwapBuffers、glfwPollEvents
前者交换窗口的前、后颜色缓存。
后者是处理glfw以外的系统消息。
glfwSwapBuffers(window);
glfwPollEvents();
绘制效果
完整代码
// jave.lin
#include"glad/glad.h"
#include"GLFW/glfw3.h"
#include<iostream>
#define PRINT_VERSION // 打印版本相关信息
#ifdef PRINT_VERSION
#define PROFILE_NAME_CORE "core"
#define PROFILE_NAME_COMPAT "compat"
static const char* get_api_name(int api) {
if (api == GLFW_OPENGL_API)
return "OpenGL";
else if (api == GLFW_OPENGL_ES_API)
return "OpenGL ES";
return "Unknown API";
}
static const char* get_profile_name_gl(GLint mask) {
if (mask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT)
return PROFILE_NAME_COMPAT;
if (mask & GL_CONTEXT_CORE_PROFILE_BIT)
return PROFILE_NAME_CORE;
return "unknown";
}
// 打印各种版本信息
static void print_infos(GLFWwindow* window) {
//
// ====== GLFW Library的版本 ======
//
int glfw_major, glfw_minor, glfw_revision;
glfwGetVersion(&glfw_major, &glfw_minor, &glfw_revision);
// 头文件声明版本
printf("GLFW header version: %u.%u.%u\n",
GLFW_VERSION_MAJOR,
GLFW_VERSION_MINOR,
GLFW_VERSION_REVISION);
// 库版本
printf("GLFW library version: %u.%u.%u\n", glfw_major, glfw_minor, glfw_revision);
// 库版本的字符串描述
printf("GLFW library version string: \"%s\"\n", glfwGetVersionString());
//
// ====== client, context, profile 的版本 ======
//
int ch, client, major, minor, revision, profile;
client = glfwGetWindowAttrib(window, GLFW_CLIENT_API);
major = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);
minor = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);
revision = glfwGetWindowAttrib(window, GLFW_CONTEXT_REVISION);
profile = glfwGetWindowAttrib(window, GLFW_OPENGL_PROFILE);
printf("%s context version string: \"%s\"\n",
get_api_name(client),
glGetString(GL_VERSION));
printf("%s context version parsed by GLFW: %u.%u.%u\n",
get_api_name(client),
major, minor, revision);
GLint mask;
glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &mask);
printf("%s profile mask (0x%08x): %s\n",
get_api_name(client),
mask,
get_profile_name_gl(mask));
//
// ====== render, vendor 的信息 ======
//
printf("%s context renderer string: \"%s\"\n",
get_api_name(client),
glGetString(GL_RENDERER));
printf("%s context vendor string: \"%s\"\n",
get_api_name(client),
glGetString(GL_VENDOR));
printf("%s context shading language version: \"%s\"\n",
get_api_name(client),
glGetString(GL_SHADING_LANGUAGE_VERSION));
}
#endif // PRINT_VERSION
static const char* vertex_shader_text =
"#version 450 compatibility\n"
"attribute vec3 vPos;\n"
"void main() {\n"
" gl_Position = vec4(vPos, 1.0);\n"
"}\n";
static const char* fragment_shader_text =
"#version 450 compatibility\n"
"void main() {\n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
float vertices[] = {
// x,y,z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
static void error_callback(int error, const char* description) {
fprintf(stderr, "ErrorCode : %d(0x%08x), Error: %s\n", error, error, description);
}
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { // 当键盘按键ESCAPE按下时,设置该window为:需要关闭
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
int main() {
glfwSetErrorCallback(error_callback); // 安装glfw内部错误时的回调
if (!glfwInit()) { // 初始化glfw
std::cout << "glfwInit FAILURE" << std::endl; // 初始化失败
exit(EXIT_FAILURE);
}
// 设置最低的openGL 版本,major:主版本号,minor:次版本号
// openGl 太低版本的话是不支持CORE Profile模式的
// 会报错:ErrorCode: 65540(0x00010004), Error : Context profiles are only defined for OpenGL version 3.2 and above
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// 根据上面的错误提示,至少使用3.2才行,这里我们使用4.5
//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// core profile 下运行有问题,不显示任何内容,但不会报错。
// 着色器编译、着色器程序链接都没有错误日志信息。
// 很有可能是因为我参考的学习网站使用的API相对比较老,使用的是3.3的。
//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 所以这里我们不设置 major, minor的版本,默认使用本计算机能用的最高版本
// 使用 compatibility profile 就有内容出现了。
int width = 640;
int height = 480;
// 使用glfw创建窗体
GLFWwindow* window = glfwCreateWindow(width, height, "jave.lin - Learning OpenGL - 02 DrawTriangle", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl; // 构建窗体失败
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, key_callback); // 安装glfw内部键盘按键的回调
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { // 装载OpenGL的C函数库
std::cout << "Failed to initialize OpenGL context" << std::endl; // 装载报错
glfwTerminate();
exit(EXIT_FAILURE);
}
#ifdef PRINT_VERSION
// 打印版本信息
print_infos(window);
#endif
GLint vpos_location;
GLuint vertex_buffer, vertex_shader, fragment_shader, program;
GLint success, infoLogLen;
glGenBuffers(1, &vertex_buffer); // 创建 VBO
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); // 绑定 VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置 VBO 数据
vertex_shader = glCreateShader(GL_VERTEX_SHADER); // 创建 顶点着色器
glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL); // 设置 顶点着色器源码
glCompileShader(vertex_shader); // 编译 顶点着色器
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success); // 获取着色器编译状态
if (!success) { // 如果编译不成功
glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(vertex_shader, infoLogLen, NULL, infoLog);
std::cout << "Vertex Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); // 创建 片元着色器
glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL); // 设置 片元着色器源码
glCompileShader(fragment_shader); // 编译 片元着色器
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success); // 获取着色器编译状态
if (!success) { // 如果编译不成功
glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果编译失败,则将编译日志储存到:infoLog 中
glGetShaderInfoLog(fragment_shader, infoLogLen, NULL, infoLog);
std::cout << "Fragment Shader Compiling Error Status: " // 输出编译错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
program = glCreateProgram(); // 创建着色器程序
glAttachShader(program, vertex_shader); // 附加 顶点着色器
glAttachShader(program, fragment_shader); // 附加 片元着色器
glLinkProgram(program); // 链接着色器程序
glGetProgramiv(program, GL_LINK_STATUS, &success); // 如果链接不成功
if (!success) {
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLen);
GLchar* infoLog = (GLchar*)malloc(infoLogLen); // 如果链接失败,则将链接日志储存到:infoLog 中
glGetProgramInfoLog(program, infoLogLen, NULL, infoLog);
std::cout << "Program Linking Error Status: " // 输出链接错误日志信息
<< success << ", Infomation Log : " << infoLog << std::endl;
free(infoLog);
exit(EXIT_FAILURE);
}
glDeleteShader(vertex_shader); // 链接成功后,就可以删掉vertex 子程序了
glDeleteShader(fragment_shader); // 链接成功后,就可以删掉fragment 子程序了
vpos_location = glGetAttribLocation(program, "vPos"); // 获取 顶点着色器中的顶点 attribute 属性的 location
glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE, // 设置 顶点属性格式
sizeof(float) * 3, (void*)0);
glEnableVertexAttribArray(vpos_location); // 启用 顶点缓存 location 位置的属性
while (!glfwWindowShouldClose(window)) { // 检测是否需要关闭窗体
glfwGetFramebufferSize(window, &width, &height); // 获取窗口大小
glViewport(0, 0, width, height); // 设置Viewport
glClearColor(0.1f, 0.2f, 0.1f, 0.f); // 设置清理颜色缓存时,填充颜色值
glClear(GL_COLOR_BUFFER_BIT); // 清理颜色缓存
glUseProgram(program); // 使用此着色器程序
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角图元,从0号顶点,开始绘制3个顶点
glfwSwapBuffers(window); // swap buffer, from backbuffer to front buffer
glfwPollEvents(); // 处理其他的系统消息
}
glfwDestroyWindow(window); // 销毁之前创建的window对象
glfwTerminate(); // 清理glfw之前申请的资源
return 0;
} // int main() {