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

如何在WebGL2着色器中使用CSS matrix3d矩阵?

问题描述

已有可用的CSS matrix3d 变换:

transform: matrix3d(1.3453,0.1357,0.0,0.0003,0.2096,1.3453,0.0,0.0003,0.0,0.0,1.0,0.0,-100.0,-100.0,0.0,1.0);
transform-origin: 0 0;

希望在WebGL2画布中给绘制的图像应用完全相同的变换,但直接传入CSS矩阵到着色器后无法得到预期效果。

核心原因

CSS和WebGL的坐标空间与矩阵应用逻辑存在差异:

  • CSS坐标以左上角为原点,Y轴向下;WebGL裁剪空间Y轴向上,且原有着色器中已通过clipSpace * vec2(1, -1)翻转了Y轴。
  • CSS矩阵直接作用于元素的像素坐标,而原有着色器先将顶点像素坐标转换到[-1,1]的归一化裁剪空间,再应用矩阵,导致变换逻辑不匹配。
解决方案

修改顶点着色器的坐标转换顺序:先对像素坐标应用CSS矩阵,再转换到WebGL的裁剪空间;同时适配WebGL的Y轴方向。

步骤1:调整顶点着色器

将先转裁剪空间再乘矩阵的逻辑,改为先对原始像素位置应用矩阵,再转裁剪空间:

#version 300 es

in vec2 a_position;
in vec2 a_texCoord;
uniform vec2 u_resolution;
uniform mat4 u_matrix;
out vec2 v_texCoord;

void main() {
  // 先对像素位置应用CSS矩阵
  vec4 transformedPos = u_matrix * vec4(a_position, 0.0, 1.0);
  vec2 pos = transformedPos.xy / transformedPos.w; // 透视除法

  // 将转换后的像素坐标转为裁剪空间
  vec2 zeroToOne = pos / u_resolution;
  vec2 zeroToTwo = zeroToOne * 2.0;
  vec2 clipSpace = zeroToTwo - 1.0;

  // 适配WebGL的Y轴方向
  gl_Position = vec4(clipSpace * vec2(1, -1), 0.0, 1.0);
  v_texCoord = a_texCoord;
}

步骤2:直接使用CSS矩阵(无需修改)

WebGL的uniformMatrix4fv默认是列主序,和CSS matrix3d的参数顺序完全一致,直接传入原矩阵即可:

const cssMatrix = new Float32Array([
  1.3453, 0.1357, 0.0, 0.0003,
  0.2096, 1.3453, 0.0, 0.0003,
  0.0, 0.0, 1.0, 0.0,
  -100.0, -100.0, 0.0, 1.0
]);
完整可运行代码

HTML

<img src="" crossOrigin="" id="image"><br>
<canvas id="canvas" width=320 height=240></canvas>

CSS

#image {
  transform: matrix3d(1.3453, 0.1357, 0.0, 0.0003, 0.2096, 1.3453, 0.0, 0.0003, 0.0, 0.0, 1.0, 0.0, -100.0, -100.0, 0.0, 1.0);
  transform-origin: 0 0;
}

img,
canvas {
  border: 1px solid #000;
}

JavaScript

// Set up matrices
const cssMatrix = new Float32Array([
  1.3453, 0.1357, 0.0, 0.0003,
  0.2096, 1.3453, 0.0, 0.0003,
  0.0, 0.0, 1.0, 0.0,
  -100.0, -100.0, 0.0, 1.0
]);
const matrixArray = cssMatrix;

// Set up shaders
const vertexShaderSource = `#version 300 es

in vec2 a_position;
in vec2 a_texCoord;
uniform vec2 u_resolution;
uniform mat4 u_matrix;
out vec2 v_texCoord;

void main() {
  vec4 transformedPos = u_matrix * vec4(a_position, 0.0, 1.0);
  vec2 pos = transformedPos.xy / transformedPos.w;

  vec2 zeroToOne = pos / u_resolution;
  vec2 zeroToTwo = zeroToOne * 2.0;
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace * vec2(1, -1), 0.0, 1.0);
  v_texCoord = a_texCoord;
}`;

const fragmentShaderSource = `#version 300 es

precision highp float;
uniform sampler2D u_image;
in vec2 v_texCoord;
out vec4 outColor;

void main() {
  outColor = texture(u_image, v_texCoord);
}`;

// Load the test image
const img = document.getElementById("image");
img.src = "https://i.imgur.com/NIt86ft.png";
img.onload = () => renderImg(img);

// WebGL utility functions
function createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback) {
  const errFn = opt_errorCallback || console.error;
  const program = gl.createProgram();
  shaders.forEach(shader => gl.attachShader(program, shader));
  if (opt_attribs) {
    opt_attribs.forEach((attrib, ndx) => {
      gl.bindAttribLocation(program, opt_locations ? opt_locations[ndx] : ndx, attrib);
    });
  }
  gl.linkProgram(program);
  const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    errFn('Error in program linking:' + gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }
  return program;
}

function loadShader(gl, shaderSource, shaderType, opt_errorCallback) {
  const errFn = opt_errorCallback || console.error;
  const shader = gl.createShader(shaderType);
  gl.shaderSource(shader, shaderSource);
  gl.compileShader(shader);
  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (!compiled) {
    errFn('*** Error compiling shader:\n' + gl.getShaderInfoLog(shader) + `\n` + shaderSource.split('\n').map((l, i) => `${i + 1}: ${l}`).join('\n'));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

function createProgramFromSources(gl, shaderSources, opt_attribs, opt_locations, opt_errorCallback) {
  const shaders = [];
  const defaultShaderType = [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER];
  for (let ii = 0; ii < shaderSources.length; ++ii) {
    shaders.push(loadShader(gl, shaderSources[ii], defaultShaderType[ii], opt_errorCallback));
  }
  return createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback);
}

function resizeCanvasToDisplaySize(canvas, multiplier) {
  multiplier = multiplier || 1;
  const width = canvas.clientWidth * multiplier | 0;
  const height = canvas.clientHeight * multiplier | 0;
  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width;
    canvas.height = height;
    return true;
  }
  return false;
}

function renderImg(image) {
  const canvas = document.getElementById("canvas");
  const gl = canvas.getContext("webgl2");
  if (!gl) return;

  const program = createProgramFromSources(gl, [vertexShaderSource, fragmentShaderSource]);

  const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
  const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
  const imageLocation = gl.getUniformLocation(program, "u_image");
  const matrixLocation = gl.getUniformLocation(program, "u_matrix");

  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);

  // Position buffer
  const positionBuffer = gl.createBuffer();
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

  // TexCoord buffer
  const texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    0.0, 0.0,
    1.0, 0.0,
    0.0, 1.0,
    0.0, 1.0,
    1.0, 0.0,
    1.0, 1.0,
  ]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordAttributeLocation);
  gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);

  // Texture setup
  const texture = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

  // Set rectangle position
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  setRectangle(gl, 0, 0, image.width, image.height);

  // Render
  resizeCanvasToDisplaySize(gl.canvas);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.useProgram(program);
  gl.bindVertexArray(vao);
  gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
  gl.uniform1i(imageLocation, 0);
  gl.uniformMatrix4fv(matrixLocation, false, matrixArray);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

function setRectangle(gl, x, y, width, height) {
  const x1 = x;
  const x2 = x + width;
  const y1 = y;
  const y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    x1, y1,
    x2, y1,
    x1, y2,
    x1, y2,
    x2, y1,
    x2, y2,
  ]), gl.STATIC_DRAW);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 19:25:52