概述

本节核心就是一张图:

变换

局部空间(Local Space)

局部空间是物体所在的空间,比如在DCC软件中创建的模型所在的空间.

世界空间(World Space)

世界空间是全局坐标系,为世界中的所有物体提供一个坐标系.

观察空间(View Space)

观察空间是摄像机所在的空间,它是相机的视角,是摄像机看到的世界的局部空间.

裁剪空间(Clip Space)

在我们的着色器中,我们希望坐标都能落在一个特定的范围,否则在着色器运算中会增加很多复杂的步骤,因此需要对观察空间进行裁剪变换到NDC空间.

应用

glm库已经为我们实现好了计算这些变换矩阵的方法:

glm::mat4 model = glm::mat4(1.0f); // 模型矩阵
glm::mat4 view = glm::mat4(1.0f); // 观察矩阵
glm::mat4 projection = glm::mat4(1.0f); // 投影矩阵

model = glm::translate(model, glm::vec3(0.0f, 0.0f, -3.0f)); // 平移
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, 1.0f, 0.0f)); // 观察
projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f); // 投影

接下来只要把mvp矩阵传给着色器就可以变换我们的正方形了.

但是在此之前,我们先升级我们的正方形为立方体.

float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,

-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,

-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,

0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,

-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,

-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};

然后我们会得到类似的效果:

oops! 这看起来十分奇怪,原因是我们没有开启z-buffer,所以OpenGL并不知道哪些物体应该在前面,哪些物体应该在后面.

glEnable(GL_DEPTH_TEST); // 开启深度测试

再次运行程序,我们会得到正确的渲染效果:

我们可以通过进行多次drawCall来实现渲染多个立方体.

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);
}

但是过多的DrawCall会导致性能问题,如何减少DrawCall就成为了一个主流的优化方向.主流思想就是一次打包尽可能多的物体为一个batche,然后一次性渲染.

摄像机

我们在前文中会发现一个重要的存在,但我们至今仍未提起–摄像机.

可以发现,VP矩阵都和摄像机有关,因此我们可以创建一个摄像机类.

Camera.hpp:

/*
* @Author: Vanish
* @Date: 2024-09-14 13:51:13
* @LastEditTime: 2024-09-14 15:58:34
* Also View: http://vanishing.cc
* Copyright@ https://creativecommons.org/licenses/by/4.0/deed.zh-hans
*/
#pragma once

#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"

class Camera
{
public:
glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 forward = glm::vec3(0.0f, 0.0f, 1.0f);
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 right = glm::vec3(1.0f, 0.0f, 0.0f);

int imgWidth, imgHeight;
double fov;
double nearZ = 0.1, farZ = 1000.0;
double aspectRatio;
public:
Camera(const int &width, const int &height,const int &fov)
{
imgWidth = width;
imgHeight = height;
this->fov = fov;

aspectRatio = (double)imgWidth / (double)imgHeight;
}
~Camera() {}

public:
glm::mat4 GetViewMatrix() const;
glm::mat4 GetProjectionMatrix() const;

public:
void SetPosition(const glm::vec3 &pos);
void SetForward(const glm::vec3 &forward);

public:
void Move(const glm::vec3 &moveDirAndDist);
void Rotate(const float &yaw, const float &pitch);
void Zoom(const float &zoomFOV);

};

Camera.cpp:

/*
* @Author: Vanish
* @Date: 2024-09-14 13:51:18
* @LastEditTime: 2024-09-14 16:04:00
* Also View: http://vanishing.cc
* Copyright@ https://creativecommons.org/licenses/by/4.0/deed.zh-hans
*/
#include "Camera/Camera.hpp"


glm::mat4 Camera::GetViewMatrix() const
{
return glm::lookAt(position, position + forward, up);
}
glm::mat4 Camera::GetProjectionMatrix() const
{
return glm::perspective(glm::radians(fov), aspectRatio, nearZ, farZ);
}
void Camera::SetPosition(const glm::vec3 &position)
{
this->position = position;
}
void Camera::SetForward(const glm::vec3 &forward)
{
this->forward = forward;
this->right = glm::normalize(glm::cross(forward, up));
}
void Camera::Move(const glm::vec3 &moveDirAndDist)
{
this->position += moveDirAndDist;
}
void Camera::Rotate(const float &yaw, const float &pitch)
{
glm::mat4 rotationMatrix(1.0f);
rotationMatrix = glm::rotate(rotationMatrix, glm::radians(pitch), glm::vec3(1.0f, 0.0f, 0.0f));
rotationMatrix = glm::rotate(rotationMatrix, glm::radians(yaw), glm::vec3(0.0f, 1.0f, 0.0f));
this->position = glm::vec3(rotationMatrix * glm::vec4(this->position, 1.0f));
}
void Camera::Zoom(const float &zoomFOV)
{
this->fov += zoomFOV;
}

这样子我们就可以简化main中的代码了.

同时通过接受输入,可以调用Camera的成员函数来控制摄像机的移动,旋转,缩放.比如:

if(glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.Move(camera.forward * 0.02f);
if(glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.Move(camera.forward * -0.02f);
if(glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.Move(camera.right * -0.02f);
if(glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.Move(camera.right * 0.02f);