欢迎光临
我们一直在努力

Building a Simple Engine -- Camera Transformations -- Math foundations

相机与变换:数学基础

3D 图形学的数学基础

在开始实现相机功能前,我们先回顾构成 3D 图形编程基础的核心数学概念。理解这些概念是实现健壮 camera 系统的关键。

3D 图形学中的向量

向量是 3D 图形学的基础,用于表示空间中的位置、方向和运动。在 Vulkan 应用中,我们主要使用以下两种向量:

  • 3D 向量 (x, y, z):用于表示位置、方向和法向量
  • 4D 向量 (x, y, z, w):用于变换中的齐次坐标

向量在图形学中的重要性

在相机系统中,向量承担多项关键作用:

  • 相机的位置由 3D 向量表示
  • 相机的观察方向是一个 3D 向量
  • 确定相机朝向的上方向(up) 同样由向量表示

向量运算及其应用

  • 加法与减法:用于计算相对位置和运动示例:newPosition = currentPosition + movementDirection * speed
  • 标量乘法:用于缩放运动和方向示例:将速度乘以小于 1 的系数,降低相机移动速度
  • 点积(Dot Product):计算两个向量的点积,归一化向量的点积等于两向量夹角的余弦值应用:判断物体是否朝向相机、计算光照强度
  • 叉积(Cross Product):生成与两个输入向量都垂直的新向量应用:由相机的前方向(forward) 和上方向(up) 生成右方向(right) 向量叉积的方向遵循右手定则(下文详解)
  • 归一化(Normalization):保持向量方向不变,将其长度设为 1应用:确保相机向任意方向的移动速度一致
  • 右手定则(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 矩阵的原因:

  • 可同时表示平移(移动)、旋转和缩放变换
  • 矩阵可通过乘法组合,实现复杂的复合变换
  • 与齐次坐标 (x, y, z, w) 兼容,是透视投影的必要条件
  • 常见变换矩阵

  • 平移矩阵(Translation Matrix):在 3D 空间中移动物体相机系统中的应用:改变相机位置
  • 旋转矩阵(Rotation Matrix):绕某一轴旋转物体相机系统中的应用:改变相机的观察方向
  • 缩放矩阵(Scale Matrix):改变物体的大小相机系统中使用较少,但对场景中的物体至关重要
  • 模型矩阵(Model Matrix):组合各类变换,将物体定位到世界空间以世界原点为基准确定物体位置
  • 视图矩阵(View Matrix):将世界空间变换为相机空间本质是将世界坐标系相对于相机进行定位
  • 投影矩阵(Projection Matrix):将相机空间变换为裁剪空间定义 3D 物体如何投影到 2D 屏幕控制透视效果、视野(FOV)和可视范围(近 / 远裁剪面)
  • 基于 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 同时支持行优先和列优先格式,但需显式指定所使用的格式:

  • Vulkan 默认期望列优先矩阵格式
  • 可在着色器中使用row_major限定符指定行优先格式
  • GLM(Vulkan 开发的常用数学库)默认列优先,也可配置为行优先
  • 实际影响:

    • 不同布局下,矩阵乘法的顺序可能需要反转
    • 调试时,矩阵元素的显示形式可能与数学表达式中的转置形式一致
    • 在不同 API 间移植代码时,可能需要对矩阵进行转置处理

    仿射变换(Affine Transformations)

    仿射变换是计算机图形学的基础概念,它保持平行线的关系(但不一定保持角度和距离),是表示 3D 图形学中大多数常用操作的核心。

    仿射变换的特性

    仿射变换可表示为线性变换与平移的组合:

    • 线性变换:旋转、缩放、错切
    • 平移:空间中的位置移动

    数学表达式为:f(x) = A·x + b其中,A 为线性变换矩阵,b 为平移向量。

    仿射变换在图形学中的重要性

  • 保持共线性:直线上的点变换后仍在同一直线上
  • 保持线段上的距离比例
  • 可表示 3D 图形学中所有常用的基础变换
  • 可通过矩阵乘法高效地组合(复合)
  • 基于齐次坐标的仿射变换表示

    在 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; // 应用旋转

    位姿矩阵的应用

    位姿矩阵是图形引擎的核心组件,主要用于:

  • 物体定位:定义物体在空间中的位置和朝向示例:将角色模型放置在世界中,并设置正确的朝向
  • 相机表示:定义相机的位置和朝向示例:视图矩阵(View Matrix)是相机位姿矩阵的逆矩阵
  • 层级变换:由简单组件构建复杂物体示例:角色的手部位置依赖于手臂,手臂位置依赖于躯干
  • 动画:在不同位姿间进行插值示例:相机从一个位置 / 朝向平滑过渡到另一个
  • 从位姿矩阵中提取信息

    我们可以从位姿矩阵中提取出位置、方向等关键信息:

    // 从位姿矩阵中提取位置
    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 函数的作用

  • 让相机朝向 3D 空间中的指定点
  • 建立相机的局部坐标系(right、up、forward 向量)
  • 生成将世界坐标转换为相机坐标的视图矩阵
  • 通过聚焦目标简化相机控制,无需手动管理旋转角度
  • 数学原理

    Look-At 函数通过构建标准正交基(三个互相垂直的单位向量)定义相机的朝向:

  • 前方向向量(Z):从相机位置指向目标位置
  • 右方向向量(X):同时垂直于前方向向量和世界上方向向量
  • 上方向向量(Y):同时垂直于前方向向量和右方向向量
  • 这三个向量结合相机位置,共同构成将世界坐标转换为相机坐标的视图矩阵。

    分步实现

    我们通过实现一个自定义的 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 函数广泛应用于各类相机控制场景:

  • 第一人称相机:朝向移动方向观察
  • 第三人称相机:跟随角色并始终朝向角色
  • 轨道相机:绕兴趣点做圆周运动
  • 影视级相机:创建聚焦关键物体的平滑相机运动
  • 物体检视:允许用户从不同角度查看 3D 模型
  • 示例 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 场景中点击的物体从相机出发,沿鼠标位置发射射线,找到最近的相交物体
  • 碰撞检测:检查物体沿运动路径是否会发生碰撞沿运动方向发射射线,检测潜在的碰撞
  • 视线检测:判断一个物体是否能 “看到” 另一个物体在两个物体间发射射线,检查是否有遮挡物
  • 地形高度采样:获取指定位置的地形高度从地形上方向下发射射线,获取交点高度
  • 物理模拟:实现真实的物理行为射线投射是许多物理引擎碰撞解析的基础
  • AI 导航:辅助 AI 角色在环境中导航射线投射可检测障碍物,确定有效移动路径
  • 射线投射的性能优化

    对于包含大量物体的复杂场景,射线投射的计算成本较高。以下是几种常用的优化技巧:

  • 空间划分:使用八叉树、BVH、k-d 树等数据结构,快速排除不可能与射线相交的物体
  • 包围盒层级:先检测简单的包围盒(球体、AABB),再对细节几何执行更耗时的检测
  • 细节层次(LOD):对远距离物体使用简化的碰撞几何
  • 射线批处理:批量处理多条射线,利用 SIMD 指令提升效率
  • 提前终止:若只需判断是否存在相交,找到第一个交点后立即停止检测
  • 3D 图形学中的投影(Projection)

    投影是将相机空间中的 3D 坐标转换为屏幕上 2D 坐标的过程。在计算机图形学中,我们使用投影矩阵完成这一变换。

    投影的类型

    3D 图形学中主要有两种投影方式:

  • 透视投影(Perspective Projection):物体距离越远,显示尺寸越小,模拟人眼的视觉效果
  • 正交投影(Orthographic Projection):物体的显示尺寸与距离无关,适用于技术绘图、2D 游戏和 UI 元素
  • 透视投影

    透视投影能创建具有真实深度感的视图,远处的物体会自然缩小:

    // 创建透视投影矩阵
    glm::mat4 createPerspectiveMatrix(
    float fovY, // 垂直视野,单位为角度
    float aspectRatio, // 视口的宽高比(宽度/高度)
    float nearPlane, // 近裁剪面的距离
    float farPlane // 远裁剪面的距离
    ) {
    return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane);
    }

    透视投影矩阵主要完成以下变换:

  • 根据视野和宽高比,缩放视锥体
  • 将视景体映射为标准视景体(各维度范围为 – 1 到 1 的立方体)
  • 执行透视除法(除以 w 分量),实现透视效果
  • 透视投影矩阵的标准结构:

    // 透视投影矩阵结构
    // [ (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 场景几何转换为 2D 屏幕像素
  • 阴影映射(Shadow Mapping):从光源视角投影场景,判断阴影区域
  • 反射 / 折射:计算光线在表面的反射和折射方向
  • 纹理投影:基于投影器视角将纹理映射到表面
  • 屏幕空间效果:实现屏幕空间反射、环境光遮蔽等后处理效果
  • 投影方式的选择

    透视投影和正交投影的选择取决于具体的应用场景:

    选择透视投影的场景
    • 第一人称 / 第三人称游戏
    • 真实的 3D 可视化场景
    • 所有需要深度感知的应用
    选择正交投影的场景
    • 包含 3D 元素的 2D 游戏
    • 技术绘图和 CAD 应用
    • 不受透视影响的 UI 元素
    • 等轴测或俯视视角游戏

    用于旋转的四元数(Quaternions for Rotations)

    旋转矩阵虽能实现旋转功能,但四元数在某些旋转操作中具有明显优势,尤其是在实现相机平滑运动和避免万向节死锁(Gimbal Lock,特定朝向下车由度丢失)时。

    为何使用四元数

  • 表示更紧凑:4 个分量,相比旋转矩阵的 9 个分量更节省内存
  • 插值更平滑:更容易在不同朝向间实现平滑插值(对相机动画至关重要)
  • 避免万向节死锁:解决欧拉角(俯仰 pitch、偏航 yaw、滚转 roll)存在的万向节死锁问题
  • 基于 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)

    理解不同的坐标系是实现相机系统的关键。在渲染管线中,数据会依次经过多个坐标系的变换:

    各坐标系介绍

  • 局部空间(Local Space/Object Space):相对于物体原点的坐标顶点最初相对于自身物体的原点定义
  • 世界空间(World Space):相对于世界原点的坐标场景中物体彼此之间的相对位置
  • 相机空间(View Space/Camera Space):相对于相机的坐标从相机的位置和朝向观察到的世界相机位于原点 (0,0,0),沿负 Z 轴观察
  • 裁剪空间(Clip Space):投影后的坐标,各轴范围为 [-w, w]决定屏幕上的可视区域(视锥体内部)
  • 屏幕空间(Screen Space):最终用于屏幕显示的 2D 坐标物体在屏幕上的实际像素位置
  • 坐标系的手性(Handedness of Coordinate Systems)

    图形 API 和引擎主要使用右手坐标系和左手坐标系:

  • 右手坐标系(OpenGL 和 Vulkan 的默认约定)
    • X 轴指向右侧
    • Y 轴指向上方
    • Z 轴指向屏幕外(朝向观察者)
    • 叉积:Z = X × Y(遵循右手定则)
  • 左手坐标系(DirectX 采用)
    • X 轴指向右侧
    • Y 轴指向上方
    • Z 轴指向屏幕内(远离观察者)
    • 叉积:Z = X × Y(遵循左手定则)
  • // Vulkan中通常使用右手坐标系
    // 若需要,可在两种坐标系间进行转换

    // 将点从左手坐标系转换为右手坐标系
    // 只需翻转Z坐标
    glm::vec3 leftHandedPoint(x, y, z);
    glm::vec3 rightHandedPoint(x, y, -z);

    // 设置相机时,坐标系的手性会影响视图矩阵
    // 右手坐标系中,相机通常沿负Z轴观察
    // 这也是相机代码中常将-Z作为前方向的原因

    对相机系统的影响

    坐标系的手性直接影响相机的设置方式:

    右手坐标系(Vulkan 约定)的相机设置要点
  • 相机通常沿负 Z 轴观察
  • 观察向量(look vector)通常存储为负 Z 方向
  • 视图矩阵的构建遵循右手定则的叉积计算
  • 从视图矩阵中提取轴向量的规则:
    • 右方向向量:视图矩阵的 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 图形学和相机系统中的数学概念。如果对某个概念理解困难,建议参考多个资源,不同的讲解方式可能更贴合你的学习风格。

    赞(0)
    未经允许不得转载:171主机测评 » Building a Simple Engine -- Camera Transformations -- Math foundations
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址