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

如何在WebGL中将YUV420图像作为纹理并在着色器中转RGB?

问题分析与解决方案

问题原因

  1. 黑白无色彩:你错误地将YUV420平面格式的数据以RGBA格式加载到纹理中,WebGL会把连续4字节解析为一个RGBA像素,但YUV420的Y、U、V分量是分开存储的,导致色彩信息完全丢失,只能显示Y分量的灰度。
  2. 图像重复与线条:640x480的YUV420数据总字节数为640*480*1.5=460800,而RGBA格式的同尺寸纹理需要640*480*4=1228800字节,WebGL会循环复用现有数据填充纹理,导致图像重复;同时生成mipmap会放大数据重复问题,叠加纹理过滤的影响,出现奇怪线条。

修复方案

核心思路是将YUV420数据作为单通道纹理加载,在片段着色器中根据字节偏移计算Y、U、V分量的采样位置,再通过着色器完成YUV到RGB的转换,避免拆分多纹理。

修改后的完整代码

const gl = document.querySelector("canvas").getContext("webgl2");
const imageWidth = 640;
const imageHeight = 480;
const textureTotalHeight = imageHeight * 1.5; // 720,对应YUV420总字节数的纹理高度

fetch("input.yuv")
  .then(async (image) => {
    // 顶点着色器(无需修改)
    const vertexShaderSrc = `#version 300 es
in vec4 aPosition;
in vec2 aTexCoord;

out vec2 vTexCoord;
void main()
{
    vTexCoord = aTexCoord;
    gl_Position = aPosition;
}`;

    // 修复后的片段着色器:实现单纹理内YUV采样与转RGB
    const fragmentShaderSrc = `#version 300 es
#pragma vscode_glsllint_stage: frag

precision mediump float;

in vec2 vTexCoord;

uniform sampler2D uSampler;
uniform float uImageWidth;
uniform float uImageHeight;
uniform float uTextureTotalHeight;

out vec4 fragColor;

void main() {
    // 采样Y分量(纹理前480行)
    vec2 yCoord = vec2(vTexCoord.x, vTexCoord.y * (uImageHeight / uTextureTotalHeight));
    float Y = texture(uSampler, yCoord).r;

    // 计算U分量的字节偏移与采样坐标
    float x = vTexCoord.x * uImageWidth;
    float y = vTexCoord.y * uImageHeight;
    float uOffset = uImageWidth * uImageHeight + floor(y / 2.0) * (uImageWidth / 2.0) + floor(x / 2.0);
    vec2 uCoord = vec2(mod(uOffset, uImageWidth) / uImageWidth, floor(uOffset / uImageWidth) / uTextureTotalHeight);
    float U = texture(uSampler, uCoord).r;

    // 计算V分量的字节偏移与采样坐标
    float vOffset = uOffset + (uImageWidth * uImageHeight) / 4.0;
    vec2 vCoord = vec2(mod(vOffset, uImageWidth) / uImageWidth, floor(vOffset / uImageWidth) / uTextureTotalHeight);
    float V = texture(uSampler, vCoord).r;

    // YUV转RGB(BT.601标准,适配SD视频)
    float yLinear = Y * 255.0 - 16.0;
    float cb = U * 255.0 - 128.0;
    float cr = V * 255.0 - 128.0;

    float r = 1.164 * yLinear + 1.596 * cr;
    float g = 1.164 * yLinear - 0.392 * cb - 0.813 * cr;
    float b = 1.164 * yLinear + 2.017 * cb;

    // 归一化到0-1范围并输出
    fragColor = vec4(clamp(r / 255.0, 0.0, 1.0),
                     clamp(g / 255.0, 0.0, 1.0),
                     clamp(b / 255.0, 0.0, 1.0),
                     1.0);
}`;

    // 编译着色器与链接程序(无需修改)
    const program = gl.createProgram();

    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexShaderSrc);
    gl.compileShader(vertexShader);
    gl.attachShader(program, vertexShader);

    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fragmentShaderSrc);
    gl.compileShader(fragmentShader);
    gl.attachShader(program, fragmentShader);

    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.log(gl.getShaderInfoLog(vertexShader));
      console.log(gl.getShaderInfoLog(fragmentShader));
    }

    gl.useProgram(program);

    // 顶点与纹理坐标缓冲(无需修改)
    const vertexBufferData = new Float32Array([
      -0.9, 0.9, -0.9, -0.9, 0.9, 0.9, 0.9, -0.9,
    ]);

    const texCoordBufferData = new Float32Array([
      0, 1, 0, 0, 1, 1, 1, 0,
    ]);

    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.STATIC_DRAW);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(0);

    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, texCoordBufferData, gl.STATIC_DRAW);
    gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(1);

    // 设置纹理与uniform变量
    gl.activeTexture(gl.TEXTURE0);
    gl.uniform1i(gl.getUniformLocation(program, "uSampler"), 0);

    // 传递图像尺寸参数给着色器
    const uImageWidthLoc = gl.getUniformLocation(program, "uImageWidth");
    const uImageHeightLoc = gl.getUniformLocation(program, "uImageHeight");
    const uTextureTotalHeightLoc = gl.getUniformLocation(program, "uTextureTotalHeight");
    gl.uniform1f(uImageWidthLoc, imageWidth);
    gl.uniform1f(uImageHeightLoc, imageHeight);
    gl.uniform1f(uTextureTotalHeightLoc, textureTotalHeight);

    // 创建单通道纹理并加载YUV数据
    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    let imageData = await image.arrayBuffer();
    // 使用R8单通道格式加载数据,匹配YUV的字节结构
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, imageWidth, textureTotalHeight, 0, gl.RED, gl.UNSIGNED_BYTE, new Uint8Array(imageData));

    // 纹理参数设置:关闭mipmap,使用线性过滤
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    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.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  });

关键修改点

  1. 纹理格式调整:将纹理格式改为单通道gl.R8,直接匹配YUV420的字节存储结构,避免格式解析错误。
  2. 着色器逻辑补充:在片段着色器中根据YUV420的平面存储规则,计算U、V分量的字节偏移与采样坐标,再通过BT.601标准公式完成YUV到RGB的转换。
  3. 纹理参数优化:关闭mipmap生成(无需多级纹理),使用线性过滤提升显示效果,同时设置CLAMP_TO_EDGE避免纹理边缘重复。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 17:45:56