news 2026/10/31 23:24:31

Games101作业0环境搭建与Eigen齐次坐标变换实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Games101作业0环境搭建与Eigen齐次坐标变换实战

简介:这份PDF是GAMES101(计算机图形学与混合现实研讨会)课程作业0的官方指导文档,面向刚入门计算机图形学、需要搭建统一开发环境的学生与自学者,重点解决虚拟机安装与作业框架配置中的环境差异问题。文档围绕Oracle VM VirtualBox展开,依次讲解虚拟机安装、虚拟硬盘文件下载与挂载、内存等参数设置、Guest Additions增强功能的安装与失败后的命令行补救方案,并说明如何把作业框架从主机拖入虚拟机、用Visual Studio Code打开编辑,以及在命令行中编译运行。文末还附有作业框架说明、C++头文件与函数体示例、常见编译错误与段错误排查提示,以及Eigen线性代数库的使用注意事项,可当作环境搭建与入门语法的速查手册。资源包共1个PDF文件,大小约874KB,轻量易读,已有435人学习。适合配合课程首次作业逐步对照操作,减少环境配置带来的额外消耗。

1. 一份环境基线,比一份答案更值钱

拿到「Games101 作业0」这份 PDF 的人,八成第一反应是失望——不就讲怎么装个虚拟机吗,跟图形学有什么关系。真跑过一遍就知道,这一页纸解决的是后面十几个作业的公共分母:一个所有人编译结果都一致的 Ubuntu 环境、一套已经配好的 Eigen 头文件、一条从 main.cpp 走到可执行文件的 CMake 链路。它适合三类人:刚入门图形学、被环境配置劝退过的自学者;想复现课程作业但不想重装系统的从业者;以及需要一套可复制的 C++ 教学环境的高校实验课。下面按我实际搭过几遍的顺序,把每一步的参数、命令和翻车点摊开讲。

2. 虚拟机镜像上手:VDI 挂载、增强功能和文件互传的三道关

这一章解决的是“环境从零到能编译”的问题。很多人卡在这一步不是因为命令难,而是因为不知道每个参数动了会有什么后果,出问题又没有参照物。我把虚拟硬盘挂载、虚拟机参数、增强功能安装、文件互传拆成四段,每段给出能直接抄的命令和失败时的判断依据。

2.1 虚拟硬盘文件挂载:一份 VDI 省掉两小时装系统

课程给的是一份虚拟硬盘文件(.vdi),不是 ISO 安装镜像。这两者的差别决定了你后面两小时是喝咖啡还是折腾分区表。VDI 是 VirtualBox 的原生磁盘格式,里面已经是一个装好系统、配好软件源、预装 g++、cmake、Eigen 头文件和编辑器的完整根文件系统。新建虚拟机时选“使用已有的虚拟硬盘文件”,虚拟机启动时直接把这个文件当硬盘读写,开机就是可用状态。

走 ISO 全新安装的路径,你要依次面对分区方案、用户名密码、软件源选哪个镜像站、apt update 卡不卡、依赖装漏了怎么补这一串问题,任何一步网络抖动都可能从头再来。对于一份只要求你熟悉编译链路的教学作业,这些时间花得不值。

提示:挂载之前先把 .vdi 复制一份留底。虚拟机的所有写操作都直接落在这个文件上,一旦跑废了,没有副本就只能重新下载。这份后悔药只有几百毫秒的成本。

具体做法:解压得到 .vdi 后,在 VirtualBox 里点“新建”,名称随意,类型选 Linux,版本选 Ubuntu (64-bit),内存给 2048MB 起步(宿主机内存富裕就给 4096MB,Eigen 的模板展开在编译期吃内存,2GB 会比较慢),硬盘那一步选“使用已有的虚拟硬盘文件”,指到刚解压的 .vdi 上,点创建。启动后按提示输入系统密码即可进桌面。

2.2 新建虚拟机时的四个参数和两个开关

参数填错不会立刻报错,但会在后面某一步以奇怪的方式体现出来。下面这张表是我踩过几轮之后固定下来的值:

参数建议值为什么是这个值
内存2048~4096 MB低于 1536MB 时编译 Eigen 模板明显变慢,甚至被 OOM killer 干掉
CPU 核数2与后面make -j2对应,给多了宿主机卡,给 1 编译慢一倍
显存128 MB低于 64MB 时桌面分辨率上不去,编辑器窗口显示不全
网络NAT默认即可,虚拟机内 apt 走宿主机网络,不需要额外配置
3D 加速关闭部分显卡驱动下开这个会黑屏或卡在登录界面,玄学问题的主要来源
拖放与共享粘贴板双向不开这两个,作业框架只能靠共享文件夹或 scp 传

习惯图形界面的按上表填就行。如果是在无桌面的服务器上批量准备环境,用 VBoxManage 一条条来更省事:

# 注册一台名为 G101 的虚拟机,类型指定为 64 位 Ubuntu VBoxManage createvm --name G101 --ostype Ubuntu_64 --register # 配置硬件:内存 4G、2 核、显存 128M,拖放和剪贴板都开双向 VBoxManage modifyvm G101 \ --memory 4096 --cpus 2 --vram 128 \ --clipboard-mode bidirectional \ --draganddrop bidirectional \ --graphicscontroller vmsvga # 挂一块 SATA 控制器,再把已有的 vdi 当作硬盘接上去 VBoxManage storagectl G101 --name SATA --add sata --controller IntelAhci VBoxManage storageattach G101 \ --storagectl SATA --port 0 --device 0 \ --type hdd --medium ./G101.vdi

这里几个参数值得单独说:--ostype决定 VirtualBox 给虚拟机套哪套默认配置模板,填错会影响默认的芯片组和网卡型号;--graphicscontroller vmsvga比默认的 vboxvga 在新版宿主机上兼容性好,黑屏时优先换这一项试试;--medium后面跟的是 vdi 文件的相对或绝对路径,路径里有空格记得加引号。建完之后VBoxManage list vms能看到这台机器就说明注册成功了。

2.3 增强功能:图形界面装不上就用终端兜底

增强功能(Guest Additions)不是可选项。没有它,虚拟机分辨率锁死在 800x600,鼠标要在宿主机和虚拟机之间来回“抢”,拖放和共享文件夹全部失效。正常路径是进系统后点菜单“设备 → 安装增强功能”,它会往虚拟光驱里塞一张 ISO,系统一般会自动弹出安装提示,跟着点完重启就行。

失败的情况很常见,典型现象是点了没反应,或者跑到一半终端里刷出一堆modprobe相关的错误。根因通常是两个:一是虚拟光驱没挂上,二是系统里缺内核头文件和编译工具,导致增强功能里的内核模块编不出来。这时候别在图形界面里反复点,直接开终端:

# 先补齐编译内核模块需要的工具链和当前内核版本的头文件 sudo apt-get update sudo apt-get install -y build-essential linux-headers-$(uname -r) # 手动把光驱挂到 /media/cdrom 再执行安装脚本 sudo mkdir -p /media/cdrom sudo mount -t auto /dev/cdrom /media/cdrom cd /media/cdrom sudo sh VBoxLinuxAdditions.run # 装完必须重启,内核模块才会被加载 sudo reboot

逐条解释一下为什么是这个顺序:linux-headers-$(uname -r)里的$(uname -r)会被 shell 替换成当前运行的内核版本号,头文件版本和内核版本对不上,模块编译一定失败,这是最常见的一类翻车;mount -t auto让系统自己识别光盘文件系统,比硬写 iso9660 兼容性好;VBoxLinuxAdditions.run必须在/media/cdrom目录下执行,因为它要读同目录下的安装包。

重启之后验证一下模块有没有真的加载:

# 有输出说明 vboxguest 模块已在内核中 lsmod | grep vboxguest # 查看当前屏幕支持的分辨率列表,能列出多种说明增强功能生效了 xrandr

如果lsmod什么都没有,回到上一步看sh VBoxLinuxAdditions.run的输出,里面通常有一行明确的失败原因,照着搜比瞎试快得多。

2.4 作业框架进虚拟机:拖放、共享文件夹、scp 三种传法

框架传进虚拟机有三种方式,适用场景不一样。拖放最省事,适合十几个文件的小目录,但文件一多或者目录层级深,VirtualBox 的拖放实现容易中途卡住,传一半断了你还看不出来。共享文件夹最稳,宿主机上的目录实时映射进虚拟机,改完立刻生效,适合反复修改的作业。scp 适合无桌面环境或者想脚本化的情况。

共享文件夹的配置,图形界面在虚拟机设置的“共享文件夹”里加一条,勾上自动挂载。命令行等价写法:

# 宿主机执行:把宿主机的 ~/g101_hw 映射成虚拟机里的 hw VBoxManage sharedfolder add G101 \ --name hw \ --hostpath ~/g101_hw \ --automount

自动挂载偶尔会失效,进系统后/media/sf_hw不存在,这时手动挂:

# 虚拟机内执行:先建挂载点,再按 vboxsf 类型挂上来 sudo mkdir -p /mnt/hw sudo mount -t vboxsf hw /mnt/hw # 把自己的用户加进 vboxsf 组,否则访问会报权限不足 sudo usermod -aG vboxsf $USER

-t vboxsf是 VirtualBox 提供的虚拟文件系统类型,只能在装了增强功能的虚拟机里用;hw是前面--name指定的共享名,两边必须一致;$USER展开成当前登录用户名。加组之后要重新登录或者重启才生效,这一点经常被忽略,加完组立刻去访问还是权限不足,很多人以为是挂载失败,其实是组没生效。

框架传进去之后,用虚拟机里自带的编辑器打开目录就能改代码。我一般把整个框架目录放在共享文件夹里,在宿主机上用顺手的编辑器改,虚拟机里只负责编译运行,两边视线不打架。

3. Eigen 与 C++ 地基:头文件路径、定长矩阵和弧度制陷阱

环境通了之后,第二道坎是代码本身。这份作业的代码量很小,主函数几十行,但它一次性把后面所有作业都要用的三样东西摆出来了:Eigen 的向量矩阵类型、C++ 的浮点与三角函数行为、以及一套可复现的编译方式。这一章把这三样拆开讲清楚,比抄一遍样例程序有用。

3.1 头文件路径:<eigen3/Eigen/Core>里的 eigen3 是怎么来的

样例程序第一行写的是#include <eigen3/Eigen/Core>,很多人照抄能跑,但换成<Eigen/Core>就报找不到文件,或者反过来。原因在安装位置:Eigen 是纯头文件库,通过包管理器装完之后,头文件落在/usr/include/eigen3/Eigen/下面,注意中间多了一层eigen3。编译器默认只搜/usr/include,所以写<eigen3/Eigen/Core>正好能拼出完整路径。

想确认自己机器上的实际位置,用包管理器查:

# 列出 Eigen 开发包安装的所有文件,看头文件到底落在哪 dpkg -L libeigen3-dev | head -20 # 或者直接找 Core 文件 find /usr/include -name "Core" -path "*Eigen*"

知道位置之后就有两条路:一是老老实实写<eigen3/Eigen/Core>,不改任何配置;二是在 CMake 里把/usr/include/eigen3加进包含路径,代码里就能写<Eigen/Core>,这样代码搬到别的平台上更容易复用。两种都行,别混着来——一半文件写长路径一半写短路径,编译到后面突然报错,排查起来很烦。

提示:Eigen 是 header-only 的,不需要在 CMakeLists.txt 里写target_link_libraries。写了不报错但没用,漏写了也不报错,这一点跟大部分第三方库不一样。

3.2 Vector3f 与 Matrix3f:定长类型、逗号初始化和未定义值

Eigen 的类型命名规则是「类型 + 维度 + 标量」,Vector3f就是三维 float 向量,等价于Matrix<float, 3, 1>。这种定长类型在栈上分配,没有堆内存开销,适合图形学里大量的小向量小矩阵运算。下面这段把作业里会用到的操作过一遍:

#include <eigen3/Eigen/Core> #include <iostream> int main() { // 定长三维 float 向量,栈上分配 Eigen::Vector3f v(1.0f, 2.0f, 3.0f); Eigen::Vector3f w(1.0f, 0.0f, 0.0f); std::cout << v + w << std::endl; // 逐分量相加 std::cout << 3.0f * v << std::endl; // 数乘,标量在前在后都行 std::cout << v.dot(w) << std::endl; // 点积,返回标量 std::cout << v.cross(w) << std::endl; // 叉积,返回向量 // 3x3 矩阵,逗号初始化按行填充 Eigen::Matrix3f m; m << 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f; std::cout << m * v << std::endl; // 矩阵乘列向量 std::cout << m.transpose() << std::endl; // 转置 std::cout << m.inverse() << std::endl; // 求逆,奇异矩阵会出 inf/nan return 0; }

参数和易错点逐个说:1.0f后面的f不能省,省了是 double,赋给 float 会有隐式转换警告,开-Wall时会刷一屏;逗号初始化严格按行主序填,九个数字对应三行三列,写的时候按行换行排版,出错率能降一半;最关键的一点——定长矩阵声明之后不赋值,内容是不确定的,栈上残留什么就是什么。我见过不止一次有人声明了Matrix3f R;只填了前六个元素,跑出来结果每次都不一样,还以为是编译器玄学。养成声明就setZero()或者一次性填完的习惯。

3.3 弧度制:sin(30.0)为什么不是 0.5

C++ 标准库的三角函数全部吃弧度。写std::sin(30.0)算的是 30 弧度,结果约 -0.988,跟 0.5 差得离谱。这个坑在图形学里出现频率极高,因为作业描述里给的角度全是度数,而 API 全要弧度。

#include <cmath> #include <iostream> int main() { // 用 acos(-1) 求 pi,比手写 3.14159 精度高,也不用记常量名 const double pi = std::acos(-1.0); std::cout << std::sin(30.0) << std::endl; // 30 弧度,错 std::cout << std::sin(30.0 / 180.0 * pi) << std::endl; // 30 度,对 std::cout << std::cos(pi) << std::endl; // 约 -1 std::cout << std::cos(pi / 4.0) << std::endl; // 45 度,约 0.7071 return 0; }

std::acos(-1.0)是求 π 的常用写法,反余弦在 -1 处正好是 π,精度能到 double 的极限。也有人用M_PI,但那不是标准 C++ 的东西,某些编译器和标准组合下要额外定义宏才可见,跨平台时容易翻车。Eigen 里的旋转构造同样吃弧度,Eigen::AngleAxisf(pi / 4.0f, Eigen::Vector3f::UnitZ())里第一个参数就是弧度,写成 45 会得到一个完全错误的旋转。

顺带说浮点比较:std::cos(pi)打印出来可能是 -1,也可能是 -0.9999999999999999,取决于精度和打印格式。判断相等时用std::abs(a - b) < 1e-6,别写==。

3.4 五类报错的定位顺序

编译和运行阶段的错误就那么几类,认识它们能省掉大量瞎猜。下面这张表是我自己排查时的固定顺序,从上往下走:

报错关键字大致含义先看哪里
error: ...带文件名和行号编译错误,语法或类型不匹配报错的第一行,不是最后一行;模板报错往下翻到required from那行
undefined reference to ...链接错误,函数声明了但没实现CMakeLists.txt 里add_executable是否包含了所有源文件
Segmentation fault段错误,非法内存访问数组下标、指针是否为空、Eigen 矩阵是否越界访问
Bus error总线错误,成因与段错误接近同上,另外看是否对未对齐地址做了强制类型转换
结果为nan或inf数学错误除数是否为 0、矩阵是否奇异、是否对负数开方

undefined reference这一类在 CMake 项目里尤其常见,因为源文件不会自动被编译,add_executable(Transformation main.cpp)里少写一个文件,那个文件里的函数就是未定义。段错误里最隐蔽的一种是 Eigen 的动态矩阵访问越界,matrix(i, j)的索引超出实际尺寸时不一定立刻崩,可能跑到几十行之后才炸,这时候用-fsanitize=address重新编译一遍,能直接指出越界的那一行。

4. CMake 构建链路:从 main.cpp 到可执行文件的四步

手写g++ main.cpp -o main能编译单文件,但作业要求用 CMake,原因不是折腾人:一旦工程里出现多个源文件、多个目录、外部头文件路径,手写编译命令就会变成一坨没法维护的字符串。CMake 把这些描述固化成文件,换台机器重跑一遍就行。这一章把最小可用的 CMakeLists.txt 和构建流程讲透,顺带把作业本体的数学部分落地。

4.1 CMakeLists.txt 的最小骨架

# 声明 CMake 最低版本,低于这个版本会直接报错退出 cmake_minimum_required(VERSION 3.10) # 项目名和语言,LANGUAGES CXX 表示只用 C++,跳过 C 和 Fortran 的检测 project(Transformation LANGUAGES CXX) # 指定 C++ 标准,Eigen 的部分特性需要 C++11 以上 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Eigen 头文件所在目录,让编译器能拼出 <Eigen/Core> 这样的短路径 include_directories(/usr/include/eigen3) # 编译警告全开,很多隐式转换和未使用变量的问题在这一步暴露 add_compile_options(-Wall -Wextra) # 最终产物:可执行文件名 + 参与编译的源文件列表 add_executable(Transformation main.cpp)

逐行说明:cmake_minimum_required写低了某些语法不支持,写高了在老环境上跑不了,3.10 是个比较安全的档位;project()里的名字和add_executable()里的名字可以不一样,前者是工程名,后者是产物名,./Transformation跑的是后者;CMAKE_CXX_STANDARD_REQUIRED ON保证编译器不支持指定标准时直接报错,而不是悄悄降级;-Wall -Wextra建议一直开着,图形学代码里 float/double 混用的情况太多,靠编译器提醒比靠眼睛看靠谱。

有一点要注意:include_directories用的是绝对路径,换台机器如果 Eigen 装在别处就会失效。更通用的写法是用find_package(Eigen3)配合target_link_libraries(Transformation Eigen3::Eigen),但对这份作业来说属于杀鸡用牛刀,先把手动路径跑通,理解原理之后再换。

4.2 四行命令的完整执行路径

课程给的流程是建 build 目录、进目录、跑 cmake、跑 make。为什么不在源码目录里直接cmake .?因为 CMake 会在当前目录生成一堆缓存文件和中间产物,和源码混在一起,想清理的时候根本分不清哪些能删。外部构建(out-of-source build)把所有产物关进 build 目录,删掉整个目录等于彻底重来。

# 进到源码目录,main.cpp 和 CMakeLists.txt 在这里 cd ~/hw0 # 建构建目录并进入 mkdir -p build cd build # 生成构建系统,.. 指向源码目录(CMakeLists.txt 所在处) cmake .. # 编译,-j2 对应虚拟机分到的 2 个核 make -j2 # 运行产物,名字必须和 add_executable 里的一致 ./Transformation

cmake ..里的..是相对路径,指上一级目录,也就是源码目录;如果你在源码目录里执行cmake .,那个.指当前目录,效果是内部构建,不推荐。make -j2的-j后面跟并行任务数,虚拟机给了 2 核就写 2,写 8 反而因为上下文切换变慢。

出问题的时候,别只看终端最后一行。CMake 的报错格式是CMake Error at CMakeLists.txt:行号,从这一行往下读;make 的报错是error:开头,往上翻到第一个error:,后面的通常都是它的连锁反应。想彻底重来,删掉 build 目录再走一遍,比在缓存上修补快:

# 彻底清理:构建目录整个删掉,源码不受影响 cd ~/hw0 rm -rf build

4.3 齐次坐标:先旋转再平移,矩阵该左乘谁

作业本体是一道两维变换题:给定点 P=(2,1),先绕原点逆时针旋转 45°,再平移 (1,2),用齐次坐标算出变换后的坐标。题目本身不难,难的是矩阵乘法的顺序——顺序写反,结果差一个平移量,而且看起来“像是对的”,不手算对照根本发现不了。

齐次坐标的作用是把平移也写成矩阵乘法。二维点写成 (x, y, 1),变换矩阵是 3×3。绕原点逆时针旋转 θ 的矩阵:

R(θ) = [ cosθ -sinθ 0 ] [ sinθ cosθ 0 ] [ 0 0 1 ]

平移 (tx, ty) 的矩阵:

T(tx,ty) = [ 1 0 tx ] [ 0 1 ty ] [ 0 0 1 ]

关键在于组合。点先旋转再平移,正确的写法是T * R * p,从右往左作用。如果写成R * T * p,等价于先平移再旋转,那个平移量 (1,2) 会被一起转 45°,结果完全不同。矩阵乘法不满足交换律,这是图形学里最常见的翻车点之一,血泪经验就是:写完之后用单位矩阵试一遍,P 点不动就说明顺序没搭错。

代入数值,θ = 45°,cos45 = sin45 ≈ 0.7071:

T * R = [ 0.7071 -0.7071 1 ] [ 0.7071 0.7071 2 ] [ 0 0 1 ]

乘上 P=(2,1,1),得到 x = 0.7071×2 − 0.7071×1 + 1 ≈ 1.7071,y = 0.7071×2 + 0.7071×1 + 2 ≈ 4.1213。这个数记下来,后面自检要用。

4.4 用 Eigen 把变换矩阵拼出来

数学清楚了,代码就是把上面几个矩阵按顺序填进去再相乘:

#include <eigen3/Eigen/Core> #include <iostream> #include <cmath> int main() { const float pi = std::acos(-1.0f); const float theta = pi / 4.0f; // 45 度转弧度,别直接写 45 // 齐次坐标下的点 P=(2,1,1),第三维固定为 1 Eigen::Vector3f p(2.0f, 1.0f, 1.0f); // 旋转矩阵:逆时针绕 z 轴转 theta 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; // 平移矩阵:齐次坐标下平移量放在第三列 Eigen::Matrix3f T; T << 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f; // 先旋转再平移:左乘顺序是 T * R,写成 R * T 就错了 Eigen::Vector3f result = T * R * p; std::cout << "result = " << result.transpose() << std::endl; std::cout << "x = " << result.x() << ", y = " << result.y() << std::endl; return 0; }

几个参数点:std::cos(theta)和std::sin(theta)返回 double,赋给Matrix3f时会被截成 float,开-Wall会有警告,介意的话用std::cosf或者显式加上static_cast<float>;R << ...的填充顺序是按行,写的时候按矩阵形状排版,九个元素对不上的时候一眼能看出来;T * R * p的计算顺序是从右往左,先R * p得到旋转后的齐次点,再左乘 T 完成平移,和数学推导严格对应。

输出应该是1.70711 4.12132 1。第三维保持 1 是对的,如果出现别的值,说明矩阵填错了或者乘了不该乘的东西。

5. 五类翻车现场:从黑屏到段错误的排查顺序

这一章记的是我自己和周围人反复踩到的坑。每条按「现象 → 原因 → 解决」写,遇到问题按顺序对照,比漫无目的地搜要快。

5.1 虚拟机启动黑屏或卡在登录界面

现象:点启动之后窗口全黑,或者进度条转完停在一个没有桌面的状态,等十分钟也没反应。

原因:多数是显卡控制器和宿主机驱动不兼容,VirtualBox 默认的 vboxvga 在新显卡上容易出问题;另一种是宿主机开了某些虚拟化相关的系统功能,和 VirtualBox 抢硬件资源。

解决:先在虚拟机设置里把“显示 → 图形控制器”从 vboxvga 换成 vmsvga,把 3D 加速勾掉,重启虚拟机。还不行就在宿主机上临时关掉冲突的虚拟化功能再启动。这一条属于玄学范畴,同样配置在不同机器上一个能跑一个黑屏,换控制器是性价比最高的一招。

5.2 增强功能装完,拖放和分辨率还是不正常

现象:VBoxLinuxAdditions.run跑完没报错,重启后分辨率还是 800x600,鼠标照样要按键切换,拖文件没反应。

原因:三种可能。一是装完之后没重启,内核模块没加载;二是拖放功能只在虚拟机设置里开了“双向”,但装增强功能之前的旧设置在重启后被重置;三是装的时候内核头文件版本不对,模块编译失败但脚本没让整个安装失败。

解决:先lsmod | grep vboxguest确认模块在不在,不在就回去装头文件重装一遍;再检查“设备 → 拖放”和“设备 → 共享粘贴板”是否都是双向;分辨率问题在重启后用xrandr看能不能列出多种分辨率,列不出来说明增强功能没生效。

5.3 cmake 报找不到 Eigen 头文件

现象:fatal error: Eigen/Core: No such file or directory,但代码里明明用的是#include <eigen3/Eigen/Core>。

原因:头文件路径写法与 CMakeLists.txt 里的include_directories不匹配。写了短路径<Eigen/Core>却没加包含目录,或者加了/usr/include/eigen3却还在用长路径,编译器拼出来的路径里出现两层 eigen3。

解决:先dpkg -L libeigen3-dev确认实际安装位置,然后二选一——要么代码统一写<eigen3/Eigen/Core>并删掉include_directories,要么代码写<Eigen/Core>并保留include_directories(/usr/include/eigen3)。改完必须删掉 build 目录重跑 cmake,因为包含路径会被缓存。

5.4 make 报 undefined reference 或重复定义

现象:编译阶段过去了,链接阶段报undefined reference to 'main',或者multiple definition of 'main'。

原因:add_executable后面的源文件列表不对。前者是漏了包含 main 函数的文件,后者是有两个文件都定义了 main——常见于把备份文件main_backup.cpp也放进目录,然后手滑用了通配符收集源文件。

解决:打开 CMakeLists.txt,逐个核对add_executable(Transformation后面跟的文件名,确保有且只有一个文件定义 main。用file(GLOB ...)收集源文件时尤其要注意,目录里的临时文件会被一起收进来。

5.5 程序跑起来直接段错误或输出 nan

现象:./Transformation一执行就Segmentation fault,或者结果里出现nan、inf。

原因:段错误多半是 Eigen 矩阵越界访问(比如声明的是Matrix3f却访问了m(3,0)),或者用了未初始化的指针;nan一般是除数为 0、对负数开平方、或者对奇异矩阵求逆。定长矩阵未初始化也是重灾区,栈上残留的值可能是任意大的浮点数,参与运算后溢出成 inf。

解决:先用-fsanitize=address -g重新编译,段错误会直接指出文件和行号:

# 手动编译一份带地址检查的版本,越界访问会打印完整调用栈 g++ -std=c++14 -fsanitize=address -g \ -I/usr/include/eigen3 \ main.cpp -o Transformation_debug ./Transformation_debug

-fsanitize=address会在每次内存访问时检查边界,代价是运行变慢,但定位越界几乎一击必中。nan则在每个可疑的除法或求逆之前打印一下输入,看是不是出现了 0 或奇异矩阵。

6. 数值自检:把变换结果和手算对上的三个习惯

代码能跑不等于结果对。图形学代码有个讨厌的特点:矩阵顺序写错、角度少转一次、坐标系约定不一致,程序照样跑完,输出一串看着挺正常的浮点数,你不主动验证就发现不了。我现在的固定做法是三步,每写完一个变换都走一遍。

第一步,用手算结果做黄金值。像这道题,心算一下就知道 x 应该在 1.7 附近、y 在 4.1 附近。程序输出1.70711 4.12132就是对的,输出2.41421 3.12132就说明平移被一起旋转了,是R * T而不是T * R。手算不需要精确,量级和符号对上就能排除大部分错误。

第二步,用单位元验算。把角度设成 0、平移量设成 (0,0),变换矩阵应该退化成一个单位矩阵,点原地不动。这一步能抓出矩阵填充错位的问题——九个数字填错一行,在 θ=0 时不一定暴露,但在 P 点取特殊值(比如原点)时会立刻现形。我一般还会把 P 换成 (1,0,1) 和 (0,1,1) 各跑一次,看看旋转方向对不对:逆时针 90° 时 (1,0) 应该转到 (0,1),如果转到 (0,-1) 就是旋转矩阵的符号写反了。

第三步,把中间量打印出来,别只打印最终结果。矩阵乘法出错时,光看输入输出很难定位是哪一步坏的。写代码时临时加几行:

// 调试期打开,确认每个矩阵的实际内容 std::cout << "R =\n" << R << std::endl; std::cout << "T =\n" << T << std::endl; std::cout << "R * p =" << (R * p).transpose() << std::endl; std::cout << "T * R =\n" << (T * R) << std::endl;

R * p的输出应该是 (0.70711, 2.12132, 1),这就是旋转后的点;T * R应该是第一行0.7071 -0.7071 1。这两个中间值和预期对不上,就不用往下看了,问题一定在前面。调通之后把这些打印注释掉或者删掉,别留在提交的代码里。

还有两个小习惯:编译时一直开着-Wall -Wextra,float 和 double 混用、变量未使用这类问题编译器会替你盯着;提交之前把 build 目录整个删掉再打包,只交源码和 CMakeLists.txt,构建产物几十兆,传上去既慢又没必要。

从那以后我每写一个变换矩阵,都强制先跑一遍单位元、再跑一遍特殊点、最后和手算值对一眼,三次里有一次对不上就停下来查,绝不抱着“看着差不多”的心态往下走。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 23:19:06

单人全栈研发工程重构:多Agent协作与React19+Elysia类型安全实践

一个人怎么撑起一个全栈产品的研发&#xff1f;这个问题我琢磨了快两年&#xff0c;真正找到可复用的答案&#xff0c;是在把工作方式彻底重构之后。标题里那行字不是口号&#xff1a;多Agent负责并行协作&#xff0c;React19撑起前端交互&#xff0c;Elysia统一API层的类型安全…

作者头像 李华
网站建设 2026/10/31 23:14:28

AI智能体如何重塑PPT制作:PPTSight全解析

最近这半年&#xff0c;AI 智能体成了办公软件圈子里最热的话题&#xff0c;几乎每周都有新产品冒出来。我自己也陆续试用了不少号称“重塑生产力”的工具&#xff0c;大多数是套了一层壳的聊天机器人&#xff0c;真正能落地解决实际问题的并不多。直到我上手了 PPTSight 这个专…

作者头像 李华
网站建设 2026/10/31 23:08:37

0011AI平台与Claude Code集成开发指南

1. 0011AI平台概述与核心能力解析0011AI作为新兴的AI开发与部署平台&#xff0c;正在技术社区引发广泛关注。这个平台最显著的特点是提供了从模型训练到应用落地的全流程支持&#xff0c;特别适合中小型团队快速构建AI解决方案。我初次接触这个平台是在一个自然语言处理项目中&…

作者头像 李华
网站建设 2026/10/31 23:03:21

PPI才是决定屏幕清晰度的核心指标

1. 为什么我花三天重算了一块7英寸屏的PPI&#xff0c;只因它标称“高清”却看着发虚&#xff1f;刚接手一个车载信息屏的UI适配任务时&#xff0c;我盯着参数表里那行“7英寸、1024600、HD”直皱眉。按常规理解&#xff0c;HD至少该是1280720&#xff0c;可这块屏偏偏用“HD”…

作者头像 李华
网站建设 2026/10/31 22:46:25

基于VLM的智能体循环执行系统设计与实现

1. 项目背景与核心需求在AI智能体开发领域&#xff0c;构建具有记忆能力的循环执行系统正成为关键需求。最近接到一个具体的技术需求&#xff1a;开发一个基于VLM&#xff08;Vision-Language Model&#xff09;的智能体&#xff0c;要求能够以1分钟为间隔周期运行&#xff0c;…

作者头像 李华
网站建设 2026/10/31 22:45:25

MySQL递归查询:原理、应用与优化指南

1. MySQL递归查询概述递归查询是数据库领域中一种强大的查询技术&#xff0c;它允许我们处理具有层级关系的数据结构。在MySQL中&#xff0c;递归查询主要通过公用表表达式(CTE)的WITH RECURSIVE语法实现。这种查询方式特别适合处理树形结构、组织架构、评论回复链等具有父子关…

作者头像 李华