一、变换:
1、向量:
定义:方向+大小。
向量与标量:数学上不能进行运算,线性代数库对它有支持(GLM)
取反:向量前加负号
向量的加法:分量的加法
向量的乘法:
①点乘——判断向量是否正交或平行,用于计算光照。若是两个单位向量点乘,将对应分量逐个相乘,然后再把所得积相加来计算。
②叉乘——两个正交向量叉乘可以得到三个互相正交的向量
2、矩阵:
矩阵与标量:数学上没有加减运算,但是很多线性代数的库都有支持(GLM)。
矩阵的缩放:
矩阵的位移:
【注】齐次坐标——向量w分量。若齐次向量→3D向量:要把x, y, z坐标分别除以w坐标。
使用齐次坐标的好处:它允许我们在3D向量上进行位移,若没有w向量,是没办法位移向量的,w值可以创建3D效果。
方向向量——齐次坐标为0,此时,向量不能位移。
矩阵的旋转:
角度与弧度的转化:角度 = 弧度 * (180.0f / PI)
沿x轴:
沿y轴:
沿z轴:
先后绕多轴旋转会导致一个问题——万向节死锁。避免万向节死锁的真正解决方案是使用四元数。
3、GLM:OpenGL的数学库
GLM——只有头文件的库。(只需要包含头文件,不需要链接和编译。)
头文件(大多数GLM的功能都能在这里找到):
#include<glm/glm.cpp>
#include<glm/matrix_transform.hpp>
#include<glm/gtc/type_ptr.hpp>
把向量(1,0,0)位移(1,1,0):
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
//下面是矩阵初始化的例子,如果使用的是0.9.9以上的版本
//下面折行代码就需要改为:
//glm::mat4 trans = glm::mat4(1.0f)
//之后将不再进行提示
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout<<vec.x<<vec.y<<vec.z<<std::endl;
旋转缩放箱子(逆时针旋转90度,缩放0.5倍):
glm::mat4 trans;
//沿z轴旋转90度
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
//每个轴都缩放0.5倍
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
将矩阵传递给着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
//把位置向量传递给gl_Position之前,先添加uniform,并将其与变换矩阵相乘
uniform mat4 tranform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0-aTexCoord.y);
}
把矩阵数据发送给着色器:
//查询uniform的变量地址
unsigned int tranformLoc = glGenUniformLocation(ourShader.ID, "transform");
//把矩阵数据发送给着色器
//第一个参数:uniform位置值
//第二个参数:发送多少矩阵
//第三个参数:是否需要转置矩阵(默认列主序)
//第四个参数:真正矩阵数据,需要用value_ptr变换这些数据
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
让箱子随着时间旋转:
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
//运用GLFW的时间函数获取不同时间的角度:
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
二、坐标系统:
重要的5个不同的坐标系统:
局部空间(也叫物体空间)、世界空间、观察空间(也叫视觉空间)、裁剪空间、屏幕空间
1、概述:
一个坐标系到另一个坐标系:模型→观察→投影
坐标变换整个流程:
2、局部空间:
局部空间:物体所在空间,即对象最开始所在的地方。
3、世界空间:
世界空间:与其他物体构成的空间。
4、观察空间:
观察空间(OpenGL的摄像机或摄像机空间或视觉空间):将世界空间坐标转化为用户视野前方的坐标而产生的结果。通常由一系列位移和旋转组合来完成。
观察矩阵:一系列的位移和旋转组合在一起的变换存储地方,被用来将世界坐标变换到观察空间。
5、裁剪空间:
裁剪空间:顶点着色器运行后,所有坐标都落在一个特定范围,任何范围之外的点都被裁剪掉,剩下的坐标变为屏幕上的可见片段。此时,指定的坐标集会变换回标准化设备坐标系。
投影矩阵:将顶点坐标从观察变换到裁剪空间,它指定了一个范围坐标。
【注】若只是图元(如三角形)的一部分超出裁剪体积,OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。
平截头体:由投影矩阵创建的视察箱,每个出现在这个范围内的坐标都会最终出现在用户的屏幕上。
投影:将特定范围内的坐标转化到标准化设备坐标系的过程。
透视除法:一旦所有顶点被变换到裁剪空间的最终操作,这个过程会将位置向量x, y, z分量分别除以向量的齐次w分量。它是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。(每个顶点着色器运行的最后被自动执行)
【附】裁剪空间通过视口变换最终的坐标将会被映射到屏幕空间中,并变换成片段。观察坐标变换为裁剪坐标矩阵的两种不同形式(每种形式都定义了不同的平截头体),我们可以选择创建一个正射投影矩阵或一个透视投影矩阵。
6、正射投影:
正设投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,这个空间之外的定点会被裁掉。不改变w值。
创建一个正设投影矩阵(这个投影矩阵会将处于这些x, y, z值范围内的坐标变换为标准化设备坐标):
//第一、二个参数:指定了平截头体的左右坐标
//第三、四个参数:指定了平截头体底部和顶部
//通过这四个参数定义了近平面和远平面大小
//第五、六个参数:指定来近平面和远平面的距离
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
正设投影矩阵直接将坐标映射到2D平面中,但不真实。
【用途】正射投影主要用于二维渲染以及一些建筑或工程的程序
7、透视投影:
透视投影矩阵将给定的平截头体范围映射到裁剪空间,另外还修改了每个顶点的w坐标,使得观察感觉是近大远小,另外被裁剪的空间坐标会在-w到w之间。
创建一个透视投影矩阵:
//第一个参数:定义了fov的值,表示视野,并设置了观察空间的大小。如果需要真实效果,其值通常设置为45.0f,若需要第一人称射击风格,可能需要设置更大的值
//第二个参数:设置来宽高比,由视口的宽高决定
//第三、四个参数:设置来平截头体的近、远平面,近距离通常设置为0.1f,远距离设置为100.0f
glm::mat4 proj = glm::perspective(glm::radians(45.0f),(float)width/(float)height, 0.1f, 100.0f);
【注】当把near值设置太大时,OpenGL会将靠近的摄像机坐标斗裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果,太过靠近一个物体的时候,你的视线会直接穿过去。
8、组合:
顶点坐标变换到裁剪坐标:
OpenGL对裁剪坐标执行透视除法,从而将它们变换到标准化设备坐标。
使用glViewPort内部参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点——视口变换。
9、3D:
通过3D到2D:
创建一个模型矩阵(包含位移、缩放、旋转):
glm::mat4 model;
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f);
创建一个观察矩阵(相反于摄像机移动的方向移动整个场景,另外OpenGL是右手坐标系):
glm::mat4 view;
//注意,我们将矩阵向我们要进行移动的场景动反方向移动
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f);
定义一个投影矩阵:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0,1f, 100.0f);
传入着色器:
声明一个uniform变换矩阵乘顶点坐标:
#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
//注意乘法要从右向左读
gl_Position = projection * view * model* vec4(aPos, 1.0);
...
}
将矩阵传入着色器(通常在每次的渲染迭代中进行):
int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
...
...//观察矩阵和投影矩阵与之类似
最终物体会:①稍微向后倾斜至地板方向; ②离我们有一些距离; ③有透视效果(顶点越远,变得越小)。
立方体随时间旋转:
model = glm::rotate(model,(float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f);
绘制立方体:
glDrawArrays(GL_TRIANGLES, 0, 36);
【注】此时出现bug,本该被遮挡的部分没有遮挡。原因:OpenGL绘制图像是一个三角形一个三角形来绘制的。
10、Z缓冲:
定义:OpenGL存储它所有深度信息于一个Z缓冲(Z-buffer)中,GLFW自动生成,且存储在每个片段里(z值)。
深度测试:当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,若当前动片段在其他片段之后,它将会被丢弃,否则会被覆盖。
开启glEnable和关闭glDisable深度测试GL_DEPTH_TEST:
glEnable(GL_DEPTH_TEST);
每次渲染迭代之前清除深度缓冲(否则前一帧动深度信息仍然保存在缓冲中):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
11、更多立方体:
立方体一样,区别是位置和旋转角度不同,布局已经定义好——此时不需要改变缓冲数组和属性数组,只需要改变每个对象的模型矩阵:
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,0.2f,-1.5f),
glm::vec3(1.5f,2.0f,-2.5f),
glm::vec3(-1.3f,1.0f,-1.5f),
};
添加旋转:
glBindVertexArray(VAO);
for(unsigned int i = 0; i<10; i++)
{
glm::mat4 model;
model = glm::translate(model,cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle),glm::vec3(1.0f, 0.3f, 0.5f);
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES,0,36);
}
二、摄像机
1、摄像机/观察空间
【注】摄像机望向z轴的负方向。
(1)摄像机的位置
glm::vec3 cameraPos= glm::vec3(0.0f, 0.0f, 3.0f);
(2)摄像机的方向
获得指向摄像机正z轴方向的向量
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos-cameraTarget);
(3)右轴
获得右向量:
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight= glm::normalize(glm::cross(up, cameraDirection));
(4)上轴
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
(5)Look At
创建LookAt矩阵:
其中,R是右向量,U是上向量,D是方向向量,P是摄像机位置向量。
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));
预先定义圆的半径radius,每次渲染迭代扩大这个圆
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);
3、自由移动
实现摄像机移动:
定义摄像机变量:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f,3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
定义摄像机的方向:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
添加几个需要检查的按键命令:
void processInput(GLFWwindow *window)
{
...
float cameraSpeed = 0.05f;
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 -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
【注】通过标准化,使得移动速度不变,否则移动会变速。
4、移动速度
为了解决不同处理器配置不同带来的绘制快慢体验感不同,引入deltaTime处理。
跟踪两个全局变量计算deltaTime值:
float deltaTime = 0.0f;//当前帧与上一帧的时间差
float lastFrame = 0.0f;//上一帧的时间
每一帧计算出新的deltaTime以备后用:
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
实现体验感一样:
void processInput(GLFWwindow *window)
{
float cameraSpeed = 2.5f * deltaTime;
...
}
5、视角移动
(1)欧拉角
分类:俯仰角【上下】(pitch)、偏航角【左右】(yaw)、滚转角【翻滚】(roll)
(设斜边为1)
俯仰角y = sinθ:
direction.y = sin(glm::radians(pitch));
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
把俯仰角转化为用来自由旋转视角的摄像机的3维方向向量:
direction.x = cos(glm::radians(pitch))*cos(glm::radians(yaw));//译注:direction代表摄像机的前轴
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch))*sin(glm::radians(yaw));
(2)鼠标输入
隐藏光标并捕捉它:
//此时光标不会显示,也不会离开窗口
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLEDE);
监听鼠标移动事件,计算俯角和仰角
void mouse_callback(GLFWwindow *window, double xpos, double ypos);
注册回调函数,鼠标一移动就被调用:
glfwSetCursorPosCallback(window, mouse_callback);
处理FPS风格摄像机的鼠标输入,必须在最终获取方向向量之前做下面几步:
①计算鼠标距上一帧的偏移量:
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;
③偏移量加到全局变量上:
yaw += xoffset;
pitch += yoffset;
④给摄像机添加限制:
if(pitch > 89.0f)
pitch = 89.0f;
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 = glm::normalize(front);
【注】窗口第一次获取焦点会跳一次,原因是:在你的鼠标移动进窗口的那一刻,鼠标回调函数就会被调用。此时xpos和ypos就会等于鼠标刚刚进入屏幕的那个位置,这通常是一个距离屏幕中心很远的地方。
解决跳的方法:
if(firstMouse)//这个bool变量初始时是设定为true
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
最后的代码实现:
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;
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);
}
(3)缩放
鼠标滚轮回调函数:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if(fov >= 1.0f && fov <= 45.0f)
fov-= yoffset;
if(fov <= 1.0f)
fov = 1.0f;
if(fov >= 45.0f)
fov = 45.0f;
}
每一帧必须把透视投影矩阵上传到GPU,使用fov变量作为它的视野:
projection = glm::perspective(glm::radians(fov), 800.0f/600.0f,0.1f, 100.0f);
最后不要忘记注册鼠标滚轮的回调函数:
glfwSetScrollCallback(window, scroll_callback);
【注】比起欧拉角可能引入万向节死锁,最好使用四元数。
9、摄像机类
(以上风格为FPS风格,满足大多数情况且兼容欧拉角)