09 二维旋转矩阵与绕任意点旋转
学完本章能做什么
你将能写出二维旋转矩阵,判断角度正负、矩阵乘法顺序,并让图案绕指定点旋转。更重要的是,你会区分“旋转几何位置”和“反向旋转用于查询图案的坐标”。
先看现象
示例中,箭头原本朝右。调整角度,它绕蓝色圆点旋转;拖动圆点位置,旋转中心也随之改变。画面上的十字线固定在平面中心,可用来判断旋转中心是否真的移动。
1. 从单位圆得到旋转矩阵
逆时针旋转角度 θ 后,原来的 x 轴单位向量 (1, 0) 变成 (cos θ, sin θ),y 轴单位向量 (0, 1) 变成 (-sin θ, cos θ)。把这两个结果分别作为矩阵的两列:
R(θ) = | cos θ -sin θ |
| sin θ cos θ |
对点 (x, y),结果是 (x cos θ - y sin θ, x sin θ + y cos θ)。长度和夹角保持不变;旋转矩阵不会把圆拉成椭圆。GLSL 的 mat2 构造参数按列排列,因此写成:
// 顶点或片元阶段;二维坐标所属空间由传入的 point 决定。
mat2 rotate2D( float angle ) {
float c = cos( angle );
float s = sin( angle );
return mat2( c, s, -s, c );
}
rotate2D(angle) * vec2(1.0, 0.0) 得到 (cos(angle), sin(angle))。若误写成 mat2(c, -s, s, c),得到的是相反方向。GLSL 三角函数接收弧度:180° = π,90° = π/2。JavaScript 控件用度数时,可先乘 Math.PI / 180,再传入 uAngle。
2. 绕任意点:先平移,再旋转,最后移回
设旋转中心为 pivot,原点为 point。先求相对向量 point - pivot,旋转这个向量,再加回中心:
// 顶点阶段;point 和 pivot 必须处于同一个坐标空间。
vec2 rotated = pivot + rotate2D( uAngle ) * ( point - pivot );
代入 point == pivot,可得 rotated == pivot,因此中心不会移动。若把 pivot 加到旋转之前,中心本身也会被旋转,效果会变成绕别处公转。两个点必须使用相同单位:不能把 UV 的 (0.5, 0.5) 当作局部坐标中的 (0.5, 0.5)。
若确实要旋转网格顶点,可以在顶点着色器修改局部 position.xy,再用 projectionMatrix * modelViewMatrix 投影。此时网格边界、插值 UV 和后续光照都会跟着几何体变化。
3. 片元绘图为什么使用负角度
片元着色器不是把已经画好的像素推到新位置;它逐像素回答“此处应该是什么颜色”。假设未旋转的图案由 shape(localPoint) 定义,屏幕点 point 对应的旧图案坐标应满足:
point = pivot + R(θ) × localPoint
localPoint = R(-θ) × (point - pivot)
所以,要让图案看起来逆时针旋转 θ,就用负角度查询原图案:
// 片元阶段;point、pivot 均为按目标平面比例修正后的二维坐标。
vec2 localPoint = rotate2D( -uAngle ) * ( point - pivot );
float mask = arrowMask( localPoint );
示例中的箭头先定义为朝 +x,然后对每个片元反查 localPoint。例如目标像素恰好在旋转后的箭头尖端,反向旋转会把它带回未旋转时的尖端位置,于是 arrowMask 返回 1。把这里的 -uAngle 改为 uAngle,图案会朝控件所示角度的反方向旋转。
这也是纹理变形的常见思路:变换采样坐标,然后用该坐标读取固定图案或纹理。第 11 章会再次使用它。
4. 在非正方形平面上先统一长度单位
示例平面宽高比为 A = 宽 / 高。先把 UV 中心移到原点,并把 x 乘 A:
// 片元阶段;point 和 pivot 都以“平面高度”为单位。
vec2 point = ( vUv - 0.5 ) * vec2( uAspect, 1.0 );
vec2 pivot = uPivot; // JavaScript 已按相同规则提供该值。
vec2 localPoint = rotate2D( -uAngle ) * ( point - pivot );
若直接旋转原始 UV,在宽平面上 x、y 对应的屏幕长度不同。矩阵虽然在 UV 数值上保长,屏幕中的圆和旋转路径仍会拉伸。顺序应为:修正比例 → 平移到旋转中心 → 旋转 → 查询图案。如果需要回到 UV,则对 x 除以 A,再加 0.5。
画布宽高比、平面宽高比和图片原始宽高比是三个概念。这里使用的是正对相机的目标平面宽高比。若网格倾斜、受透视投影影响,“屏幕上的圆”还要在屏幕空间另行处理。
5. 可交换顺序吗?
通常不能。先旋转再平移的 R(point) + offset,与先平移再旋转的 R(point + offset) 不同;后者连 offset 一起转。两个不同中心的旋转也通常不能任意交换顺序。调试时把中心画成单独的颜色,并检查角度为 0、π/2 和 π 的位置,最容易发现顺序错误。
常见问题
- **90° 实际只转了一点点:**把度数直接交给了
sin/cos;先换成弧度。 - **箭头朝反方向转:**片元“反向查询坐标”与顶点“正向移动位置”的符号混用了。
- **换个旋转中心后形状飞走:**确认
point和pivot使用相同的坐标空间,并检查point - pivot、最后加回pivot的位置。 - **在宽平面上旋转时发生拉伸:**先做比例修正,再用旋转矩阵。
- **旋转的图案被平面边缘裁掉:**片元着色器只能绘制已有网格覆盖的像素;增大网格或缩小图案。
本章小结
二维旋转矩阵保留长度;绕点旋转用 pivot + R(angle) * (point - pivot)。画顶点时通常正向移动点,画片元图案时则用 R(-angle) 反查图案。比例修正必须发生在旋转之前。