☰
图形学新手入门:用虚拟机+Eigen快速跑通齐次坐标变换
2026/10/27 2:13:38 网站建设 项目流程

简介:本资源是GAMES101《计算机图形学与混合现实研讨会》课程2020年春季学期的首份实践作业指南,面向初学图形学的学生,专为解决环境配置复杂、开发平台不统一等入门痛点而设计。文档以PDF形式提供完整虚拟机使用全流程:涵盖VirtualBox安装、清华云盘下载Ubuntu 18.04虚拟硬盘(含解压密码)、虚拟机新建与内存配置、Guest Additions增强功能安装(含GUI与终端双路径)、VS Code代码编辑与拖放传输设置,以及C++基础语法、Eigen库使用和常见编译/运行错误排查说明。资源共1个PDF文件,大小874KB,内容结构清晰,含操作截图提示、命令行示例及关键注意事项(如弧度制三角函数、段错误成因等),便于零基础学生快速搭建标准化实验环境。目前已有435人学习下载,是GAMES101课程实践环节不可或缺的入门支撑材料。

1. 这不是装系统,是「图形学新手的第一把钥匙」:用虚拟机绕过所有环境配置玄学

你有没有试过:刚下定决心学计算机图形学,结果卡在第一步——连“Hello World”都跑不起来?不是代码写错了,而是编译器报错说找不到Eigen,或者cmake提示C++17 not supported,再一查,发现自己的 macOS 版本太新、Ubuntu 源太旧、显卡驱动不兼容……折腾三天,连第一个点都没画出来。这不是你不行,是环境配置本身就像个黑匣子:没人告诉你哪一步该按什么顺序、哪个参数不能改、为什么改了就崩。而 Games101 作业 0 就是专为这种时刻设计的——它不教你画三角形,它先给你一个“开箱即用”的 Ubuntu 虚拟机,里面已经装好 VS Code、CMake、Eigen、GCC 全套工具链,连密码都设好了(Ilovegraphics)。你唯一要做的,就是点一下“启动”,然后直接写代码。它解决的不是“怎么画图”,而是“怎么让电脑听你的话”。适合谁?零基础、没碰过 Linux、被编译错误劝退过三次、只想专注学图形学数学和算法,不想当系统管理员的新手。这不是偷懒,是把最耗时间的“环境踩坑”阶段,压缩成一次下载、一次解压、一次点击。


2. 先搞懂一个概念:虚拟机不是“另一个电脑”,而是「图形学实验的安全沙盒」

很多人第一次听到“用虚拟机做作业”,下意识觉得:“啊?还要装系统?好麻烦。” 其实完全相反——虚拟机在这里的作用,根本不是让你学 Linux 系统管理,而是给你一个绝对干净、绝对一致、绝对可重置的实验台。想象一下:全班 500 人,有人用 Windows 10,有人用 macOS Sonoma,有人用国产 Linux 发行版,如果直接让大家在自己电脑上配环境,光是“为什么我的 Eigen 报错而他的不报”就能吵翻论坛。而作业 0 提供的.vdi文件,就是一个完整的、拍过快照的 Ubuntu 18.04 系统镜像——它像一张高清照片,你双击打开,看到的就是授课老师当年亲手调好的那一套环境。你改坏了?关机,重新启动,一切还原。你误删了头文件?没关系,镜像里还存着。它不追求“最新”,而追求“确定性”;不强调“个性”,而强调“统一性”。这才是图形学入门最需要的:把注意力从“我的电脑怎么又不行了”拉回到“这个旋转矩阵到底该怎么乘”。

2.1 为什么非得用 VirtualBox,而不是 Docker 或 WSL?

这里有个关键区别必须讲透:Docker 是进程级隔离,WSL 是内核级兼容层,它们都依赖宿主机系统的底层支持。但图形学作业早期就要调用 OpenGL 上下文、读取帧缓冲、甚至未来要跑 GUI 程序(比如用 GLFW 创建窗口),这些操作需要真正的图形设备模拟和 X11 图形协议支持。VirtualBox 的 Guest Additions 正是干这个的——它在虚拟机里装了一个轻量级显卡驱动 + 图形服务代理,让 Ubuntu 能真正“看见”你的显示器、鼠标、剪贴板。而 Docker 默认没图形栈,WSL2 虽然能跑 GUI,但需要额外配 X Server、处理 DPI 缩放、权限转发,对新手来说又是新坑。所以作业 0 明确指定 VirtualBox,不是守旧,是经过血泪经验验证的最低失败率路径。你不用理解 virtio-gpu 或 VMSVGA,只要记住:要跑图形学代码,先要让虚拟机“长出眼睛和手”,VirtualBox + Guest Additions 就是目前最稳的那副义眼+义肢。

2.2 下载那个.rar文件时,你真正拿到的是什么?

别被后缀.rar欺骗——它里面封存的不是一个压缩包,而是一个13GB 的“操作系统硬盘快照”。你可以把它理解成一台已经装好所有软件、连桌面壁纸都设好的实体电脑的“硬盘拔下来”,塞进一个加密 U 盘里寄给你。解压后得到的.vdi文件,就是 VirtualBox 能直接识别的虚拟硬盘格式(Virtual Disk Image)。它不是 ISO 镜像(那种要安装的),也不是 OVA 包(那种要导入的),它是“即插即用”的终极形态:你新建一台虚拟机,告诉它“就用这个 .vdi 当硬盘”,它就直接从这个硬盘启动,连 GRUB 引导菜单都不用选。清华云盘链接里的密码games101,就是打开这台“预装电脑”的钥匙。注意:这个密码只用于解压.rar,不是登录系统的密码——登录密码是文档里写的Ilovegraphics,两个密码用途完全不同,新手常在这儿翻车。

2.3 安装 Guest Additions:为什么这步不能跳,也不能靠“自动安装”蒙混过关?

很多新手看到“点击设备 → 安装增强功能”就以为完事了,结果拖文件进不去、屏幕分辨率卡在 800×600、鼠标进虚拟机就消失……这是因为 Guest Additions 的安装分两层:

  • 第一层是挂载光盘镜像(你点菜单那一下);
  • 第二层是真正在 Ubuntu 里编译并安装驱动模块(这步必须成功执行)。

而 Ubuntu 18.04 默认没装编译必需的build-essential和内核头文件linux-headers-$(uname -r),所以图形界面的“安装增强功能”经常卡在“正在运行安装脚本…”然后不动。这时候必须切到终端(Ctrl+Alt+T),手动走一遍命令流:

sudo mkdir -p /media/cdrom sudo mount -t auto /dev/cdrom /media/cdrom cd /media/cdrom sudo sh VBoxLinuxAdditions.run

提示:mkdir -p中的-p很关键,它表示“如果目录不存在就创建,存在也不报错”;mount -t auto让系统自动判断光盘文件系统类型;最后的sh是显式调用 shell 执行安装脚本,比双击更可靠。

执行完看到Installing new version of kernel modules和Kernel modules update was successful才算真正完成。之后必须重启(sudo reboot),否则新驱动不生效。这步看似繁琐,但它换来的是:拖放文件、共享剪贴板、无缝窗口模式、自动适配分辨率——所有让你感觉“这台虚拟机就是我电脑一部分”的体验,全靠它。


3. 第一个能跑通的例子:三分钟写出你的第一个图形学计算(不用画图,先算对)

别急着打开 VS Code 写渲染循环。作业 0 的第一个实战目标非常朴素:用齐次坐标,算出点 P=(2,1) 绕原点逆时针转 45° 再平移 (1,2) 后的新坐标。它不涉及 OpenGL,不调用 GPU,纯 CPU 上的矩阵乘法。但正是这个“简单计算”,暴露了图形学最核心的思维转换——从“直觉坐标”到“齐次空间”。我们来拆解它怎么一步步跑通。

3.1 齐次坐标不是炫技,是为了一件事:把旋转和平移写进同一个矩阵里

你在中学学的旋转公式是:
x' = x·cosθ − y·sinθ
y' = x·sinθ + y·cosθ
平移公式是:
x'' = x' + tx
y'' = y' + ty

看起来很清晰?但问题来了:旋转可以用 2×2 矩阵乘向量实现,平移却是向量加法,没法写成矩阵乘。而图形学管线要求所有变换(模型、视图、投影)都用矩阵乘串联。怎么办?加一维,变成 (x, y, 1),这就是齐次坐标。此时:

  • 旋转矩阵变成 3×3:
[ cosθ -sinθ 0 ] [ sinθ cosθ 0 ] [ 0 0 1 ]
  • 平移矩阵也变成 3×3:
[ 1 0 tx ] [ 0 1 ty ] [ 0 0 1 ]

现在,旋转再平移 = 平移矩阵 × 旋转矩阵 × 点(齐次)——全部是矩阵乘!作业 0 就是让你亲手把这个乘出来,验证你理解了“为什么要有第三维”。

3.2 用 Eigen 写出来:5 行代码,看清每一步在干什么

打开虚拟机,用 VS Code 新建main.cpp,粘贴以下代码(注意:这是完整可运行版本,不是片段):

#include <iostream> #include <cmath> #include <eigen3/Eigen/Core> int main() { // 1. 定义原始点 P = (2, 1) 的齐次坐标(第三维是 1) Eigen::Vector3f P(2.0f, 1.0f, 1.0f); // 2. 构造 45° 旋转矩阵(注意:cos/sin 输入是弧度!45° = π/4 ≈ 0.785398) float theta = M_PI / 4.0f; Eigen::Matrix3f R; R << std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1; // 3. 构造平移矩阵:tx=1, ty=2 Eigen::Matrix3f T; T << 1, 0, 1, 0, 1, 2, 0, 0, 1; // 4. 计算:先旋转,再平移 → T * R * P Eigen::Vector3f result = T * R * P; // 5. 输出结果(齐次坐标第三维仍是 1,所以直接取前两维) std::cout << "Transformed point: (" << result(0) << ", " << result(1) << ")" << std::endl; return 0; }

解释每一行:

  • #include <eigen3/Eigen/Core>是 Eigen 库的“最小必要头文件”,只引入向量、矩阵、基本运算,不加载花哨功能,启动快、编译快;
  • M_PI是 C 标准库定义的 π 值(需确保编译时定义_GNU_SOURCE,但 Ubuntu 18.04 默认已启用);
  • R << ...是 Eigen 的逗号初始化语法,按行优先填入数值,比循环赋值直观得多;
  • result(0)和result(1)是 Eigen 向量的索引方式(从 0 开始),不是result.x(),新手常在这里写错;
  • 最后输出的(x, y)就是你要的答案,应该接近(2.121, 4.242)(可手算验证:旋转后约 (1.121, 2.242),再平移得此结果)。

3.3 编译运行:cmake 不是摆设,是帮你绕过链接地狱的“自动配单员”

别用g++ main.cpp -o app硬编,那样会报undefined reference to Eigen::...——因为 Eigen 是 header-only 库,但 cmake 脚本里还预埋了编译选项(如-std=c++17)。按文档走标准流程:

mkdir build cd build cmake .. # 注意是两个点,指向 main.cpp 所在目录 make ./Transformation

为什么cmake ..必须在build目录下执行?因为 cmake 的哲学是“源码与构建分离”:..目录里有CMakeLists.txt(作业框架自带),它告诉 cmake “我要用 C++17,要链接 Eigen,可执行文件叫 Transformation”。cmake 读取后,在build目录下生成Makefile,make才真正调用 g++ 编译。如果你在build外执行cmake .,它会试图在源码目录生成中间文件,污染代码,且下次git status会看到一堆乱码文件。

执行./Transformation后,终端应打印:
Transformed point: (2.12132, 4.24264)
恭喜,你完成了图形学世界里的“第一次呼吸”——没有显卡,没有窗口,只有纯粹的数学在 CPU 上流动。


4. 新手必踩的 5 个坑:现象、原因、一招解决(亲测有效)

刚跑通例子不代表万事大吉。下面这些坑,是某高校图形学助教整理的 200+ 份作业提交中,出现频率最高的“新手三秒崩溃点”。每一条都按“你看到什么 → 为什么这样 → 怎么立刻修好”写清楚,不讲原理,只给解药。

4.1 现象:VS Code 里写#include <eigen3/Eigen/Core>,左侧却标红,提示 “cannot open source file”

原因:VS Code 的 IntelliSense(智能提示)没找到 Eigen 头文件路径,但它不影响编译!因为 cmake 脚本里已通过target_include_directories告诉编译器去哪里找,只是编辑器自己不知道。
解决:按Ctrl+Shift+P打开命令面板 → 输入C/C++: Edit Configurations (UI)→ 在Include path里添加/usr/include/eigen3(Ubuntu 18.04 默认路径)→ 回车保存。立刻不红了。> 注意:不要改#include语句本身,<eigen3/Eigen/Core>是正确写法,改了反而编译不过。

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

原因:你没在build目录下执行cmake ..,而是在build外执行了cmake .或cmake ../xxx,导致 cmake 找不到CMakeLists.txt。
解决:立刻cd build,然后严格输入cmake ..(两个英文点,中间无空格)。输完回车,看第一行是否显示-- The CXX compiler identification is GNU 7.5.0。如果是,说明找到了;如果报CMake Error: The source directory ".../build" does not appear to contain CMakeLists.txt,就是路径错了,删掉整个build目录重来。

4.3 现象:程序编译通过,但运行时报Segmentation fault (core dumped)

原因:最常见的是访问了未初始化的 Eigen 向量/矩阵元素,比如写了Eigen::Vector3f v; std::cout << v(0) << std::endl;——v是默认构造,内存未初始化,读(0)就崩。
解决:所有 Eigen 对象声明时必须初始化!要么用括号赋初值Eigen::Vector3f v(1,2,3),要么用setZero()setOnes()等成员函数:

Eigen::Vector3f v; v.setZero(); // 安全 Eigen::Matrix3f m; m << 0; // 错!<< 是赋值操作符,但 m 未初始化,<< 可能写到随机内存 Eigen::Matrix3f m = Eigen::Matrix3f::Zero(); // 正确:用静态成员函数初始化

4.4 现象:./Transformation运行后啥也不输出,光标直接回到下一行

原因:std::cout默认带缓冲,如果程序结束太快,输出可能被卡在缓冲区没刷出来。尤其当你忘了std::endl或\n。
解决:在std::cout末尾强制刷新:

std::cout << "Result: " << result(0) << "," << result(1) << std::endl; // ✅ 有 endl // 或者 std::cout << "Result: " << result(0) << "," << result(1) << '\n' << std::flush; // ✅ 用 \n + flush

提示:std::endl=\n+std::flush,'\n'是字符,"\n"是字符串,混用会编译报错,新手常栽在这儿。

4.5 现象:拖文件进虚拟机没反应,或拖进去后是灰色不可用状态

原因:VirtualBox 的“双向拖放”功能没开,或者 Guest Additions 没装成功(见 2.3 节)。
解决:先确认 Guest Additions 已装(终端里lsmod | grep vboxguest应有输出);再进虚拟机 → 顶部菜单栏设备→拖放→双向;如果还是不行,重启虚拟机。> 终极方案:不用拖放!在主机上把main.cpp放进某个文件夹,然后在 VirtualBox 管理器里选中该虚拟机 →设置→共享文件夹→ 添加一个共享文件夹(勾选“自动挂载”和“固定分配”),启动后在虚拟机里ls /media/sf_你的文件夹名就能看到,直接cp进去。


5. 下一步学什么:从“算对一个点”到“画出第一个三角形”的通关地图

跑通作业 0 只是拿到了钥匙,门后才是图形学真正的世界。接下来三个月,你会沿着一条被无数人验证过的路径前进。这条路径不求快,但求每一步都踩在实地上,不让你在某个深夜对着黑屏调试 4 小时。我把它拆成三个明确阶段,每个阶段给你一个“本周必须亲手完成”的硬指标,以及一个防翻车的检查清单。

5.1 阶段一:夯实数学地基(1~2 周)——目标:手推 3 个关键变换矩阵

作业 0 只练了旋转+平移,但图形学还有缩放、镜像、透视投影。别急着写代码,先拿纸笔推导:

  • 缩放矩阵:S(sx,sy) 作用于点 (x,y,1),结果是什么?
  • 绕任意点 (a,b) 旋转 θ:为什么不能直接用 R(θ)?必须“平移过去→旋转→平移回来”,写出完整矩阵乘积 T(a,b) × R(θ) × T(-a,-b);
  • 正交投影到 xy 平面:z 坐标丢弃,但齐次坐标怎么写?[1 0 0 0; 0 1 0 0; 0 0 0 0; 0 0 0 1] 对吗?为什么最后一行是 [0 0 0 1] 而不是 [0 0 0 0]?

检查清单:

  • ✅ 推导过程每一步都标注用了哪条矩阵性质(如结合律、分配律);
  • ✅ 用 Eigen 写代码验证:输入 (2,1,1),应用你推的矩阵,输出是否符合直觉;
  • ✅ 故意把矩阵写错一位(比如 R 的 (2,1) 位置写成 cos 写成 sin),看 Eigen 运行结果是否明显异常——这是培养“数值直觉”的关键。

5.2 阶段二:打通编译-运行闭环(1 周)——目标:独立编译并运行一个带 GLFW 窗口的程序

作业 0 是命令行程序,下一步必须接触真实图形管线。下载官方提供的作业 1 框架(通常含CMakeLists.txt+main.cpp+glad.c+glfw预编译库),目标不是立刻画图,而是:

  • 修改CMakeLists.txt,让target_link_libraries正确链接glfw和X11(Ubuntu 18.04 需要);
  • 在main.cpp里调用glfwInit()→glfwCreateWindow()→glfwMakeContextCurrent()→glfwSwapBuffers(),确保窗口弹出又关闭,不崩溃;
  • 在窗口循环里加一句std::cout << "Frame: " << ++frame_count << "\n";,确认主循环在跑。

防坑指南:

  • 如果glfwCreateWindow返回nullptr,八成是libX11.so没装:sudo apt update && sudo apt install libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev;
  • glad.c必须放在add_executable的源文件列表里,不能只#include;
  • glfwMakeContextCurrent(window)必须在gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)之后调用,顺序错了必崩。

5.3 阶段三:理解“管线即数据流”(持续进行)——目标:画出第一个彩色三角形,并说出每个顶点经历了哪 5 次坐标变换

这是质变点。当你能看着顶点着色器里的gl_Position = projection * view * model * vec4(aPos, 1.0);,清晰说出:

  1. aPos是模型坐标(局部坐标系);
  2. model把它转到世界坐标;
  3. view把它转到相机坐标(眼睛看到的位置);
  4. projection把它转到裁剪坐标(-1~1 立方体);
  5. GPU 自动做透视除法(w 除法)转到归一化设备坐标(NDC);
    ——你就真正入门了。

我第一次学的时候,把model矩阵故意设成单位阵,观察三角形不动;再设成scale(2,2,2),看它变大;再设成rotateY(45),看它绕 Y 轴转——不是为了炫技,是为了让矩阵从“抽象符号”变成“可触摸的杠杆”。从那以后我每次写 shader,都强制在纸上画出坐标系箭头,标出当前变量属于哪个空间。希望帮到你。

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

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

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

立即咨询