Flutter 中的 Matrix4 变换:透视投影、四元数旋转与坐标系转换底层

Flutter 中的 Matrix4 变换:透视投影、四元数旋转与坐标系转换底层

大家好,今天我们来深入探讨 Flutter 中 Matrix4 变换,重点关注透视投影、四元数旋转以及坐标系转换的底层原理。Matrix4 在 Flutter 中扮演着至关重要的角色,理解其背后的数学原理对于构建复杂的 3D 效果、实现自定义动画以及高效地进行坐标系转换至关重要。

1. Matrix4 的基础:线性变换

Matrix4 本质上是一个 4×4 的矩阵,它代表了一个线性变换。线性变换必须满足两个条件:

  • 可加性: T(u + v) = T(u) + T(v)
  • 齐次性: T(cu) = cT(u),其中 c 是标量

常见的线性变换包括缩放、旋转、剪切和反射。平移不是线性变换,但可以通过齐次坐标系将其嵌入到 Matrix4 中。

1.1 齐次坐标

为了将平移也纳入矩阵变换的框架中,我们引入齐次坐标。在三维空间中,一个点 (x, y, z) 的齐次坐标表示为 (x, y, z, w)。通常情况下,w=1。当 w!=1 时,我们可以通过将所有坐标除以 w 来将其转换为标准坐标: (x/w, y/w, z/w)。

使用齐次坐标,平移操作就可以用一个 Matrix4 来表示:

import 'package:vector_math/vector_math_64.dart';

Matrix4 translationMatrix(double x, double y, double z) {
  final matrix = Matrix4.identity(); // 初始化为单位矩阵
  matrix.translate(x, y, z); // 使用 translate 方法
  return matrix;
}

void main() {
  // 创建一个平移矩阵,将点向 X 轴移动 2 个单位,向 Y 轴移动 3 个单位,向 Z 轴移动 4 个单位。
  final translate = translationMatrix(2.0, 3.0, 4.0);

  // 创建一个点
  final point = Vector3(1.0, 1.0, 1.0);

  // 应用平移矩阵
  final translatedPoint = translate.transform3(point);

  print('原始点: $point');
  print('平移后的点: $translatedPoint');
}

1.2 Matrix4 的组成

一个 Matrix4 矩阵可以分解为多个部分,每个部分负责不同的变换:

| m00  m01  m02  m03 |  <-- X 轴方向向量 (x-axis)
| m10  m11  m12  m13 |  <-- Y 轴方向向量 (y-axis)
| m20  m21  m22  m23 |  <-- Z 轴方向向量 (z-axis)
| m30  m31  m32  m33 |  <-- 平移分量 (translation)
  • m00, m11, m22: 通常与缩放有关。
  • m01, m02, m10, m12, m20, m21: 通常与旋转和剪切有关。
  • m03, m13, m23: 透视投影相关。
  • m30, m31, m32: 平移分量,分别代表 X, Y, Z 轴上的平移距离。
  • m33: 通常为 1,控制齐次坐标的 w 分量。 如果m33 不为 1, 会影响透视效果。

2. 透视投影

透视投影是一种模拟人眼观察世界的投影方式,它会使远处的物体看起来比近处的物体小,从而产生深度感。透视投影是 3D 图形学中非常重要的概念。

2.1 透视投影矩阵

透视投影的核心在于构建一个透视投影矩阵。这个矩阵会将 3D 空间中的点投影到 2D 屏幕上。Flutter 中,可以使用 Matrix4.perspective 来创建透视投影矩阵。

import 'package:vector_math/vector_math_64.dart';
import 'dart:math';

Matrix4 perspectiveMatrix(double fov, double aspectRatio, double near, double far) {
  final f = 1.0 / tan(fov / 2.0);
  final rangeInv = 1.0 / (near - far);

  final matrix = Matrix4.zero(); // 初始化为零矩阵
  matrix.setEntry(0, 0, f / aspectRatio);
  matrix.setEntry(1, 1, f);
  matrix.setEntry(2, 2, (near + far) * rangeInv);
  matrix.setEntry(2, 3, -1.0);
  matrix.setEntry(3, 2, near * far * rangeInv * 2.0);
  matrix.setEntry(3, 3, 0.0);

  return matrix;
}

void main() {
  // 设置透视投影的参数
  final fov = pi / 4; // 视场角 ( Field of View ),45 度
  final aspectRatio = 16.0 / 9.0; // 宽高比
  final near = 0.1; // 近裁剪面
  final far = 100.0; // 远裁剪面

  // 创建透视投影矩阵
  final projectionMatrix = perspectiveMatrix(fov, aspectRatio, near, far);

  print('透视投影矩阵:n$projectionMatrix');

  // 创建一个点
  final point = Vector3(1.0, 1.0, -5.0); // 注意:Z 坐标为负数,表示在相机前方

  // 将点转换为齐次坐标
  final pointHomogeneous = Vector4(point.x, point.y, point.z, 1.0);

  // 应用透视投影矩阵
  final projectedPointHomogeneous = projectionMatrix.transform(pointHomogeneous);

  print('齐次坐标下的点: $pointHomogeneous');
  print('投影后的齐次坐标: $projectedPointHomogeneous');

  // 执行透视除法(Perspective Divide)将齐次坐标转换为标准设备坐标 (NDC)
  final projectedPoint = Vector3(
    projectedPointHomogeneous.x / projectedPointHomogeneous.w,
    projectedPointHomogeneous.y / projectedPointHomogeneous.w,
    projectedPointHomogeneous.z / projectedPointHomogeneous.w,
  );

  print('投影后的点(NDC): $projectedPoint');
}

上述代码中,perspectiveMatrix 函数根据视场角 (fov)、宽高比 (aspectRatio)、近裁剪面 (near) 和远裁剪面 (far) 计算透视投影矩阵。 fov 决定了视野的广度,aspectRatio 决定了屏幕的形状,nearfar 定义了可见的深度范围。

关键步骤:

  • 创建透视投影矩阵: 使用 perspectiveMatrix 函数。
  • 将 3D 点转换为齐次坐标: 添加 w=1
  • 应用透视投影矩阵: 使用 transform 方法。
  • 透视除法 (Perspective Divide): 将齐次坐标的 x, y, z 分量除以 w 分量,得到归一化的设备坐标 (NDC)。NDC 的 x, y, z 范围通常在 [-1, 1] 之间。超出这个范围的点会被裁剪掉。

2.2 透视投影矩阵的推导

透视投影矩阵的推导涉及到一些几何知识。核心思想是将视锥体 (Frustum) 变换为一个标准立方体 (Canonical View Volume),其坐标范围为 x: [-1, 1], y: [-1, 1], z: [-1, 1]。

这个过程可以分解为以下几个步骤:

  1. 视锥体平截头体到正交投影的转换: 将视锥体 "压扁" 成一个长方体。
  2. 缩放: 将长方体缩放到单位立方体。
  3. 平移: 将立方体平移到原点。

每个步骤都可以用一个矩阵来表示,最终的透视投影矩阵是这些矩阵的乘积。

3. 四元数旋转

四元数是一种扩展的复数,它可以用来表示 3D 空间中的旋转。相比于欧拉角和旋转矩阵,四元数具有以下优点:

  • 避免万向锁 (Gimbal Lock): 欧拉角在某些特定角度组合下会失去一个自由度,导致旋转出现问题。四元数可以避免这种情况。
  • 更紧凑的表示: 四元数只需要 4 个数来表示旋转,而旋转矩阵需要 9 个数。
  • 插值更平滑: 在动画中,使用四元数进行插值可以得到更平滑的旋转效果。

3.1 四元数的定义

一个四元数 q 可以表示为:

q = w + xi + yj + zk

其中:

  • w 是实部。
  • x, y, z 是虚部。
  • i, j, k 是虚数单位,满足以下关系:

    • i² = j² = k² = -1
    • ij = k, ji = -k
    • jk = i, kj = -i
    • ki = j, ik = -j

3.2 四元数与旋转

一个单位四元数(模长为 1 的四元数)可以表示一个 3D 旋转。给定一个旋转轴 n (单位向量) 和一个旋转角度 θ,对应的四元数为:

q = cos(θ/2) + sin(θ/2)(xi + yj + zk)

其中 n = (x, y, z) 是旋转轴的方向向量。

3.3 使用四元数进行旋转

要使用四元数 q 旋转一个向量 v,需要进行以下计算:

v' = qvq⁻¹

其中:

  • v 被视为一个纯四元数: v = 0 + vx i + vy j + vz k
  • q⁻¹q 的逆四元数。对于单位四元数,q⁻¹ 等于 q 的共轭四元数: q⁻¹ = w - xi - yj - zk

3.4 Flutter 中使用四元数

Fluttervector_math 库提供了四元数的支持。

import 'package:vector_math/vector_math_64.dart';

void main() {
  // 定义旋转轴和旋转角度
  final axis = Vector3(0, 1, 0); // 绕 Y 轴旋转
  final angle = radians(45); // 旋转 45 度

  // 创建四元数
  final rotation = Quaternion.axisAngle(axis, angle);

  // 创建一个向量
  final vector = Vector3(1, 0, 0);

  // 使用四元数旋转向量
  final rotatedVector = rotation.rotate(vector);

  print('原始向量: $vector');
  print('旋转后的向量: $rotatedVector');

  // 将四元数转换为 Matrix4
  final rotationMatrix = rotation.asRotationMatrix();
  print('旋转矩阵:n$rotationMatrix');

  // 使用 Matrix4 旋转向量
  final rotatedVector2 = rotationMatrix.transform3(vector);
  print('使用矩阵旋转后的向量: $rotatedVector2');
}

上述代码演示了如何使用 Quaternion.axisAngle 创建四元数,然后使用 rotate 方法旋转向量。 此外,还展示了如何将四元数转换为 Matrix4,并使用 Matrix4transform3 方法旋转向量。

4. 坐标系转换

在 3D 图形学中,经常需要在不同的坐标系之间进行转换。常见的坐标系包括:

  • 世界坐标系 (World Space): 场景的全局坐标系。
  • 模型坐标系 (Model Space): 模型自身的坐标系。
  • 相机坐标系 (Camera Space/View Space): 以相机为原点的坐标系。
  • 裁剪坐标系 (Clip Space): 透视投影后的坐标系,用于裁剪。
  • 屏幕坐标系 (Screen Space): 最终显示在屏幕上的 2D 坐标系。

4.1 坐标系转换矩阵

可以使用 Matrix4 来表示坐标系之间的转换关系。例如,要将一个点从模型坐标系转换到世界坐标系,需要使用模型到世界的变换矩阵 (Model-to-World Matrix)。

import 'package:vector_math/vector_math_64.dart';

void main() {
  // 模型坐标系中的点
  final modelPoint = Vector3(1, 1, 1);

  // 模型到世界的变换矩阵
  final modelToWorld = Matrix4.identity();
  modelToWorld.translate(2, 3, 4); // 平移
  modelToWorld.rotateY(radians(30)); // 绕 Y 轴旋转 30 度
  modelToWorld.scale(2); // 缩放

  // 将模型坐标系中的点转换到世界坐标系
  final worldPoint = modelToWorld.transform3(modelPoint);

  print('模型坐标系中的点: $modelPoint');
  print('模型到世界的变换矩阵:n$modelToWorld');
  print('世界坐标系中的点: $worldPoint');

  // 世界到相机的变换矩阵 (假设相机位于世界坐标系原点,朝向 Z 轴负方向)
  final worldToCamera = Matrix4.identity();
  worldToCamera.rotateY(radians(-30)); // 旋转与 modelToWorld 相反
  worldToCamera.translate(-2, -3, -4);

  final cameraPoint = worldToCamera.transform3(worldPoint);
  print('相机坐标系中的点: $cameraPoint');
}

上述代码演示了如何使用 Matrix4 进行模型到世界、世界到相机的坐标系转换。 关键在于构建正确的变换矩阵,这通常涉及平移、旋转和缩放操作。

4.2 坐标系转换的顺序

坐标系转换的顺序非常重要。通常情况下,变换的顺序是从右向左应用的。例如,如果一个物体先旋转,后平移,那么对应的变换矩阵应该是:

M = T * R

其中 R 是旋转矩阵,T 是平移矩阵。 这意味着先应用旋转 R,再应用平移 T

5. Matrix4 的优化

在性能敏感的应用中,Matrix4 的优化非常重要。以下是一些优化技巧:

  • 避免不必要的矩阵乘法: 尽可能将多个变换合并成一个矩阵。
  • 使用缓存: 如果一个矩阵需要多次使用,可以将其缓存起来。
  • 利用 Flutter 的渲染流水线: Flutter 的渲染流水线会对 Matrix4 进行优化。
  • 避免频繁创建 Matrix4 对象: 尽量重用现有的 Matrix4 对象。
  • 使用 Matrix4.copy 代替赋值操作: 直接赋值会复制引用,而不是复制矩阵本身。 使用 Matrix4.copy 可以创建矩阵的副本。

透视投影、四元数与坐标系转换:构建 3D 世界的关键

我们探讨了 Matrix4 的基础概念,透视投影的实现,四元数旋转的原理,以及坐标系转换的应用。掌握这些知识,你就可以在 Flutter 中构建更复杂的 3D 场景,并实现更高级的动画效果。 理解这些底层原理,可以帮助你更好地理解 Flutter 的渲染机制,并在性能优化方面做出更明智的决策。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注