如何通过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)做离屏渲染——把第一次模糊的结果暂存到一个纹理中,再将这个纹理作为第二次模糊的输入,而不是在片元着色器里独立计算两次模糊后合并。
具体步骤
- 创建帧缓冲与纹理:生成两个帧缓冲和对应纹理,用来交替存储中间模糊结果(WebGL不允许同时读写同一个纹理)。
- 第一次模糊:以原始视频帧为输入,应用第一个滑块的模糊参数,渲染到第一个帧缓冲的纹理上。
- 第二次模糊:以第一个帧缓冲的纹理为输入,应用第二个滑块的模糊参数,渲染到第二个帧缓冲的纹理上。
- 最终渲染:把第二次模糊后的纹理输出到主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
相关产品推荐
相关产品推荐

