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

如何通过WebGL对视频帧多次应用模糊滤镜

问题描述

我希望用WebGL着色器给视频帧添加模糊滤镜,渲染到HTML Canvas上,且需要多次应用模糊滤镜——前一次的模糊结果作为下一次的输入。我对WebGL不熟悉,目前仅实现了单次模糊的简单示例,尝试多次调用blur函数并用mix合并结果,但没能成功。示例里有两个滑块,第二个滑块应该基于第一次模糊的结果再次执行模糊操作。

对应的片元着色器代码如下:

precision mediump float;
uniform sampler2D texture0;
uniform sampler2D texture1;
uniform float texWidth;
uniform float texHeight;

uniform float u_sigma;
uniform float u_sigma2;

float CalcGauss( float x, float sigma )
{
  return exp(-0.5*(x*x)/(sigma*sigma));
}

vec4 blur(vec2 texCoord, sampler2D texture, float sigma) {
    vec2 texC = texCoord;
    vec4 texCol = texture2D( texture, texC );

    vec4 gaussCol = vec4( texCol.rgb, 1.0 );
    vec2 steph     = vec2(1.0, 0.0) / texWidth;
    for ( int i = 1; i <= 32; ++ i )
    {
        float weight = CalcGauss( float(i) / 32.0, sigma * 0.5 );
        if ( weight < 1.0/255.0 )
            break;
        texCol    = texture2D( texture, texC + steph * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
        texCol    = texture2D( texture, texC - steph * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
    }
    
    gaussCol.rgb = clamp( gaussCol.rgb / gaussCol.w, 0.0, 1.0 );
    return vec4( gaussCol.rgb, 1.0 );
}

void main(void) {
    vec2 texCoord = vec2(gl_FragCoord.x/texWidth, 1.0 - (gl_FragCoord.y/texHeight));
    vec4 blur0 = blur(texCoord, texture0, u_sigma);
    vec4 blur1 = blur(texCoord, texture1, u_sigma2);
    gl_FragColor = mix(blur0, blur1, blur0.a);
}
解决方案

要实现“基于前一次模糊结果再次模糊”,核心是用帧缓冲(Framebuffer)做离屏渲染——把第一次模糊的结果暂存到一个纹理中,再将这个纹理作为第二次模糊的输入,而不是在片元着色器里独立计算两次模糊后合并。

具体步骤

  1. 创建帧缓冲与纹理:生成两个帧缓冲和对应纹理,用来交替存储中间模糊结果(WebGL不允许同时读写同一个纹理)。
  2. 第一次模糊:以原始视频帧为输入,应用第一个滑块的模糊参数,渲染到第一个帧缓冲的纹理上。
  3. 第二次模糊:以第一个帧缓冲的纹理为输入,应用第二个滑块的模糊参数,渲染到第二个帧缓冲的纹理上。
  4. 最终渲染:把第二次模糊后的纹理输出到主Canvas。

着色器优化

不需要同时传入两个纹理,每次渲染只需要当前输入纹理和对应模糊参数,同时补充垂直方向模糊(原始代码仅做了水平模糊,完整高斯模糊需要水平+垂直两次处理):

precision mediump float;
uniform sampler2D texture;
uniform float texWidth;
uniform float texHeight;
uniform float u_sigma;

float CalcGauss( float x, float sigma )
{
  return exp(-0.5*(x*x)/(sigma*sigma));
}

vec4 blur(vec2 texCoord, sampler2D texture, float sigma) {
    vec2 texC = texCoord;
    vec4 texCol = texture2D( texture, texC );

    vec4 gaussCol = vec4( texCol.rgb, 1.0 );
    // 水平方向模糊
    vec2 steph = vec2(1.0, 0.0) / texWidth;
    for ( int i = 1; i <= 32; ++ i )
    {
        float weight = CalcGauss( float(i) / 32.0, sigma * 0.5 );
        if ( weight < 1.0/255.0 )
            break;
        texCol    = texture2D( texture, texC + steph * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
        texCol    = texture2D( texture, texC - steph * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
    }
    // 垂直方向模糊
    vec2 stepv = vec2(0.0, 1.0) / texHeight;
    for ( int i = 1; i <= 32; ++ i )
    {
        float weight = CalcGauss( float(i) / 32.0, sigma * 0.5 );
        if ( weight < 1.0/255.0 )
            break;
        texCol    = texture2D( texture, texC + stepv * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
        texCol    = texture2D( texture, texC - stepv * float(i) );
        gaussCol += vec4( texCol.rgb * weight, weight );
    }
    
    gaussCol.rgb = clamp( gaussCol.rgb / gaussCol.w, 0.0, 1.0 );
    return vec4( gaussCol.rgb, 1.0 );
}

void main(void) {
    vec2 texCoord = vec2(gl_FragCoord.x/texWidth, 1.0 - (gl_FragCoord.y/texHeight));
    gl_FragColor = blur(texCoord, texture, u_sigma);
}

JavaScript核心逻辑调整

初始化帧缓冲

function createFramebuffer(width, height) {
  const fb = gl.createFramebuffer();
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  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.bindFramebuffer(gl.FRAMEBUFFER, fb);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.bindTexture(gl.TEXTURE_2D, null);
  
  return { fb, texture };
}

// 假设video为视频元素,创建与视频尺寸匹配的帧缓冲
const fb1 = createFramebuffer(video.width, video.height);
const fb2 = createFramebuffer(video.width, video.height);

渲染循环执行两次模糊

// 假设已获取着色器uniform变量位置:u_sigma_location
// drawFullScreenQuad为绘制全屏四边形的函数(用来覆盖整个渲染区域)
function render() {
  // 第一次模糊:原始视频帧→fb1
  gl.bindFramebuffer(gl.FRAMEBUFFER, fb1.fb);
  gl.viewport(0, 0, video.width, video.height);
  gl.uniform1f(u_sigma_location, sigma1Value); // 第一个滑块的值
  gl.bindTexture(gl.TEXTURE_2D, videoTexture); // 原始视频纹理
  drawFullScreenQuad();

  // 第二次模糊:fb1的纹理→fb2
  gl.bindFramebuffer(gl.FRAMEBUFFER, fb2.fb);
  gl.uniform1f(u_sigma_location, sigma2Value); // 第二个滑块的值
  gl.bindTexture(gl.TEXTURE_2D, fb1.texture);
  drawFullScreenQuad();

  // 将最终结果渲染到主Canvas
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, canvas.width, canvas.height);
  // 使用简单着色器直接绘制纹理
  gl.useProgram(simpleDrawProgram);
  gl.bindTexture(gl.TEXTURE_2D, fb2.texture);
  drawFullScreenQuad();

  requestAnimationFrame(render);
}

注意事项

  • 帧缓冲的宽高必须和视频帧一致,避免拉伸或失真。
  • 每次切换帧缓冲时要同步设置viewport,确保渲染区域匹配纹理尺寸。
  • 分两次做水平+垂直模糊,比单次同时处理两个方向的效率更高,也能得到更自然的高斯模糊效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 06:27:53