Xungerrrr's Blog

Camera

Compute Graphics - Homework 5

Word count: 1.4kReading time: 6 min
2019/04/17 Share

投影(Projection)

把上次作业绘制的cube放置在(-1.5, 0.5, -1.5)位置,要求6个面颜色不一致

因为放置在(-1.5, 0.5, -1.5)时透视投影效果不明显,我将立方体放置在了(-3.5, 2.5, -1.5)位置。用glm::translate即可实现平移。

1
model = glm::translate(model, glm::vec3(-3.5, 2.5f, -1.5f));

正交投影(orthographic projection):实现正交投影,使用多组(left, right, bottom, top, near, far)参数,比较结果差异

先设计一个GUI交互界面,可以选择投影类型,并输入不同的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
float left = -12.0f, right = 12.0f, bottom = -9.0f, top = 9.0f, nearDist = 0.1f, farDist = 100.0f;
ImGui::Begin("Projection Type");
ImGui::BeginGroup();
ImGui::RadioButton("Orthographic", &projectionType, 1);
ImGui::PushItemWidth(60);
if (projectionType == 1) {
ImGui::InputFloat("Left", &left);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Right", &right);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Bottom", &bottom);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Top", &top);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Near", &nearDist);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Far", &farDist);
projection = glm::ortho(left, right, bottom, top, nearDist, farDist);
}
ImGui::RadioButton("Perspective", &projectionType, 2);
if (projectionType == 2) {
ImGui::InputFloat("FOV", &fov);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Aspect Ratio", &ratio);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Near", &nearDist);
ImGui::SameLine(0.0f, 10.0f);
ImGui::InputFloat("Far", &farDist);
projection = glm::perspective(fov, ratio, nearDist, farDist);
}
ImGui::EndGroup();
ImGui::End();

效果:

多组参数的结果:

改变left、right、bottom和top,这决定了视口的左右范围和上下范围。数值范围越大,显示的范围也越大,物体也就越小。

如果长宽比与窗口相同,则显示的内容与真实的比例相同,否则会发生比例的变化。

near和far参数是显示的最近距离和最远距离。摄像机的z坐标是15,立方体中心的z坐标是-1.5,立方体的边长为4,立方体的正面离摄像机的距离是14.5。所以当把near设为15时,立方体的正面便不会显示出来,而显示立方体的背面。

透视投影(perspective projection):实现透视投影,使用多组参数,比较结果差异

多组参数的结果:

fov代表了视角大小,视角越小,看到的范围越小,物体就越大。

aspect ratio代表了宽高比,如果与窗口相同,则显示的内容与真实的比例相同,否则会发生比例的变化。

near和far决定了显示的前后范围。当把far设为17时,立方体的后半部分不显示。

当把near设为15时,立方体的前半部分不显示。

视角变换(View Changing)

把cube放置在(0, 0, 0)处,做透视投影,使摄像机围绕cube旋转,并且时刻看着cube中心

使摄像机位置始终在半径为15的圆周上,可以用三角函数实现:

1
2
3
4
5
6
7
if (ImGui::RadioButton("View Changing", &function, 2)) {
rotateTime = (float)glfwGetTime();
}
float time = (float)glfwGetTime();
float radius = 15.0f;
float camX = sin(time - rotateTime) * radius;
float camZ = cos(time - rotateTime) * radius;

使用摄像机位置,创建LookAt矩阵,令摄像机始终看着(0, 0, 0)。投影矩阵使用透视投影。

1
2
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));
projection = glm::perspective(glm::radians(45.0f), (float)windowWidth / windowHeight, 0.1f, 100.0f);

效果:

在现实生活中,我们一般将摄像机摆放的空间View matrix和被拍摄的物体摆设的空间Model matrix分开,但是在OpenGL中却将两个合二为一设为ModelView matrix,通过上面的作业启发,你认为是为什么呢?

因为OpenGL本身并没有摄像机这个概念,我们的摄像机,只是通过将场景中的物体往相反的方向移动模拟出来的。由于实际上只有一个坐标系统,坐标变换本质上都是在物体上进行,所以将View和Model矩阵合并成ModelView矩阵。而在多个摄像机的情况下,只需定义不同的ModelView矩阵,就可以实现摄像机的切换,不需要在多个坐标系统之间进行切换,实现起来比较方便。

Bonus:实现一个camera类,能够使用键盘和鼠标控制视角,实现类似FPS的游戏场景

类的头文件定义如下,具体实现请看源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// default camera values
const float YAW = -90.0f;
const float PITCH = 0.0f;
const float FOV = 45.0f;

class Camera {
public:
// Constructor with vectors
Camera(glm::vec3 _position = glm::vec3(0.0f, 0.0f, 15.0f),
glm::vec3 _front = glm::vec3(0.0f, 0.0f, -1.0f),
glm::vec3 _up = glm::vec3(0.0f, 1.0f, 0.0f),
float _yaw = YAW, float _pitch = PITCH);
// Returns the view matrix
glm::mat4 getViewMatrix();
// Move the camera
void moveForward(float distance);
void moveBack(float distance);
void moveRight(float distance);
void moveLeft(float distance);
// Rotate the camera using pitch and yaw
void rotate(float _pitch, float _yaw);
// Change the fov to zoom
void zoom(float yoffset);
// Get the fov
float getFov();
// Reset the camera
void reset();

private:
// Calculates the front vector from the Camera's (updated) Euler Angles
void updateCameraVectors();
// Camera Attributes
glm::vec3 position;
glm::vec3 front;
glm::vec3 up;
glm::vec3 right;
glm::vec3 world_up;
// Euler Angles
float yaw;
float pitch;
// Camera options
float fov;
};

检测键盘输入,调用Camera类的四个移动方法,实现摄像机的移动:

1
2
3
4
5
6
7
8
9
float cameraSpeed = 20.0f * deltaTime;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.moveForward(cameraSpeed);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.moveBack(cameraSpeed);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.moveLeft(cameraSpeed);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.moveRight(cameraSpeed);

在鼠标回调函数中,调用Camera类的rotate方法,实现视角的转动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
camera.rotate(yoffset, xoffset);
}

在滚动的回调函数中,调用Camera类的zoom方法,实现放大和缩小:

1
2
3
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
camera.zoom(yoffset);
}

渲染的时候,通过Camera类获取View Matrix和FOV:

1
2
view = camera.getViewMatrix();
projection = glm::perspective(camera.getFov(), (float)windowWidth / windowHeight, 0.1f, 100.0f);

实现效果:

CATALOG
  1. 1. 投影(Projection)
    1. 1.1. 把上次作业绘制的cube放置在(-1.5, 0.5, -1.5)位置,要求6个面颜色不一致
    2. 1.2. 正交投影(orthographic projection):实现正交投影,使用多组(left, right, bottom, top, near, far)参数,比较结果差异
    3. 1.3. 透视投影(perspective projection):实现透视投影,使用多组参数,比较结果差异
  2. 2. 视角变换(View Changing)
    1. 2.1. 把cube放置在(0, 0, 0)处,做透视投影,使摄像机围绕cube旋转,并且时刻看着cube中心
  3. 3. 在现实生活中,我们一般将摄像机摆放的空间View matrix和被拍摄的物体摆设的空间Model matrix分开,但是在OpenGL中却将两个合二为一设为ModelView matrix,通过上面的作业启发,你认为是为什么呢?
  4. 4. Bonus:实现一个camera类,能够使用键盘和鼠标控制视角,实现类似FPS的游戏场景