
简介这是GAMES101课程作业0的PDF文档由闫令琪老师授课的计算机图形学与混合现实研讨会配套讲义面向选修该课程或自学GAMES101的计算机图形学学习者。文档重点解决作业环境中虚拟机的安装与使用问题基于Oracle VM VirtualBox从虚拟硬盘下载、虚拟机配置、Guest Additions增强功能安装到作业框架传输与编辑均有逐步说明适合刚接触Linux虚拟机或需要统一作业环境的初学者。资源为单份PDF文件体积仅874KB方便快速查阅与对照操作目前已有433人学习下载。内容还包含Visual Studio Code与Sublime开发工具介绍、C常用语法与常见错误解释如编译错误、段错误、总线错误等以及Eigen线性代数库的使用提示结构清晰、排错思路明确可作为完成作业0前的速查手册帮助读者高效搭建并熟悉作业环境。1. 拿到 Games101 作业0 PDF 之后真正的关卡其实是环境配置如果你刚把 Games101 作业0 的 PDF 下下来第一反应多半是这也叫作业写几行矩阵运算、把一张图片翻个方向撑死三四十分钟。但真实情况是很多第一次接触图形学课程的同学恰恰是卡在环境上——不是题目难而是 PDF 里默认你已经准备好了 Linux、CMake、Eigen 头文件和一个能看 PNG 的看图工具。每一步单独看都不复杂串起来却能把人折腾一晚上。这篇文章就顺着这份作业0 PDF 的脉络把环境选型、工程搭建、代码实现和排障过程完整过一遍让不熟悉 Linux 的同学也能在半天内跑通并且为后续几次作业铺好路。2. 环境选型为什么作业0 要求 Linux虚拟机与 WSL 怎么选2.1 图形学作业为什么绕不开 Linux作业0 并不是一门纯手算课程它要求你写 C、用 CMake 构建、调用 Eigen 做矩阵运算还要处理一张 PNG 图片的读写和坐标变换。Linux 生态对这条链路几乎是“原生支持”的编译器、构建工具、头文件库、图像查看器全部来自系统仓库一条 apt 命令就能装齐不存在配环境变量和折腾 IDE 工程配置的问题。另一个现实因素是后续作业。第 3 次作业开始要加载三维模型、做 MVP 变换第 4 次作业涉及纹理映射和着色器这些任务在 Linux 下的编译问题最少。常见做法是在 Ubuntu 的长期支持版本上做开发把一个个 C 工程串起来遇到问题也更容易在社区里搜到相同环境的答案。有人会问 Windows 下面装 Visual Studio 行不行。技术上当然可行但你要自己处理 Eigen 的包含路径、OpenCV 的二进制分发、还有不同编译器的 ABI 差异这些都属于“很花时间但对图形学没有帮助”的事情。作业0 的 PDF 建议你用 Linux本质上不是强制而是帮你避开最没价值的时间损耗。2.2 虚拟机与 WSL 的取舍跑 Linux 环境最常见的是两条路虚拟机或者 WSL。如果你手头内存有 16GB 以上我一般会优先推荐虚拟机。用 VirtualBox 或 VMware Workstation Player 装一个 Ubuntu图形界面完整镜像可以随时复制备份折腾坏了直接回滚快照对新手非常友好。缺点是内存开销大宿主机和虚拟机各占一份内存而且文件拖拽偶尔失灵需要额外装增强功能才能顺畅共享剪贴板和文件夹。如果机器内存只有 8GB 左右装虚拟机容易卡顿那可以考虑 WSL。WSL 启动快、和 Windows 文件系统互通直接在 /mnt/c 路径下操作宿主机文件VSCode 也能通过 Remote 扩展直接连进去写代码。新版 WSL 自带 WSLgLinux 图形程序可以直接弹窗显示看图工具也能跑起来。缺点是第一次跑 GUI 程序偶尔会有显示异常显卡透传在部分机器上没默认打开遇到问题需要多花一点时间调。也有同学用云主机做作业0只需要编译和算结果终端里跑完程序把输出图片下载回来看倒也够用。但云主机没有物理显示器实时调图像效果很不方便第 3 次作业之后涉及 OpenGL 窗口和交互操作云主机基本就没法用了。所以我的建议是能用本机虚拟化就优先本机弄好一次后面四五个作业都受益。选型判断很简单CPU 支持虚拟化且内存充足选虚拟机内存紧张或者懒得装 VMware选 WSL。不要在环境选择上花太多时间尽快进入编译和写代码的阶段才是正事。2.3 最小环境清单装什么、不装什么基于作业0 到后续作业的常见需求最小可用的环境清单大概是这样组件用途是否需要说明Ubuntu LTS基准系统必须选择长期支持版本即可build-essentialgcc/g、make必须编译 C 的基础cmake构建工程必须版本身份不低于 3.10libeigen3-devEigen 头文件必须作业0 矩阵计算的核心库nomacs看图工具推荐支持快速缩放和像素坐标查看libopencv-dev图像读写推荐省去手写 PNG 解码的麻烦git保存作业可选方便备份每次作业安装命令合并成一条即可sudo apt update sudo apt install -y build-essential cmake libeigen3-dev nomacs libopencv-dev git这组依赖覆盖了作业0 到作业5 的大部分需求。OpenCV 虽然作业0 不强制但对后续图像读写和可视化很有用建议一次装好。nomacs 看输出图时能直接显示鼠标位置的像素值和坐标这在验证旋转结果时非常好用。这里不建议一上来就装集成 IDE。一个 VSCode 加上终端配合 Remote 或者直接本地编辑已经足够。集成 IDE 的工程配置反而容易盖过 CMake 本身的逻辑让你搞不清楚编译到底是怎么发生的。3. 把作业0 跑通CMake 工程、完整代码与验证输出3.1 先读懂 PDF作业0 要完成的三个目标下载到的作业0 PDF 通常描述了三件事。第一件事是用 Eigen 完成一次二维旋转和平移计算给定一个点、一个旋转角度和一个平移向量输出变换后的坐标。第二件事是读取一张图片把图片绕左上角原点旋转 90 度再平移到可视范围内输出一张新图片。第三件事是把代码和输出图片作为作业结果提交。很多人不看 PDF 就开写代码结果最容易栽在物理单位上旋转角度在手算时是 45 度但 C 里的 cos、sin 接收的是弧度不转就全算错了。所以拿到 PDF 之后先不要急着建工程拿一张纸把坐标变换的式子列出来明确哪个矩阵在前、哪个矩阵在后再动手写代码。3.2 搭建 CMake 工程目录结构与首个编译项目目录结构我习惯于这样组织hw0/ ├── CMakeLists.txt ├── images/ │ ├── test.png │ └── flip.png └── main.cppimages 目录放输入输出图片main.cpp 放作业代码CMakeLists.txt 管构建。这个结构同样适合后续几次作业每次新建一个目录、改一改 CMakeLists 里的项目名就行。CMakeLists.txt 的内容如下cmake_minimum_required(VERSION 3.10) project(Games101_HW0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(OpenCV REQUIRED) add_executable(hw0 main.cpp) target_link_libraries(hw0 PRIVATE ${OpenCV_LIBS}) target_include_directories(hw0 PRIVATE /usr/include/eigen3)这里最需要注意的一点我没有用 find_package(Eigen3)因为 Ubuntu 通过 apt 安装的 libeigen3-dev 在很多版本里并没有导出官方的 CMake 配置文件find_package 经常找不到库这是作业0 里的一个经典坑。直接通过 target_include_directories 把 /usr/include/eigen3 塞进头文件搜索路径是最简单可靠的做法。进入项目目录后执行编译cd hw0 mkdir -p build cd build cmake .. make -j$(nproc)为什么需要单独的 build 目录因为 CMake 会把一堆中间产物放在当前目录直接放进项目源码目录会污染代码结构。用 build 目录隔离之后想看编译日志、删掉重来都很干净。make 后面的 -j$(nproc) 是让编译器用满 CPU 核心数并行编译作业0 只有一个源文件感觉不出差异但后面作业文件多了能明显加快速度。3.3 核心代码旋转矩阵与像素重映射main.cpp 的完整实现如下代码里把计算题和图像题分成了两段#include Eigen/Core #include Eigen/Dense #include opencv2/opencv.hpp #include iostream #include cmath int main() { // 计算题点 (2,1) 先绕原点逆时针旋转 45 度再平移 (1,2) Eigen::Vector3d p(2.0, 1.0, 1.0); double theta 45.0 * M_PI / 180.0; Eigen::Matrix3d R; R std::cos(theta), -std::sin(theta), 0.0, std::sin(theta), std::cos(theta), 0.0, 0.0, 0.0, 1.0; Eigen::Matrix3d T; T 1.0, 0.0, 1.0, 0.0, 1.0, 2.0, 0.0, 0.0, 1.0; Eigen::Vector3d ans T * R * p; std::cout ans: ans.transpose() std::endl; // 图像题把 test.png 绕图像左上角原点逆时针旋转 90 度再平移到可视范围 cv::Mat img cv::imread(../images/test.png); if (img.empty()) { std::cerr failed to load image std::endl; return -1; } int w img.cols; int h img.rows; // 旋转后宽高互换新图行数为原宽度列数为原高度 cv::Mat dst(w, h, img.type()); for (int y 0; y h; y) { for (int x 0; x w; x) { // 像素坐标 (x, y)x 向右y 向下 // 绕原点逆时针旋转(x, y) - (-y, x) // 加上平移 (h-1, 0) 让所有坐标落在非负范围内 int nx h - 1 - y; int ny x; dst.atcv::Vec3b(ny, nx) img.atcv::Vec3b(y, x); } } cv::imwrite(../images/flip.png, dst); std::cout flip.png saved std::endl; return 0; }逻辑说明分两部分。计算题里我把点用齐次坐标表示成 Vector3d旋转矩阵和平移矩阵都是 3x3 的齐次矩阵这样一次矩阵乘法就能同时完成旋转和平移不需要先带外一个 Vector2d 再加偏移。先旋转再平移所以代码里是 T * R * p顺序不能反。图像题里最容易混淆的是坐标方向和矩阵维度。OpenCV 的 Mat 构造函数是 (rows, cols)rows 是行数也就是图像高度cols 是列数也就是图像宽度。原图宽 w 高 h绕左上角原点逆时针转 90 度后新图的宽度变成原来的高度 h高度变成原来的宽度 w所以目标矩阵要创建成 dst(w, h)。像素遍历时外层循环 y 表示原图行号内层 x 表示原图列号。旋转后的新坐标 nx 取 h - 1 - y而不是 h - y原因是图像坐标左上角为原点原图最后一行的 y 等于 h - 1如果不减 1对应列号会越界。这个“差一”错误会直接导致靠近图像底部的像素写到错误的列上输出图靠近左边缘的部分会出现一条错位的色带。计算题的等价手算参考值可以这样验证点 (2,1) 旋转 45 度后约为 (0.7071, 2.1213)再平移 (1,2) 得到约 (1.7071, 4.1213)。程序输出和这个数字对上说明矩阵构造没问题。3.4 编译、运行与肉眼验证编译完成后运行cd build ./hw0终端会先输出 ans: 1.7071 4.1213 1表示计算题通过二进制验证。随后会在 images 目录下生成 flip.png。用 nomacs 打开输出图片可以看到原图的内容被逆时针旋转 90 度并且完整显示在新图里没有黑边、没有截断。建议用 nomacs 的坐标跟随功能把鼠标移到旋转后图像左上角附近观察像素坐标和颜色值的变化这比凭直觉看是否正确更可靠。如果输出图像有黑边请回去检查 dst 的创建尺寸如果输出图只保留了半张图像内容检查平移量是不是写成了 h 而不是 h - 1。这两类问题几乎覆盖了作业0 图像部分的全部报错来源。4. 作业0 的 5 个经典踩坑从 Eigen 头文件到黑边截断4.1 现象CMake 报 “Could not find Eigen3”原因Ubuntu 的 libeigen3-dev 包安装后头文件在 /usr/include/eigen3但很多版本没有提供可供 find_package 查找的 Eigen3Config.cmake 文件。有些同学改用 find_package(Eigen3 REQUIRED)在部分系统上能过换一台机器或换一个 Ubuntu 小版本就失败很玄学。解决放弃 find_package直接在 CMakeLists 里写 target_include_directories(hw0 PRIVATE /usr/include/eigen3)。代码里用#include Eigen/Core而不是#include eigen3/Eigen/Core因为前者在 Eigen 官方文档和所有后续课程代码里都是标准写法后者只有加了那一层 eigen3 子目录才能用。4.2 现象nomacs 打开 flip.png 显示黑色或者程序直接闪退原因输出图片没有成功写入。最常见的是 imread 的路径写错程序在 build 目录下运行而相对路径 ../images/test.png 需要从 build 目录往上一层找到 images如果直接复制代码到别的位置路径就对不上了。另一种可能是图片本身读取为空imread 返回空的 Mat代码里没有做判空处理后面 at 访问直接越界。解决在 imread 之后立刻检查 img.empty()为空时打印错误并退出。输出路径也建议先确认在 build 目录下运行ls ../images/看看 test.png 是否真的存在。如果程序退出码是 0 但图片仍是黑色把 imwrite 的路径改成绝对路径再试一次。4.3 现象旋转后的图像只有一部分内容剩余区域是黑色原因目标矩阵的行列没有互换。原图是 640x480旋转后应当是 480x640但如果你仍然创建了 640x480 的目标图那么循环里 nx 的取值范围 (0 到 h-1) 会超过目标图的列数范围OpenCV 的 at 访问越界后行为就是写入错位或黑边。解决记住一句话OpenCV 的 Mat 参数顺序是 rows 再 cols旋转 90 度后 rows 等于原宽度cols 等于原高度。代码里统一用int new_rows w; int new_cols h;这样的命名来避免混淆。4.4 现象宿主机文件拖不进虚拟机共享目录也挂载不上原因VirtualBox 的增强功能没有正确安装或者当前用户不在 vboxsf 用户组里。虚拟机里默认只有 root 能访问共享文件夹普通用户访问就会被拒绝。解决安装增强功能后在虚拟机里执行一次sudo usermod -aG vboxsf $USER然后注销重新登录。如果还是不行可以不用共享目录直接在虚拟机里用命令行工具拉取文件或者用 VSCode 的 Remote 方式直接编辑虚拟机里的代码避免宿主机和虚拟机来回传文件。4.5 现象旋转结果和手算对不上数值差很多原因角度单位写错。C 的 std::cos 和 std::sin 接收的是弧度直接把 45 传进去实际上是求 cos(45) 而 45 被当成 45 弧度结果完全不对。解决定义角度时永远写double theta 45.0 * M_PI / 180.0;不要手写 0.785 之类的魔法数字。后续作业里涉及三维旋转和欧拉角角度转弧度这一步会反复出现最好在工程头部定义一个constexpr double DEG2RAD M_PI / 180.0;后面直接用 45.0 * DEG2RAD清晰且不容易漏。5. 作业0 之后两个能用到课程结尾的验证习惯作业0 跑通只是起点真正有价值的是把跑通过程中沉淀下来的验证方法带到后面几次作业里。这里分享两个我每次都会用的小技巧。第一个技巧是用 Python 快速双写一遍矩阵计算作为 C 输出的参照。Python 里用 NumPy 做同样的旋转和平移几行就能完成import numpy as np theta np.deg2rad(45) c, s np.cos(theta), np.sin(theta) R np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) T np.array([[1, 0, 1], [0, 1, 2], [0, 0, 1]]) p np.array([2, 1, 1]) print(T R p)这个脚本不需要任何额外配置系统装了 Python 和 NumPy 就能跑。把 C 程序的输出和它对比数字一致说明矩阵构造、乘法顺序、角度单位都没问题。后面作业里做 MVP 变换、旋转矩阵推导时这个脚本稍微改一下就能复用比重新打开计算器靠谱得多。第二个技巧是把作业里手写的旋转矩阵换成 Eigen 的 AngleAxis 再验证一遍。Eigen 提供了一套更高级的旋转表达方式Eigen::AngleAxisd rot(theta, Eigen::Vector3d::UnitZ()); Eigen::Matrix3d R rot.toRotationMatrix();这段代码生成的是绕 Z 轴旋转 theta 角度的旋转矩阵和手写的 2D 旋转矩阵在数值上完全一致。作业0 里可以先用这个方法验证手写矩阵的正确性同时也为第 3 次作业的三维旋转做好铺垫。到了三维场景绕任意轴旋转用 AngleAxis 或者 Rodrigues 公式会比手动展开矩阵快得多而且不容易把符号写反。最后说一个我自己的教训第一次做作业0 时没有先读 PDF直接凭印象写代码结果旋转中心搞错输出图转了 90 度却平移到了图外最后对着黑屏图排查了半个小时才发现是少了一个减 1。后来养成了先整理坐标公式、再动手写代码的习惯后面五次作业基本没再因为矩阵顺序和索引边界翻过车。希望帮到你。本文还有配套的精品资源点击获取