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 决定了屏幕的形状,near 和 far 定义了可见的深度范围。
关键步骤:
- 创建透视投影矩阵: 使用
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]。
这个过程可以分解为以下几个步骤:
- 视锥体平截头体到正交投影的转换: 将视锥体 "压扁" 成一个长方体。
- 缩放: 将长方体缩放到单位立方体。
- 平移: 将立方体平移到原点。
每个步骤都可以用一个矩阵来表示,最终的透视投影矩阵是这些矩阵的乘积。
3. 四元数旋转
四元数是一种扩展的复数,它可以用来表示 3D 空间中的旋转。相比于欧拉角和旋转矩阵,四元数具有以下优点:
- 避免万向锁 (Gimbal Lock): 欧拉角在某些特定角度组合下会失去一个自由度,导致旋转出现问题。四元数可以避免这种情况。
- 更紧凑的表示: 四元数只需要 4 个数来表示旋转,而旋转矩阵需要 9 个数。
- 插值更平滑: 在动画中,使用四元数进行插值可以得到更平滑的旋转效果。
3.1 四元数的定义
一个四元数 q 可以表示为:
q = w + xi + yj + zk
其中:
w是实部。x, y, z是虚部。-
i, j, k是虚数单位,满足以下关系:i² = j² = k² = -1ij = k, ji = -kjk = i, kj = -iki = 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 kq⁻¹是q的逆四元数。对于单位四元数,q⁻¹等于q的共轭四元数:q⁻¹ = w - xi - yj - zk
3.4 Flutter 中使用四元数
Flutter 的 vector_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,并使用 Matrix4 的 transform3 方法旋转向量。
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 的渲染机制,并在性能优化方面做出更明智的决策。