如何在WebGL中将YUV420图像作为纹理并在着色器中转RGB?
问题分析与解决方案
问题原因
- 黑白无色彩:你错误地将YUV420平面格式的数据以RGBA格式加载到纹理中,WebGL会把连续4字节解析为一个RGBA像素,但YUV420的Y、U、V分量是分开存储的,导致色彩信息完全丢失,只能显示Y分量的灰度。
- 图像重复与线条: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); });
关键修改点
- 纹理格式调整:将纹理格式改为单通道
gl.R8,直接匹配YUV420的字节存储结构,避免格式解析错误。 - 着色器逻辑补充:在片段着色器中根据YUV420的平面存储规则,计算U、V分量的字节偏移与采样坐标,再通过BT.601标准公式完成YUV到RGB的转换。
- 纹理参数优化:关闭mipmap生成(无需多级纹理),使用线性过滤提升显示效果,同时设置
CLAMP_TO_EDGE避免纹理边缘重复。
内容的提问来源于stack exchange,提问作者8MB_SSD
相关产品推荐
相关产品推荐

