变换
1. 前言
在计算机图形学中,变换(transformation)是对点、向量的坐标进行修改的操作。渲染管线的各个环节(比如摆放物体、设置摄像机、把三维场景投影到屏幕上等)本质上都离不开变换。本节从缩放、旋转、平移这三种基础变换入手,介绍它们对应的矩阵,最后把二维的结论推广到三维空间。
在此先行强调一下:我们的变换通过矩阵乘法实现,即让在一个 矩阵/向量 上左乘一个变换矩阵,实现坐标的修改。
1.1 一些事实
在推导变换矩阵之前,先列出几个关于向量与矩阵的基本事实,后面的内容会反复用到:
- 一个 维向量左乘 阶单位矩阵(identity matrix),得到它本身:
-
若矩阵 可逆,则它与逆矩阵(inverse matrix) 相乘得到单位矩阵:。
-
矩阵乘法满足结合律(associative law):。
-
矩阵乘法一般不满足交换律(commutative law):。这一点直接导致变换的先后顺序会影响最终结果。
1.2 变换
我们的变换通过矩阵乘法实现:给定一个变换矩阵 ,把它左乘到原始向量 上,就得到变换后的向量 :
通过矩阵乘法进行变换可以合并。对 依次施加 、 两个变换:
根据结合律,可以先把矩阵乘起来,再用合并后的矩阵 一次性作用于 。图形学中物体往往要经过一连串变换,合并矩阵能减少大量计算。
注意乘法的顺序:矩阵链中最靠近 的矩阵最先作用, 表示先施加 ,再施加 。
2. 二维空间中的三种变换
以下我们分析思路都是:先写出我们希望得到的变换后的坐标(从 变换到 ),再据此配凑出一个矩阵左乘在向量上,以图实现此变换。
由于目前还没有建立起模型的概念,我们的变换暂时都是对一个点来进行。在完全理解其含义后,我们会在概念上将其推广到对整个物体的变换。
这一节,我们将介绍三种最基础的变换:缩放(scale)、旋转(rotate)、平移(translate)。
2.1 缩放变换(Scale Transformation)
缩放变换用于改变一个物体的大小。简言之,我们需要做到让一个点的位置沿 轴缩放 倍、沿 轴缩放 倍。因此,我们希望得到的坐标变化为:
尝试凑出一个矩阵左乘在向量上:
因此可以得到缩放矩阵:
按 、 的取值不同,缩放有几种典型情况:
| 取值 | 效果 |
|---|---|
| 均匀缩放(uniform scaling),整体放大或缩小,形状不变 | |
| 非均匀缩放(non-uniform scaling),物体会被拉伸或压扁 | |
| 或 为负 | 相应坐标轴上发生翻转(reflection),比如 沿 轴镜像 |
| 或 为 0 | 坐标被压缩到坐标轴上,矩阵不可逆,一般不使用 |
2.2 旋转变换(Rotate Transformation)
旋转变换用于改变一个物体的朝向。我们接下来需要做到让一个点绕原点旋转,且以逆时针为正方向。
先分析我们希望得到的坐标变化。假设原始坐标为 ,旋转角度为 ,旋转后的坐标为 。
- 设向量 的模长为 ,与 轴正方向的夹角为 ,则:
- 把 绕原点逆时针旋转 后,夹角变为 ,新坐标为:
- 将其写成矩阵乘法:
最终,我们得到了旋转矩阵
几个特殊的旋转角度:
| 效果 | ||
|---|---|---|
| 逆时针旋转 90°, 变为 | ||
| 相当于 ,关于原点翻转 | ||
| 顺时针旋转 90° |
注:,旋转矩阵的逆等于它的转置,这类矩阵称为 正交矩阵。
2.3 平移变换(Translate Transformation)
平移变换用于把物体移动一段距离。例如我们令一个点沿 轴移动 、沿 轴移动 ,坐标变化为:
可以比较简单地写成向量加法:
但是这里出现了一个问题:平移找不到对应的 矩阵。
任何 矩阵作用于零向量,结果仍是零向量,即矩阵乘法总是把原点映射到原点;而平移会把原点 移动到 。也就是说,平移不是线性变换(linear transformation,保持原点不变的变换),无法用 矩阵表示。
为了让平移也能用矩阵乘法表达,引入齐次坐标(homogeneous coordinates):给二维坐标增加一个分量 ,用三维坐标表示二维的点与向量:
在齐次坐标下,平移写作 矩阵乘法:
可以得到平移矩阵
以下是两个有关平移变换的细节:
- 向量平移不变:
- 线性变换与平移合称仿射变换(affine transformation)。在齐次坐标下,仿射变换统一为
其中 是 的线性变换部分(缩放、旋转等), 是平移部分,。
2.4 齐次坐标下的缩放、旋转的矩阵表示
缩放与旋转在齐次坐标下也会升级为 的矩阵:
至此,缩放、旋转、平移统一成了同一种形式:矩阵乘法。
3. 三维空间中的三种变换
三维变换是二维变换的直接推广:齐次坐标下点记作 ,向量记作 ,变换矩阵升级为 。
缩放:沿三个坐标轴分别缩放 、、 倍,矩阵为
平移:沿三个坐标轴分别平移 、、,矩阵为
旋转比较复杂,在三维世界里,我们要实现让一个点绕一个过原点的轴的旋转,可以拆分为以下两种情况:
- 绕坐标轴旋转:绕坐标轴旋转时,该轴上的分量保持不变,另外两个分量按二维旋转矩阵变化
- 绕任意轴旋转:绕单位向量 旋转 角的矩阵为(罗德里格斯旋转公式(Rodrigues’ rotation formula)):
当 取坐标轴方向时,该矩阵退化为上面绕坐标轴的旋转矩阵。
4. 更加深入地理解变换
我们刚才实现的变换都是对一个点来进行。但在计算机图形学中,我们经常需要对一个物体进行变换。
一个物体可以看作是它的所有点组成的集合,因此我们可以把物体的变换理解为对物体中每个点都施加同样的变换。从这个角度来分析,我们可以对刚才的三种变换有更直观的理解:
- 让一个物体的所有点都沿 轴缩放 、沿 轴缩放 、沿 轴缩放 ,物体本身就会沿着 、、 轴缩放 、、
- 让一个物体的所有点都绕过原点的某个轴旋转 角,物体本身就会绕该轴旋转 角(这里当然允许轴在物体内或轴在物体外)
- 让一个物体的所有点都沿 轴平移 、沿 轴平移 、沿 轴平移 ,物体本身就会沿着 、、 轴平移 、、
4.1 例一:绕空间中不过原点的轴旋转
那么,如果需要让一个物体绕空间中的任意一个轴(不一定过原点)来旋转 角,实现步骤就是:
- 先对物体进行平移,使得旋转轴过原点,设平移矩阵是
- 再令物体绕该轴旋转 角,设旋转矩阵是
- 最后再把物体平移回原来的位置,设平移矩阵是
- 最终,我们可以让物体的每个点依次施加这三个变换,或者先将这三个变换矩阵合并为一个矩阵:
4.2 例二:对改变坐标系的理解
更进一步地,对世界中所有的点都施加了同样的变换,也可以认为对整个世界都施加了这个变换(更常用的说法是改变了坐标系)。
举个例子:如果有一个长方体的中心在 轴正半轴的 点,它的所有棱都与某个坐标轴平行(这是一个位置很规整的长方体)。长方体沿着 、 轴的棱的棱长是 1,沿着 轴的棱的棱长是 2。我们希望把它的中心移动到原点,并且将其压缩为一个边长为 1 的正方体。可以通过以下两个不同的路径实现:
- 先平移、再缩放:先将其沿着 轴平移 ,再沿着 轴缩放为 。最终得到的矩阵为:
- 先缩放、再平移:先沿着 轴缩放为 ,再沿着 轴平移 。最终得到的矩阵为:
这里不难注意到,第二种方法的平移量是 ,而不是 。这是因为我们缩放的过程改变了坐标系,因此平移量也要随之改变。
4.3 例三:沿着任意方向进行缩放
由 看这里看这里看这里看这里这个特别重要 不难得到,我们可以以任意方向的单位向量 为 轴,建立一个新的坐标系:在 的垂直平面上任意选取两个互相垂直的单位向量 、,则 就是这个新坐标系的三个基向量。
因而可以得到旋转矩阵:
这里矩阵 的作用是:把"新坐标系下的坐标"变回"世界坐标"。由于我们设置的 、、 都是单位向量,且两两垂直,因此 是一个 正交矩阵,满足它的逆等于它的转置。其转置矩阵的作用是:把"世界坐标"变回"新坐标系下的坐标"。
因此,我们实际的操作是:
- 先将物体旋转到新坐标系下,旋转矩阵为 ;
- 再沿着新坐标系的 缩放到原来的 k 倍,缩放矩阵为 ;
- 最后再将物体旋转回原来的坐标系,旋转矩阵为 。
- 最终,我们可以让物体的每个点依次施加这三个变换,或者先将这三个变换矩阵合并为一个矩阵:
5. 欧拉角与四元数表示旋转
欧拉角与四元数都是描述三维旋转的方式。
5.1 欧拉角(Euler angles)
任意三维旋转都可以分解成绕三个坐标轴旋转的组合:
其中 、、 称为欧拉角(Euler angles),分别表示绕 、、 轴的旋转角。分解方式不唯一,使用时需要约定绕轴顺序。
5.2 四元数
四元数(quaternion)可以看作复数的扩展:三个虚数单位 、、 分别对应三个坐标轴,一个四元数写为
其中 是实部(标量), 是虚部(向量)。虚数单位满足
也常写成 的形式。
用四元数表示旋转:绕单位向量 旋转 角,对应单位四元数
把点 写成四元数 ,旋转操作是
的虚部就是旋转后的坐标。单位四元数的逆等于它的共轭(conjugate)。
四元数转旋转矩阵:单位四元数 ,,对应的旋转矩阵为
用齐次坐标补齐最后一行为 即可。
四元数的几个优势:
- 只需 4 个数,比 旋转矩阵的 9 个数更紧凑;
- 不会出现欧拉角的万向节锁(gimbal lock);
- 两个旋转之间可以用球面线性插值(spherical linear interpolation,简称 slerp)平滑过渡,动画中常用。
6. 以上内容的 C++ 封装
6.1 向量类
#pragma once
#include <cstddef>
#include <concepts>
#include <cmath>
#include <utility> // for std::exchange,可选择不用
namespace MY
{
namespace Math
{
template<typename T>
concept IsDecimal = std::is_same_v<T, float> || std::is_same_v<T, double>;
} // namespace Math
/**
* 封装一个 N 维 T 类型变量的向量
*/
template<typename T, size_t N>
class Vector
{
public:
Vector() = default;
~Vector();
// 深拷贝
Vector(const Vector& other);
Vector& operator=(const Vector& other);
// 移动
Vector(Vector&& other) noexcept;
Vector& operator=(Vector&& other) noexcept;
// 支持初始化
template<typename... Args>
requires(sizeof...(Args) <= N && (std::is_convertible_v<Args, T> && ...))
Vector(Args... args)
{
init(0, args...);
}
// 获取向量维度
static constexpr size_t get_dimension() { return N; }
// 通过 [] 访问向量元素
// 担心越界访问可以额外添加边界检查
T& operator[](size_t index) { return m_data[index]; }
const T& operator[](size_t index) const { return m_data[index]; }
private:
// 底层成员
T* m_data = new T[N]();
template<typename... Args>
void init(size_t index, T first, Args... args)
{
m_data[index] = first;
if constexpr (sizeof...(args) > 0)
{
init(index + 1, args...);
}
}
public:
// 对 T 为 float 或 double 类型的向量支持基础向量操作
// 计算向量的模
T get_length() const requires Math::IsDecimal<T>;
/**
* 判断向量是否为零向量
* 由于底层要调用 get_length(),允许传入一个指针来获取向量的模,避免重复计算
*/
bool is_zero(T* length = nullptr) const requires Math::IsDecimal<T>;
// 对向量进行单位化
void normalize() requires Math::IsDecimal<T>;
// 实现向量的加减运算
Vector operator+(const Vector& other) const requires Math::IsDecimal<T>;
Vector operator-(const Vector& other) const requires Math::IsDecimal<T>;
Vector& operator+=(const Vector& other) requires Math::IsDecimal<T>;
Vector& operator-=(const Vector& other) requires Math::IsDecimal<T>;
// 实现向量的数乘运算
Vector operator*(T scalar) const requires Math::IsDecimal<T>;
Vector& operator*=(T scalar) requires Math::IsDecimal<T>;
// 实现向量的逐项乘运算
Vector operator*(const Vector& other) const requires Math::IsDecimal<T>;
Vector& operator*=(const Vector& other) requires Math::IsDecimal<T>;
// 实现向量的数除运算
Vector operator/(T scalar) const requires Math::IsDecimal<T>;
Vector& operator/=(T scalar) requires Math::IsDecimal<T>;
// 判断向量是否相等
bool operator==(const Vector& other) const requires Math::IsDecimal<T>;
bool operator!=(const Vector& other) const requires Math::IsDecimal<T>;
// 实现向量的点积运算
T dot(const Vector& other) const requires Math::IsDecimal<T>;
// 仅对三维向量实现叉乘运算
Vector cross(const Vector& other) const requires Math::IsDecimal<T> && (N == 3);
public:
// 对迭代器的支持
struct Iterator
{
T* m_ptr;
Iterator(T* ptr) : m_ptr(ptr) {}
T& operator*() { return *m_ptr; }
Iterator& operator++() { ++m_ptr; return *this; }
bool operator!=(const Iterator& other) const { return m_ptr != other.m_ptr; }
};
struct ConstIterator
{
const T* m_ptr;
ConstIterator(const T* ptr) : m_ptr(ptr) {}
const T& operator*() const { return *m_ptr; }
ConstIterator& operator++() { ++m_ptr; return *this; }
bool operator!=(const ConstIterator& other) const { return m_ptr != other.m_ptr; }
};
Iterator begin() { return Iterator(m_data); }
Iterator end() { return Iterator(m_data + N); }
ConstIterator begin() const { return ConstIterator(m_data); }
ConstIterator end() const { return ConstIterator(m_data + N); }
};
template<typename T, size_t N>
Vector<T, N>::~Vector()
{
delete[] m_data;
}
template<typename T, size_t N>
Vector<T, N>::Vector(const Vector<T, N>& other) : m_data(new T[N])
{
for(size_t i = 0; i < N; ++i) m_data[i] = other.m_data[i];
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator=(const Vector<T, N>& other)
{
if(this == &other) return *this;
T* new_data = new T[N];
for(size_t i = 0; i < N; ++i) new_data[i] = other.m_data[i];
delete[] m_data;
m_data = new_data;
return *this;
}
template<typename T, size_t N>
Vector<T, N>::Vector(Vector<T, N>&& other) noexcept
: m_data(std::exchange(other.m_data, nullptr))
{
// 也可不用 std::exchange
// 直接写 m_data(other.m_data);
// m_data = other.m_data;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator=(Vector<T, N>&& other) noexcept
{
if(this == &other) return *this;
delete[] m_data;
m_data = other.m_data;
other.m_data = nullptr;
return *this;
}
template<typename T, size_t N>
T Vector<T, N>::get_length() const requires Math::IsDecimal<T>
{
T sum = 0;
for(size_t i = 0; i < N; ++i)
{
sum += m_data[i] * m_data[i];
}
return std::sqrt(sum);
}
template<typename T, size_t N>
bool Vector<T, N>::is_zero(T* length) const requires Math::IsDecimal<T>
{
T len = get_length();
if(length) *length = len;
return len == T(0);
}
template<typename T, size_t N>
void Vector<T, N>::normalize() requires Math::IsDecimal<T>
{
T len = get_length();
// 如果有报错逻辑可以在这里添加,此处静默返回
if(len == T(0)) return;
for(size_t i = 0; i < N; ++i) m_data[i] /= len;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::operator+(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
Vector<T, N> result;
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i] + other.m_data[i];
return result;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::operator-(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
Vector<T, N> result;
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i] - other.m_data[i];
return result;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator+=(const Vector<T, N>& other) requires Math::IsDecimal<T>
{
for(size_t i = 0; i < N; ++i) m_data[i] += other.m_data[i];
return *this;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator-=(const Vector<T, N>& other) requires Math::IsDecimal<T>
{
for(size_t i = 0; i < N; ++i) m_data[i] -= other.m_data[i];
return *this;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::operator*(T scalar) const requires Math::IsDecimal<T>
{
Vector<T, N> result;
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i] * scalar;
return result;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator*=(T scalar) requires Math::IsDecimal<T>
{
for(size_t i = 0; i < N; ++i) m_data[i] *= scalar;
return *this;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::operator*(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
Vector<T, N> result;
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i] * other.m_data[i];
return result;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator*=(const Vector<T, N>& other) requires Math::IsDecimal<T>
{
for(size_t i = 0; i < N; ++i) m_data[i] *= other.m_data[i];
return *this;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::operator/(T scalar) const requires Math::IsDecimal<T>
{
Vector<T, N> result;
if(scalar == T(0))
{
// 如果有报错逻辑可以在这里添加,此处静默返回原向量
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i];
return result;
}
for(size_t i = 0; i < N; ++i) result.m_data[i] = m_data[i] / scalar;
return result;
}
template<typename T, size_t N>
Vector<T, N>& Vector<T, N>::operator/=(T scalar) requires Math::IsDecimal<T>
{
// 如果有报错逻辑可以在这里添加,此处静默返回原向量
if(scalar == T(0)) return *this;
for(size_t i = 0; i < N; ++i) m_data[i] /= scalar;
return *this;
}
template<typename T, size_t N>
bool Vector<T, N>::operator==(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
for(size_t i = 0; i < N; ++i)
{
if(m_data[i] != other.m_data[i]) return false;
}
return true;
}
template<typename T, size_t N>
bool Vector<T, N>::operator!=(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
return !(*this == other);
}
template<typename T, size_t N>
T Vector<T, N>::dot(const Vector<T, N>& other) const requires Math::IsDecimal<T>
{
T sum = T(0);
for(size_t i = 0; i < N; ++i) sum += m_data[i] * other.m_data[i];
return sum;
}
template<typename T, size_t N>
Vector<T, N> Vector<T, N>::cross(const Vector<T, N>& other) const requires Math::IsDecimal<T> && (N == 3)
{
Vector<T, N> result;
result.m_data[0] = m_data[1] * other.m_data[2] - m_data[2] * other.m_data[1];
result.m_data[1] = m_data[2] * other.m_data[0] - m_data[0] * other.m_data[2];
result.m_data[2] = m_data[0] * other.m_data[1] - m_data[1] * other.m_data[0];
return result;
}
} // namespace MY
namespace MY
{
// 提供一些常用的向量类型别名
using Vec2 = Vector<float, 2>;
using Vec3 = Vector<float, 3>;
using Vec4 = Vector<float, 4>;
namespace Math
{
float dot(const Vec2& a, const Vec2& b)
{
return a.dot(b);
}
float dot(const Vec3& a, const Vec3& b)
{
return a.dot(b);
}
float dot(const Vec4& a, const Vec4& b)
{
return a.dot(b);
}
Vec3 cross(const Vec3& a, const Vec3& b)
{
return a.cross(b);
}
} // namespace Math
} // namespace MY


