如何在OpenGL ES 2.0片段着色器中获取先前片段颜色并实现Canvas风格混合效果?
实现WebGL中类似Canvas的混合模式(darken/lighten/burn等)
要在WebGL里实现和Canvas globalCompositeOperation 一致的混合效果,核心思路是用帧缓冲(Framebuffer)保存已绘制的内容,再在新绘制的片段着色器中采样这个内容,手动实现混合公式。下面是具体的实现步骤和代码示例:
1. 创建离屏帧缓冲保存绘制状态
WebGL默认无法在绘制当前片段时直接读取同一画布上已存在的像素,所以我们需要先把背景(以及之前绘制的粒子)渲染到一个离屏帧缓冲里,再将这个帧缓冲作为纹理传递给粒子的着色器。
function createFramebuffer(gl, width, height) { const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // 创建颜色附件纹理 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.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); // 检查帧缓冲是否完整 if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) { console.error("Framebuffer is not complete!"); return null; } // 解绑 gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); return { framebuffer, texture }; } // 初始化时创建和画布同尺寸的帧缓冲 const canvas = document.getElementById('glCanvas'); const gl = canvas.getContext('webgl'); const fb = createFramebuffer(gl, canvas.width, canvas.height);
2. 先绘制背景到帧缓冲
切换到离屏帧缓冲,绘制你的背景图,完成后切回默认帧缓冲:
// 绑定帧缓冲,开始离屏渲染 gl.bindFramebuffer(gl.FRAMEBUFFER, fb.framebuffer); // 清除帧缓冲 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 这里执行你的背景绘制代码(比如绘制一张纹理图) drawBackground(gl); // 切回默认帧缓冲 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3. 绘制粒子时采样帧缓冲纹理并实现混合
在粒子的片段着色器中,我们需要:
- 接收帧缓冲纹理(已绘制的背景+之前的粒子)
- 接收当前粒子的颜色
- 根据选择的混合模式,计算最终输出颜色
片段着色器代码
precision mediump float; // 已绘制的内容(帧缓冲纹理) uniform sampler2D u_previousTexture; // 当前粒子的颜色 uniform vec4 u_particleColor; // 混合模式:0=darken, 1=lighten, 2=burn uniform int u_blendMode; // 画布尺寸(用于计算纹理坐标) uniform vec2 u_canvasSize; void main() { // 采样已绘制的像素颜色 vec2 texCoord = gl_FragCoord.xy / u_canvasSize; vec4 baseColor = texture2D(u_previousTexture, texCoord); // 当前粒子的颜色(示例用纯色,可替换为粒子纹理采样) vec4 blendColor = u_particleColor; vec4 finalColor; if (u_blendMode == 0) { // Darken模式:取每个通道的最小值 finalColor = vec4(min(baseColor.rgb, blendColor.rgb), base.a + blend.a - base.a * blend.a); } else if (u_blendMode == 1) { // Lighten模式:取每个通道的最大值 finalColor = vec4(max(baseColor.rgb, blendColor.rgb), base.a + blend.a - base.a * blend.a); } else if (u_blendMode == 2) { // Burn模式:模拟加深效果,处理预乘Alpha vec3 baseRGB = baseColor.rgb / baseColor.a; vec3 blendRGB = blendColor.rgb / blendColor.a; vec3 resultRGB = 1.0 - (1.0 - baseRGB) / blendRGB; resultRGB = clamp(resultRGB, 0.0, 1.0); float resultAlpha = baseColor.a + blendColor.a - baseColor.a * blendColor.a; finalColor = vec4(resultRGB * resultAlpha, resultAlpha); } else { // 默认直接覆盖 finalColor = blendColor; } gl_FragColor = finalColor; }
绘制粒子的JS代码
每次绘制粒子前,需要把帧缓冲纹理传递给着色器,并且绘制完成后,要把新的结果再写回帧缓冲(这样下次绘制粒子时能包含之前的所有内容):
function drawParticles(gl, fb, blendMode) { // 绑定帧缓冲,粒子会叠加到之前的背景/粒子上 gl.bindFramebuffer(gl.FRAMEBUFFER, fb.framebuffer); // 设置着色器 uniforms const prevTexLoc = gl.getUniformLocation(program, 'u_previousTexture'); gl.uniform1i(prevTexLoc, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fb.texture); const canvasSizeLoc = gl.getUniformLocation(program, 'u_canvasSize'); gl.uniform2f(canvasSizeLoc, canvas.width, canvas.height); const particleColorLoc = gl.getUniformLocation(program, 'u_particleColor'); gl.uniform4f(particleColorLoc, 1.0, 0.5, 0.0, 0.8); // 示例橙色半透明粒子 const blendModeLoc = gl.getUniformLocation(program, 'u_blendMode'); gl.uniform1i(blendModeLoc, blendMode); // 执行粒子绘制代码(比如绘制多个点/三角形) renderParticleBatch(gl); // 把帧缓冲的最终内容绘制到画布,让用户看到效果 gl.bindFramebuffer(gl.FRAMEBUFFER, null); drawFullscreenQuad(gl, fb.texture); }
关键细节说明
- 预乘Alpha处理:像burn这类混合模式需要处理透明度,建议先把颜色转到预乘Alpha空间计算,再转换回来,避免混合错误。
- 帧缓冲尺寸同步:如果画布尺寸变化,需要重新创建帧缓冲,确保纹理尺寸和画布一致,避免采样偏移。
- 混合模式扩展:你可以参考Canvas的
globalCompositeOperation标准,扩展更多混合模式(如multiply、screen等),只需要在着色器中添加对应的计算函数即可。
内容的提问来源于stack exchange,提问作者younes rami
相关产品推荐
相关产品推荐

