You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 17:40:07