Three.js后处理实现透视变换:将四边形映射为画布矩形
在Three.js中实现透视变换后处理的方案
核心思路
要实现将四边形区域扭曲为画布矩形的效果,核心是在片段着色器中对每个像素做反向透视映射:从目标画布的像素坐标反推其在原渲染纹理中的对应位置,再采样原纹理颜色。通过ShaderPass将自定义着色器接入Three.js后处理管线即可实现。
步骤详解
1. 计算透视变换逆矩阵
因为我们需要从目标画布(矩形)反推原画面(四边形)的像素位置,所以需要计算源四边形到目标矩形的透视变换矩阵的逆矩阵。
以下是一个手动计算3x3透视变换矩阵并求逆的函数(可直接复用):
function computePerspectiveInverseMatrix(srcPoints, dstPoints) { // srcPoints:四边形四个顶点的归一化屏幕坐标(0-1范围) // dstPoints:画布四个角的归一化坐标,固定为[[0,0],[1,0],[1,1],[0,1]] const matrix = new THREE.Matrix3(); const A = []; const B = []; // 为每个点构建方程组 for (let i = 0; i < 4; i++) { const [x, y] = srcPoints[i]; const [tx, ty] = dstPoints[i]; A.push([x, y, 1, 0, 0, 0, -x * tx, -y * tx]); B.push(tx); A.push([0, 0, 0, x, y, 1, -x * ty, -y * ty]); B.push(ty); } // 解线性方程组(这里用math.js简化计算,也可手动实现高斯消元) const solution = math.lusolve(A, B).flat(); const [m00, m01, m02, m10, m11, m12, m20, m21] = solution; matrix.set(m00, m01, m02, m10, m11, m12, m20, m21, 1); // 返回逆矩阵 return matrix.getInverse(matrix); }
注:若不想引入
math.js,可以用OpenCV.js的getPerspectiveTransform函数计算矩阵后再求逆,逻辑一致。
2. 自定义透视变换着色器
编写顶点/片段着色器,实现像素级的透视映射:
const warpShader = { uniforms: { tDiffuse: { value: null }, // 原渲染纹理(由ShaderPass自动传入) perspectiveMatrix: { value: new THREE.Matrix3() } // 透视逆矩阵 }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform mat3 perspectiveMatrix; void main() { // 将当前UV转换为齐次坐标 vec3 uv = vec3(vUv, 1.0); // 应用透视逆矩阵,得到原纹理中的对应坐标 vec3 transformedUv = perspectiveMatrix * uv; // 除以齐次分量,得到最终采样UV vec2 finalUv = transformedUv.xy / transformedUv.z; // 限制UV范围,避免采样纹理外的无效数据 finalUv = clamp(finalUv, 0.0, 1.0); gl_FragColor = texture2D(tDiffuse, finalUv); } ` };
3. 接入后处理管线
将自定义着色器包装为ShaderPass,加入Three.js的EffectComposer管线:
// 初始化后处理 composer const composer = new THREE.EffectComposer(renderer); const renderPass = new THREE.RenderPass(scene, camera); composer.addPass(renderPass); // 添加透视变换Pass const warpPass = new THREE.ShaderPass(warpShader); composer.addPass(warpPass);
4. 每帧更新矩阵
由于相机移动会导致四边形的屏幕坐标变化,需要在动画循环中实时更新透视矩阵:
function animate() { requestAnimationFrame(animate); // 1. 获取四边形顶点的归一化屏幕坐标 const srcPoints = []; quadVertices.forEach(vertex => { const screenPos = vertex.clone().project(camera); // 将NDC坐标(-1~1)转换为0~1的UV坐标,同时反转Y轴(纹理Y轴向下) const uvX = (screenPos.x + 1) / 2; const uvY = (1 - screenPos.y) / 2; srcPoints.push([uvX, uvY]); }); // 2. 目标矩形的四个角坐标 const dstPoints = [[0,0], [1,0], [1,1], [0,1]]; // 3. 计算并传入逆矩阵 const inverseMatrix = computePerspectiveInverseMatrix(srcPoints, dstPoints); warpPass.uniforms.perspectiveMatrix.value = inverseMatrix; // 4. 用composer渲染画面,而非直接调用renderer.render composer.render(); }
关键注意事项
- 纹理Wrap模式:设置原渲染纹理的
wrapS和wrapT为THREE.ClampToEdgeWrapping,避免UV超出范围时出现重复或黑边。 - 坐标转换:Three.js的
project()返回NDC坐标(-11),必须转换为01的UV坐标,同时注意Y轴方向的反转。 - 矩阵正确性:一定要使用逆矩阵,因为我们是从目标像素反向查找原纹理位置,而非正向变换。
内容的提问来源于stack exchange,提问作者Luca Hilbrich
相关产品推荐
相关产品推荐

