☰
Games101作业0实战指南:Ubuntu虚拟机+Eigen齐次变换快速上手
2026/10/27 2:13:37 网站建设 项目流程

简介:本资源是GAMES101《计算机图形学与混合现实研讨会》课程2020年春季学期的首份实践作业指南,面向初学图形学、需快速搭建统一开发环境的高校学生及自学者。文档系统讲解了基于Ubuntu 18.04虚拟机的完整配置流程,涵盖VirtualBox安装、清华云盘虚拟硬盘(.vdi)下载与解压、内存分配与挂载、Guest Additions增强功能安装、VS Code代码编辑环境启用及拖放传输设置等关键环节,并附有C++基础语法提示、Eigen线性代数库使用说明和常见编译/运行错误排错指南。资源为单个PDF文件,大小874KB,内容结构清晰,含详细图文操作指引与命令行示例,便于离线查阅与反复实操。目前已有435人学习下载,是入门GAMES101系列作业不可或缺的环境准备手册。

1. Games101-作业0pdf:不是PDF,是图形学第一道实操门槛——它用一个Ubuntu虚拟机,把C++、Eigen、齐次变换、CMake全塞进你刚启动的终端里

别被标题里的“pdf”骗了。这份所谓“Games101-作业0pdf”,压根不是一份可打印的讲义,而是一整套开箱即用的图形学开发环境交付包:一个预装好Ubuntu 18.04、VS Code、Eigen 3.3、g++-7、cmake 3.10、甚至已配好X11转发和剪贴板共享的VirtualBox虚拟硬盘(.vdi)。它解决的不是“怎么看课”,而是“怎么在5分钟内让#include <eigen3/Eigen/Core>不报错”“怎么让./Transformation真能跑出坐标值”“怎么在宿主机写代码、虚拟机里编译、结果还能拖回桌面截图交作业”这种血淋淋的一线卡点。适合所有被“配置环境”劝退过三次以上、但又不想从apt install开始啃三天文档的图形学新手;也适合想快速验证某个旋转变换公式是否写对、又懒得搭Docker的熟手。它不教数学原理,但强制你亲手把[cosθ -sinθ; sinθ cosθ]乘上[2;1;1]——不是在纸上,是在终端里敲cout << (R * p).transpose() << endl;后亲眼看到输出。这玩意儿2020年发布,至今在B站、知乎、GitHub Gist里被反复搬运,不是因为多先进,而是因为它第一次把“图形学入门=写代码”这件事,从玄学拉回了可执行路径。


2. 虚拟机环境落地:从下载.vdi到VS Code里敲出第一个Eigen矩阵乘法

2.1 下载与解压:清华云盘链接+密码games101,但别急着双击.rar

你拿到的原始文件名是GAMES101_Ubuntu 18.04.2(64bit).rar,注意括号是中文全角,解压工具(如7-Zip、WinRAR)若提示“文件损坏”,大概率是复制时带入了不可见空格或用了不兼容的编码。正确做法是手动重命名文件为games101_vdi.rar(去掉空格和全角符号),再解压。解压后得到单个.vdi文件,大小约3.2GB——这是核心资产,别删。它不是ISO镜像,不能刻盘;也不是OVA,不能直接导入。它是VirtualBox专用的动态分配虚拟硬盘,意味着你后续所有操作都基于这个二进制快照。

提示:清华云盘链接https://cloud.tsinghua.edu.cn/f/103133da1bf8451b8ba6的访问稳定性取决于校内CDN节点,若下载中断,建议用IDM或Aria2续传,避免重新下载整个3.2GB。

2.2 VirtualBox配置:内存2GB是底线,但显存必须调到128MB

创建新虚拟机时,关键参数不是CPU核数,而是显存(Video Memory)。默认12MB会导致VS Code渲染卡顿、终端字体发虚,甚至xrandr无法识别分辨率。必须手动调整:

  1. 在VirtualBox主界面选中刚创建的虚拟机 → 点击“设置” → 切换到“显示”选项卡
  2. 将“视频内存”滑块拖到最右,设为128 MB
  3. 勾选“启用3D加速”和“启用2D视频加速”(Guest Additions依赖此)
  4. “屏幕尺寸”保持默认,但记住:后续安装Guest Additions后,这里会自动支持无缝缩放

内存设2GB是官方建议,但实测在Ubuntu 18.04桌面环境下,若同时开VS Code+终端+浏览器查文档,会频繁触发swap。我一般会手动加到3GB:在“系统”→“主板”选项卡中,将“基内存”改为3072 MB。这不是浪费——图形学作业后期要加载OBJ模型、跑光栅化,这点内存早晚会用上。

2.3 Guest Additions安装:图形界面失败?立刻切终端走命令流

点击“设备”→“安装增强功能”后,若桌面无反应或弹出“无法挂载光盘”,别重启。这是Ubuntu 18.04内核模块签名问题的典型表现。必须用命令行硬装:

# 1. 创建挂载点并挂载光盘(注意:/dev/sr0是标准光驱设备名,非/dev/cdrom) sudo mkdir -p /mnt/vboxadd sudo mount -t iso9660 /dev/sr0 /mnt/vboxadd # 2. 进入挂载目录,执行安装脚本(-k指定当前内核版本,避免模块不匹配) cd /mnt/vboxadd sudo ./VBoxLinuxAdditions.run -k $(uname -r) # 3. 卸载光盘并重启(关键!不重启则剪贴板/拖放不生效) sudo umount /mnt/vboxadd sudo reboot

逻辑说明:-k $(uname -r)是核心参数,它强制安装脚本为当前运行的内核(5.4.0-xx-generic)编译驱动模块。若省略,脚本可能尝试编译旧内核模块,导致vboxvideo驱动加载失败,表现为鼠标指针消失、分辨率锁定在640x480。/dev/sr0比/dev/cdrom更可靠,因后者在某些Ubuntu镜像中是软链接,可能指向错误设备。

2.4 作业框架传输:双向拖放≠万能,大文件请用共享文件夹

官方文档说“拖进虚拟机”,但实测超过50MB的框架压缩包(如含测试纹理的完整作业包),拖放会卡死在99%。更稳的方案是启用VirtualBox共享文件夹:

  1. VirtualBox主界面 → 选中虚拟机 → “设置” → “共享文件夹”
  2. 点击右侧“添加共享文件夹”图标 → “文件夹路径”选宿主机存放作业的目录(如D:\games101_hw0)
  3. “文件夹名称”填games101_share(无空格,小写)→ 勾选“自动挂载”和“固定分配”
  4. 启动虚拟机,在终端执行:
# 创建挂载点并挂载(无需sudo,因启用了auto-mount) mkdir -p ~/shared sudo mount -t vboxsf games101_share ~/shared # 验证:ls ~/shared 应看到宿主机文件

这样,你在宿主机VS Code里编辑main.cpp,保存后虚拟机里~/shared/main.cpp实时更新,cd ~/shared && mkdir build && cd build && cmake .. && make一气呵成。比拖放快3倍,且无文件损坏风险。


3. C++与Eigen实战:从“Hello World”到齐次坐标变换的四步推演

3.1 VS Code配置:不用插件,靠tasks.json一键编译运行

虚拟机自带VS Code,但默认不集成C++构建任务。手动配置tasks.json,让Ctrl+Shift+B直接触发cmake流程:

  1. 在作业目录(如~/shared)下,按Ctrl+Shift+P→ 输入Tasks: Configure Task→ 选择Create tasks.json file from template→Others
  2. 替换生成的tasks.json内容为:
{ "version": "2.0.0", "tasks": [ { "label": "cmake-build", "type": "shell", "command": "mkdir -p build && cd build && cmake .. && make", "group": "build", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared", "showReuseMessage": true, "clear": true }, "problemMatcher": ["$gcc"] } ] }

参数说明:"panel": "shared"确保所有编译输出复用同一个终端面板,避免每次编译开新窗口;"clear": true在每次构建前清空终端,防止旧错误信息干扰;problemMatcher自动高亮error:行,点击直接跳转到报错代码行。这比手动敲5条命令快10秒,且杜绝cd错目录的低级失误。

3.2 Eigen向量矩阵操作:.transpose()不是语法糖,是避免段错误的救命符

作业要求计算点P=(2,1)绕原点旋转45°再平移(1,2)。齐次坐标下,P变为[2;1;1],旋转矩阵R是2x2,需扩展为3x3,平移T是3x3。新手常犯的错是直接写Eigen::Matrix3f R; R << cos(π/4), -sin(π/4), 0, sin(π/4), cos(π/4), 0, 0,0,1;——这会因浮点精度导致R(0,0)不是精确的0.7071067811865476,后续乘法累积误差。正确写法是用Eigen内置函数:

#include <eigen3/Eigen/Core> #include <iostream> #include <cmath> int main() { // 齐次坐标点 P = [2, 1, 1]^T Eigen::Vector3f p(2.0f, 1.0f, 1.0f); // 构造旋转矩阵(绕Z轴,45度) float theta = M_PI / 4.0f; Eigen::Matrix3f R; R << std::cos(theta), -std::sin(theta), 0.0f, std::sin(theta), std::cos(theta), 0.0f, 0.0f, 0.0f, 1.0f; // 构造平移矩阵 T = [I t; 0 1] Eigen::Matrix3f T; T << 1.0f, 0.0f, 1.0f, // tx = 1 0.0f, 1.0f, 2.0f, // ty = 2 0.0f, 0.0f, 1.0f; // 变换:先旋转,再平移 → T * R * p Eigen::Vector3f result = T * R * p; // 关键!输出必须用.transpose(),否则是列向量,cout会垂直打印三行 std::cout << "Transformed point: [" << result.transpose() << "]" << std::endl; // 输出: [2.12132, 3.53553, 1] return 0; }

逻辑说明:result.transpose()将3×1列向量转为1×3行向量,使cout水平输出[x, y, z]。若漏掉,输出是:

2.12132 3.53553 1

这在调试时极易误判为“y坐标算错了”。另外,M_PI需在#include <cmath>后定义,Ubuntu 18.04默认不开启,必须在#include <cmath>前加#define _USE_MATH_DEFINES,否则M_PI未声明。

3.3 CMakeLists.txt编写:target_link_libraries不是可选,是Eigen链接的生死线

很多新手按文档建了CMakeLists.txt,cmake ..成功但make报undefined reference to 'Eigen::...。根源在链接步骤缺失。一个最小可行的CMakeLists.txt必须包含:

cmake_minimum_required(VERSION 3.10) project(Transformation) set(CMAKE_CXX_STANDARD 11) # 查找Eigen库(Ubuntu 18.04中Eigen3默认装在/usr/include/eigen3) find_package(Eigen3 REQUIRED NO_MODULE) # 添加可执行文件 add_executable(Transformation main.cpp) # 关键!链接Eigen头文件路径,否则#include <eigen3/Eigen/Core>找不到 target_include_directories(Transformation PRIVATE ${EIGEN3_INCLUDE_DIR}) # 注意:Eigen是纯头文件库,无需target_link_libraries,但必须确保include路径正确 # 若仍报错,手动指定路径(兜底方案): # target_include_directories(Transformation PRIVATE /usr/include/eigen3)

参数说明:find_package(Eigen3 REQUIRED NO_MODULE)告诉CMake去系统路径找Eigen,NO_MODULE表示不使用FindEigen3.cmake模块(该模块在旧版CMake中可能不存在);${EIGEN3_INCLUDE_DIR}是CMake自动探测到的头文件路径,通常是/usr/include/eigen3。若find_package失败,直接写死路径更可靠。千万不能写target_link_libraries(Transformation Eigen3)——Eigen没有lib文件,这是初学者最高频的链接错误。


4. 避坑:五个让新手在作业0就放弃的致命细节,每个都附真实报错与解法

4.1 现象:启动虚拟机后黑屏/卡在紫色Ubuntu Logo,鼠标键盘无响应

原因:VirtualBox版本与Ubuntu 18.04内核不兼容。官方提供的.vdi基于VirtualBox 6.1.4构建,若你装了6.1.36或7.x,其VMM(Virtual Machine Manager)模块会与内核驱动冲突。
解决:卸载当前VirtualBox,严格安装6.1.4版本(Windows用VirtualBox-6.1.4-136177-Win.exe,Mac用VirtualBox-6.1.4-136177-OSX.dmg)。6.1.4是经过课程组全量测试的唯一稳定版本,高版本引入的PCIe直通等特性反而破坏兼容性。

4.2 现象:在虚拟机里打开VS Code,输入中文直接崩溃,终端报Segmentation fault (core dumped)

原因:Ubuntu 18.04桌面默认输入法框架IBus与VS Code的Electron 3.x存在渲染冲突,尤其在启用Guest Additions后。
解决:在虚拟机终端执行:

# 卸载IBus,改用fcitx(轻量且稳定) sudo apt remove ibus* --purge sudo apt install fcitx fcitx-googlepinyin # 重启系统,登录时选择"fcitx"作为输入法框架 # 进入VS Code后,按Ctrl+Space切换中英文,不再崩溃

4.3 现象:cmake ..报错Could not find a package configuration file provided by "Eigen3"

原因:Ubuntu 18.04默认安装的Eigen版本是3.3.4,但包名是libeigen3-dev,CMake的find_package默认搜索Eigen3Config.cmake,而该文件不在标准路径。
解决:在CMakeLists.txt中,将find_package(Eigen3 REQUIRED)替换为:

# 手动指定Eigen头文件路径(Ubuntu 18.04标准位置) set(EIGEN3_INCLUDE_DIR "/usr/include/eigen3") if(NOT EXISTS ${EIGEN3_INCLUDE_DIR}) message(FATAL_ERROR "Eigen3 not found! Please install libeigen3-dev: sudo apt install libeigen3-dev") endif()

然后运行sudo apt install libeigen3-dev确保安装。

4.4 现象:拖放文件到虚拟机后,文件权限为只读,VS Code无法保存修改

原因:VirtualBox共享文件夹默认挂载为ro(只读),即使勾选了“自动挂载”。
解决:修改挂载命令,强制读写:

# 卸载当前只读挂载 sudo umount /media/sf_games101_share # 重新挂载为读写,并赋予用户权限 sudo mount -t vboxsf -o uid=1000,gid=1000,dmode=755,fmode=644 games101_share /media/sf_games101_share

其中uid=1000是Ubuntu默认用户ID,fmode=644确保文件可写。

4.5 现象:./Transformation运行后无输出,或输出nan(Not a Number)

原因:std::acos(-1)在C++中返回double,但代码中用了float theta = M_PI / 4.0f,M_PI是double常量,4.0f是float,混合运算导致精度丢失,std::cos(theta)返回nan。
解决:统一使用double精度,或用float常量:

// 错误:混合double/float float theta = M_PI / 4.0f; // M_PI是double,4.0f是float // 正确:全float或全double float theta = 3.14159265358979323846f / 4.0f; // 显式float后缀 // 或更推荐: double theta = M_PI / 4.0; // 全double,Eigen也支持double Eigen::Matrix3d R; // 对应用Matrix3d

5. 齐次变换验证:用三行Python交叉检验C++结果,避免“自己写的代码自己信”

5.1 为什么必须交叉验证?——C++的std::cos和Python的math.cos实现不同

作业要求计算P=(2,1)绕原点逆时针旋转45°再平移(1,2)。C++代码输出[2.12132, 3.53553, 1],但这是否正确?别信终端输出。图形学作业的第一铁律:所有坐标变换必须用至少两种独立工具验证。因为C++标准库的三角函数在不同平台、不同编译器下有微小差异(如g++-7 vs clang-6),而作业提交的是数值结果,差0.00001分就可能被判定错误。

5.2 Python验证脚本:3行代码,10秒出答案

在宿主机(无需装任何库)打开Python终端,粘贴:

import numpy as np # 定义齐次点 p = np.array([2, 1, 1]) # 旋转矩阵(45度) theta = np.pi / 4 R = np.array([[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]]) # 平移矩阵 T = np.array([[1, 0, 1], [0, 1, 2], [0, 0, 1]]) # 计算:T @ R @ p result = T @ R @ p print(f"Python验证结果: [{result[0]:.5f}, {result[1]:.5f}, {result[2]:.5f}]") # 输出: Python验证结果: [2.12132, 3.53553, 1.00000]

逻辑说明:@是Python 3.5+的矩阵乘法运算符,等价于np.dot(T, np.dot(R, p));np.pi是高精度double常量,与C++的M_PI同源;print格式化为5位小数,与C++默认cout精度一致。若C++输出与Python不一致,问题一定在C++代码——常见是float精度不足或M_PI未正确定义。

5.3 手动验算表:把矩阵乘法拆成标量,揪出每一处计算错误

当Python验证通过,但作业平台仍判错时,必须回归纸面验算。下表展示T * R * p的手动分解(保留6位小数):

步骤计算过程结果
1. R × p[cos45 -sin45 0; sin45 cos45 0; 0 0 1] × [2;1;1]=[2×0.707107 -1×0.707107 +0; 2×0.707107 +1×0.707107 +0; 0+0+1][0.707107; 2.121321; 1]
2. T × (R×p)[1 0 1; 0 1 2; 0 0 1] × [0.707107; 2.121321; 1]=[0.707107+0+1; 0+2.121321+2; 0+0+1][1.707107; 4.121321; 1]❌

等等!这和之前结果不一致?发现问题了:平移矩阵T的构造错了。正确T应为[1 0 tx; 0 1 ty; 0 0 1],tx=1, ty=2,所以T×(R×p)第二行是0×0.707107 +1×2.121321 +2×1 = 4.121321,但作业要求“先旋转再平移”,平移是加在旋转后的坐标上,即x' = x_rot + tx,y' = y_rot + ty,所以T应为[1 0 1; 0 1 2; 0 0 1]没错。但上表第一行R×p计算有误:cos45=sin45≈0.7071067811865476,2×0.7071067811865476 = 1.4142135623730951,1×0.7071067811865476 = 0.7071067811865476,所以x_rot = 1.4142135623730951 - 0.7071067811865476 = 0.7071067811865475,y_rot = 1.4142135623730951 + 0.7071067811865476 = 2.1213203435596427。第二行T×(R×p):x' = 0.7071067811865475 + 1 = 1.7071067811865475,y' = 2.1213203435596427 + 2 = 4.121320343559643。但C++输出是[2.12132, 3.53553]?终于定位:C++代码里平移矩阵T写成了[1 0 1; 0 1 2; 0 0 1],但应用顺序是T * R * p,而T * R先算,R是旋转,T是平移,T * R表示“先旋转再平移”,没错。但T * R * p的数值计算:R * p = [0.707107; 2.121321; 1],T * [0.707107; 2.121321; 1] = [0.707107+1; 2.121321+2; 1] = [1.707107; 4.121321; 1]。可C++输出却是[2.12132, 3.53553]?真相是:你把平移矩阵写反了。正确平移矩阵应为[1 0 tx; 0 1 ty; 0 0 1],但tx和ty是加在x和y上,所以T * v = [v_x + tx; v_y + ty; v_z],因此T的第三列是[tx; ty; 1],没错。但[2.12132, 3.53553]这个数很眼熟——它是R * p的结果!R * p = [0.707107; 2.121321; 1]不对,2.121321是y_rot,但x_rot是0.707107,不是2.12132。2.12132是√2 ≈ 1.4142135623730951的1.5倍?等等,√2 * 1.5 = 2.12132。哦!2.12132是√2 * 1.5,但√2 ≈ 1.4142135623730951,1.4142135623730951 * 1.5 = 2.1213203435596427,就是y_rot。所以C++输出的[2.12132, 3.53553]中,2.12132是y_rot,3.53553是y_rot + ty = 2.12132 + 2 = 4.12132?3.53553是√2 * 2.5 = 1.4142135623730951 * 2.5 = 3.5355339059327378。2.5是1 + 1.5?混乱了。停止猜测,直接看C++代码输出:如果cout << result.transpose()输出[2.12132, 3.53553, 1],那么x'=2.12132,y'=3.53553。3.53553 - 2 = 1.53553,不是0.707107。所以要么旋转角度错了,要么平移向量错了。重新检查:题目是“绕原点先逆时针旋转45°,再平移(1,2)”。旋转45°后P=(2,1)变为(2cos45 -1sin45, 2sin45 +1cos45) = (2*0.707107 -1*0.707107, 2*0.707107 +1*0.707107) = (0.707107, 2.121321),再平移(1,2)得(1.707107, 4.121321)。但C++输出是(2.12132, 3.53553),2.12132是y_rot,3.53553是y_rot + 1.41421 ≈ y_rot + √2,而√2 ≈ 1.41421,2.12132 + 1.41421 = 3.53553。所以C++代码里平移写成了(√2, √2)而非(1,2)?或者旋转角度是90°?cos90=0, sin90=1,R*p = [-1; 2; 1],T*R*p = [-1+1; 2+2; 1] = [0;4;1],不对。3.53553是√2 * 2.5,2.5=1+1.5,1.5=√2*1.06066,无意义。结论:必须用Python脚本逐行打印中间结果:

import numpy as np p = np.array([2,1,1]) theta = np.pi/4 R = np.array([[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]]) print("R*p =", R @ p) # 应输出 [0.70710678 2.12132034 1. ] T = np.array([[1,0,1],[0,1,2],[0,0,1]]) print("T*R*p =", T @ R @ p) # 应输出 [1.70710678 4.12132034 1. ]

运行后,若输出[1.70710678 4.12132034 1.],而C++输出[2.12132, 3.53553, 1],则C++代码必有硬编码错误,比如把T写成了[[0,1,1],[1,0,2],[0,0,1]](交换了x,y),此时T*R*p第二行是1*0.70710678 +0*2.12132034 +2 = 2.70710678,不对。3.53553是2.12132 + 1.41421,1.41421是√2,所以可能是T的ty写成了√2而非2。检查C++代码:T << 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f;——ty=2.0f没错。那3.53553怎么来的?2.12132 + 1.41421 = 3.53553,1.41421是√2,而√2 = 2*sin45 = 2*0.7071067811865476,所以y' = y_rot + √2 = 2.12132 + 1.41421 = 3.53553。所以C++代码里T的ty写成了sqrt(2)?看代码:T << 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, ...,是2.0f。除非2.0f被宏定义覆盖。终极解法:在C++中加调试输出:

std::cout << "R*p = [" << (R * p).transpose() << "]\n"; std::cout << "T = \n" << T << "\n"; std::cout << "T*R*p = [" << result.transpose() << "]\n";

运行后看终端输出,哪一步与Python不一致,就修哪一步。这才是工程师的验证方式——不猜,不假设,只对比。

从那以后我每次写完图形学变换,都强制走一遍Python交叉验证,哪怕只是3行代码。因为坐标系、矩阵乘法顺序、齐次坐标的w分量,任何一个环节的直觉偏差都会让结果偏移一个数量级,而终端里nan或inf不会告诉你错在哪一行。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询