实践项目https://github.com/JackieLong/OpenGL/tree/main/project_camera_test

OpenGL本身并没有摄像机概念,它只是用这个形象的概念来描述一个这样一种视觉现象:我们在3D场景中移动时的视角。

摄像机就相当于我们的“眼睛”。

一、数学模型

它的数学本质,其实就是坐标系变换问题:将“老”坐标系的坐标转换成“新”坐标系中的坐标,其中:

  • “老”坐标系:世界坐标系
  • “新”坐标系:摄像机坐标系
    • 坐标原点:摄像机所在位置
    • X轴正方向:摄像机正右方向
    • Y轴正方向:摄像机正上方向
    • Z轴正方向:摄像机正前方向

“老”坐标系变换成“新”坐标系分两个过程:

  • 平移:“老”坐标系原点平移至“新”坐标系原点
  • 旋转:“老”坐标系坐标轴旋转到“新”坐标系坐标轴。

我们需要解决的问题是,求出该变换矩阵(OpenGL_视图变换矩阵 - 图1齐次矩阵,实现线性变换+平移变换)。

该问题数学模型如下:
设在世界坐标系中,摄像机坐标OpenGL_视图变换矩阵 - 图2,摄像机朝向坐标原点OpenGL_视图变换矩阵 - 图3,求世界坐标系任意坐标OpenGL_视图变换矩阵 - 图4在摄像机坐标系中的坐标OpenGL_视图变换矩阵 - 图5。摄像机坐标系定义如下:

  • 坐标原点:摄像机位置,设为OpenGL_视图变换矩阵 - 图6
  • X轴正方向:摄像机的正右方,设其基向量为OpenGL_视图变换矩阵 - 图7(在世界坐标系中的值,下同)
  • Y轴正方向:摄像机的正上方,设其基向量为OpenGL_视图变换矩阵 - 图8
  • Z轴正方向:摄像机的正前方,设其基向量为OpenGL_视图变换矩阵 - 图9

计算过程如下:
这里先从一个特殊情况考虑,使问题简单化,假设摄像机一直是正放着,摄像机没有沿着它的中轴转动。
Z轴正方向基向量OpenGL_视图变换矩阵 - 图10
设世界坐标系中的“正向上”向量OpenGL_视图变换矩阵 - 图11。根据向量叉乘的几何特性,则可以得到X轴基向量OpenGL_视图变换矩阵 - 图12。再通过OpenGL_视图变换矩阵 - 图13轴基向量叉乘得到OpenGL_视图变换矩阵 - 图14轴基向量OpenGL_视图变换矩阵 - 图15,令矩阵OpenGL_视图变换矩阵 - 图16,则OpenGL_视图变换矩阵 - 图17是一个平移变换矩阵,平移变换效果为OpenGL_视图变换矩阵 - 图18,即相当于将世界坐标系原点平移到摄像机位置。
OpenGL_视图变换矩阵 - 图19,则OpenGL_视图变换矩阵 - 图20是一个旋转变换矩阵,其变换效果相当于将世界坐标OpenGL_视图变换矩阵 - 图21轴旋转到摄像机坐标系的OpenGL_视图变换矩阵 - 图22轴,综上则有:OpenGL_视图变换矩阵 - 图23

LookAt矩阵

从上面我们已经知道如何将坐标变换到摄像机视角下的坐标的。结合实际,我们可以定义这样一个非常有用的矩阵,LookAt矩阵,它的作用就是字面意思,创建一个看着(Look at)给定目标的观察矩阵(将坐标变换到指定坐标系下),LookAt矩阵定义如下:
OpenGL_视图变换矩阵 - 图24
OpenGL_视图变换矩阵 - 图25OpenGL_视图变换矩阵 - 图26轴正方向基向量。
OpenGL_视图变换矩阵 - 图27OpenGL_视图变换矩阵 - 图28轴正方向基向量。
OpenGL_视图变换矩阵 - 图29OpenGL_视图变换矩阵 - 图30轴正方向基向量。
OpenGL_视图变换矩阵 - 图31:摄像机坐标。

为什么是有红色的OpenGL_视图变换矩阵 - 图32号,因为在实践当中,我们并不是移动摄像机,而是将场景“反方向”移动。
把这个LookAt矩阵作为视图矩阵(View Matrix)可以很高效地把所有世界坐标变换到刚刚定义的观察空间。

二、程序设计

当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。

从上面我们可以知道,定义一个摄像机其实就是定义一个坐标系,定义一个坐标系我们至少需要三个条件:

  • 坐标原点位置
  • x、y、z轴中的任意两个,第三个可以通过叉乘得到。

要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。
OpenGL_视图变换矩阵 - 图33

摄像机位置

摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。

  1. // 这里使用openGL库glm(openGL math库)
  2. glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); // 摄像机坐标位置
  3. // z轴正方向是指向屏幕前的你,所以摄像机远离屏幕,则要增大z分量值。

摄像机方向

指的是摄像机指向哪个方向。我们让摄像机指向场景原点:(0, 0, 0)。通过向量减法,可以得到摄像机的指向向量。
OpenGL_视图变换矩阵 - 图34OpenGL_视图变换矩阵 - 图35:摄像机指向的目标位置。
用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向,但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:

  1. // 摄像机指向坐标原点
  2. glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
  3. // 摄像机的指向的反方向向量。
  4. glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
  5. // 摄像机的指向向量。
  6. // glm::vec3 cameraDirection = glm::normalize(cameraTarget - cameraPos);

向右轴

还需要一个右向量(Right Vector),它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:先定义一个世界坐标系中的向上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量):

  1. glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); // 世界坐标系中的向上向量。
  2. glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection)); // 叉乘得到X轴正方向基向量。

向上轴

x轴向量和z轴向量叉乘得到正y轴向量。

  1. glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight); // x、z轴叉乘得到y轴。

创建LookAt

我们已经知道LookAt矩阵的数学计算过程,OpenGL库glm已经帮我们完成了这个计算,只需要提供初始数据即可:

  1. glm::mat4 view(1.0f);
  2. view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f) // 摄像机位置
  3. glm::vec3(0.0f, 0.0f, 0.0f) // 摄像机的拍摄目标target位置
  4. glm::vec3(0.0f, 1.0f, 0.0f)); // 世界坐标系中的向上向量