GAMES101学习记录

本博客用于记录学习GAMES101时碰到的问题与记录

GAMES101的作业实现也会记录在此

同时,完成作业的过程碰到的问题与解决方案也会记录在此,包括但不限于环境配置、库的基本使用、稀奇古怪的bug

Assignment-0

此次作业是配置环境相关内容

由于我在做NJU的icsPA时已经装好了一个Ubuntu的虚拟机,因此这一系列作业都将使用Ubuntu虚拟机完成

下载安装VS Code

用火狐进入VS Code官网,选择.deb文件进行下载,默认应该保存在~/Downloads/目录下

进入对应目录,运行sudo dpkg -i code_1.89.1-1715060508_amd64.deb

这里code_1.89.1-1715060508_amd64.deb是刚下载的.deb文件,不同版本名字不一样,记得替换

配置VS Code

还是跟Windows上的配置一样

设置字体为Consolas(Linux默认这个太难看了),参考链接将 Linux 上的 VSCode 改为 Consolas 字体

下载Consolas字体的具体执行过程如下:

1
2
3
4
5
6
7
>wget https://down.gloriousdays.pw/Fonts/Consolas.zip
>unzip Consolas.zip
>sudo mkdir -p /usr/share/fonts/consolas
>sudo cp consola*.ttf /usr/share/fonts/consolas/
>sudo chmod 644 /usr/share/fonts/consolas/consola*.ttf
>cd /usr/share/fonts/consolas
>sudo mkfontscale && sudo mkfontdir && sudo fc-cache -fv

再设置中文字体为微软雅黑,这个字体可以从Windows系统里偷过来,不过这个字体后缀名是.ttc而不是常见的.ttf

.ttf.ttc都是字体文件的格式,但它们之间有一些区别:

  • .ttf(TrueType Font):这是由Apple和Microsoft共同推出的一种字体文件格式。每个.ttf文件只包含一种字形。凡是支持矢量字体的平台,基本都支持.ttf格式字体。
  • .ttc(TrueType Collection):这是Microsoft开发的一种新的字体格式标准。.ttc文件可以在同一文件结构中包含多个字体,以便更有效地共享轮廓数据,缩减字体文件大小。TTC档会含超过一种字型,例如繁体 Windows 的 Ming.ttc 就包含细明体及新细明体两种字型 (两款字型不同处只是英文固定间距),而 TTF 就只会含一种字型字体文件后缀。TTC是几个TTF合成的字库,安装后字体列表中会看到两个以上的字体。

所以,你看到的微软雅黑字体文件后缀是.ttc,是因为它包含了多种字形。

把微软雅黑下载到Download目录下后,再执行与上文几乎相同的操作:

1
2
3
4
5
sudo mkdir -p /usr/share/fonts/msyh
sudo cp msyh.ttc /usr/share/fonts/msyh/
sudo chmod 644 /usr/share/fonts/msyh/msyh.ttc
cd /usr/share/fonts/msyh
sudo mkfontscale && sudo mkfontdir && sudo fc-cache -fv

字体安装完后,直接在VS Code的设置里搜索“Font Family”,将'Consolas', 'Microsoft YaHei', monospace写入,就完成了这两个字体的设置(还得是visual studio的字体配置,真的好看又优雅,Ubuntu自带的字体太难绷了)

在设置里选上"Format on Save",即在保存时自动排版

安装C++拓展

安装Code Runner插件,并设置"Code-runner: Run In Terminal",即在终端里显示运行结果

安装Eigen

Eigen是本课程使用的线性代数运算库

直接运行sudo apt install libeigen3-dev即可

Eigen库基本用法

这里是Eigen库的官方文档

头文件

首先是头文件#include <eigen3/Eigen/Dense>

Eigen库的基础使用,只需要用到Eigen/Dense这个文件,官方文档的解释是:

#include <Eigen/Dense>

Includes Core, Geometry, LU, Cholesky, SVD, QR, and Eigenvalues header files

<Eigen/Dense>包含了全部基本运算

因为我是Ubuntu系统,安装的是libeigen3-dev,因此需要在/usr/include/eigen3目录下找到Eigen目录

矩阵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <eigen3/Eigen/Dense>
// Includes Core, Geometry, LU, Cholesky, SVD, QR, and Eigenvalues header files

using namespace std;
using namespace Eigen;

int main()
{
// 生成2x2矩阵,未初始化默认为未定义,本机为0
MatrixXd m(2, 2);
// 用(i,j)运算符来访问第i行第j列元素
m(0, 0) = 3;
m(1, 0) = 2.5;
m(0, 1) = -1;
m(1, 1) = m(1, 0) + m(0, 1);
// 可以直接输出到输出流
cout << m << endl;
}

输出结果为

1
2
  3  -1
2.5 1.5

由此可见,MatrixXd是任意维度的矩阵(由倒数第二个字符X表示任意维度,任意指初始化时决定维度),其中每个条目都是双精度的浮点数(由倒数第一个字符d表示double

同理,MatrixXi为任意维度的int矩阵

同时,如果不初始化数值,默认是“未定义”的,在我机子上结果是0

问了bing,如果想生成全零矩阵怎么做:

如果你想创建一个所有元素都为0的矩阵,你可以使用Zero函数

例如:

1
>Eigen::MatrixXd m = Eigen::MatrixXd::Zero(3, 3); // 创建一个3x3的全零矩阵

矩阵要用(i,j)运算符来访问第i行第j列元素

向量

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
#include <iostream>
#include <eigen3/Eigen/Dense>

using namespace std;
using namespace Eigen;

int main()
{
// 随机生成3x3矩阵,随机值在-1到1
MatrixXd m = MatrixXd::Random(3, 3);
m = (m + MatrixXd::Constant(3, 3, 1.2)) * 50;
cout << "m =" << endl
<< m << endl;

// 先初始化
VectorXd v(3);
// 再赋值
v << 1, 2, 3;
cout << "v =" << endl
<< v << endl;

// 矩阵乘法
cout << "m * v =" << endl
<< m * v << endl;
}

输出为:

1
2
3
4
5
6
7
8
9
10
11
12
m =
94.0188 89.844 43.5223
49.4383 101.165 86.823
88.3099 29.7551 37.7775
v =
1
2
3
m * v =
404.274
512.237
261.153

MatrixXd::Random(3, 3)这个函数生成的是一个3x3矩阵,随机值在-11之间

MatrixXd::Constant(3, 3, 1.2)这个函数,生成的是一个3x3矩阵,每一个元素都为双精度浮点数1.2(因此上文中创建一个所有元素都为0的矩阵也可以用这个函数)

根据输出结果可见,向量默认为列向量

如果想实现行向量,就用RowVectorXd实现

总结

简单使用方法目前介绍到此为止,后续实验中如果碰到新的内容会在对应位置记录,现阶段总结如下:

一般都是先初始化矩阵或向量,再用<<运算符赋值,如

1
2
3
4
5
6
7
MatrixXf m(3, 3);
m << 1, 2, 3,
4, 5, 6,
7, 8, 9;

VectorXd v(3);
v << 1, 2, 3;

初始化时可以用对应的函数,如用MatrixXd初始化的矩阵m,可以用MatrixXd对应的函数初始化,如

1
MatrixXf m = MatrixXf::Zero(3, 3);

矩阵和向量可以直接用<<运算符输出到输出流

矩阵和向量都要用()运算符访问元素

作业代码

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <eigen3/Eigen/Dense>
#include <cmath>

using namespace std;
using namespace Eigen;

int main()
{
VectorXd P(3);
P << 2, 1, 1;
MatrixXd RotationMat(3, 3);
double angle = (double)45 / 180 * acos(-1);
RotationMat << cos(angle), -sin(angle), 0,
sin(angle), cos(angle), 0,
0, 0, 1;
MatrixXd TranslateMat(3, 3);
TranslateMat << 1, 0, 1,
0, 1, 2,
0, 0, 1;
P = TranslateMat * RotationMat * P;
cout << P << endl;
}

注意double angle = (double)45 / 180 * acos(-1);这一句,如果45不加(double)就要写成45.0,不然会被识别成int的45,除法结果为0,不符预期

Assignment-1

unzip中文乱码问题

这次作业给了代码框架,于是就需要在Ubuntu下载这次作业

运行如下指令:

1
wget http://games-cn.org/wp-content/uploads/2020/02/Assignment1.zip

就能获得这个zip压缩包,放到对应位置后解压:

1
unzip Assignment1.zip

但是发现输出和最后解压的结果是乱码,我应该安装了中文,只是环境用的英文环境,挺奇怪的

经过一系列查找资料后,发现问题所在:unzip命令没有指定字符集,于是执行unzip -O CP936 Assignment1.zip就能正常实现了

但是如果每次unzip都要加这个那就太麻烦了,于是直接给写进环境变量里:

在环境变量中,指定unzip参数,总是以指定的字符集显示和解压文件

/etc/environment中加入2行

1
2
3
>UNZIP="-O CP936"

>ZIPINFO="-O CP936"

添加完需要重启,才能起效

参考Linux环境变量配置全攻略

Ubuntu 22.04中文拼音输入法

我没想到这个还有点烦人

最后发现是没有重启的原因,有点难绷

参考Ubuntu 22.04 Chinese (simplified) pinyin input support

Ubuntu中切换输入法快捷键:win + 空格

Ubuntu上OpenCV安装

好啊,又折磨了一下午,终于把这个库装好了

比Windows的安装麻烦许多,中途还是踩了一些坑的,最大的坑就是opencv4.pc这个文件

正确的安装过程

主要参考linux下编译安装opencv生成opencv.pc

  • 首先安装cmake和所需依赖环境

    1
    2
    3
    sudo apt-get install cmake
    sudo apt-get install build-essential libgtk2.0-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev
    sudo apt-get install pkg-config
  • 然后去官网下载对应版本的Sources,我安装的版本为4.8.0

    下载完了直接解压即可,cd进入对应目录,如/home/winkyspeed/Libs/opencv-4.8.0

  • 此目录下新建build文件夹,并且进入

    1
    mkdir build && cd build
  • 接下来用cmake命令进行配置

    1
    sudo cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=ON -D CMAKE_INSTALL_PREFIX=/usr/local ..

    其中,-D用于定义变量,使用-D选项来设置CMake的变量,这些变量会在CMake运行时被使用

    CMAKE_BUILD_TYPE=Release指定构件类型为Release版,类似于Windows里Visual Studio里选择Debug模式还是Release模式

    OPENCV_GENERATE_PKGCONFIG=ON这个可太重要了,选了这个选项就代表少踩一个大坑,这个选项选上的时候,编译过程中会自动生成pkg-config的配置文件,即opencv4.cp

    CMAKE_INSTALL_PREFIX=/usr/local指定编译结果安装到/usr/local目录下,这也是默认的编译路径

    ..:最后还有两个点,指向当前目录的父目录,表示CMake会在父目录中查找CMakeLists.txt文件

  • 进行编译,执行如下指令

    1
    sudo make -j8

    指定多线程编译,会稍微快一点

    这里需要编译一段时间,耐心等待即可

  • 进行安装,执行如下指令

    1
    sudo make install
  • 配置环境

    • 用文本编辑器(如VS Code或vim)打开/etc/ld.so.conf,在文件最后一行添加:

      1
      include /usr/local/lib

      添加的这个路径就是OpenCV的lib库,即动态链接库

      添加完保存退出,再运行如下指令来更新缓存

      1
      sudo ldconfig

      在Linux系统中,/etc/ld.so.conf文件记录了编译时使用的动态库的路径,也就是加载.so库的路径。默认情况下,编译器只会使用/lib/usr/lib这两个目录下的库文件。如果你的库文件安装在其他路径(比如/usr/local/lib),那么你需要在/etc/ld.so.conf文件中添加这个路径。

      当你在/etc/ld.so.conf文件中添加新的库路径后,你需要运行ldconfig命令来更新动态链接库的缓存。

    • 用文本编辑器打开/etc/bash.bashrc,在文本末尾添加:

      1
      2
      3
      PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig

      export PKG_CONFIG_PATH

      新添加的这个PKG_CONFIG_PATH环境变量指定了pkg-config工具在指定目录下查找.pc文件,这个文件包含了库的编译和链接信息

      pkg-config的默认路径是:/usr/lib/pkgconfig//usr/share/pkgconfig/,这里我们新添加一个路径:/usr/local/lib/pkgconfig

      因为cmake命令配置时OPENCV_GENERATE_PKGCONFIG=ON这个命令,OpenCV在编译过程中自动生成了这个opencv4.pc文件,如图

      因此,我们需要把这个路径加入pkg-config的查找路径,使之能查找到我们需要的opencv4.pc

      添加完保存退出,再运行如下指令来更新缓存

      1
      source /etc/bash.bashrc
  • 检验是否安装成功

    终端输入如下指令

    1
    pkg-config opencv4 --modversion

    有版本输出,说明安装成功


我经历的折磨

记录最开始未成功的解决方案,成功的解决方案是上文正确的安装过程

最折磨的一点就是opencv4.pc这个文件,我最开始在编译的时候没选OPENCV_GENERATE_PKGCONFIG=ON自动生成,而是在出错之后想着手动写入opencv.pc,但因为库之间是有依赖关系的,所以如果顺序放错也会找不到,最好还是自动生成

主要参考ubuntu安装opencv的正确方法,但是最后那块会出现问题,具体什么问题下文会详细记录,因此又参考在Ubuntu18.04中安装OpenCV4.5这篇文章的部分内容(手动创建opencv.pc及之后部分),部分解决方案参考opencv2在opencv4文件夹内


前面逻辑大体一致,跟着教程走直到最后pkg-config opencv --modversion这一步,我发现报错了:

1
2
3
4
Package opencv was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv.pc'
to the PKG_CONFIG_PATH environment variable
No package 'opencv' found

可能是因为OpenCV在高版本就不自动生成.pc文件了

看评论区有人提到了另一篇教程,手动创建opencv.pc部分,又照着实现,勉强能在控制台输出版本号了,我就以为结束了,殊不知编译代码的时候根本找不到这些链接库,烦死了

然后我再去尝试跑最简单的CV库代码,读取一张图并显示出来,又出问题了:

我们需要包含的头文件<opencv.hpp>/usr/local/include/opencv4/opencv2/目录下

但是正常写代码的时候一般都写#include <opencv2/opencv.hpp>,而搜索的时候只会在/usr/local/include/等目录中搜索,理所当然的搜索不到opencv2目录下的opencv.hpp文件,导致无法正常运行

我就去找了这样的一篇文章,用创建一个“软链接”的方法,在/usr/local/include/目录中创建一个指向opencv2目录的软链接

1
sudo ln -s /usr/local/include/opencv4/opencv2 /usr/local/include/

这样在VS Code中可以正确找到对应的头文件,我就以为问题解决了(但是并没有),不过这个步骤还是挺有用的


为了避免别人再犯一样的错误,我这里把自动生成.pc文件放在这里,如果想手动输入可以参考,但是可能会因为版本号对不上再导致新的问题,还是建议自动生成

1
2
3
4
5
6
7
8
9
10
11
12
13
# Package Information for pkg-config

prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include/opencv4

Name: OpenCV
Description: Open Source Computer Vision Library
Version: 4.8.0
Libs: -L${exec_prefix}/lib -lopencv_gapi -lopencv_highgui -lopencv_ml -lopencv_objdetect -lopencv_photo -lopencv_stitching -lopencv_video -lopencv_calib3d -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_videoio -lopencv_imgcodecs -lopencv_imgproc -lopencv_core
Libs.private: -ldl -lm -lpthread -lrt
Cflags: -I${includedir}

我在知道能自动生成.pc文件后直接去问bing解决措施:

我现在想重新设置cmake,多加一个-D OPENCV_GENERATE_PKGCONFIG=ON 我该怎么做?重装OpenCV吗?


你不需要重新安装OpenCV,只需要重新配置并编译即可。你可以按照以下步骤操作:

  1. 首先,你需要清理之前的构建文件。在你的build目录下执行以下命令:

1
make clean

  1. 然后,你可以重新运行cmake命令,并添加-D OPENCV_GENERATE_PKGCONFIG=ON选项:

1
sudo cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=ON -D CMAKE_INSTALL_PREFIX=/usr/local ..

  1. 最后,重新编译和安装OpenCV:

1
2
sudo make -j8
sudo make install

完成以上步骤后,你应该可以在/usr/local/lib/pkgconfig/目录下找到opencv4.pc文件了。

于是就安装成功了

这件事告诉我们,有时候走错了路或者选错了方向以后,是可以回头重新审视修改的,不需要为了证明在一个方向是错误的还是正确的而一条路走到黑,退后一步也许就会海阔天空

VS Code配置Code Runner运行OpenCV

我是喜欢用Code Runner来跑代码的,如果喜欢手动编译也是可以的

配置这个还是比较容易的(前提是前文的OpenCV安装成功)

OpenCV安装成功的标志是:在终端运行如下指令

1
pkg-config --cflags --libs opencv4

输出如下:

1
2
winkyspeed@VMachine:~$ pkg-config --cflags --libs opencv4
-I/usr/local/include/opencv4 -L/usr/local/lib -lopencv_gapi -lopencv_highgui -lopencv_ml -lopencv_objdetect -lopencv_photo -lopencv_stitching -lopencv_video -lopencv_calib3d -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_videoio -lopencv_imgcodecs -lopencv_imgproc -lopencv_core

就说明成功了


打开VS Code的设置,选择工作区(比如GAMES101,工作区需要手动添加),搜索Code-runner:Executor Map,点击Edit in settings.json

这样就会在工作区目录下创建一个.vscode目录,下面就有一个settings.json文件

cpp一行修改为如下内容:

1
"cpp": "cd $dir && g++ -std=c++11 $fileName -o $fileNameWithoutExt `pkg-config --cflags --libs opencv4` && $dir$fileNameWithoutExt"

就可以了

成功跑通最基础的代码,说明OpenCV库安装成功并且代码能够正常执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main()
{
Mat test = imread("5.jpg");
imshow("test", test);
waitKey(0);
return 0;
}

结果如下:

不得不说我选的图真好

本来没考虑那么多,想着选个石头门的全家福用来测试OpenCV库的,结果第一遍配置完成的时候代码却不能正常跑通

当时就给我整不会了,这可是最基本的代码啊,而且我为了装这个库已经花了一个下午的时间了

所以当我解决全部问题,终于跑出来这个结果时,看到了蛋糕上的おめでとう,就感觉这一切值得了(虽然桶子的脑袋只留下了一半)

VS Code找不到头文件

如果#include <opencv2/opencv.hpp>这一行爆红了,应该是找不到头文件的问题

如果按照上文正确配置了,代码应该是能正常运行的,即编译器在编译时能找到文件,但是编辑器没找到文件

正常使用时需要包含的头文件<opencv.hpp>/usr/local/include/opencv4/opencv2/目录下

搜索的时候只会在/usr/local/include/等目录中搜索,理所当然的搜索不到opencv2目录下的opencv.hpp文件,导致无法正常运行

解决方法也很容易,有以下两种

软链接

我找到了这样一篇文章,用创建一个“软链接”的方法,在/usr/local/include/目录中创建一个指向opencv2目录的软链接

1
sudo ln -s /usr/local/include/opencv4/opencv2 /usr/local/include/

这样在VS Code中可以正确找到对应的头文件


设置VS Code工作区

前文中也已经对这个工作区的Code Runner进行自定义过了,因此也可以再自定义include路径

  • 打开VSCode的工作区设置(.vscode/c_cpp_properties.json文件),如果这个文件不存在,可以通过Ctrl+Shift+P打开命令面板,然后输入C/C++: Edit Configurations (JSON)来创建

  • c_cpp_properties.json文件中,找到includePath字段,然后添加OpenCV头文件的路径,即/usr/local/include/opencv4

    1
    2
    3
    4
    "includePath": [
    "${workspaceFolder}/**",
    "/usr/local/include/opencv4"
    ],

这样,VSCode也可以找到opencv2目录,进而找到文件

作业代码

还是比较容易

最终的算法是viewport * Ortho * Pers->Ortho * ModelView * 原始坐标,依次左乘一系列矩阵得到最终的答案

参考关于透视投影矩阵的问题

基础实现

旋转矩阵:

按照旋转矩阵,得到绕XYZ三个轴旋转的结果如下

我在参数里多加了一个int axis,默认赋值为0,指明绕Z, Y, X哪个轴旋转

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
// 绕XYZ三个轴旋转
Eigen::Matrix4f get_model_matrix(float rotation_angle, int axis = 0)
{
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

// TODO: Implement this function
// Create the model matrix for rotating the triangle around the Z axis.
// Then return it.
double angle = rotation_angle / 180 * MY_PI;
Eigen::Matrix4f Rotation;

if (axis == 0)
{
// Z axis
Rotation << cos(angle), -sin(angle), 0, 0,
sin(angle), cos(angle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
}
else if (axis == 1)
{
// Y axis
Rotation << cos(angle), 0, sin(angle), 0,
0, 1, 0, 0,
-sin(angle), 0, cos(angle), 0,
0, 0, 0, 1;
}
else
{
// X axis
Rotation << 1, 0, 0, 0,
0, cos(angle), -sin(angle), 0,
0, sin(angle), cos(angle), 0,
0, 0, 0, 1;
}

model = Rotation * model;
return model;
}

再看投影矩阵函数

需要根据FOV和宽高比计算出对应大小的立方体,再进行相应计算

我这里是用水平FOV进行计算的

有一点需要注意:GAMES101课程中讲的zNearzFar都是负数

但是主函数调用时代入参数为正数:r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

因此,如果按课程内容进行运算时,就要乘-1

还有一点需要注意:计算对应坐标时,用到了zNear长度,故需要调用abs()函数求绝对值,不然即使改了zNearzFar都是负数以后,也仍然会出现倒三角的情况

代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
// Students will implement this function

Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();

// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.

// GAMES101课程中讲的zNear和zFar都是负数
// 但是代入的值都是正数
// 因此按课程内容进行运算时,就要乘-1
zNear *= -1;
zFar *= -1;

// 透视矩阵
Eigen::Matrix4f Persp;
Persp << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear * zFar,
0, 0, 1, 0;

// 通过fov和宽高比例计算出l, r, b, t
// aspect_ratio就是宽高比
float left, right, bottom, top;
float angle = eye_fov / 180 * MY_PI;
// 注意课程内容里是用长度,计算时要加 abs 算绝对值
left = -tan(angle / 2) * abs(zNear);
right = tan(angle / 2) * abs(zNear);
// 如果用如下代码(即不计算绝对值),则会导致倒三角的情况
// **************************************
// left = -tan(angle / 2) * zNear; *
// right = tan(angle / 2) * zNear; *
// **************************************
bottom = left / aspect_ratio;
top = right / aspect_ratio;

// 位移矩阵
Eigen::Matrix4f translate;
translate << 1, 0, 0, -(left + right) / 2,
0, 1, 0, -(top + bottom) / 2,
0, 0, 1, -(zNear + zFar) / 2,
0, 0, 0, 1;
// 缩放矩阵
Eigen::Matrix4f scale;
scale << 2 / (right - left), 0, 0, 0,
0, 2 / (top - bottom), 0, 0,
0, 0, 2 / (zNear - zFar), 0,
0, 0, 0, 1;
projection = scale * translate * Persp * projection;

return projection;
}

编译运行

第一次动手用cmake编译自己的项目,因此记录

main.cpp同级目录下(即“代码框架”目录下)运行以下指令:

1
2
3
4
5
mkdir build     // 创建build文件夹用于保留工程文件
cd build // 进入build文件夹
cmake .. // 通过提供CMakeLists.txt文件的路径(父目录)作为参数来运行CMake
make −j8 // 通过make编译代码, −j8 表示8个内核进行并行化编译
./Rasterizer // 运行代码

结果如下:

按A键三角形逆时针旋转,按D键三角形顺时针旋转


进阶实现——任意轴

这个也不是很难,不过需要注意“任意轴”是任意方向,并且过任意点

“任意轴”的方向可以用一个Vector3f给定,“任意点”的位置同理

罗德里德斯旋转公式默认是绕过原点(0,0,0)的轴旋转的

因此需要在旋转前把这个任意点移动到原点,再绕原点旋转,再把点移动回去,就能得到结果

具体做法是:Translate_back * Rotation * Translate

有一点需要注意:按照罗德里德斯旋转公式得到的结果是一个3x3的矩阵,变换要考虑齐次坐标,即变成4x4的矩阵


Rodrigues’ Rotation Formula 如下:

注意,罗德里德斯旋转公式的旋转轴要是一个单位向量,因此需要做正则化处理

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
44
45
46
47
48
// 绕任意轴旋转任意角度
// 任意轴的定义方法:过任意点的任意向量(不一定是单位向量,指明方向即可)
// 代入的方向与点都是三维的,不需要考虑齐次坐标
Eigen::Matrix4f get_model_matrix_arbitrary_axis(Eigen::Vector3f axis_direction,
Eigen::Vector3f point,
float rotation_angle)
{
// 先将这个向量过的点移回原点
Eigen::Matrix4f Translate;
Translate << 1, 0, 0, -point(0),
0, 1, 0, -point(1),
0, 0, 1, -point(2),
0, 0, 0, 1;
// 再旋转相应角度,运用罗德里格斯旋转公式
// 方向向量正则化
axis_direction.normalize();
// 计算角度
float theta = rotation_angle / 180 * MY_PI;
// 计算cos和sin
float cos_theta = cos(theta);
float sin_theta = sin(theta);
// 根据公式计算旋转矩阵,公式来源: GAMES101_Lecture_04 P10
// Rodrigues’ Rotation Formula: Rotation by angle theta around axis n
// R(n, theta) = cos(theta) * I + (1 - cos(theta)) * n * n.T + sin(theta) * N
// N = { 0, -nz, ny,
// nz, 0, -nx,
// -ny, nx, 0 }
// 注意:这个矩阵算出来是3x3的,最后要加齐次坐标变成4x4的旋转矩阵
Matrix3f N;
N << 0, -axis_direction(2), axis_direction(1),
axis_direction(2), 0, -axis_direction(0),
-axis_direction(1), axis_direction(0), 0;
Matrix3f Rotation_3x3;
Matrix3f Eye = Matrix3f::Identity();
Rotation_3x3 = cos_theta * Eye + (1 - cos_theta) * axis_direction * axis_direction.transpose() + sin_theta * N;
// 将3x3旋转矩阵扩展为4x4旋转矩阵
Matrix4f Rotation_4x4 = Matrix4f::Identity();
Rotation_4x4.block<3, 3>(0, 0) = Rotation_3x3; // 将3x3旋转矩阵嵌入到左上角
// 最后将这个向量过的点移回去
Eigen::Matrix4f Translate_back;
Translate_back << 1, 0, 0, point(0),
0, 1, 0, point(1),
0, 0, 1, point(2),
0, 0, 0, 1;
// 计算最终的4x4矩阵
Eigen::Matrix4f result = Translate_back * Rotation_4x4 * Translate;
return result;
}

对应main函数也需要做修改,把get_model_matrix(angle)注释掉,换成刚刚写的新函数,并且前面指定“轴”和“任意点”的变量

1
2
3
4
5
6
7
// 过任意轴, 指定Y正方向,过(0,0,-2)
Vector3f axis, point;
axis << 0, 1, 0;
point << 0, 0, -2;
...
// r.set_model(get_model_matrix(angle));
r.set_model(get_model_matrix_arbitrary_axis(axis, point, angle));

我指定了绕过(0,0,-2)的,沿Y轴正方向的轴旋转,结果如下:





参考资料

GAMES101

将 Linux 上的 VSCode 改为 Consolas 字体

Eigen库的官方文档

ubuntu下中文乱码解决方案(全)

Ubuntu 22.04 Chinese (simplified) pinyin input support

Linux环境变量配置全攻略

ubuntu安装opencv的正确方法

在Ubuntu18.04中安装OpenCV4.5

opencv2在opencv4文件夹内

linux下编译安装opencv生成opencv.pc

关于透视投影矩阵的问题