WebGL中如何高效检测大尺寸纹理是否全为透明像素?
WebGL2中高效检测全空纹理的方法
这里的“全空”指纹理所有像素都是(0, 0, 0, 0),针对大尺寸纹理(比如4K×5K),直接用readPixels读回CPU检测速度太慢,用GPU降采样的方法能大幅提升性能。
核心逻辑
利用GPU并行处理能力,把大纹理逐步缩小(降采样),每一步都在着色器里检查当前采样区域内是否存在非(0,0,0,0)的像素。最终把纹理缩小到1×1的尺寸,只需要读取这一个像素的值,就能判断原纹理是否全空:如果结果是0,说明全是空像素;非0则说明存在有效像素。
具体实现步骤
1. 准备降采样用的着色器和帧缓冲
首先写一个降采样片段着色器,它会对输入纹理的2×2区域采样,只要其中有一个像素不是(0,0,0,0),就输出1.0,否则输出0.0:
// 降采样片段着色器 #version 300 es precision highp float; uniform sampler2D u_sourceTexture; in vec2 v_uv; out float o_result; void main() { // 采样当前像素周围的2x2区域 vec2 texelSize = 1.0 / vec2(textureSize(u_sourceTexture, 0)); vec4 samples[4]; samples[0] = texture(u_sourceTexture, v_uv + vec2(-texelSize.x, -texelSize.y)); samples[1] = texture(u_sourceTexture, v_uv + vec2( texelSize.x, -texelSize.y)); samples[2] = texture(u_sourceTexture, v_uv + vec2(-texelSize.x, texelSize.y)); samples[3] = texture(u_sourceTexture, v_uv + vec2( texelSize.x, texelSize.y)); // 检查是否有非零像素 o_result = 0.0; for (int i = 0; i < 4; i++) { if (any(notEqual(samples[i], vec4(0.0)))) { o_result = 1.0; break; } } }
同时需要一个简单的顶点着色器来传递UV坐标:
#version 300 es in vec2 a_position; out vec2 v_uv; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_uv = a_position * 0.5 + 0.5; // 把[-1,1]范围转成[0,1]UV }
然后创建两个帧缓冲(FBO)和对应的纹理,用来交替存储降采样的结果(避免频繁创建销毁资源):
function createDownsampleFramebuffer(gl, width, height) { const fbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, width, height, 0, gl.RED, gl.FLOAT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 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); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); return { fbo, texture, width, height }; }
2. 迭代降采样直到纹理缩小到1×1
从原纹理的尺寸开始,每次把宽高减半(向上取整,避免丢失边界像素),直到宽高都为1:
function isTextureAllZero(gl, sourceTexture, sourceWidth, sourceHeight) { // 初始化两个FBO用于交替降采样 let currentWidth = sourceWidth; let currentHeight = sourceHeight; let fboA = createDownsampleFramebuffer(gl, Math.ceil(currentWidth / 2), Math.ceil(currentHeight / 2)); let fboB = null; // 绑定降采样程序和顶点数据(假设已经创建好覆盖全屏的顶点缓冲) gl.useProgram(downsampleProgram); gl.bindBuffer(gl.ARRAY_BUFFER, fullscreenQuadBuffer); gl.enableVertexAttribArray(aPositionLocation); gl.vertexAttribPointer(aPositionLocation, 2, gl.FLOAT, false, 0, 0); // 设置源纹理的uniform gl.uniform1i(uSourceTextureLocation, 0); let currentTexture = sourceTexture; // 迭代降采样 while (currentWidth > 1 || currentHeight > 1) { const nextWidth = Math.ceil(currentWidth / 2); const nextHeight = Math.ceil(currentHeight / 2); // 选择当前要写入的FBO const targetFbo = (fboB === null) ? fboA : (currentWidth % 2 === 0 ? fboB : fboA); // 如果需要,创建第二个FBO if (fboB === null && nextWidth > 1 && nextHeight > 1) { fboB = createDownsampleFramebuffer(gl, Math.ceil(nextWidth / 2), Math.ceil(nextHeight / 2)); } // 绑定目标FBO,设置视口 gl.bindFramebuffer(gl.FRAMEBUFFER, targetFbo.fbo); gl.viewport(0, 0, targetFbo.width, targetFbo.height); // 绑定当前纹理到纹理单元0 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, currentTexture); // 绘制全屏四边形,执行降采样 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 更新当前状态 currentTexture = targetFbo.texture; currentWidth = nextWidth; currentHeight = nextHeight; } // 读取最终1x1像素的结果 const result = new Float32Array(1); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, currentTexture); gl.readPixels(0, 0, 1, 1, gl.RED, gl.FLOAT, result); // 清理资源(如果需要重复使用,可以保留FBO不用销毁) gl.deleteFramebuffer(fboA.fbo); gl.deleteTexture(fboA.texture); if (fboB) { gl.deleteFramebuffer(fboB.fbo); gl.deleteTexture(fboB.texture); } return result[0] === 0.0; }
注意事项
- 如果原纹理尺寸不是2的幂,每次降采样要向上取整,确保边界像素被检查到,避免遗漏。
- 纹理的格式要匹配:如果原纹理是RGBA8,着色器里采样得到的是0~1范围的浮点数,和
vec4(0.0)比较是没问题的;如果是其他格式(比如整数格式),需要调整着色器里的比较逻辑。 - 可以把降采样用的FBO和程序缓存起来,避免每次检测都重新创建,进一步提升性能。
- 如果纹理有mipmap,可以直接利用mipmap的最顶层(1x1),但前提是mipmap是正确生成的,且生成时包含了非零像素的检测逻辑——不过自定义降采样更可控,能确保检测逻辑准确。
内容的提问来源于stack exchange,提问作者Subkey
相关产品推荐
相关产品推荐

