上一次笔记学习了OpenGL中的着色器的基本操作,纹理的使用,以及变换矩阵,我已经可以绘制出一个有纹理,会移动的2D三角形了。经过这一次的学习,我能够通过实现坐标系统将一个3D空间中的3D立方体绘制到屏幕空间中,还可以自定义摄像机类,在3D空间中自由移动。
先讲一些理论的东西。
将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。目前比较重要的坐标系统有:
在这些空间之间转换的过程中中有3个重要的矩阵:模型(model),观察(view),投影(projection)矩阵。
这一系列的空间中的坐标初始为vec4,即三个位置坐标和一个齐次坐标。
局部空间中的坐标为局部坐标,位置坐标是模型的导入时的坐标,齐次坐标一般设为1.0。局部空间的原点由建模人员在建模时决定的,一般在物体中心。
世界空间就是整个3D世界的空间,该空间中的坐标称为世界坐标。
模型坐标需要变换操作(缩放,旋转,平移)变为世界坐标,这个变换操作通过模型矩阵实现,模型矩阵是模型相关的。
观察空间也叫摄像机空间(Camera Space)或者视觉空间(Eye Space),该阶段坐标称为观察坐标。
从世界空间到观察空间的过程是把物体相对于世界原点的坐标转化为相对于摄像机的坐标,具体做法就是把摄像机相对于世界原点的位移、旋转操作反向加到所有物体的世界坐标上,这个操作就是观察矩阵做的事。观察矩阵是摄像机相关的。
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。
从观察空间到裁剪空间就是完成下面两个空间的映射(透视投影时)
该映射需要两个过程,先将观察坐标通过投影矩阵转化成裁剪坐标,再通过透视除法将剪裁坐标变成标准化设备坐标(normalized device coordinates NDC)。如下图所示
裁剪坐标是为了方便计算定义出来的坐标。
从观察坐标转到裁剪坐标需要投影矩阵来操作,投影矩阵有两个主要的参数,投影方式和摄像机的观察箱(一个平截头体Frustum)。投影矩阵是摄像机相关的。
投影方式有两种:
当透视投影时,矩阵乘出的坐标是裁剪坐标,OpenGL会自动进行一个叫做透视除法的操作,将裁剪坐标变成标准化设备坐标:
o u t = ( x / w y / w z / w ) out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix} out=⎝⎛x/wy/wz/w⎠⎞
这个操作会使更远处坐标的绝对值变小,形成下面的效果:
当正射投影时,矩阵乘出的坐标直接是标准化设备坐标,不进行透视除法。
将标准化设备坐标变换为屏幕坐标,OpenGL会使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的裁剪坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
在上面几个空间的转换中,我们需要向顶点着色器提供三个矩阵model,view,projection。其中model矩阵是模型相关,一个模型对应一个model矩阵;view,projection矩阵是摄像机相关。
坐标转换的公式:
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
在得到Vclip后,OpenGL会自动对顶点坐标进行透视除法和视口转换。
该矩阵可以用上一节的知识自己定义模型的位置、大小、旋转。
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
view矩阵就是摄像机位置的反向操作
glm::mat4 view;
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
正射投影
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
透视投影
glm::perspective(glm::radians(45.0f), (float)window_width/(float)window_height, 0.1f, 100.0f);
#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);
...
}
3D世界中摄像机摄像机的实现思想是通过捕捉用户的键盘和鼠标的输入来改变摄像机的观察位置和摄像机的观察方向。
上面提到我需要提供3个矩阵:model,veiw,projection来完成3d世界的绘制。其中view矩阵和projection矩阵与摄像机相关。view与摄像机的姿态相关:位置,旋转。projection与摄像机的“镜头”相关:视界。
所以我们要做的就是把用户的输入映射到摄像机变化上。
键盘wasd 改变 摄像机位置
鼠标滑动 改变 摄像机姿态
鼠标滚轮 改变 摄像机的视界(放大缩小)
LookAt矩阵可以乘以任何向量来将其变换到某个坐标空间,该坐标空间由R(右向量),U(上向量),D(方向向量),三个正交向量构成的右手坐标系和一个位置向量P定义:
L o o k A t = [ R x R y R z 0 U x U y U z 0 D x D y D z 0 0 0 0 1 ] ∗ [ 1 0 0 − P x 0 1 0 − P y 0 0 1 − P z 0 0 0 1 ] LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix} LookAt=⎣⎢⎢⎡RxUxDx0RyUyDy0RzUzDz00001⎦⎥⎥⎤∗⎣⎢⎢⎡100001000010−Px−Py−Pz1⎦⎥⎥⎤
LookAt做的事其实就是view矩阵要实现的事,所以我们只需要实时的监控摄像机的位置向量P和三个资态向量R、U、D,生成摄像机坐标系的LookAt矩阵,作为view矩阵传给定点着色器。
总结一下一个摄像机类要做的事情:读取用户在键盘WASD、鼠标滑动、鼠标滑轮的输入,输出LookAt矩阵和fov值。
如何将鼠标的操作送到摄像机类中。
首先要隐藏光标并捕捉(Capture)光标,捕捉光标的意思是,如果焦点在你的程序上,光标应该停留在窗口中(除非程序失去焦点或者退出)。
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
然后监听鼠标的移动事件
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
glfwSetCursorPosCallback(window, mouse_callback);
处理移动事件
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; // y的坐标是上面小,下面大
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
滚轮输入类似
......
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
......
int main(){
......
glfwSetScrollCallback(window, scroll_callback);
......
}
......
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset){
// yoffset代表滚轮滚动的值
camera.ProcessMouseScroll(yoffset);
}
在每一帧渲染的过程中进行事件捕捉:
void processInput(GLFWwindow *window){
.......
// 获得渲染时间,保证速度相同
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 键盘移动
if(glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if(glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if(glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if(glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
下面是摄像机类的属性和方法
#ifndef CAMERA_H
#define CAMERA_H
#include
#include
#include
#include
// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
enum Camera_Movement {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
// Default camera values
const float YAW = -90.0f;
const float PITCH = 0.0f;
const float SPEED = 2.5f;
const float SENSITIVITY = 0.1f;
const float ZOOM = 45.0f;
// An abstract camera class that processes input and calculates the corresponding Euler Angles, Vectors and Matrices for use in OpenGL
class Camera{
public:
// Camera Attributes
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
// Euler Angles
float Yaw;
float Pitch;
// Camera options
float MovementSpeed;
float MouseSensitivity;
float Zoom;
// Constructor with vectors
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM){
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// Constructor with scalar values
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM){
Position = glm::vec3(posX, posY, posZ);
WorldUp = glm::vec3(upX, upY, upZ);
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
// Returns the view matrix calculated using Euler Angles and the LookAt Matrix
glm::mat4 GetViewMatrix();
// Processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems)
void ProcessKeyboard(Camera_Movement direction, float deltaTime);
// Processes input received from a mouse input system. Expects the offset value in both the x and y direction.
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true);
// Processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis
void ProcessMouseScroll(float yoffset);
private:
// Calculates the front vector from the Camera's (updated) Euler Angles
void updateCameraVectors();
};
#endif
摄像机首先要做的是接受输入,维护自己的姿态状态:
void ProcessKeyboard(Camera_Movement direction, float deltaTime){
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
}
void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true){
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;
Yaw += xoffset;
Pitch += yoffset;
// Make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrainPitch){
if (Pitch > 89.0f)
Pitch = 89.0f;
if (Pitch < -89.0f)
Pitch = -89.0f;
}
// Update Front, Right and Up Vectors using the updated Euler angles
updateCameraVectors();
}
void ProcessMouseScroll(float yoffset){
if (Zoom >= 1.0f && Zoom <= 45.0f)
Zoom -= yoffset;
if (Zoom <= 1.0f)
Zoom = 1.0f;
if (Zoom >= 45.0f)
Zoom = 45.0f;
}
摄像机主要的旋转姿态是通过维护两个角度俯仰角(Pitch)、偏航角(Yaw),同步更新向量。
void updateCameraVectors(){
// Calculate the new Front vector
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));
Front = glm::normalize(front);
// Also re-calculate the Right and Up vector
Right = glm::normalize(glm::cross(Front, WorldUp)); // Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
Up = glm::normalize(glm::cross(Right, Front));
}
LookAt矩阵的生成可以直接用glm提供的函数
glm::mat4 GetViewMatrix(){
return glm::lookAt(Position, Position + Front, Up);
}
glm::LookAt函数需要一个位置、目标和上向量。
这样Camera类就整好了,在主函数中每次渲染的时候获取实时LookAt矩阵和zoom值,生成相应的view矩阵和projection矩阵就可以实现摄像机的移动了。
可以参考我的代码
本文的思路和出现的图来自于 learnopengl-cn.github.io