21 · Matrix3:二维齐次变换与 UV

返回系列目录 · 上一章 · 下一章

本章代码默认 import * as THREE from 'three';。纹理采样不是把像素图案直接搬动,而是为每个表面位置计算“去图片的哪里取颜色”。这解释了为什么采样坐标与可见图案经常朝相反方向移动。

makeTranslation(x, y) 与 makeTranslation(vector2)

接受两个平移数值,或一个 Vector2。覆盖整个矩阵为二维平移,返回 this,输入 Vector2 不变:

T=[10tx01ty001]. T=\begin{bmatrix}1&0&t_x\\0&1&t_y\\0&0&1\end{bmatrix}.

new Vector2(1,2).applyMatrix3(new Matrix3().makeTranslation(3,4)) 得到 (4,6)。Vector3 输入 (1,2,0) 则不平移,因为齐次分量为 0。该方法不是三维平移,也不是在原矩阵上叠加;旧的旋转和缩放会被覆盖。数值重载必须提供 y。

makeRotation(theta)

接受弧度,覆盖矩阵为二维绕原点旋转,返回 this:

R(θ)=[cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001]. R(\theta)=\begin{bmatrix}\cos\theta&-\sin\theta&0\\\sin\theta&\cos\theta&0\\0&0&1\end{bmatrix}.

在 X 向右、Y 向上的平面中,正角为逆时针;theta=π/2 把 (1,0) 变成约 (0,1)。画布 Y 向下时视觉方向相反。要绕中心旋转,组合 T(c)R T(-c),不能期待该方法接受旋转中心。

makeScale(x, y)

接受两个轴的倍率,覆盖矩阵为 diag(x,y,1),返回 this。makeScale(2,3) 把二维点 (1,2) 变成 (2,6)。负倍率表示镜像,零倍率表示压扁,因而不可逆。

此方法缩放坐标,而非直接缩放可见纹理:采样 UV 扩大两倍,在允许重复时通常会显示更多图案,相应每份图案看起来更小。绕指定中心缩放同样需要先搬到原点。

translate(tx, ty):r185 已弃用

接受二维平移数字,左乘平移到当前矩阵,修改并返回 this。例如已有 S(2,2) 再 translate(3,0),输入 (1,1) 得到 (5,2),不是 (8,2)。

r185 会发出弃用警告。保持行为的迁移方式是:

const translation = new THREE.Matrix3().makeTranslation(3, 0);
matrix.premultiply(translation); // 等价于旧 matrix.translate(3, 0)

直接改成 matrix.makeTranslation(3,0) 会抹去已有缩放,不是等价替换。

rotate(theta):r185 已弃用,注意符号

接受弧度,执行 this = R(-theta) * this,修改并返回 this。单位矩阵调用 rotate(π/2) 后,二维 (1,0) 变成约 (0,-1)。它沿用了纹理变换的历史方向,与 makeRotation(theta) 的坐标旋转方向相反。

等价迁移是 matrix.premultiply(rotation.makeRotation(-theta))。既要保留左乘,又要保留负号;仅按弃用警告中的方法名逐字替换会改变效果。

scale(sx, sy):r185 已弃用

接受两个倍率,执行 this = S * this,修改并返回 this。因此已有平移也会被缩放:T(3,0) 再 scale(2,2),原点变为 (6,0)。

等价迁移是 matrix.premultiply(scaleMatrix.makeScale(sx,sy))。makeScale() 单独调用仅用于重新创建纯缩放。零和负倍率的几何含义与上一节相同。

setUvTransform(tx, ty, sx, sy, rotation, cx, cy)

七个数分别为偏移、重复倍率、旋转弧度、中心。覆盖接收者,返回 this。其精确坐标映射是:

uv′=T(c+t) S(sx,sy) R(−θ) T(−c) uv. uv'=T(c+t)\,S(s_x,s_y)\,R(-\theta)\,T(-c)\,uv.

展开得到:

u′=sx[cos⁡θ(u−cx)+sin⁡θ(v−cy)]+cx+tx, u'=s_x[\cos\theta(u-c_x)+\sin\theta(v-c_y)]+c_x+t_x,
v′=sy[−sin⁡θ(u−cx)+cos⁡θ(v−cy)]+cy+ty. v'=s_y[-\sin\theta(u-c_x)+\cos\theta(v-c_y)]+c_y+t_y.

先减中心、顺时针旋转采样坐标、按轴缩放,再加回中心和偏移。因而可见图案在通常的 UV 朝向下呈逆时针旋转。sx != sy 时不要擅自交换缩放与旋转。

const uvMatrix = new THREE.Matrix3().setUvTransform(
  0, 0, 1, 1, Math.PI / 2, 0.5, 0.5
);
const sample = new THREE.Vector2(1, 0.5).applyMatrix3(uvMatrix);
console.log(sample.toArray()); // ≈ [0.5, 0]

表面右侧的点改为采样图片底部,中心 (0.5,0.5) 则保持不动。加入 tx=0.1 后,中心的采样坐标变成 (0.6,0.5);在连续图案上,可见特征往左移。

实践:Three.js 纹理与自定义 Shader 两种接法

对于标准材质已有的纹理对象 texture,让纹理属性自动生成矩阵最省事:

texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.needsUpdate = true; // 若改变了已上传纹理的 wrap 配置,需要更新。
texture.center.set(0.5, 0.5);
texture.repeat.set(2, 2);
texture.rotation = Math.PI / 4;
texture.offset.set(0.1, 0);
texture.updateMatrix(); // matrixAutoUpdate 默认 true;手动调用便于立即检查。

若要完全自己控制 texture.matrix,先设 texture.matrixAutoUpdate=false,再写入矩阵,避免后续被 offset/repeat/rotation 自动覆盖。

自定义 ShaderMaterial 可以把 Matrix3 直接作为 uniform 的 value。JavaScript 更新同一个矩阵对象即可,不必每帧重新创建材质:

const uniforms = { uUvMatrix: { value: new THREE.Matrix3() } };
uniforms.uUvMatrix.value.setUvTransform(0, 0, 2, 2, 0, 0.5, 0.5);
// 顶点着色器片段;vUv 需要在片元着色器中同名声明后用于 texture2D。
uniform mat3 uUvMatrix;
varying vec2 vUv;
void main() {
  vUv = (uUvMatrix * vec3(uv, 1.0)).xy;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

UV 来自几何体 attribute,经矩阵变换后作为 varying 插值,片元着色器再按它取样。仿射 UV 变换可放到顶点阶段;如果要逐片元做非线性扭曲,插值后的结果未必等于逐片元计算。

图片与表面宽高比不一致时,UV 中的“圆”不一定显示为圆;需要先定义保持图片比例、铺满或裁切的需求,再增加比例校正。ClampToEdgeWrapping 也不会自动重复,UV 超界可能只得到边缘颜色。

练习

  1. 验证 setUvTransform() 在零偏移时始终固定中心,无论缩放和旋转如何变化。
  2. 对比 makeRotation(π/2)、旧 rotate(π/2) 和 setUvTransform() 的采样结果。
  3. 将旧 identity().scale(2,1).rotate(0.3).translate(0.2,0) 改写成不使用弃用接口的等价代码。
  4. 为宽高比 2:1 的图片设计保持比例的旋转方案,说明希望保留的是图片物理比例还是 UV 比例。