相机与变换:数学基础
3D 图形学的数学基础
在开始实现相机功能前,我们先回顾构成 3D 图形编程基础的核心数学概念。理解这些概念是实现健壮 camera 系统的关键。
3D 图形学中的向量
向量是 3D 图形学的基础,用于表示空间中的位置、方向和运动。在 Vulkan 应用中,我们主要使用以下两种向量:
- 3D 向量 (x, y, z):用于表示位置、方向和法向量
- 4D 向量 (x, y, z, w):用于变换中的齐次坐标
向量在图形学中的重要性
在相机系统中,向量承担多项关键作用:
- 相机的位置由 3D 向量表示
- 相机的观察方向是一个 3D 向量
- 确定相机朝向的上方向(up) 同样由向量表示
向量运算及其应用
右手定则(The Right-Hand Rule)
右手定则是 3D 图形学和数学中的通用约定,用于确定坐标系的朝向和叉积的方向。
- 叉积的右手定则:计算 A × B 时
- 右手食指指向向量 A 的方向
- 右手中指指向向量 B 的方向(与 A 垂直)
- 大拇指所指方向即为叉积结果的方向
- 坐标系的右手定则:右手坐标系中
- 右手食指沿 X 轴正方向
- 右手中指沿 Y 轴正方向
- 大拇指沿 Z 轴正方向
// 叉积方向遵循右手定则
glm::vec3 xAxis(1.0f, 0.0f, 0.0f); // 指向右侧(X轴正方向)
glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // 指向上方(Y轴正方向)
// 右手坐标系中,叉积得到Z轴方向
glm::vec3 zAxis = glm::cross(xAxis, yAxis); // 指向前方(Z轴正方向)
// zAxis 结果为 (0.0f, 0.0f, 1.0f)
// 交换向量顺序,得到相反方向
glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // 指向后方(Z轴负方向)
// negativeZ 结果为 (0.0f, 0.0f, -1.0f)
叉积(Cross Product)
生成与两个输入向量均垂直的新向量应用:
通过相机的前方向(forward)和上方向(up)向量,生成相机的右方向(right)
向量叉积方向遵循前文所述的右手定则(right-hand rule)
归一化(Normalization)
保持向量方向不变,将其长度归一化为 1
应用:确保相机向任意方向移动时,速度保持一致
glm::vec3 a(1.0f, 2.0f, 3.0f);
glm::vec3 b(4.0f, 5.0f, 6.0f);
// 加法 – 合并位置或偏移量
glm::vec3 sum = a + b; // 结果为 (5.0, 7.0, 9.0)
// 点积 – 常用于光照计算
float dotProduct = glm::dot(a, b); // 结果为 32.0
// 若向量已归一化,点积等于两向量夹角的余弦值
float cosAngle = glm::dot(glm::normalize(a), glm::normalize(b)); // 结果约为 0.974
// 叉积 – 生成垂直向量(如相机朝向计算)
glm::vec3 crossProduct = glm::cross(a, b); // 结果为 (-3.0, 6.0, -3.0)
// 归一化 – 确保移动速度一致
glm::vec3 normalized = glm::normalize(a); // 结果为 (0.267, 0.535, 0.802)
矩阵与变换(Matrices and Transformations)
矩阵用于表示 3D 空间中的变换。在 Vulkan 及其他图形 API 中,我们通常使用 4×4 矩阵表示齐次坐标下的变换。
为何使用 4×4 矩阵
尽管我们处于 3D 空间,仍选择 4×4 矩阵的原因:
常见变换矩阵
基于 GLM 的矩阵变换示例
// 平移矩阵 – 移动物体
glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 2.0f, 3.0f));
// 旋转矩阵 – 绕Y轴旋转45度
glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));
// 缩放矩阵 – 缩放物体
glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f, 2.0f, 2.0f));
// 复合变换(先缩放、再旋转、最后平移)
// 矩阵顺序至关重要!最右侧的变换最先执行
glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix;
矩阵的运算顺序(Matrix Order Matters)
矩阵乘法的顺序是核心要点,因为变换的执行顺序为从右到左。顺序错误会导致物体的最终位置和朝向完全偏离预期。
举一个实际例子:若要让立方体绕自身中心旋转,再移动到新位置,必须按正确顺序执行变换:
// 正确顺序:先缩放 → 再旋转 → 最后平移
// 实现立方体绕自身中心旋转后再移动
glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix;
// 错误顺序:先平移 → 再旋转
// 立方体先远离原点,再绕世界原点旋转
// 最终效果为立方体绕世界中心做轨道运动,而非绕自身旋转!
glm::mat4 wrongMatrix = rotationMatrix * translationMatrix * scaleMatrix;
在相机渲染管线中,变换顺序为:projectionMatrix * viewMatrix * modelMatrix * 顶点坐标。每个变换为后续阶段处理数据做准备,更改顺序会直接破坏渲染管线。
直观示例:矩阵顺序的重要性
下图展示了立方体变换中,正确的 T×R×S 与错误的 R×T×S 矩阵乘法顺序的差异:

图 1 矩阵变换顺序对比(矩阵顺序对比:正确的 T×R×S 变换序列 vs 错误的 R×T×S 变换序列)
行优先与列优先存储(Row-Major vs. Column-Major Representation)
在图形编程中使用矩阵时,必须理解行优先和列优先存储的区别:
- 行优先(Row-Major):矩阵元素按行依次存入内存被 DirectX、C/C++ 多维数组采用矩阵访问方式为 M [row][column]
- 列优先(Column-Major):矩阵元素按列依次存入内存被 OpenGL、GLSL 采用,也是 GLM 的默认存储方式从内存布局角度,矩阵访问方式为 M [column][row]
// 3×3矩阵的行优先与列优先存储示例
// 原矩阵:
// [ a b c ]
// [ d e f ]
// [ g h i ]
// 行优先内存布局:
// [a, b, c, d, e, f, g, h, i]
// 列优先内存布局:
// [a, d, g, b, e, h, c, f, i]
// GLM中矩阵默认采用列优先存储
glm::mat4 matrix = glm::mat4(1.0f); // 列优先格式的单位矩阵
// 向Vulkan着色器传递矩阵时,需注意内存布局
// Vulkan默认期望列优先矩阵,与GLM默认设置一致
Vulkan 与矩阵布局
Vulkan 同时支持行优先和列优先格式,但需显式指定所使用的格式:
实际影响:
- 不同布局下,矩阵乘法的顺序可能需要反转
- 调试时,矩阵元素的显示形式可能与数学表达式中的转置形式一致
- 在不同 API 间移植代码时,可能需要对矩阵进行转置处理
仿射变换(Affine Transformations)
仿射变换是计算机图形学的基础概念,它保持平行线的关系(但不一定保持角度和距离),是表示 3D 图形学中大多数常用操作的核心。
仿射变换的特性
仿射变换可表示为线性变换与平移的组合:
- 线性变换:旋转、缩放、错切
- 平移:空间中的位置移动
数学表达式为:f(x) = A·x + b其中,A 为线性变换矩阵,b 为平移向量。
仿射变换在图形学中的重要性
基于齐次坐标的仿射变换表示
在 3D 图形学中,我们使用 4×4 矩阵结合齐次坐标表示仿射变换:
// 表示仿射变换的4×4矩阵结构
// [ R R R Tx ]
// [ R R R Ty ]
// [ R R R Tz ]
// [ 0 0 0 1 ]
// 其中,R代表旋转/缩放/错切变换,T代表平移变换
// GLM中仿射变换矩阵的示例
glm::mat4 affineTransform = glm::mat4(
glm::vec4(r11, r12, r13, tx), // 第一行
glm::vec4(r21, r22, r23, ty), // 第二行
glm::vec4(r31, r32, r33, tz), // 第三行
glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) // 仿射变换的最后一行恒为(0,0,0,1)
);
仿射变换的实际应用
在 Vulkan 应用中,我们执行的几乎所有变换都是仿射变换:
- 场景中物体的移动(平移)
- 旋转物体以改变朝向
- 缩放物体调整大小
- 组合上述操作,实现物体的定位和朝向设置
位姿矩阵(Pose Matrices)
位姿矩阵(也称为变换矩阵或刚体变换矩阵)是一种特殊的仿射变换,用于表示 3D 空间中物体的位置和朝向。
位姿矩阵的结构
位姿矩阵将旋转和平移整合到一个 4×4 矩阵中:
// 位姿矩阵的标准结构
// [ R R R Tx ]
// [ R R R Ty ]
// [ R R R Tz ]
// [ 0 0 0 1 ]
// 其中,3×3的R子矩阵表示旋转,[Tx,Ty,Tz]表示平移
// GLM中创建位姿矩阵的示例
glm::mat4 poseMatrix = glm::mat4(1.0f); // 从单位矩阵开始
poseMatrix = glm::translate(poseMatrix, position); // 应用平移
poseMatrix = poseMatrix * rotationMatrix; // 应用旋转
位姿矩阵的应用
位姿矩阵是图形引擎的核心组件,主要用于:
从位姿矩阵中提取信息
我们可以从位姿矩阵中提取出位置、方向等关键信息:
// 从位姿矩阵中提取位置
glm::vec3 extractPosition(const glm::mat4& poseMatrix) {
return glm::vec3(poseMatrix[3]); // 平移信息存储在矩阵的最后一列
}
// 从位姿矩阵中提取前方向(基于标准OpenGL朝向)
glm::vec3 extractForwardDirection(const glm::mat4& poseMatrix) {
return -glm::vec3(poseMatrix[2]); // 负Z轴方向(第三列)
}
// 从位姿矩阵中提取上方向
glm::vec3 extractUpDirection(const glm::mat4& poseMatrix) {
return glm::vec3(poseMatrix[1]); // Y轴方向(第二列)
}
Look-At 函数的实现
Look-At 函数是相机系统的基础工具,用于创建视图矩阵,使相机朝向空间中的指定目标点。这是 3D 图形学中最常用的操作之一,也是上述数学概念实际应用的典型示例。
Look-At 函数的作用
数学原理
Look-At 函数通过构建标准正交基(三个互相垂直的单位向量)定义相机的朝向:
这三个向量结合相机位置,共同构成将世界坐标转换为相机坐标的视图矩阵。
分步实现
我们通过实现一个自定义的 Look-At 函数,理解其底层逻辑:
glm::mat4 createLookAtMatrix(
const glm::vec3& cameraPosition, // 相机位置
const glm::vec3& targetPosition, // 相机观察的目标位置
const glm::vec3& worldUpVector // 世界上方向(通常为Y轴)
) {
// 步骤1:计算相机的前方向(Z轴)
// 注意:在OpenGL/Vulkan中,相机沿负Z轴观察,因此需要取反
glm::vec3 forward = glm::normalize(cameraPosition – targetPosition);
// 步骤2:计算相机的右方向(X轴)
// 世界上方向与前方向的叉积
glm::vec3 right = glm::normalize(glm::cross(worldUpVector, forward));
// 步骤3:计算相机的上方向(Y轴)
// 前方向与右方向的叉积,确保正交性
glm::vec3 up = glm::cross(forward, right);
// 步骤4:构建视图矩阵的旋转部分
// 矩阵的每一行对应相机的一个基向量
glm::mat4 rotation = glm::mat4(1.0f);
rotation[0][0] = right.x;
rotation[1][0] = right.y;
rotation[2][0] = right.z;
rotation[0][1] = up.x;
rotation[1][1] = up.y;
rotation[2][1] = up.z;
rotation[0][2] = forward.x;
rotation[1][2] = forward.y;
rotation[2][2] = forward.z;
// 步骤5:构建视图矩阵的平移部分
glm::mat4 translation = glm::mat4(1.0f);
translation[3][0] = -cameraPosition.x;
translation[3][1] = -cameraPosition.y;
translation[3][2] = -cameraPosition.z;
// 步骤6:组合旋转和平移矩阵
// 先执行平移,再执行旋转
return rotation * translation;
}
使用 GLM 内置的 Look-At 函数
实际开发中,我们通常直接使用 GLM 内置的lookAt函数,其底层实现与上述自定义函数一致:
// 使用GLM内置的lookAt函数
glm::mat4 viewMatrix = glm::lookAt(
glm::vec3(0.0f, 0.0f, 5.0f), // 相机位置
glm::vec3(0.0f, 0.0f, 0.0f), // 目标位置(世界原点)
glm::vec3(0.0f, 1.0f, 0.0f) // 世界上方向(Y轴)
);
实际应用场景
Look-At 函数广泛应用于各类相机控制场景:
示例 1:实现轨道相机
以下示例展示如何使用 Look-At 函数实现绕目标旋转的轨道相机:
// 轨道相机实现
void updateOrbitCamera(float deltaTime) {
// 根据时间更新轨道旋转角度
orbitAngle += orbitSpeed * deltaTime;
// 计算相机在目标周围圆周上的位置
float radius = 10.0f;
glm::vec3 cameraPosition(
targetPosition.x + radius * cos(orbitAngle),
targetPosition.y + 5.0f, // 相机略高于目标
targetPosition.z + radius * sin(orbitAngle)
);
// 使用lookAt创建视图矩阵
viewMatrix = glm::lookAt(
cameraPosition,
targetPosition,
glm::vec3(0.0f, 1.0f, 0.0f)
);
}
示例 2:相机平滑过渡
Look-At 函数也可用于实现相机在不同位置和目标间的平滑过渡:
// 相机平滑过渡
void transitionCamera(float t) { // t的取值范围为0.0 ~ 1.0
// 在起始位置和结束位置间插值
glm::vec3 currentPosition = glm::mix(startPosition, endPosition, t);
// 在起始目标和结束目标间插值
glm::vec3 currentTarget = glm::mix(startTarget, endTarget, t);
// 更新视图矩阵
viewMatrix = glm::lookAt(
currentPosition,
currentTarget,
glm::vec3(0.0f, 1.0f, 0.0f)
);
}
理解 Look-At 函数的工作原理,能帮助你掌握相机在 3D 空间中的朝向逻辑,以及视图矩阵如何从相机视角对世界进行变换。
3D 图形学中的射线投射(Raycasting)
射线投射是 3D 图形学的核心技术,指从某一点向场景中发射射线,并判断射线与场景中物体的相交情况。该技术应用广泛,包括场景中的物体拾取、碰撞检测、可见性判断等。
射线的表示
3D 空间中的射线由原点和方向向量定义:
struct Ray {
glm::vec3 origin; // 射线的起点
glm::vec3 direction; // 归一化的方向向量
};
// 创建射线
Ray createRay(const glm::vec3& origin, const glm::vec3& direction) {
Ray ray;
ray.origin = origin;
ray.direction = glm::normalize(direction); // 确保方向向量归一化
return ray;
}
射线 – 物体相交检测
射线投射的核心是判断射线是否与场景物体相交,以及相交的位置。以下是几种常见的相交检测算法:
射线 – 球体相交检测
射线与球体的相交检测是最简单的相交检测算法之一:
struct Sphere {
glm::vec3 center;
float radius;
};
bool rayIntersectsSphere(const Ray& ray, const Sphere& sphere, float& t) {
// 从射线原点到球心的向量
glm::vec3 oc = sphere.center – ray.origin;
// 二次方程系数
float a = glm::dot(ray.direction, ray.direction); // 方向向量归一化后,a恒为1
float b = 2.0f * glm::dot(oc, ray.direction);
float c = glm::dot(oc, oc) – sphere.radius * sphere.radius;
// 判别式
float discriminant = b * b – 4 * a * c;
if (discriminant < 0) {
// 无交点
return false;
}
// 找到最近的交点
float sqrtDiscriminant = sqrt(discriminant);
float t0 = (-b – sqrtDiscriminant) / (2 * a);
float t1 = (-b + sqrtDiscriminant) / (2 * a);
// 检查交点是否在射线的正方向
if (t0 > 0) {
t = t0;
return true;
}
if (t1 > 0) {
t = t1;
return true;
}
// 两个交点均在射线的反方向
return false;
}
射线 – 三角形相交检测
三角形相交检测是 3D 模型射线投射的基础:
struct Triangle {
glm::vec3 v0, v1, v2; // 三角形的三个顶点
};
bool rayIntersectsTriangle(const Ray& ray, const Triangle& triangle, float& t, glm::vec2& barycentricCoords) {
// Möller–Trumbore 算法
glm::vec3 edge1 = triangle.v1 – triangle.v0;
glm::vec3 edge2 = triangle.v2 – triangle.v0;
glm::vec3 h = glm::cross(ray.direction, edge2);
float a = glm::dot(edge1, h);
// 检查射线是否与三角形平行
if (a > -0.00001f && a < 0.00001f) {
return false;
}
float f = 1.0f / a;
glm::vec3 s = ray.origin – triangle.v0;
float u = f * glm::dot(s, h);
// 检查交点是否在三角形外部
if (u < 0.0f || u > 1.0f) {
return false;
}
glm::vec3 q = glm::cross(s, edge1);
float v = f * glm::dot(ray.direction, q);
// 检查交点是否在三角形外部
if (v < 0.0f || u + v > 1.0f) {
return false;
}
// 计算交点到射线原点的距离
t = f * glm::dot(edge2, q);
// 检查交点是否在射线的反方向
if (t <= 0.0f) {
return false;
}
// 存储重心坐标,用于插值计算
barycentricCoords = glm::vec2(u, v);
return true;
}
射线 – 轴对齐包围盒相交检测
轴对齐包围盒(AABB)相交检测常用于粗阶段碰撞检测:
struct AABB {
glm::vec3 min; // 包围盒的最小角
glm::vec3 max; // 包围盒的最大角
};
bool rayIntersectsAABB(const Ray& ray, const AABB& aabb, float& tMin, float& tMax) {
// 计算射线与每个轴对齐平面的交点
glm::vec3 invDir = 1.0f / ray.direction;
glm::vec3 t0 = (aabb.min – ray.origin) * invDir;
glm::vec3 t1 = (aabb.max – ray.origin) * invDir;
// 处理射线方向为负的情况
glm::vec3 tSmaller = glm::min(t0, t1);
glm::vec3 tBigger = glm::max(t0, t1);
// 找到射线进入和离开包围盒的距离
tMin = glm::max(tSmaller.x, glm::max(tSmaller.y, tSmaller.z));
tMax = glm::min(tBigger.x, glm::min(tBigger.y, tBigger.z));
// 检查是否存在有效交点
return tMax >= tMin && tMax > 0;
}
相机射线的创建
相机射线的创建是射线投射最常见的应用之一,是实现物体拾取和光线追踪的基础:
Ray createCameraRay(
const glm::vec2& screenCoord, // 归一化的屏幕坐标(范围-1到1)
const glm::mat4& viewMatrix, // 相机视图矩阵
const glm::mat4& projectionMatrix // 相机投影矩阵
) {
// 转换为裁剪空间坐标
glm::vec4 clipCoords(screenCoord.x, screenCoord.y, -1.0f, 1.0f);
// 转换为相机空间坐标
glm::mat4 invProjection = glm::inverse(projectionMatrix);
glm::vec4 viewCoords = invProjection * clipCoords;
viewCoords.z = -1.0f; // 指向相机空间的负Z轴
viewCoords.w = 0.0f; // 转换为方向向量
// 转换为世界空间坐标
glm::mat4 invView = glm::inverse(viewMatrix);
glm::vec4 worldCoords = invView * viewCoords;
// 创建相机射线
Ray ray;
ray.origin = glm::vec3(invView[3]); // 世界空间中的相机位置
ray.direction = glm::normalize(glm::vec3(worldCoords));
return ray;
}
射线投射在图形学中的应用
射线投射在 3D 图形学和游戏开发中应用广泛:
射线投射的性能优化
对于包含大量物体的复杂场景,射线投射的计算成本较高。以下是几种常用的优化技巧:
3D 图形学中的投影(Projection)
投影是将相机空间中的 3D 坐标转换为屏幕上 2D 坐标的过程。在计算机图形学中,我们使用投影矩阵完成这一变换。
投影的类型
3D 图形学中主要有两种投影方式:
透视投影
透视投影能创建具有真实深度感的视图,远处的物体会自然缩小:
// 创建透视投影矩阵
glm::mat4 createPerspectiveMatrix(
float fovY, // 垂直视野,单位为角度
float aspectRatio, // 视口的宽高比(宽度/高度)
float nearPlane, // 近裁剪面的距离
float farPlane // 远裁剪面的距离
) {
return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane);
}
透视投影矩阵主要完成以下变换:
透视投影矩阵的标准结构:
// 透视投影矩阵结构
// [ (h/w)*cot(fovY/2) 0 0 0 ]
// [ 0 cot(fovY/2) 0 0 ]
// [ 0 0 -(f+n)/(f-n) -2*f*n/(f-n) ]
// [ 0 0 -1 0 ]
// 其中:
// – fovY:垂直视野
// – w/h:视口宽高比
// – n:近裁剪面距离
// – f:远裁剪面距离
正交投影
正交投影中,物体的显示尺寸与到相机的距离无关,无透视变形:
// 创建正交投影矩阵
glm::mat4 createOrthographicMatrix(
float left, // 左裁剪面坐标
float right, // 右裁剪面坐标
float bottom, // 下裁剪面坐标
float top, // 上裁剪面坐标
float nearPlane, // 近裁剪面距离
float farPlane // 远裁剪面距离
) {
return glm::ortho(left, right, bottom, top, nearPlane, farPlane);
}
正交投影矩阵仅将视景体缩放和平移至标准视景体,不执行透视除法:
// 正交投影矩阵结构
// [ 2/(r-l) 0 0 -(r+l)/(r-l) ]
// [ 0 2/(t-b) 0 -(t+b)/(t-b) ]
// [ 0 0 -2/(f-n) -(f+n)/(f-n) ]
// [ 0 0 0 1 ]
// 其中:
// – l, r:左、右裁剪面坐标
// – b, t:下、上裁剪面坐标
// – n, f:近、远裁剪面距离
视锥体(The View Frustum)
视锥体是相机的可视空间范围,透视投影的视锥体为平截头体(截去顶部的金字塔):
- 近裁剪面(Near Plane):相机最近的可视平面,渲染从该平面开始
- 远裁剪面(Far Plane):相机最远的可视平面,渲染到该平面结束
- 视野(FOV):决定视图宽度的角度
- 宽高比(Aspect Ratio):视口的宽度与高度的比值
// 计算视锥体的8个顶点
void calculateFrustumCorners(
float fovY,
float aspectRatio,
float nearPlane,
float farPlane,
glm::vec3 corners[8] // 输出视锥体的8个顶点
) {
float tanHalfFovY = tan(glm::radians(fovY) / 2.0f);
// 近裁剪面的尺寸
float nearHeight = 2.0f * nearPlane * tanHalfFovY;
float nearWidth = nearHeight * aspectRatio;
// 远裁剪面的尺寸
float farHeight = 2.0f * farPlane * tanHalfFovY;
float farWidth = farHeight * aspectRatio;
// 相机空间中的近裁剪面顶点
corners[0] = glm::vec3(-nearWidth/2, -nearHeight/2, -nearPlane); // 左下
corners[1] = glm::vec3( nearWidth/2, -nearHeight/2, -nearPlane); // 右下
corners[2] = glm::vec3( nearWidth/2, nearHeight/2, -nearPlane); // 右上
corners[3] = glm::vec3(-nearWidth/2, nearHeight/2, -nearPlane); // 左上
// 相机空间中的远裁剪面顶点
corners[4] = glm::vec3(-farWidth/2, -farHeight/2, -farPlane); // 左下
corners[5] = glm::vec3( farWidth/2, -farHeight/2, -farPlane); // 右下
corners[6] = glm::vec3( farWidth/2, farHeight/2, -farPlane); // 右上
corners[7] = glm::vec3(-farWidth/2, farHeight/2, -farPlane); // 左上
}
投影与反投影
投影将 3D 世界坐标转换为 2D 屏幕坐标,反投影则执行逆向操作。以下示例用于帮助理解投影和反投影的数学原理,虽不直接用于基础渲染管线,但对实现物体拾取、3D 物体与鼠标交互、自定义渲染技术至关重要:
// 将3D点投影到屏幕空间
glm::vec2 projectPoint(
const glm::vec3& worldPoint,
const glm::mat4& viewMatrix,
const glm::mat4& projectionMatrix,
const glm::vec4& viewport // 视口参数 (x, y, width, height)
) {
// 转换为裁剪空间坐标
glm::vec4 clipSpace = projectionMatrix * viewMatrix * glm::vec4(worldPoint, 1.0f);
// 执行透视除法
glm::vec3 ndcSpace = glm::vec3(clipSpace) / clipSpace.w;
// 映射到视口坐标
glm::vec2 screenPos;
screenPos.x = (ndcSpace.x + 1.0f) * 0.5f * viewport.z + viewport.x;
screenPos.y = (1.0f – ndcSpace.y) * 0.5f * viewport.w + viewport.y; // Y轴翻转
return screenPos;
}
// 将屏幕点反投影为世界空间中的射线
Ray unprojectScreenPoint(
const glm::vec2& screenPoint,
const glm::mat4& viewMatrix,
const glm::mat4& projectionMatrix,
const glm::vec4& viewport // 视口参数 (x, y, width, height)
) {
// 转换为标准化设备坐标(NDC)
glm::vec3 ndcPos;
ndcPos.x = 2.0f * (screenPoint.x – viewport.x) / viewport.z – 1.0f;
ndcPos.y = 1.0f – 2.0f * (screenPoint.y – viewport.y) / viewport.w; // Y轴翻转
ndcPos.z = -1.0f; // 近裁剪面位置
// 创建从相机出发,穿过该点的射线
return createCameraRay(glm::vec2(ndcPos.x, ndcPos.y), viewMatrix, projectionMatrix);
}
投影在图形学中的应用
投影矩阵在 3D 图形学中有多种应用:
投影方式的选择
透视投影和正交投影的选择取决于具体的应用场景:
选择透视投影的场景
- 第一人称 / 第三人称游戏
- 真实的 3D 可视化场景
- 所有需要深度感知的应用
选择正交投影的场景
- 包含 3D 元素的 2D 游戏
- 技术绘图和 CAD 应用
- 不受透视影响的 UI 元素
- 等轴测或俯视视角游戏
用于旋转的四元数(Quaternions for Rotations)
旋转矩阵虽能实现旋转功能,但四元数在某些旋转操作中具有明显优势,尤其是在实现相机平滑运动和避免万向节死锁(Gimbal Lock,特定朝向下车由度丢失)时。
为何使用四元数
基于 GLM 的四元数运算示例
// 由欧拉角创建四元数(单位为弧度)
glm::quat rotation = glm::quat(glm::vec3(
glm::radians(30.0f), // 俯仰(X轴)- 上下看
glm::radians(45.0f), // 偏航(Y轴)- 左右看
glm::radians(60.0f) // 滚转(Z轴)- 相机倾斜
));
// 将四元数转换为旋转矩阵,用于渲染
glm::mat4 rotationMatrix = glm::mat4_cast(rotation);
// 使用四元数旋转向量(如旋转相机的前方向向量)
glm::vec3 original(1.0f, 0.0f, 0.0f);
glm::vec3 rotated = rotation * original;
3D 图形学中的坐标系(Coordinate Systems in 3D Graphics)
理解不同的坐标系是实现相机系统的关键。在渲染管线中,数据会依次经过多个坐标系的变换:
各坐标系介绍
坐标系的手性(Handedness of Coordinate Systems)
图形 API 和引擎主要使用右手坐标系和左手坐标系:
- X 轴指向右侧
- Y 轴指向上方
- Z 轴指向屏幕外(朝向观察者)
- 叉积:Z = X × Y(遵循右手定则)
- X 轴指向右侧
- Y 轴指向上方
- Z 轴指向屏幕内(远离观察者)
- 叉积:Z = X × Y(遵循左手定则)
// Vulkan中通常使用右手坐标系
// 若需要,可在两种坐标系间进行转换
// 将点从左手坐标系转换为右手坐标系
// 只需翻转Z坐标
glm::vec3 leftHandedPoint(x, y, z);
glm::vec3 rightHandedPoint(x, y, -z);
// 设置相机时,坐标系的手性会影响视图矩阵
// 右手坐标系中,相机通常沿负Z轴观察
// 这也是相机代码中常将-Z作为前方向的原因
对相机系统的影响
坐标系的手性直接影响相机的设置方式:
右手坐标系(Vulkan 约定)的相机设置要点
- 右方向向量:视图矩阵的 X 轴
- 上方向向量:视图矩阵的 Y 轴
- 前方向向量:视图矩阵的负 Z 轴
变换管线(The Transformation Pipeline)
3D 图形学的标准变换管线遵循以下顺序:局部空间 → 世界空间 → 相机空间 → 裁剪空间 → 屏幕空间
着色器中典型的顶点变换代码:
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
在下一节中,我们将结合上述数学概念,为 Vulkan 应用实现一个灵活的相机系统。
拓展资源
如果你对部分数学概念感到困惑,或希望加深理解,以下是按主题分类的优质学习资源:
通用 3D 数学资源
书籍
- 《Mathematics for 3D Game Programming and Computer Graphics》- Eric Lengyel:3D 数学的综合参考手册
- 《3D Math Primer for Graphics and Game Development》- Fletcher Dunn & Ian Parberry:适合入门的 3D 数学基础教程
- 《Essential Mathematics for Games and Interactive Applications》- James M. Van Verth & Lars M. Bishop:偏实际应用,包含大量代码示例
在线课程
- Scratchapixel: Vectors:免费的向量和矩阵基础课程
- 3Blue1Brown: Essence of Linear Algebra:讲解向量、矩阵和变换的核心知识
交互式工具
- Dot Product Visualization :四元数旋转的交互式可视化工具
- Dot Product Visualization :可实验各类 3D 变换的交互式工具
向量与向量运算
教程
- Scratchapixel: Vectors :带图形的详细向量讲解
- 3Blue1Brown: Essence of Linear Algebra :向量的可视化讲解经典教程
交互式工具
- GeoGebra: Vector Operations :向量加、减、点积、叉积的交互式工具
- Dot Product Visualization :点积的交互式可视化工具
矩阵与变换
教程
- Scratchapixel: Transformations:变换矩阵的详细讲解
- LearnOpenGL: Transformations:图形学中变换的实用指南
交互式工具
- ShaderToy: Matrix Transformations:矩阵变换的交互式可视化工具
- Red Blob Games: Interactive Transformations:2D 变换的可视化讲解(概念可扩展到 3D)
四元数
教程
- 3Blue1Brown: Quaternions and 3D rotation:四元数的可视化讲解
- Understanding Quaternions :带代码示例的四元数实用指南
交互式工具
- Quaternion Visualizer:四元数旋转的交互式可视化工具
- ShaderToy: Quaternion Rotation:四元数旋转的交互式实验工具
坐标系与手性
教程
- LearnOpenGL: Coordinate Systems:图形学中不同坐标系的讲解
- Scratchapixel: Coordinate Systems:带图形的详细讲解
参考文档
- OpenGL Wiki: Coordinate Transformations :坐标变换的官方参考
- Microsoft Docs: Coordinate Systems:左手 / 右手坐标系的讲解
GLM 数学库(本文示例使用)
文档
- GLM Manual:GLM 的官方手册
- GLM API Documentation :GLM 的 API 参考
教程
- LearnOpenGL: Transformations with GLM :使用 GLM 实现变换的实用指南
- GLM Tutorial :GLM 图形学数学应用教程
交互式学习工具
可视化工具
- GeoGebra 3D Calculator:创建和操作 3D 物体与变换
- ShaderToy:实验使用 3D 数学的着色器代码
练习题
- Khan Academy: Vectors and Spaces :向量数学的练习题
- Khan Academy: Matrix Transformations:矩阵变换的练习题
这些资源能帮助你更深入地理解 3D 图形学和相机系统中的数学概念。如果对某个概念理解困难,建议参考多个资源,不同的讲解方式可能更贴合你的学习风格。



