36 · 纹理编辑器:平移、缩放和绕中心旋转
需求与采样思路
商品贴纸编辑器需要调整图片位置、大小和旋转中心。这里不移动平面顶点,而是改变每个片元“去图片哪里取颜色”的 UV。二维坐标 (u,v) 扩成 (u,v,1) 后,Matrix3 可以统一表达平移、旋转和缩放。
注意采样变换的方向:令 u' = u + 0.2,固定屏幕位置会读取图片更右侧内容,所以图片特征看起来向左移动。u' = 2u 会在相同平面范围扫过两倍纹理范围,重复模式下出现更多图案,而不是把图案放大。
setUvTransform(tx, ty, sx, sy, rotation, cx, cy) 与 Three.js 纹理自身的 offset/repeat/rotation/center 约定一致。正旋转角对应其特定采样旋转矩阵;不要假设它与 makeRotation() 在所有组合中可直接互换。最可靠的方式是观察带非对称标记的贴图,并区分采样方向与可见图案方向。
完整核心代码
本例用 CanvasTexture 生成非对称网格,无外部图片或跨域依赖。平面为正方形,避免把宽高比问题混进初次验证。
import { createDemo } from './runtime.js';
const { THREE, scene, camera, onFrame } = createDemo();
camera.position.set(0, 0, 6);
camera.lookAt(0, 0, 0);
const source = document.createElement('canvas');
source.width = 256;
source.height = 256;
const context = source.getContext('2d');
context.fillStyle = '#eee9dd';
context.fillRect(0, 0, 256, 256);
context.strokeStyle = '#334455';
for (let i = 0; i <= 256; i += 32) {
context.beginPath();
context.moveTo(i, 0);
context.lineTo(i, 256);
context.moveTo(0, i);
context.lineTo(256, i);
context.stroke();
}
context.fillStyle = '#dd5533';
context.font = 'bold 72px sans-serif';
context.fillText('R→', 30, 140);
const texture = new THREE.CanvasTexture(source);
texture.colorSpace = THREE.SRGBColorSpace;
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const uvMatrix = new THREE.Matrix3();
const material = new THREE.ShaderMaterial({
uniforms: {
uMap: { value: texture },
uUvMatrix: { value: uvMatrix }
},
vertexShader: `
uniform mat3 uUvMatrix;
varying vec2 vUv;
void main() {
vUv = (uUvMatrix * vec3(uv, 1.0)).xy;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D uMap;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(uMap, vUv);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
`
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material));
const parameters = { x: 0, y: 0, repeatX: 1, repeatY: 1, angle: 0, centerX: 0.5, centerY: 0.5 };
onFrame((dt, elapsed) => {
parameters.angle = elapsed * 0.2;
uvMatrix.setUvTransform(
parameters.x, parameters.y, parameters.repeatX, parameters.repeatY,
parameters.angle, parameters.centerX, parameters.centerY
);
});
调用外壳的 dispose() 会收集材质 uniform 中的纹理并释放,因此这里不再重复注册纹理释放。若将示例移入自己的应用,则应由材质或场景所属组件负责释放 CanvasTexture。
宽高比与矩阵上传
UV 两个轴都是 0 到 1,但一个横向 UV 单位与一个纵向 UV 单位不一定对应相同屏幕长度。若平面宽 W、高 H,要在平面的物理度量中旋转,可先把中心化 UV 乘 diag(W,H),旋转后乘 diag(1/W,1/H)。源图宽高比与展示区域宽高比的 contain/cover 是另一层缩放,应分别确定再组合,避免盲目乘一次 aspect。
uniform 持有同一个 Matrix3 对象,因此修改其元素即可,不必每帧创建新矩阵。矩阵元素交给 Three.js 上传,不需要为了 GLSL 再手动转置。此处顶点阶段执行仿射 UV 变换,之后插值,等价于片元阶段对插值 UV 执行同一仿射变换;非线性变形则未必等价。
验证与限制
固定角度为 0,分别修改 x 和 repeatX,观察标记实际移动与尺寸。再把中心切换到 (0,0) 比较旋转轨迹。ClampToEdgeWrapping 会拉伸边缘颜色,而不是生成空白;贴纸边界透明需要额外判断 UV 范围或使用透明源图。
内建材质已支持纹理变换时优先使用 texture.offset/repeat/rotation/center。自定义 ShaderMaterial 适合多个 UV 层、特殊混合或程序化采样,但要自己处理颜色输出与材质所需行为。
练习(暂不提供答案)
- 暂停自动旋转,令 x 增加 0.25,记录图案移动方向。
- 实现“可见图案向右移动”的操作,并推导采样偏移符号。
- 改成宽 6、高 3 的平面,修正旋转时的几何比例。
- 超出 0–1 的 UV 显示透明,而不是重复或边缘拉伸。