可以使用矩阵来变换物体。
补充:两个向量相减会得到两个向量指向位置的差:a-b可以得到由b指向a的一个向量
补充:OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。我们刚刚的缩放操作是不均匀(Non-uniform)缩放,
因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫均匀缩放(Uniform Scale)。
补充:位移就是在原始向量的基础上加了另一个向量,就是在原来的基础上移动了原始向量。(用矩阵点乘可以得到)
补充:向量的w分量叫做齐次坐标,w分量存在则可以在3D向量上进行位移,如果w分量为0,则坐标是方向向量,且不能位移
如果由一个顶点,希望先缩放再位移。建议:先缩放再位移,如果反着来,位移的向量也会被缩放,导致和预期不符
GLM是一个只有头文件的库,从0.9.9版本开始,默认将矩阵类型初始化为一个0矩阵,而不是单位矩阵。
可以使用glm::mat4 mat = glm::mat4(1.0f)来进行改变为单位矩阵。
GLM大多数功能都可以从以下头文件找到:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
补充:如果用translate进行位移则第一个参数需要一个单位矩阵,第二个参数是要位移大小的向量;再通过用位移矩阵*原向量,可以得到位移后的向量
补充:旋转用这个函数————glm::rotate(trans, glm::radians(90.0f), glm::vec(0,0,1));【GLM用的弧度制,需要先用radians转化角度为弧度,z轴为1表示绕z旋转】
然后再进行缩放————glm::scale(trans, glm::vec3(0.5,0.5,0.5));
矩阵传递给着色器,通过GLSL中的mat4类型,修改顶点着色器接收一个mat4的uniform变量,再用矩阵uniform乘以位置向量
在把位置向量传递给顶点着色器之前,先添加一个uniform,将其与变换矩阵相乘,当然还需要把变换矩阵传递给着色器,步骤如下:
首先用glGetUniformLocation获得位置值,其次通过调用glUniformMatrix4fv函数把矩阵数据发送给着色器,这样就能得到transform矩阵的值了;
结合上面说的顶点着色器接收了一个mat4的变量,也就是transform变量,用该矩阵乘以位置向量可以得到最终的位置。
补充:glUniformMatrix4fv第一个参数是位置值,第二个参数是1代表了要发送矩阵的个数,第三个参数表示是否希望矩阵进行转置,OpenGL通常使用内部矩阵布局,
叫做列主序,且GLM默认也是列主序,填GL_FALSE;第四个参数主要是矩阵的数据,需要用GLM自带的函数value_ptr变换为OpenGL接受的矩阵。
下面再看下坐标:局部,世界,观察,裁剪空间…
坐标变为标准化坐标,再变为屏幕坐标,这个过程是分步进行的。
一个顶点再被转化为片段之前主要经历五个坐标:局部——世界——观察——裁剪——屏幕 空间
将坐标从一个坐标系变换为另一个坐标系,用到三个重要的矩阵:模型——观察——投影 矩阵
局部到世界空间(用到模型矩阵)
观察空间被称为摄像机(用到观察矩阵)
裁剪空间用到投影矩阵,所有不在范围内的坐标不会被映射出来。投影矩阵创建的观察箱被称作平截头体,一旦所有顶点被变换到裁剪空间,最后执行
透视除法:将位置向量的xyz分量分别除以向量的齐次w分量;该透视除法是将4D裁剪空间坐标变为3D标准化设备坐标的过程。
该过程会在每一个顶点着色器运行的最后被自动执行。之后,坐标会被映射到屏幕空间(glViewport)
观察坐标->裁剪坐标 的投影矩阵:正射投影,和透视投影 矩阵
正射投影:需要宽,高,近平面,远平面————没有把透视考虑进去,需要透视投影矩阵解决该问题
glm::ortho(0f, 800f, 0f, 600f, 0.1f, 100f);
前两个是左右坐标,三四个是底部和顶部坐标,五六个是近平面和远平面的距离
透视投影:离观察者越远的顶点坐标w分量越大。创建透视投影矩阵如下:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
第一个定义了视野;第二个是宽高比;第三四个是近和远平面
将3D物体变换到屏幕空间中:
1.创建一个模型矩阵,包含了位移,缩放和旋转操作。会被应用到所有物体的顶点上。举例:让其绕x轴旋转
再通过顶点坐标乘以这个模型矩阵,就可以将顶点坐标变换到世界坐标了。
glm::mat4 model;
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
2.创建一个观察矩阵,举例:让场景往后移动一些。因为再世界空间,摄像机位于(0,0,0),场景往后移动后才能看得到物体
且把摄像机往后移,和场景往前移的效果是一样的。
则创建一个观察矩阵如下:
glm::mat4 view;
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
3.创建一个投影矩阵
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
4.将上述矩阵传入着色器中。首先再顶点着色器中声明一个uniform变换矩阵然后乘以顶点坐标:
gl_Position = projection * view * model * vec4(aPos, 1.0);
5.将矩阵传入着色器(每次渲染迭代时候进行)
int modelLoc = glGetUniformLocation(ourShader.ID, “model”));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
… // 观察矩阵和投影矩阵与之类似
学习Z缓冲,可以知道什么时候覆盖一个像素,以及什么时候不覆盖覆盖。————也被称为深度缓冲。
深度缓冲存储在每个片段里,当片段想要输出颜色的时候,将它的深度值和z缓冲进行比较,如果当前的片段在其他片段之后,就会被丢弃,否则会被覆盖,这个过程叫深度测试。
因为默认深度测试时关闭的,所以需要先告诉OpenGL要启用深度测试,通过glEnable开启深度测试。glEnable和glDisable允许启用或禁用某个功能。glEnable(GL_DEPTH_TEST);
每次渲染迭代之前需要清除深度缓冲,否则上一帧的深度信息仍然保存在缓冲中。可以通过函数glClear指定的宏进行清除:
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
下面再看下摄像机的内容
可以把场景向相反方向移动,模拟出摄像机的感觉,产生一种我们自己在移动的感觉。
定义一个摄像机,需要在世界空间中的位置方向,观察方向,一个指向它右侧的向量,一个指向它上方的向量。
上述几个向量描述步骤如下:
1.摄像机位置
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
补充:z轴是从屏幕指向自己。要摄像机往后移动,则是沿z轴正方向移动
2.摄像机观察方向
指的是摄像机指向的方向,如果让摄像机指向原点,得到的是一个(0,0,-x)向量,但是摄像机指向z轴负方向,希望方向向量指向z轴正方向。
交换相减的顺序,就可以得到一个指向摄像机z轴正方向的向量:
glm::vec3 cameraTarget = glm::vec3(0.0f,0.0f,0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);————向量被标准化,映射到-1~1之间
3.右侧方向
代表了摄像机的x轴正方向。方法:先定义一个向上的方向向量,把方向向量和观察方向叉乘,得到一个垂直向量,得到了指向x轴正方向的向量
补充:如果交换叉乘的顺序会得到相反的指向x轴负方向的向量(所以是方向向量X观察方向向量)
glm::vec3 up = glm::vec(0.0f,1.0f,0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
4.上方的向量
可以把右方向X观察方向得到上方的向量
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
得到上述几个向量后就可以创建LookAt矩阵了。
上面得到了3个相互垂直的轴定义的一个坐标空间,再加一个平移向量可以创建一个矩阵————可以用这个矩阵乘以任何向量将其变换到那个坐标空间
右向量 0 100-x
上向量 0 * 010-y
观察方向 0 001-z
000 1 000 1
补充:位置向量是相反的,因为是要移动场景到与摄像机移动的相反方向。将LookAt矩阵作为观察矩阵,可以把世界坐标变换到观察坐标
制作LookAt矩阵步骤:定义摄像机位置,目标位置,一个表示世界空间中的上向量
glm::mat4 view;
view = glm::lookAt(glm::vec3(0f,0f,3.0f),
glm::vec3(0f,0f,0f),
glm::vec3(0f,1f,0f));
通过函数以及入参,就可以得到刚才的矩阵了
假如想让摄像机在场景中旋转,可以改变摄像机位置向量来达到效果:
float radius = 10f;
float camX = sin(glfwGetTime()) * radius;
float camX = 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);
得到LookAt函数:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
只需要更新cameraPos即可:
void processInput(GLFWwindow window)
{
…
float cameraSpeed = 0.05f; // adjust accordingly
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;
}
防止移动速度不一致,可以使用两个全局变量计算deltaTime:
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
每次循环需要计算新的值:
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
将速度考虑平衡后:
float cameraSpeed = 2.5f * deltaTime;
鼠标输入
隐藏光标:glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
回调函数,监听移动:void mouse_callback(GLFWwindow* window, double xpos, double ypos);
GLFW注册回调函数:glfwSetCursorPosCallback(window, mouse_callback);
鼠标进行缩放
滚轮的回调函数:void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
每次都会改变透视投影:projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
注册鼠标滚轮的回调函数:glfwSetScrollCallback(window, scroll_callback);