OpenGL-01 入门
一、OpenGL
1. 核心模式与立即渲染模式
- 早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),OpenGL的大多数功能都被库隐藏起来。
- 从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发。
2. 扩展Extension
当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行,开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式,开发者不必等待一个新的OpenGL规范面世,就可以使用这些新的渲染特性了,只需要简单地检查一下显卡是否支持此扩展。
if(GL_ARB_extension_name)
{
// 使用硬件支持的全新的现代特性
}
else
{
// 不支持此扩展: 用旧的方式去做
}
3. 状态机
- OpenGL的状态通常被称为OpenGL上下文(Context)
- 状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作
4. 对象
- 可以看作C风格结构体
struct object_name {
float option1;
int option2;
char[] name;
};
- 一个例子
// OpenGL的状态
struct OpenGL_Context {
...
object* object_Window_Target;
...
};
// 创建对象
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);
- 好处在于,可以定义多个对象,有选择地绑定,不需要重复设置选项。
二、创建窗口
- 首先要做的就是创建一个OpenGL上下文(Context)和一个用于显示的窗口。
1. GLFW
GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入。
2. CMAKE
- CMake需要一个源代码目录和一个存放编译结果的目标文件目录。源代码目录我们选择GLFW的源代码的根目录,然后我们新建一个 build 文件夹,选中作为目标目录。
- 在设置完源代码目录和目标目录之后,点击Configure(设置)按钮,让CMake读取设置和源代码。
- 选择工程的生成器
- CMake会显示可选的编译选项用来配置最终生成的库。这里我们使用默认设置,并再次点击Configure(设置)按钮保存设置。
- 保存之后,点击Generate(生成)按钮,生成的工程文件会在你的build文件夹中。
3. 编译
- 在build文件夹里可以找到GLFW.sln文件,用Visual Studio 2019打开。因为CMake已经配置好了项目,并按照默认配置将其编译为64位的库,所以我们直接点击Build Solution(生成解决方案)按钮,然后在build/src/Debug文件夹内就会出现我们编译出的库文件glfw3.lib。
- 建立一个新的目录里面包含Libs和Include文件夹,包含所有的第三方库文件和头文件,并且在你的IDE或编译器中指定这些文件夹。
4. 链接
- 添加incluce目录与lib目录:VC++ Direction->Library Directions or Include Directions
- 链接lib文件,Linker->Additional Depedences;除了glfw3.lib之外,还有opengl32.lib。
- 添加头文件
#include <GLFE\glfw3.h>
5. GLAD
- 因为OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。
// 定义函数原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函数并赋值给函数指针
GL_GENBUFFERS glGenBuffers = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 现在函数可以被正常调用了
GLuint buffer;
glGenBuffers(1, &buffer);
- GLAD是一个开源的库,它能解决我们上面提到的那个繁琐的问题。
- 打开GLAD的在线服务,将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本(我们的教程中将使用3.3版本,但更新的版本也能用)。之后将模式(Profile)设置为Core,并且保证选中了生成加载器(Generate a loader)选项。现在可以先(暂时)忽略扩展(Extensions)中的内容。都选择完之后,点击生成(Generate)按钮来生成库文件。
- GLAD现在应该提供给你了一个zip压缩文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。
- 包含头文件
#include <glad/glad.h>
三、你好,窗口
1. 头文件
- 请确认是在包含GLFW的头文件之前包含了GLAD的头文件。GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
2. GLFW窗口初始化
- 初始化GLFW窗口:
- 首先,我们在main函数中调用glfwInit函数来初始化GLFW,然后我们可以使用glfwWindowHint函数来配置GLFW。glfwWindowHint函数的第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;第二个参数接受一个整型,用来设置这个选项的值。选项与值在这里。
- 将主版本号(Major)和次版本号(Minor)都设为3
- 明确告诉GLFW我们使用的是核心模式(Core-profile)。明确告诉GLFW我们需要使用核心模式意味着我们只能使用OpenGL功能的一个子集(没有我们已不再需要的向后兼容特性)
- 如果使用的是Mac OS X系统,你还需要加下面这行代码到你的初始化代码中这些配置才能起作用(将上面的代码解除注释)
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
return 0;
}
- 创建窗口对象
- glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数。第三个参数表示这个窗口的名称(标题),这里我们使用"LearnOpenGL",当然你也可以使用你喜欢的名称。最后两个参数我们暂时忽略。这个函数将会返回一个GLFWwindow对象,我们会在其它的GLFW操作中使用到。
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
3. GLAD初始化
- GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。
- 我们给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数。GLFW给我们的是glfwGetProcAddress,它根据我们编译的系统定义了正确的函数。
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
4. 视口
- 告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。
- glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。以将视口的维度设置为比GLFW的维度小,只显示一部分。
- OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。
glViewport(0, 0, 800, 600);
- 当用户改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。这个回调函数的原型如下:
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
- 这个帧缓冲大小函数需要一个GLFWwindow作为它的第一个参数,以及两个整数表示窗口的新维度。每当窗口改变大小,GLFW会调用这个函数并填充相应的参数供你处理。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
- 注册这个函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
- 当窗口被第一次显示的时候framebuffer_size_callback也会被调用。对于视网膜(Retina)显示屏,width和height都会明显比原输入值更高一点。
5. 准备好你的引擎
- 渲染循环(Render Loop):让GLFW退出前一直保持运行,在我们主动关闭它之前不断绘制图像并能够接受用户输入。
- glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后渲染循环便结束了,之后为我们就可以关闭应用程序了。
- glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。
- glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。
- 双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
while(!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
6. 最后一件事
- 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。
glfwTerminate();
return 0;
7. 输入
- GLFW的glfwGetKey函数,它需要一个窗口以及一个按键作为输入。这个函数将会返回这个按键是否正在被按下。
- 这里我们检查用户是否按下了返回键(Esc)(如果没有按下,glfwGetKey将会返回GLFW_RELEASE。如果被按下了,将会返回GLFW_PRESS。
- 如果用户的确按下了返回键,我们将通过glfwSetwindowShouldClose使用把WindowShouldClose属性设置为 true的方法关闭GLFW。下一次while循环的条件检测将会失败,程序将会关闭。
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
8. 渲染
- 调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。由于现在我们只关心颜色值,所以我们只清空颜色缓冲。
- 除了glClear之外,我们还调用了glClearColor来设置清空屏幕所用的颜色。当调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
- glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
四、你好,三角形
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO
- 渲染管线:
1. VBO
- 标准化设备坐标(Normalized Device Coordinates):3个轴(x、y和z)上-1.0到1.0的范围内;OpenGL仅仅处理标准化设备坐标,此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。在给出顶点属性时,没必要给出标准设备空间的坐标,因为在顶点着色器中会进行坐标变换,但是此时还未开展坐标变换的部分,为此,给出标准设备空间下的坐标。
1.1 glGenBuffers
- 首先需要生成一个buffer,在这一步我们申请一个或者多个buffer,并得到该buffer的id。这一步对不同类型的buffer来说并无区别。
- buffer的id是unsigned int。
glGenBuffers
的第一个参数是生成buffer的数量,第二个参数是对id的返回值。
unsigned int VBO;
glGenBuffers(1,&VBO);
1.2 glBindBuffer
- 然后,将得到的buffer的id绑定到某一种类型的buffer,该id对应的buffer就会得到当前上下文中对该类型buffer的操作。
- glBindBuffer的第一个参数是目标buffer类型,第二个参数是需要绑定的buffer的id。
glBindBuffer(GL_ARRAY_BUFFER,VBO);
- VBO属于GL_ARRAY_BUFFER。
1.3 glBufferData
- 当某种类型的buffer,如GL_ARRAY_BUFFER,被绑定到某个buffer上之后,可以对GL_ARRAY_BUFFER进行操作,相当于对该id对应的buffer空间进行操作。
- glBufferData就是对该buffer的空间进行数据转移,从cpu中转移到gpu中。
- 第一个参数是buffer类型,再绑定之后就是对VBO这一个GPU缓冲做操作,第二个参数是cpu数据的大小,第三个参数是cpu数据,也就是我们在cpu中定义而开辟空间并初始化的数据,第四个参数是GPU数据管理的方式,跟数据更新的要求有关,这涉及到GPU会将数据分配到哪一级的存储空间中,对于不需要更新的数据就会被分配到吞吐量比较小的地方。
glBufferData(GL_ARRAY_BUFFER,sizeof(verticles),verticles,GL_STATIC_DRAW);
- 数据管理方式:
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
2. Shader
2.1 Vertex Shader
- 一个shader首先要说明OpenGL版本和模式,然后是输出输入变量的定义,此处使用了layout(location=0)表明这个输入是通过缓冲buffer得到的,并且其输入编号是0,用于匹配缓冲。
#version 330 core
layout(location=0) in vec3 aPos;
void main()
{
gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0);
}
2.2 Compile
- 首先,需要获得shader,并将其转化为C风格字符串。
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";
- 然后,需要创建一个VertexShader。
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 然后,将源码附在shader上,并且编译他
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);
glCompileShader(vertexShader);
- 检查编译
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;
}
2.3 Fragment Shader
#version 330 core
out vec4 fragColor;
void main()
{
fragColor=vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
const char* fragmentShaderSource=
"#version 330 core\n"
"out vec4 fragColor;\n"
"\n"
"void main()\n"
"{\n"
" fragColor=vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL);
glCompileShader(fragmentShader);
2.4 Shader Program
- 创建program
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
- 链接shader
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
- 检查链接
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::PROGRAM::LINKN_FAILED\n" << infoLog << std::endl;
}
- 使用某个program,在当下上下文中,该program被当作渲染程序。
glUseProgram(shaderProgram);
- 删除shader,在链接完成之后就可以删掉了。
glDeleteShader(vertexshader);
glDeleteShader(fragmentShader);
3. 链接顶点属性
- 保存在buffer中的VBO,还没有实现被利用起来。因此,我们需要说明VBO中的数据的含义,并将其与Vertexshader中的输入对应起来。
- glVertexAttribPointer:第一个参数是在vertexShaser中的对应输入的lacation,第二个和第三个参数说明组成该变量的元素数量(即vec维度)以及基本元素的数据类型,第四个参数说明是否要标准化,第五个参数说明步长,即两个变量在buffer中的字节间隔,最后一个参数说明该变量在buffer中的偏置,需要转换成void*类型。
- 然后,使用glEnableVertexAttribArray使能。
- glVertexAttribPointer是对当前绑定GL_ARRAY_BUFFER的缓冲即VBO进行操作的。也是一个状态操作函数。但是对shaderProgram没有绑定。
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
- 绘制的大致流程如下
- 绑定buffer
- 转移数据到Buffer中
- 解释该buffer中的数据
- 使能
- 使用某一个shader program,自然的采用buffer中的数据作为vertexshader的输入
- 绘制
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(verticals),verticals,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glUseProgram(shaderProgram);
4. VAO
- 从前面可以看到,当我们替换不同的VBO时,需要重新说明buffer的组成并使能,才能让program得到合理的输入,这很繁琐;因此,使用VAO将glVertexAttribPointer配置的顶点属性以及其对应的VBO联系起来,同时保存对glEnableVertexAttribArray和glDisableVertexAttribArray的调用,则直接绑定某个VAO就可以实现上面的繁琐的过程。
- glGenVertexArrays
unsigned int VAO;
glGenVertexArrays(1,&VAO);
- glBindVertexArray:绑定VAO,然后后续的对VBO的操作就会被记录在VAO中,以后对相应的对VBO的bind、data、pointer、enable就可以通过直接绑定该VAO就可以实现了。
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(verticals),verticals,GL_STATIC_DRAW);
glVertexArribPointer(0,3,GL_FLOAT,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
//...
glUseProgram(shaderProgram);
glBindVertexrray(VAO);
5. 绘制
- 绑定VAO获得相应的VBO以及buffer数据解释,并且使用Program,基于此使用gDrawArrays来绘制图形。第一个参数说明目标图形是什么,第二个参数说明顶点数组的起始索引位置,第二个参数说明绘制几个顶点(对应的图形所需的顶点的倍数才会被全部绘制,否则只会绘制前面的图形)。
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES,0,3);
6. EBO
- 对于节点属性,不同图形之间存在着大量的共用节点,因此全部转移到GPU中会占据大量显存,为此在VBO中只保存不重复的节点属性,然后利用EBO索引来排列节点属性,此时相应的glVertexAttibPointer会按照EBO的索引顺序来解释VBO中的数据。
- 定义EBO,EBO与VBO一样都是缓冲buffer
unsigned int EBO;
glGenBuffer(1,&EBO);
- 绑定EBO,绑定EBO之后,EBO(GL_ELEMENT_ARRAY_BUFFER)的索引会用来解释相同上下文中的VBO(GL_ARRAY_BUFFER),即在上下文环境中,GL_ELEMENT_ARRAY_BUFFER与GL_ARRAY_BUFFER是匹配的。同样的需要将数据转移到GPU中。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
- 在使用EBO而非VBO时,在绘制时需要使用glDrawElements。同样的,在绘制之前,不仅需要进行VBO相关的操作,还要绑定相应的EBO。第一个参数同样是图形类型,第二个参数是绘制的顶点数量,第四个参数是偏置,这些与glDrawArrays类似,此外,第三个参数是索引类型。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
- 可见,重复操作VBO和EBO非常繁琐,因此,EBO的绑定和数据传输同样可以被记录在相同语境下的VAO中。但是要注意的是VAO只会记录其语境下的最后一个EBO,因此如果EBO被解绑就不会被记录了。此时,VAO语境下的操作就是VBO的bind与data传输、EBO的bind与data传输,最后基于EBO索引VBO并解释VBObuffer的数据,最后使能,这些会被记录在VAO中。
- 相应的,在绘制glDrawElements之前,只需要绑定VAO就可以了,此外,在此之后只需要解绑VAO就可以了,然后绘制其他图形。
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
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(float), (void*)0);
glEnableVertexAttribArray(0);
//...
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
glBindVertexArray(0);
五、着色器
1. uniform
- uniform变量是全局变量,即将CPU中的数据传送到GPU中。uniform可以放在任何一个shader中,因此不必像节点属性一样通过vertex shader来传输。如下,在片段着色器中定义了uniform变量。
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
- glGetUniformLocation:通过特定的shaderprogram的id以及其中的uniform变量名(字符串)来获得uniform变量的位置location。
- glUniform4f:通过unigorm变量的location,来将数据传入到GPU中,第一个参数是location,后续参数是变量的内容,对于glUniform4f来说,就是传入了四个float变量。相应的,当uniform的数据类型发生改变时就需要改变函数名称,这大概是因为OpenGL是比较接近C风格的C++,因此没有使用函数重载。
- 一个重要的地方是,glGetUniforLocation的返回值如果是-1,则意味着没有找到该uniform变量,因此,需要将uniformLocationID定义为int类型,而非unsigned int类型。
- 需要注意的是,glGetUniformLocation只需要提供program就可以了,并不需要program被use,但是glUniform是需要program被use的,即glUniform是对当前上下文中的program做操作的,相应的可以看出它不需要program的id作为参数。
float timeVaule=glfwGetTime();
float greenColor=sin(timeValue)/2.0f+0.5f;
int vertexColorLocation=glGetUniformLocation(shaderProgram,"ourColor");
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);
2. 多个顶点属性
- 当存在多个顶点属性时,一来在定义vertices数组时一般采取ABCABCABC的方式来保存属性,当然采用AAABBBCCC的方式也可以,这需要根据情况调整glVertexAttibPointer,二来在vertexShader中需要根据位置来定义不同的属性,三来,对于每一个属性都要进行glVertexAttibPointer来说明,并且做glEnableVertexAttribArray来使能。
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
3. 着色器类
- 在shader.h中实现着色器类。在构造函数中实现shader program的创建以及一系列操作,并将shader program 的id保存在ID成员中。此外,将着色器程序的use也封装成成员函数,并且将uniform也封装进去。
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const char* vertexPath, const char* fragmentPath);
// 使用/激活程序
void use();
// uniform工具函数
void setBool(const std::string& name, bool value) const;
void setInt(const std::string& name, int value) const;
void setFloat(const std::string& name, float value) const;
};
Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常:
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件的缓冲内容到数据流中
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 关闭文件处理器
vShaderFile.close();
fShaderFile.close();
// 转换数据流到string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
// 2. 编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片段着色器
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
// 打印编译错误(如果有的话)
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
}
void Shader::use()
{
glUseProgram(ID);
}
void Shader::setBool(const std::string& name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void Shader::setInt(const std::string& name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::setFloat(const std::string& name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
#endif
- 相应的,修改绘制
ourShader.use();
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
ourShader.setVec4("ourColor", 0.0f, greenValue, 0.0f, 1.0f);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
六、纹理
1. 纹理环绕
- 纹理坐标的范围是[0,1]^2。因此,超出范围的坐标查询到的纹理需要用到纹理环绕。
- 环绕方式有:
- GL_REPEAT
- GL_MIRRORED_REPEAT
- GL_CLAMP_TO_EDGE
- GL_CLAMP_TO_BORDER
- glTexParameteri:第一个参数是纹理目标,也就是对应上下文中的纹理实体;第二个参数是纹理的设置目标参数,此处是环绕方式,对于2D纹理是两个坐标st,三维纹理还有str,第三个参数是设置的模式,此处是镜像环绕。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_MIRRORED_REOEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_MIRRORED_REOEAT);
- 当采取GL_CLAMP_TO_BORDER时,还需要设置边界颜色参数
float borderColor[]={1.0f,1.0f,0.0f,1.0f};
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,borderColor);
2. 纹理过滤
2.1 GL_LINEAR GL_NEAREST
- 当纹理放大时MAG,可能分辨率会不够,因此需要linear,当纹理缩小时MIN,可能会产生纹理采样噪声,因此需要mipmap,如果不做处理则为nearest,即就近采样
glTextureParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,FL_NEAREST);
glTextureParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
2.2 MipMap
- 对于纹理缩小的情况,其filter可以采取mipmap,如下第一个方式表示纹理内部的filter,第二个方式指的是mipmap之间的采样方式。如果都是linear便是三线性插值。
- 相应的,纹理放大不应该使用mipmap
- GL_NEAREST_MIPMAP_NEAREST
- GL_NEAREST_MIPMAP_LINEAR
- GL_LINEAR_MIPMAP_NEAREST
- GL_LINEAR_MIPMAP_LINEAR
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
3. 纹理加载
- 使用stb_image.h来加载图片,自然可以来加载纹理
- 将文件stb_image.h加入项目中,并另外在一个头文件中加入:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
- 在加载图形之前需要调换图像的y上下方向,这是因为图像从左上角开始,而纹理从左下角开始。
stbi_set_flip_vertically_on_load(true);
- stbi_load:返回字符串,第一个参数是图片地址,第二三四个参数是图片返回的宽度、高度、以及颜色通道数
int width,height,nrChannels;
unsigned char* data=stbi_load("some.jpg",&eidth,&height,&nrChannel,0):
4.生成纹理
4.1 glGenTextures
- 与其他对象的生成类似
unsigned int texture;
glGenTextures(1,&texture);
4.2 glBindTexture
- 同样类似,用于将特定的纹理绑定为当下语境中的某种纹理,此处第一个参数是纹理类型,这里是GL_TEXTURE_2D
glBindTexture(GL_TEXTURE_2D,texture);
4.3 glTexImage2D
- glTexImage2D:用于加载图像数据到当前语境中的某种纹理上面,第一个参数是纹理类型,对应着绑定到该类型纹理上的纹理实体,第二个参数是mipmap层级,如果设置非零值则会将mipmap的相应层级绑定到这个纹理上,否则绑定原图,第三、四、五个参数是生成的纹理的通道类型以及宽高,第六个参数设置为0.第七、八个参数是原图数据的通道类型以及数据存储方式,在上文中被保存为unsigned char*,所以数据类型是无符号字符/字节的数组,最后一个参数是CPU中保存的原图像加载后的数据。
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);
- 可以使用glTexImage2D实现某种层级的mipmap,但是可以使用glGenerateMipmap,其参数是某种类型的纹理。
glGenerateMipmap(GL_TEXTURE_2D);
- 在纹理被生成之后,图像数据就可以被删掉了。
stbi_image_free(data);
4.4 流程
unsigned int texture;
glGenTextures(1,&texture);
glBindTexture(GL_TEXTURE_2D,texture);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
int width,height,nrChannels;
unsigned char* data = stbi_load("som.jpg",&width,&height,&nrChannels,0);
if(data)
{
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout<<"Failed to load texture"<<std::endl;
}
stbi_image_free(data);
5. 应用纹理
5.1 纹理坐标
- 相应的,在顶点属性中需要加入纹理坐标属性
float vertices[]={
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
- 同时修改顶点属性的指针函数
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
5.2 shader
- 相应的在vertex shader中加入一个新的vec2输入,即纹理坐标;同时在vertex shader中加入纹理坐标的输出,并在fragment shader中加入纹理坐标的输入;此外,在fragment shader中加入纹理采样器sampler2D,这是一个全局变量uniform。
- 在fragment中,通过texture函数来查找纹理,其第一个参数是对应的sampler2D变量,第二个参数是纹理坐标,是一个vec2变量,返回值是vec4的颜色。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
5.3 纹理绑定与激活
- 在得到了纹理的实例,并且在shader中加入了纹理采样器,且进行了纹理查找;此时,还需要最后一步,将纹理与program中的采样器对应起来
- 当program中只有一个sampler2D,其默认编号是0,当我们绑定一个GL_TEXTURE_2D时,当下语境中就会默认的将该纹理传输到对应的采样其中。然后就可以绘制了。
glBindTexture(GL_TEXTURE_2D,texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_BYTE,0);
- 但是,大多数情况下纹理不止一个,因此就需要用纹理单元。在绑定纹理之前,首先使用glActiveTexture激活纹理单元,其参数是GL_TEXTURE0以及后续的其他值,共16个纹理单元。激活之后,再去绑定纹理,就会将该纹理映射到这一个纹理单元上,因此,可以不断地通过激活一个单元并绑定一个纹理,实现通过纹理单元来记录多个纹理。此外,还需要将program中的采样器映射到对应的纹理单元上,具体的方法就是使用glUniform1i来对对应的采样器传输对应纹理单元的整形编号。这样以来,就可以将纹理与采样器通过纹理单元联系起来了。显然的是,glUniform1i之前需要use对应的program。
- 一个问题在于,当改变program时,对应的纹理可能会变化,所以需要对每一次use一个program时,都要重新激活纹理单元并绑定纹理,此外还需要为该program中的纹理采样器赋值。
- 因此,一个绘制流程就会变成:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,texture2);
ourShader.use();
ourShader.setInt("texture1",0);
ourShader.setInt("texture2",1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_BYTE,0);
glBindVertexArray(0);
七、变换
1. GLM
- 下载地址.
- 头文件
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
2. 变换矩阵
2.1 translate
- glm::vec4用于定义四维向量,glm::mat4用于定义四维方阵,在0.9.9以下的版本中默认初始化为单位阵。
- glm::translate函数用于生成位移矩阵并右乘在旧的变换矩阵上,第一个参数是旧的变换矩阵,第二个参数是glm:vec3类型的向量,表示位移。
- 因此,glm的变换矩阵越是在后面生成,则越先计算
glm::vec4 vec(1.0f,0.0f,0.0f,1.0f);
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f,1.0f,0.0f));
vec=trans*vec;
2.2 rotate scale
- glm::rotate用于生成旋转变换矩阵并右乘,第二个参数是旋转角度,是弧度制,可以通过glm::radians转换,第三个参数是旋转轴,也是一个glm::vec3。
- glm::scale用于生成缩放变换矩阵并右乘,第二个参数是缩放的倍数,也是一个glm::vec3类型。
trans = glm::rotate(trans,glm::radians(90.0f),glm::vec3(0.0,0.0,1.0));
trans=glm::scale(trans,glm::vec3(0.5,0.5,0.5));
3. shader
- 定义mat4类型的transform矩阵,并将其定义为uniform类型,从CPU中传输数据。将其定义在顶点着色器中,并将局部坐标转换到世界坐标。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
4.uniform
- 使用glUniformMatrix4fv来将glm::mat4传输到shader中,注意glm::mat4是glm自定义的类型,不是内置类型,所以要使用glm::value_ptr来转换。
- glUniformMatrix4fv的第一个参数和第四个参数与之前的glUniform相似,而第二个参数是矩阵的数量,第三个参数是是否转置,这个参数是因为GPU中的矩阵排布是列主序,然而glm的矩阵排序也是列主序,所以不需要转置。
unsigned int transformLoc = glGetUniformLocaion(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc,1,GL_FALSE,glm::value_ptr(trans));
八、坐标系统
1. MVP
1.1 Model Marix
- model matrix将物体从局部坐标(由顶点属性定义的坐标)变换到世界坐标
- 因此,model matrix包含平移、旋转和缩放,可以使用之前的方法来获得。
1.2 View Matrix
- view matrix将物体从世界坐标变换到观察空间,实际上是摄像机在世界空间中的变换的反变换
- 因此,view matrix同样可以使用变换矩阵的方法得到
- 注意,OpenGL中使用右手坐标系,并且摄像机看向-z方向。
1.3 Projection Matrix
- 投影矩阵将物体从观察空间变换到裁剪空间
- 正交投影将一个立方体空间的平截头体变换成标准设备空间
- 透射投影将一个视锥的平截头体变换成标准设备空间
- 他们可以使用glm::ortho或者glm::perspective来得到。ortho的六个参数分别是立方体空间的左右下上近远边界。perspective的四个参数分别是弧度制的视角、宽高比、近、远。
- 需要注意的是,glm::ortho与glm::perspective的宽高与视口的宽高并没有直接的关联,因为视口空间是从裁剪空间的[-1,1]^2变换得到的,而ortho的宽高参数是世界坐标,perspective的宽高比也是世界坐标的宽高比,但是为了不发生图像变形,应当保持ortho或perspective的宽高比与视口的宽高比一致。
glm::mat4 proj = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
glm::mat4 proj = glm::perspective(glm::radians(90.0f),(float)width/(float)height,0.1f,100.0f);
1.4 MVP变换
- 注意变换顺序:
\(V_{clip}=M_{projection}\cdot M_{view}\cdot M_{model}\cdot V_{local}\) - 经过MVP变换之后,OpenGL会将裁剪空间基于glViewPort的参数变换到视口空间。
- 画个图
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, 0.5f,-0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f,-0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, -0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
0.5f, -0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, -0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 左上
};
unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3, // 第二个三角形
4, 5, 7,
5, 6, 7,
8, 9, 11,
9, 10, 11,
12 ,13, 15,
13, 14, 15,
16, 17, 19,
17, 18, 19,
20, 21, 23,
21, 22, 23
};
//...
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
2. Z-Buffer
- glEnable与glDisable可以用于开启或者关闭深度测试
glEnable(GL_DEPTH_TEST);
- 深度测试实际上是将深度信息写入深度缓冲中,因此,在刷新每一帧时,跟颜色缓冲一样,需要clear。
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
3.绘制多个物体
- 对同一个物体进行多次绘制时,要注意到这些被绘制出来的物体共享VBO、VAO、EBO信息,他们的区别只在于Model Matrix不同,因此只需要绑定一次VAO,并且由于他们的绘制program相同,所以也只需要use一次,但是需要改变他们的Model matrix,并使用glDrawElements绘制多次。
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3(2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3(1.3f, -2.0f, -2.5f),
glm::vec3(1.5f, 2.0f, -2.5f),
glm::vec3(1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
//...
glBindVertexArray(VAO);
glm::mat4 view;
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
ourShader.setMat4("view", glm::value_ptr(view));
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH/(float)SCR_HEIGHT, 0.1f, 100.0f);
ourShader.setMat4("projection", glm::value_ptr(projection));
for (int i = 0; i != 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * (i+1);
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(angle), glm::vec3(0.0f, 1.0f, 1.0f));
ourShader.setMat4("model", glm::value_ptr(model));
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}
glBindVertexArray(0);
九、摄像机
1. glm::LookAt
- 之前通过变换矩阵来实现view matrix,但是这并不方便,因为忽视了渲染对camera的需求。
- 实际上,一个camera可以通过位置、观察方向(或者观察位置)以及上方向三个参数来定义。这便是glm::lookAt的三个参数。
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
- 具体的思路是通过摄像机位置与观察位置获得观察方向矢量(-w),通过上矢量与观察方向矢量做叉积获得右矢量(u),通过右矢量与观察位置矢量做叉积获得上矢量(v),即得到观察空间的基矢量。
- 一般的,上矢量可以取(0,1,0),表示观察者与y轴同向,不存在左右的倾斜。
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
2. 自由移动
- 自由移动就是通过对按键的监控来调整camera的位置。
- 首先,按键检测通过glfwGetKey函数与回调函数实现,在前后移动中通过加减观察矢量实现,在左右移动中则基于观察适量与上矢量获得左或者右的单位矢量并做加减来实现,相应的可以给予一个移动位移乘在这一系列单位矢量上,而移动位移可以通过移动速度参数乘上帧间时间来实现,可以避免帧率变化造成的移动步长变化。
- 注意此处,该函数中需要用到camera的三个参数,以及帧间时间,因此需要定义为全局变量。
//全局变量
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
float lastTime = 0.0f;
float deltaTime = 0.0f;
//渲染循环
float currentTime = glfwGetTime();
deltaTime = currentTime - lastTime;
lastTime = currentTime;
//函数定义
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
float cameraSpeed = 2.5f*deltaTime;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos += cameraSpeed * glm::normalize(glm::cross(cameraUp,cameraFront));
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos -= cameraSpeed * glm::normalize(glm::cross(cameraUp, cameraFront));
}
3. 视角移动
- 三种欧拉角:俯仰角、偏航角、滚转角
- 视角移动就是通过检查鼠标的位置来改变cameraFront。一般来说就是鼠标左移会造成视角左转,上移造成上仰。具体的实现方式就是,通过鼠标的偏移来对俯仰角和偏航角进行累积。进一步的,可以将观察向量用这两个角度表示成一个单位矢量。
- 需要注意的是:如果第一帧的lastX与lastY使用初始化的值,那么该帧就会产生一个未知的偏移,因此维护一个全局布尔变量用来表示第一帧,在第一帧中,将这两个值赋值为该帧的输入值,即该帧不做变化;相应的,lastX、lastY、pitch、yaw这四个变量是在不同帧之间传输的,所以使用全局变量。此外,鼠标光标的位置中,y轴是上小下大的,而x轴是左小右大的,因此这两个偏置的做差方向相反。此外,cameraFront作为函数返回信息也需要是全局变量。最后,可以给pitch设置阈值。
- 对鼠标光标的监控是通过glfwSetCursorPosCallback实现的。
- 此外,glfwSetInputMode可以实现将光标隐藏并且限制在窗口范围内。
glfwSetInputMode(window,GLFW_CURSOR,GLFW_CURSOR_DISABLED);
//全局变量
double lastX, lastY;
float yaw(-90.0f), pitch(0.0f);
bool firstMouse;
//回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = (float)(xpos - lastX);
float yoffset = (float)(lastY - ypos);
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if (pitch > 89.0f)
pitch = 89.0f;
else if (pitch < -89.0f)
pitch = -89.0f;
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = front;
}
4. 缩放
- 缩放是通过检查滑轮的移动来改变fov,进而改变投影矩阵的。
- 检查滑轮的是glfwSetScrollCallback.
- 相应的,fov也是全局变量。
//全局变量
float fov = 45.0f;
//回调函数
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
float speed = 3.0f;
if (fov >= 1.0f && fov <= 90.0f)
fov -= (float)yoffset*speed;
if (fov < 1.0f)
fov = 1.0f;
if (fov > 90.0f)
fov = 90.0f;
}
5.摄像机类
#ifndef CAMERA_H
#define CAMERA_H
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
enum Camera_Movement {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
const float YAW = 90.0f;
const float PITCH = 0.0f;
const float FOV = 45.0f;
const float MOVESPEED = 2.5f;
const float SENSITIVITY = 0.05f;
const float FOVSPEED = 3.0f;
class Camera {
public:
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 front = glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3 worldUp = glm::vec3(0.0f, 1.0f, 0.0f))
: Pitch(PITCH), Yaw(YAW), Fov(FOV), MoveSpeed(MOVESPEED), Sensitivity(SENSITIVITY),FovSpeed(FOVSPEED)
{
Position = position;
Front = front;
WorldUp = worldUp;
CoordUpdate();
}
void PositionMove(Camera_Movement direction, double deltaTime);
void ViewMove(double xoffset, double yoffset, bool pitchConstrain = true, float pitchUp = 89.0f, float pitchDown = -89.0f);
void FovMove(double offset);
glm::mat4 ViewMatrix() const;
public:
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
float Fov;
float Yaw;
float Pitch;
float MoveSpeed;
float FovSpeed;
float Sensitivity;
private:
void CoordUpdate();
};
void Camera::PositionMove(Camera_Movement direction, double deltaTime)
{
float moveLength = MoveSpeed * (float)deltaTime;
if (direction==FORWARD)
Position+= moveLength * Front;
if (direction == BACKWARD)
Position -= moveLength * Front;
if (direction == LEFT)
Position -= moveLength * Right;
if (direction == RIGHT)
Position += moveLength * Right;
}
void Camera::ViewMove(double xoffset, double yoffset, bool pitchConstrain, float pitchUp, float pitchDown)
{
xoffset *= Sensitivity;
yoffset *= Sensitivity;
Yaw += (float)xoffset;
Pitch += (float)yoffset;
if (pitchConstrain)
{
if (Pitch > pitchUp)
Pitch = pitchUp;
else if (Pitch < pitchDown)
Pitch = pitchDown;
}
glm::vec3 front;
front.x = cos(glm::radians(Pitch)) * cos(glm::radians(Yaw));
front.y = sin(glm::radians(Pitch));
front.z = cos(glm::radians(Pitch)) * sin(glm::radians(Yaw));
Front = front;
CoordUpdate();
}
void Camera::CoordUpdate()
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
void Camera::FovMove(double offset)
{
if (Fov >= 1.0f && Fov <= 90.0f)
Fov -= (float)offset * FovSpeed;
if (Fov < 1.0f)
Fov = 1.0f;
if (Fov > 90.0f)
Fov = 90.0f;
}
glm::mat4 Camera::ViewMatrix() const
{
return glm::lookAt(Position, Position + Front, WorldUp);
}
#endif
//全局
Camera camera(glm::vec3(0.0f, 0.0f, -20.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, 1.0f, 0.0f));
//渲染
glm::mat4 view = camera.ViewMatrix();
ourShader.setMat4("view", glm::value_ptr(view));
glm::mat4 projection = glm::perspective(glm::radians(camera.Fov), (float)SCR_WIDTH/(float)SCR_HEIGHT, 0.1f, 100.0f);
ourShader.setMat4("projection", glm::value_ptr(projection));
//函数
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.PositionMove(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.PositionMove(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.PositionMove(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.PositionMove(RIGHT, deltaTime);
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
double xoffset = xpos - lastX;
double yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera.ViewMove(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.FovMove(yoffset);
}
参考
本文来自博客园,作者:ETHERovo,转载请注明原文链接:https://www.cnblogs.com/etherovo/p/17369154.html