21 · Matrix3:二维齐次变换与 UV
本章代码默认 import * as THREE from 'three';。纹理采样不是把像素图案直接搬动,而是为每个表面位置计算“去图片的哪里取颜色”。这解释了为什么采样坐标与可见图案经常朝相反方向移动。
makeTranslation(x, y) 与 makeTranslation(vector2)
接受两个平移数值,或一个 Vector2。覆盖整个矩阵为二维平移,返回 this,输入 Vector2 不变:
new Vector2(1,2).applyMatrix3(new Matrix3().makeTranslation(3,4)) 得到 (4,6)。Vector3 输入 (1,2,0) 则不平移,因为齐次分量为 0。该方法不是三维平移,也不是在原矩阵上叠加;旧的旋转和缩放会被覆盖。数值重载必须提供 y。
makeRotation(theta)
接受弧度,覆盖矩阵为二维绕原点旋转,返回 this:
在 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 朝向下呈逆时针旋转。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 超界可能只得到边缘颜色。
练习
- 验证
setUvTransform()在零偏移时始终固定中心,无论缩放和旋转如何变化。 - 对比
makeRotation(π/2)、旧rotate(π/2)和setUvTransform()的采样结果。 - 将旧
identity().scale(2,1).rotate(0.3).translate(0.2,0)改写成不使用弃用接口的等价代码。 - 为宽高比 2:1 的图片设计保持比例的旋转方案,说明希望保留的是图片物理比例还是 UV 比例。