LearnOpenGL-入门章节学习笔记
- 简介
- 创建窗口
-
- 一、Main函数------实例化窗口
- [二、Callback Function 回调函数](#二、Callback Function 回调函数)
- [三、processInput 函数](#三、processInput 函数)
- 创建三角形
- 着色器
- 纹理
- 变换
- 坐标系统
- 摄像机
-
- 一、摄像机/观察空间
- 二、LookAt矩阵
- 三、deltaTime
- 四、视角移动
-
- [1.欧拉角 Euler Angle](#1.欧拉角 Euler Angle)
- 2.加入鼠标的控制
简介
- OpenGL一般被认为是图像API,其本身并不是API,而是由Khronos组织制定并维护的规范。实际的OpenGL库的开发者通常是显卡的生产商。
一、核心模式与立即渲染模式
- 早期OpenGL使用立即渲染模式(固定渲染管线):绘制图形方便,但开发者可控制的较少,效率太低
- 核心模式:用现代函数,有难度,但是提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程
- 当使用新版本的OpenGL特性时,只有新一代的显卡能够支持你的应用程序。这也是为什么大多数开发者基于较低版本的OpenGL编写程序,并只提供选项启用新版本的特性。
二、扩展
- OpenGL的一大特性就是对扩展(Extension)的支持,不用等新版OpenGL,可以直接使用新扩展就好(需要检查显卡是否支持)
- 当显卡公司有特性更新或者优化时 ,就会以 扩展 的形式在驱动中实现
三、状态机
- OpenGL本身就是一个巨大的状态机:一系列变量描述OpenGL如何运行
- OpenGL的状态通常被称为 OpenGL上下文,通过修改上下文来改变OpenGL的状态
- 状态设置函数:改变上下文
四、对象
- 在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集,可以看作一个C风格的结构体
创建窗口
导入 GLFW 和 GLAD(如何将包导入Visual Studio网上有很多教程啦~)
cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
一、Main函数------实例化窗口
cpp
int main()
{
std::cout << "Hello World!\n";
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpengl", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)(glfwGetProcAddress)))
{
std::cout << "failed to initialize GLAD" << std::endl;
}
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
}
glfwInit
函数用来初始化GLFWglfwWindowHint
配置GLFW
cpp
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);
glfwCreateWindow
函数需要窗口的 宽、高、名称(后两个暂不介绍),函数会返回一个GLFWwindow对象
cpp
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
- GLAD是用来管理OpenGL函数指针的,在调用任何OpenGL函数之前都要初始化GLAD
cpp
glViewport(0, 0, 800, 600);
- 我们需要告诉OpenGL渲染窗口的尺寸大小,即视口(viewport)
glViewport
函数,前两个参数是窗口左下角的位置,后两个是宽度和高度(像素),将OpenGL中的位置坐标转换到屏幕坐标,处理过的OpenGL坐标范围只为-1到1,因此我们将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)
cpp
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
- 创建回调函数,要使用GLFW注册该函数,每当窗口调整大小的时候调用这个函数
cpp
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
- 渲染循环loop,让我们在退出之前一直保持运行
processInput(window);
实现判断用户的输入glClear
函数来清空屏幕的颜色缓冲 缓冲位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BITglClearColor
设置清空屏幕所用的颜色,清楚颜色缓冲之后,整个颜色缓冲都会被填充为函数设置的颜色glfwSwapBuffers
函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上glfwPollEvents
函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)
双缓冲:因为图像绘制是逐像素按顺序绘制的,从左往右,从上到下,不是一瞬间生成的,所以单缓冲绘图会有闪烁等问题;双缓冲是 前缓冲 在屏幕上显示,后缓冲执行渲染指令的绘制,然后绘制完毕后,再交换前缓冲和后缓冲
二、Callback Function 回调函数
cpp
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
- 用户改变窗口大小的时候,视口也应该被调整
- 回调函数会在每次窗口大小被调整的时候调用
三、processInput 函数
cpp
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ENTER) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
- 检查用户是否按下了Enter键,如果按下了,通过
glfwSetWindowShouldClose
函数将 WindowShouldClose 属性设置为 true 来关闭GLFW
创建三角形
OpenGL的大部分工作都是关于把3D坐标转变为适应屏幕的2D像素 ,3D坐标转为2D坐标的处理过程是由图形渲染管线 来完成。图形渲染管线可以被划分为两个主要部分 :第一部分把3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素
一、顶点输入
首先,以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形
- 顶点数据是一系列顶点的集合。
- 一个顶点是一个3D的坐标数据的集合,数据由顶点属性来表示
- 图元(Primitive):指定这些数据所表示的渲染类型,任何一个绘制指令的调用都将把图元传递给OpenGL
GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
OpenGL当且仅当坐标xyz轴在[-1,1]范围(标准化设备坐标NDC )时才会处理,之外的OpenGL都会被裁剪/丢弃。首先会在GPU上创建内存(即为显存)来存储数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。定义好数据后,OpenGL会把数据作为输入传给顶点着色器,处理内存中的顶点。
- 通过顶点缓冲对象(VBO)来管理内存 :使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次
- 使用glGenBuffers函数生成一个带有缓冲ID的VBO对象
- VBO的缓冲类型是GL_ARRAY_BUFFER,缓冲类型决定了缓冲区中数据的用途和访问模式 ,可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
- 数据更新:glBufferData函数,会把之前定义的顶点数据复制到缓冲的内存中
- 访问模式 :创建或更新VBO时,你可以指定数据的访问模式
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW :数据每次绘制时都会改变。
cpp
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
二、顶点着色器
用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。
cpp
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
#version 330 core
首先是版本声明,也表示我们会使用核心模式- 下面使用
in
关键字,在顶点着色器中声明所有的输入顶点属性 (Input Vertex Attribute),也通过layout (location = 0)
设定了输入变量的位置值(Location) - 顶点着色器的输出:将gl_Position设置的值会成为该顶点着色器的输出
三、编译着色器
暂时将顶点着色器的源代码硬编码 在代码文件顶部的C风格字符串中:
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";
为了使用,需要在运行时动态编译它的源代码
- 首先是要glCreateShader创建一个着色器对象,用ID来引用的,把需要创建的着色器类型以参数形式提供给glCreateShader
c
usigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 把这个着色器源码附加到着色器对象上,然后编译它,glShaderSource函数把要编译的着色器对象作为第一个参数 。第二参数 指定了传递的源码字符串数量 ,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL
c
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
四、片段着色器
片段着色器所做的是计算像素最后的颜色输出。颜色被表示为有4个元素的数组 :红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。
c
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
- 片段着色器只需要一个输出变量 ,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为FragColor
编译片段着色器的过程与顶点着色器类似,只不过我们使用GL_FRAGMENT_SHADER常量作为着色器类型
c
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
五、着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
- 创建一个程序对象。glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用
c
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
- glAttachShader把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们,把着色器对象链接到程序对象以后,记得glDeleteShader删除着色器对象,我们不再需要它们了
c
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLinkProgram(shaderProgram);
- 可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象
c
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上,我们需要告诉OpenGL
六、链接顶点属性
我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
glVertexAttribPointer
函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
c
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer
- 第一个参数指定我们要配置的顶点属性。在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)为0
- 第二个参数指定顶点属性的大小。它由3个值组成,所以大小是3
- 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)
- 第四个参数定义我们是否希望数据被标准化(Normalize),GL_TRUE(映射到0和1之间),GL_FALSE
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔 。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)
- 例如,如果每个顶点有位置(3个浮点数)、法线(3个浮点数)和纹理坐标(2个浮点数),那么 stride 将是 3 + 3 + 2 = 8 个浮点数的大小,即 8 * sizeof(float)。
- 最后一个参数的类型是void*,表示位置数据在缓冲中起始位置的偏移量(Offset)
glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性
每个顶点属性从一个VBO管理的内存中获得它的数据 ,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用 glVertexAttribPointer 时绑定到GL_ARRAY_BUFFER的VBO决定的
c
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
对于数量很多的物体,绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事
顶点数组对象VAO
VAO提供了一种机制来封装和存储顶点属性状态,这样就可以更高效地传递顶点数据到OpenGL。定义了如何解释顶点缓冲对象(VBO)中的数据
- OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
c
unsigned int VAO;
glGenVertexArrays(1, &VAO);
- 顶点数组对象会存储以下内容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
c
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 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);
glBindVertexArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
- 绑定VAO之后,所有后续的顶点属性状态设置都将与这个VAO关联
- 在VAO绑定的情况下,设置顶点属性指针(使用glVertexAttribPointer)和启用顶点属性数组(使用glEnableVertexAttribArray)。
glBindVertexArray(0);
完成设置后,可以解绑VAO,以避免影响后续操作。- 在绘制时,只需重新绑定相应的VAO,并执行绘制调用
绘制三角形,glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元
- 第一个参数是绘制的类型
- 第二个参数是顶点数组的起始索引
- 第三个参数是我们要绘制多少个顶点
c
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
结束绘制后,要删除VAO、VBO、shaderProgram
c
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
七、元素缓冲对象
EBO,也叫索引缓冲对象IBO,它存储 OpenGL 用来决定要绘制哪些顶点的索引。解决图形顶点共用问题,只需要存储不同的顶点就好,不需要重复存储相同的顶点。
c
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 // 第二个三角形
};
创建元素缓冲对象
c
unsigned int EBO;
glGenBuffers(1, &EBO);
与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
c
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最后是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
c
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO。
c
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
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);
- 线框模式绘制:通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函数配置OpenGL如何绘制图元,之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式
- 第一个参数表示应用到所有的三角形的正面和背面
- 第二个参数告诉我们用线来绘制
着色器
一、GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
基本结构:声明版本、输入和输出变量、uniform和main函数。在main中我们处理所有的输入变量,并将结果输出到输出变量中。
二、数据类型
bool、float、int、double、uint等,也有两种容器类型:向量Vector和矩阵Matrix
向量
- vecn 包含n个float分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量
每个向量的分量可以通过 vec.x 这样的方式来获取,GLSL也允许对颜色rgba和纹理坐标stpq使用分量
c
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
三、输入与输出
GLSL定义了in和out关键字 专门来实现,每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去
- 在顶点着色器中,输入是直接从顶点数据中获取的,
layout (location = 0)
指定输入变量,这样就可以在CPU上配置顶点属性。 - 在片元着色器中,输出变量是一个vec4类型的颜色变量
如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)
四、Uniform
uniform是另一种CPU给GPU传递数据的方式,但uniform和顶点数据有些不同
- uniform是全局性(global)的。意味着uniform变量必须在每个着色器中是独一无二的,而且可以被着色器的任意阶段访问
- uniform会一直保存数据,直到被重置或者更新
使用Uniform
我们在片元着色器中使用了ourColor的uniform变量,无需通过顶点着色器来定义传递
cpp
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
需要先在着色器中找到uniform的索引值,才能更新值(这次是让颜色按照时间变化)
c
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
glfwGetTime()
函数获取时间变量,再用sin
函数让颜色在0.0-1.0之间改变,存储在greenValue里面glGetUniformLocation()
函数来获uniform变量的索引值- 查询索引值时不需要调用着色器程序,但是如果对uniform值进行更新,就需要先使用程序(glUseProgram函数)
glUniform4f
函数设置uniform值- f 函数需要一个float作为它的值
- i 函数需要一个int作为它的值
- ui 函数需要一个unsigned int作为它的值
- 3f 函数需要3个float作为它的值
- fv 函数需要一个float向量/数组作为它的值
五、更多属性
把颜色数据也添加到顶点数据中
c
float vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
改变顶点着色器,使其能接受顶点坐标和颜色数据
c
#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设置为我们从顶点数据那里得到的输入颜色
}
由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器:
c
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针
c
// 位置属性
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);
对于偏移量值,因为顶点坐标属性在前,所以偏移量为0,颜色数据在后,所以偏移量为 3* sizeof(float)
六、着色器类
把着色器类全部放在在头文件里,主要是为了学习用途,当然也方便移植
纹理
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个 纹理坐标(Texture Coordinate) ,用来标明该从纹理图像的哪个部分采样。之后在图形的其它片段上进行片元插值(Fragment Interpolation)。
使用纹理坐标获取纹理颜色叫做采样,纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
一、纹理环绕方式
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为,重复纹理图像 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色 |
使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):
- 第一个参数指定了纹理目标:GL_TEXTURE_2D
- 第二个参数需要我们指定设置的选项与应用的纹理轴:我们打算配置的是WRAP选项,并且指定S和T轴
- 最后一个参数需要我们传递一个环绕方式(Wrapping) :GL_MIRRORED_REPEAT
- 如果是GL_CLAMP_TO_BORDER,还需要指定一个边缘色,并使用glTexParameter函数的fv后缀形式
c
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
二、纹理过滤
有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)
- OpenGL会选择 中心点最接近 纹理坐标的那个像素
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)
- 会基于纹理坐标附近的纹理像素,计算出一个插值 ,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大
当进行 放大(Magnify) 和 缩小(Minify) 操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。
c
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Mipmap多级渐远纹理
(原理见Texture mapping)
纹理放大不会使用多级渐远纹理 ,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。OpenGL有一个glGenerateMipmap函数,在创建完一个纹理后调用它OpenGL就会创建一系列多级渐远纹理
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
使用 glTexParameteri 设置过滤方式
c
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
三、加载与创建纹理
stb_image.h
stb_image.h是Sean Barrett的一个非常流行的单头文件图像加载库 ,它能够加载大部分流行的文件格式,并且能够很简单得整合到工程之中。
stbi_load函数 加载图片
c
int width, height, nrChannels;//宽度、高度和颜色通道的个数
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
四、生成纹理
也是使用ID引用,glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中
c
unsigned int texture;
glGenTextures(1, &texture);
我们需要绑定到GL_TEXTURE_2D,让之后任何的纹理指令都可以配置当前绑定的纹理
c
glBindTexture(GL_TEXTURE_2D, texture);
绑定后,就可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理
- 第二个参数是mipmap的纹理级别
- 第三个是纹理的存储格式(我们的图像只有RGB值,因此我们也把纹理储存为RGB值)
- 第四个和第五个是纹理的最终的宽高
- 第七个第八个参数定义了源图像格式和数据类型(存储为char数组)
- data 是真正的图像数据
c
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
生成了纹理和相应的多级渐远纹理后,释放图像的内存是一个很好的习惯
c
stbi_image_free(data);
整个生成纹理的代码应该为
c
unsigned int texture;
glGenTexture(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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//加载并生成纹理
int width,height,nrChannels;
unsigned char* data = stbi_load("container.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);
五、应用纹理
利用glDrawElements绘制矩形
c
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 // 左上
};
c
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
顶点着色器加入vec2输出TexCoord
c
#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;
}
片元着色器使用采样器sampler来把纹理传递进来
c
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
六、纹理单元
使用 glUniform1i ,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。
一个纹理的位置值通常称为一个 纹理单元 ,纹理单元的主要目的是让我们在着色器中可以使用多个纹理。通过把纹理单元赋值给采样器 ,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。
- GL_TEXTURE0到GL_TEXTRUE15
- 可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8
c
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
修改片元着色器
c
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
- mix函数接受两个值作为参数,并对它们根据第三个参数进行线性插值
- 如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色
c
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元 。我们只需要设置一次即可,所以这个会放在渲染循环的前面。通过使用glUniform1i设置采样器,我们保证了每个uniform采样器对应着正确的纹理单元。
c
ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置
while(...)
{
[...]
}
变换
引入GLM库
c
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
初始化矩阵
c
glm::mat4 mat = glm::mat4(1.0f);
如果我想要把一个向量(1,0,0)位移(1,1,0)个单位
- 先用GLM内建的向量类定义一个叫做vec的向量
- 接下来定义一个mat4类型的trans,默认是一个4×4单位矩阵
- 下一步是创建一个变换矩阵,我们是把单位矩阵和一个位移向量传递给glm::translate函数来完成这个工作的(然后用给定的矩阵乘以位移矩阵就能获得最后需要的矩阵)
c
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 mat = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
构建旋转矩阵,绕z轴旋转90度,再缩放为原来0.5倍
c
glm::mat4 trans = glm::mat4(1.0f);
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));
把矩阵传递给着色器,GLSL里面也有mat4类型,可以用uniform变量
c
#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);
}
c
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
- 首先查询transform的位置值,然后用有Matrix4fv后缀的glUniform(在shader.use()后)
- 第一个参数是transform的位置值
- 第二个是有几个变换矩阵
- 第三个是是否对矩阵进行转置(默认是列主序)
- 第四个参数是矩阵,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据
坐标系统
OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC) 。将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的 ,也就是类似于流水线那样子。物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)
一、局部空间
局部空间是指物体所在的坐标空间,即对象最开始所在的地方
二、世界空间
指顶点相对于(游戏)世界的坐标。我们需要通过 模型矩阵(model matrix) 将物体从局部空间转换到世界空间
三、观察空间
观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。需要通过 观察矩阵(view matrix) 将物体从世界空间转换到观察空间
四、裁剪空间
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped) 。我们需要通过 投影矩阵(Projection Matrix) 将顶点坐标从观察变换到裁剪空间,它指定了一个范围的坐标,比如在每个维度上的-1000到1000,接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)
如果所有顶点都被变换到裁剪空间,就会进行透视除法 (除以w分量,将4D裁剪空间变化为3DNPC),这一步会在顶点着色器最后自动执行
从观察空间变换到裁剪空间有两个形式:正射投影矩阵 或者透视投影矩阵
1.正射投影
正射投影矩阵定义了一个类似立方体的平截头箱 ,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。
创建一个正交矩阵
c
glm::ortho(0.0f, 800.0f, 0.0f 600.0f, 0.1f, 100.0f);
- 前两个参数指定了左右坐标
- 第三第四个参数指定了底部和顶部坐标
- 第五第六个参数是近平面远平面距离
2.透视投影
透视投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大
c
glm::mat4 proj = glm::perspective(glm::radians(fov), (float)width/(float)height, 0.1f, 100.0f);
- 第一个参数定义了fov
- 第二个是宽高比aspect
- 第三四个是近远平面距离
五、组合到一起
顶点着色器要求所有的顶点都在裁剪空间内(最后的顶点应被赋值给gl_Position),然后OpenGL对裁剪坐标进行透视除法,从而将他们变换到归一化设备坐标系上以及裁剪
V c l i p = M p r o j e c t i o n ⋅ M v i e w ⋅ M m o d e l ⋅ V l o c a l V_{clip} = M_{projection} \cdot M_{view} \cdot M_{model} \cdot V_{local} Vclip=Mprojection⋅Mview⋅Mmodel⋅Vlocal
在顶点着色器中,将mvp矩阵声明为uniform变量,并与aPos相乘得到gl_Position
c
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;
void main()
{
gl_Position = proj * view * model * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
在main代码中,一个一个矩阵来
- model矩阵进行绕x轴旋转-45度,想让其看起来放在地面上一样
c
mat4 model = mat4(1.0f);
model = rotate(model, radians(-45.0f), vec3(1.0, 0.0, 0.0));
- view矩阵进行平移操作------沿z轴负方向移动(想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0),这正是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景)
c
mat4 view = mat4(1.0f);
view = translate(view, vec3(0.0, 0.0, -3.0));
- proj矩阵,我们希望在场景中使用透视投影,所以声明一个投影矩阵
c
mat4 proj = mat4(1.0f);
proj = perspective(radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
然后将mvp矩阵传入着色器
c
int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
int viewLoc = glGetUniformLocation(ourShader.ID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
int projLoc = glGetUniformLocation(ourShader.ID, "proj");
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(proj));
六、Z缓冲
OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)(原理见 z-buffer深度缓冲)在OpenGL
中默认是关闭的,我们可以通过 glEnable 来开启深度测试
c
glEnable(GL_DEPTH_TEST);
因为使用了深度测试,所以我们也需要在每次渲染之前清空深度缓冲
c
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
摄像机
OpenGL本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。
一、摄像机/观察空间
观察空间是以摄像机为原点的空间
- 摄像机的位置:就是世界空间中一个指向摄像机位置的向量(opengl使用的是右手坐标系,正z轴是从屏幕指向自己的)
c
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
- 摄像机方向:摄像机指向z轴负方向,摄像机方向 = 摄像机位置 - 原点
c
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
- 右轴:先定义向上方向,再用向上方向与摄像机方向进行叉乘,得到向右方向
c
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
- 上轴:把向右向量与摄像机方向向量进行叉乘
c
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
利用格拉姆-施密特正交化,构建观察/摄像机空间的LookAt矩阵
二、LookAt矩阵
如果使用3个相互垂直(或非线性)的轴定义了一个坐标空间,那么在加上一个平移向量来创建一个矩阵,这个矩阵就可以乘任何向量来变换到这个坐标空间
现在有三个相互垂直的轴以及摄像机位置坐标,就可以创建LookAt矩阵,作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间
在GLM中,我们只需要定义摄像机位置 、目标位置 和表示世界空间中的向上向量(计算右向量的向上方向),就可以使用glm::LookAt来创建矩阵
c
glm::mat4 view = mat4(1.0f);
view = glm::LookAt(glm::vec3(0.0, 0.0, 3.0),
glm::vec3(0.0, 0.0, 0.0),
glm::vec3(0.0, 1.0, 0.0));
三、deltaTime
图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。
c
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
四、视角移动
1.欧拉角 Euler Angle
欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值:俯仰角pitch,偏航角yaw,滚翻角roll(对于摄像机用不到)
通过所给的角度来计算方向向量的分量:俯仰角和偏航角转化为用来自由旋转视角的摄像机的3维方向向量
- 如果我们想象自己在xz平面上,看向y轴,我们可以基于第一个三角形计算来计算它的长度/y方向的强度(Strength)(我们往上或往下看多少)
c
direction.y = sin(glm::radians(pitch)); // 注意我们先把角度转为弧度
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
- 对于偏航角yaw,x分量取决于cos(yaw)的值,z值同样取决于sin(yaw)
c
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
// 译注:direction代表摄像机的前轴(Front),
//这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
2.加入鼠标的控制
偏航角和俯仰角是通过鼠标(或手柄)移动获得的,水平的移动影响偏航角,竖直的移动影响俯仰角。
原理:存储上一帧鼠标的位置,在当前帧计算鼠标位置与上一帧差多少,若水平/竖直相差越大,偏航角和俯仰角也变化越大
- 隐藏鼠标
c
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
- 让GLFW监听鼠标移动使劲按,使用一个回调函数来完成,xpos和ypos代表当前鼠标的位置
c
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
- 鼠标一移动mouse_callback函数就会被调用:
c
glfwSetCursorPosCallback(window, mouse_callback);
在处理FPS风格摄像机的鼠标输入的时候,我们必须在最终获取方向向量之前做下面这几步:
- 计算鼠标距上一帧的偏移量。
- 把偏移量添加到摄像机的俯仰角和偏航角中。
- 对偏航角和俯仰角进行最大和最小值的限制。
- 计算方向向量。
c
//计算鼠标距上一帧的偏移量,把它的初始值设置为屏幕的中心(屏幕的尺寸是800x600)
float lastX = 400, lastY = 300;
//然后在鼠标的回调函数中我们计算当前帧和上一帧鼠标位置的偏移量
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意这里是相反的,因为y坐标是从底部往顶部依次增大的
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f; //可控制鼠标移动灵敏度
xoffset *= sensitivity;
yoffset *= sensitivity;
//把偏移量加到全局变量pitch和yaw上:
yaw += xoffset;
pitch += yoffset;
//第三步,我们需要给摄像机添加一些限制,
//这样摄像机就不会发生奇怪的移动了(这样也会避免一些奇怪的问题)
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
if(yaw > 89.0f)
yaw = 89.0f;
if(yaw < -89.0f)
yaw = -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 = glm::normalize(front);
第一次进去时鼠标会跳一下,因为鼠标移动进窗口那一刻起,回调函数就会被调用,这时候xpos和ypos会从鼠标刚刚进入的位置,这通常是里屏幕中心很远的距离。
解决:可以检测是否第一次进入,如果是,就把鼠标初始位置更新为xpos和ypos
c
if(firstMouse) // 这个bool变量初始时是设定为true的
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
鼠标回调函数整体代码:
c
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if(firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
if(yaw > 89.0f)
yaw = 89.0f;
if(yaw < -89.0f)
yaw = -89.0f;
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}