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

WebGL非正方形画布图像旋转失真问题求助

问题描述

我正在开发一款支持图像变换的Web应用,其中包含图像旋转功能。当前遇到的问题是:当Canvas为正方形(宽高相等)时旋转一切正常,但非正方形时图像旋转会出现失真(见下方截图及动图)。我尝试通过以下公式根据旋转角度调整缩放比例来修复:

const scalingY = 1 + (aspectRatio - 1) * Math.pow(Math.sin(angelInRadian/2), 1);

const scalingX = (1/aspectRatio) +  (1-(1/aspectRatio)) * Math.pow(Math.cos(angelInRadian), 1);

但旋转过程中图像仍存在失真情况。以下是相关的变换矩阵代码、顶点着色器、片元着色器及WebGL初始化代码,同时附上不同角度的截图及旋转过程动图,寻求解决该失真问题的方案。


变换矩阵代码

const scalingY = 1 + (aspectRatio - 1) * Math.pow(Math.sin(angelInRadian/2), 1);

const scalingX = (1/aspectRatio) +  (1-(1/aspectRatio)) * Math.pow(Math.cos(angelInRadian), 1)
const aspectRatioCompensation = new Float32Array([
                 1, 0, 0, 0,
                 0, 1, 0, 0,
                 0, 0, 1, 0,
                 0, 0, 0, 1
                            ]);
//const aspectRatioCompensation = new Float32Array([
                 scalingX, 0, 0, 0,
                 0, scalingY, 0, 0,
                 0, 0, 1, 0,
                 0, 0, 0, 1
                            ]);

const cosTheta = Math.cos(angelInRadian);
const sinTheta = Math.sin(angelInRadian);
const rotationMatrix = new Float32Array([
               cosTheta, -sinTheta, 0, 0,
               sinTheta, cosTheta,  0, 0,
                   0,         0,    1, 0,
                   0,         0,    0, 1
                            ]);

// Combine the aspect ratio adjustment and rotation
const combinedMatrix = mat4.create();

mat4.multiply(combinedMatrix, aspectRatioCompensation, rotationMatrix );

顶点着色器(Vertex Shader)

attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_position;
varying vec2 v_texCoord;

void main() {
    gl_Position = a_position;
    v_position = a_position.xy;
    v_texCoord = a_texCoord;
}

片元着色器(Fragment Shader)

precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform vec4 u_color;
uniform int u_renderMode; // 0 for texture, 1 for shape


void main() {
        if (u_renderMode == 0) {
            gl_FragColor = texture2D(u_texture, v_texCoord);
        } else if (u_renderMode == 1) {
            gl_FragColor = u_color;
        }
}

WebGL初始化代码(updateScreen函数)

updateScreen() {

    const { gl, program, positionBuffer, texCoordBuffer } = this;

    gl.useProgram(program);


    // Bind the position buffer for this layer
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positionLocation = gl.getAttribLocation(program!, 'a_position');
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // Bind the texCoord buffer for this layer
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    const texCoordLocation = gl.getAttribLocation(program!, 'a_texCoord');
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    const error = gl.getError();

    if (error !== gl.NO_ERROR) {
        console.error('WebGL error:', error);
    }

    // Enable blending
    //TODO: need more understanding of the blending functions
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    const hitTestLayer = globalState.get('hitTestLayer');

    this.model?.documents.activeDocument!.layers.forEach((layer: any) => {

        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, layer.positionCoordArray, gl.STATIC_DRAW);

        gl.bindTexture(gl.TEXTURE_2D, layer.texture);

        const textureLocation = gl.getUniformLocation(program!, 'u_texture');

        gl.uniform1i(textureLocation, 0);

        layer.shapes.performDrawingCalls(gl, program!, 0)

        const pixels = new Uint8Array(4);

        gl.readPixels(5, 5, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

    })

    gl.useProgram(null);


}

问题截图

  • 旋转角度0度:旋转0度时的图像
  • 旋转角度约45度:旋转约45度时的图像

旋转过程动图

旋转过程动图


解决方案

核心问题分析

当前代码的关键问题有两个:

  1. 计算好的combinedMatrix从未传递给顶点着色器,也未用来修改gl_Position,导致旋转和比例补偿完全没生效;
  2. 手动编写的比例补偿公式逻辑错误,非正方形Canvas下旋转时,无法保证图像始终适配视口,导致拉伸失真。

修复步骤

1. 修改顶点着色器,引入变换矩阵

在顶点着色器中添加uniform矩阵变量,用于应用变换:

attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_position;
varying vec2 v_texCoord;
uniform mat4 u_modelMatrix; // 新增:变换矩阵

void main() {
    gl_Position = u_modelMatrix * a_position; // 应用变换到顶点位置
    v_position = a_position.xy;
    v_texCoord = a_texCoord;
}

2. 修正比例补偿与矩阵组合逻辑

替换原有的缩放计算代码,使用正确的包围盒适配逻辑:

// 计算Canvas宽高比(假设aspectRatio = 宽/高)
const aspectRatio = gl.canvas.width / gl.canvas.height;
const angleRad = angelInRadian; // 注意:变量名拼写修正为angleInRadian更规范

// 计算旋转后图像的包围盒尺寸
const sinAngle = Math.sin(angleRad);
const cosAngle = Math.cos(angleRad);
const rotatedWidth = Math.abs(cosAngle) + Math.abs(sinAngle) * aspectRatio;
const rotatedHeight = Math.abs(cosAngle) / aspectRatio + Math.abs(sinAngle);

// 计算缩放比例,让旋转后的图像刚好适配Canvas视口
const scaleX = 1 / rotatedWidth;
const scaleY = 1 / rotatedHeight;

// 构建比例补偿矩阵
const aspectRatioCompensation = mat4.fromScaling(mat4.create(), [scaleX, scaleY, 1]);

// 构建绕原点旋转的矩阵
const rotationMatrix = mat4.fromRotation(mat4.create(), angleRad, [0, 0, 1]);

// 矩阵乘法顺序:先旋转,再应用比例补偿(顺序不可颠倒)
const combinedMatrix = mat4.create();
mat4.multiply(combinedMatrix, aspectRatioCompensation, rotationMatrix);

3. 将变换矩阵传递给着色器

在updateScreen函数的图层循环内,添加代码将combinedMatrix传递给顶点着色器:

// 在layer.shapes.performDrawingCalls之前添加
const modelMatrixLocation = gl.getUniformLocation(program!, 'u_modelMatrix');
gl.uniformMatrix4fv(modelMatrixLocation, false, combinedMatrix);

4. 优化纹理采样质量

若旋转后图像出现模糊,设置纹理线性过滤:

gl.bindTexture(gl.TEXTURE_2D, layer.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

关键说明

  • 矩阵顺序:必须先执行旋转,再执行缩放补偿,否则会导致旋转轴偏移;
  • 比例逻辑:通过计算旋转后图像的包围盒尺寸,反向推导缩放比例,确保图像在任何角度下都能完整适配Canvas且无拉伸;
  • 变量名修正:原代码中angelInRadian拼写错误,建议改为angleInRadian避免潜在bug。

内容的提问来源于stack exchange,提问作者Andrew Naem

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:49:57