- 图形学
【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
本文基于 webgl-fundamentals 仓库的 WebGL e Alpha 教程(原文为英文 WebGL and Alpha 的葡语翻译版)展开,讲清 WebGL 与 OpenGL 在 backbuffer alpha 处理上的根本差异,并完整覆盖仓库给出的六种让 WebGL 的 alpha 行为更接近 OpenGL 或更贴合浏览器合成模型的处理手段。读完后,你将能够诊断透明 canvas 的颜色异常,并在getContext参数、colorMask、UNPACK_PREMULTIPLY_ALPHA_WEBGL与混合函数之间做出正确的组合选择。
背景:WebGL 的 alpha 为什么和 OpenGL 不一样
仓库文档指出的核心差异是:
- OpenGL渲染到一个 backbuffer,这个 buffer 不与任何东西合成(或等效于不与任何东西合成),操作系统窗口管理器不会去管它,因此 alpha 值是多少都无关紧要;
- WebGL的 canvas 是由浏览器与整个网页一起合成的,而且默认采用预乘 alpha(pre-multiplied alpha)——与带透明通道的 PNG
<img>标签、2D canvas 的行为一致。
理解"预乘 alpha"是理解后续所有方案的前提。预乘 alpha 意味着 RGB 分量已经被乘以了 alpha 值:例如原始 RGB 为1, 0, 0、alpha 为0.5,预乘后 RGB 就变成了0.5, 0, 0。浏览器默认期望 canvas 中的像素是预乘值。仓库另一篇问答文章 如何让 WebGL canvas 透明 给出了一个非常直观的例子:如果你把清屏色设为(1, 0, 0, 0.5)(未预乘的纯红半透明),浏览器会认为这是一个"不合法"的预乘值(a = 0.5却要求r保持1),于是合成出你在 HTML 其他地方都见不到的奇怪效果——黑色文字配红色底、边缘泛红等。该文档中给出的修复方式之一正是本文方案 #1。
方案 #1:告诉 WebGL 使用非预乘 alpha 进行合成
gl = canvas.getContext("webgl", { premultipliedAlpha: false // Ask for non-premultiplied alpha });这个选项的默认值是true。设为false后,浏览器在合成时就不会假定像素是预乘值,你在 WebGL 中写出的1, 0, 0, 0.5就能按"纯红、半透明"被正确理解。
但要注意:结果仍然会被合成为页面的一部分,叠在 canvas 下方的 CSS 背景之上——canvas 自身的 background 色、canvas 容器背景色、页面背景色、canvasz-index > 0时其背后的内容,也就是 CSS 为该页面区域定义的颜色。
仓库问答文档正是用这个选项解决了透明问题:
const gl = canvas.getContext("webgl", { premultipliedAlpha: false });原文还给出了一个调试技巧:怀疑自己有 alpha 问题吗?把 canvas 的 CSS 背景设成亮色(如红色),问题会立刻显现:
<canvas style="background: red;"></canvas>反过来,把背景设成黑色则会掩盖所有 alpha 问题——这也是很多 bug 迟迟无法被发现的原因。
方案 #2:告诉 WebGL 你的 backbuffer 不需要 alpha
gl = canvas.getContext("webgl", { alpha: false });这会让 backbuffer 只有 RGB 通道,行为上最接近 OpenGL(alpha 无意义、也不参与合成)。原文认为这很可能是最佳选项:一个合格的浏览器可以看到你没有 alpha,从而真正优化 WebGL 的合成路径。
代价也很直接:backbuffer 里真的没有 alpha 了。如果你的应用恰好依赖 backbuffer 的 alpha 通道做某些效果,这条路就走不通。原文作者表示:他所知道的应用里几乎没有在 backbuffer 上使用 alpha 的,并认为"这本来就该是默认值"。
方案 #3:在渲染末尾把 alpha 清成 1.0
.. renderScene(); .. // Set the backbuffer's alpha to 1.0 gl.clearColor(1, 1, 1, 1); gl.colorMask(false, false, false, true); gl.clear(gl.COLOR_BUFFER_BIT);思路是:场景照常渲染,最后再清一次色缓冲,但通过colorMask(false, false, false, true)只允许写入 alpha 通道,把 backbuffer 的 alpha 整体拉满到 1.0(即完全不透明)。
原文解释了为什么这种做法代价很小:clear 在大多数硬件上是有专门优化路径的,通常非常快。作者说他的大多数演示都用了这种写法,并认为多数 WebGL 库都应该默认采用这个策略:真正需要用 alpha 做合成效果的少数开发者可以显式选择别的方案,其余人则获得最好的性能和最少的意外。
方案 #4:只清一次 alpha,之后不再向 alpha 写入
// At init time. Clear the back buffer. gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); // Turn off rendering to alpha gl.colorMask(true, true, true, false);与方案 #3 相比,这里把清屏放在初始化时只执行一次,随后用colorMask(true, true, true, false)关闭对 alpha 通道的渲染写入,后续每帧的普通清屏和场景渲染都不会再动 alpha 通道。
原文提醒了一个容易踩的坑:如果你在渲染到自己的 framebuffer,切换回 canvas 之前可能需要重新打开alpha 写入(gl.colorMask(true, true, true, true)),切到屏幕后再关掉。也就是说colorMask状态是全局 GL 状态,绑定 framebuffer 切换时要自行管理。
方案 #5:纹理/图片中的 alpha——UNPACK_PREMULTIPLY_ALPHA_WEBGL
当你把带 alpha 的图片加载进 WebGL 时,默认情况下 WebGL 会原样提供 PNG 文件中的值,即颜色分量未预乘。原文作者认为这正是他做 OpenGL 程序习惯的表示,因为未预乘是无损的,而预乘是有损的。
一个直观的例子:1, 0.5, 0.5, 0(RGBA)在未预乘的语义下是完全合法的取值;而在预乘语义下这是不可能的值,因为a = 0就意味着r、g、b必须全为 0。
如果你希望 WebGL 帮你预乘,可以在上传纹理前设置:
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);该选项默认是关的(即不预乘)。
Canvas 2D 与 WebGL 的转换方向是这里最容易混淆的点:绝大多数(甚至可以说所有)Canvas 2D 实现内部使用预乘 alpha。因此当你把 Canvas 2D 的结果传给 WebGL,且UNPACK_PREMULTIPLY_ALPHA_WEBGL为 false 时,WebGL 会把它反向转换回未预乘——而这次"反预乘"是有损的,会引入误差。
仓库中的实际代码印证了这一点。文字纹理示例 webgl-text-texture.md 描述了文字边缘出现灰色描边的问题,原因正是"Canvas 2D API 只产生预乘 alpha 值,WebGL 上传纹理时尝试反预乘却无法完美还原"。修复方法有两处改动:
- 告诉 WebGL 不要反预乘:
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);- 同步修改混合函数(见下一节)。
对应的完整可运行示例在 webgl-text-texture-premultiplied-alpha.html 中,其核心片段为:
gl.bindTexture(gl.TEXTURE_2D, textTex); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);注意UNPACK_PREMULTIPLY_ALPHA_WEBGL是逐次上传的状态:它影响gl.texImage2D/gl.texSubImage2D上传的数据,而不是纹理之后的读取行为;若同一上下文要上传未预乘的图片数据,记得把该状态切回 false。
方案 #6:与预乘 alpha 匹配的混合方程
原文指出,几乎所有他写过或参与过的 OpenGL 应用都使用:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);这个组合只对未预乘alpha 的纹理成立——SRC_ALPHA的语义是"把源颜色乘以其 alpha"。
如果你确实要使用预乘alpha 纹理,则应改为:
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);因为预乘意味着纹理数据中的 RGB 已经被乘过 alpha,GPU 不需要再乘一次,源因子设为ONE(乘 1)即可。webgl-text-texture.md 原文对此的解释是:"旧的那个会把 src 颜色乘以它的 alpha……但现在纹理数据已经乘过了它的 alpha,所以不需要 GPU 再做乘法。"
webgl-text-texture-premultiplied-alpha.html 示例中的绘制流程完整演示了两套混合模式的切换:绘制不透明的 3D 场景时关闭混合(gl.disable(gl.BLEND)),绘制半透明文字纹理时再启用:
gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.depthMask(false);六种方案速查表
| 方案 | 关键 API | 作用 | 适用场景 |
|---|---|---|---|
| #1 | getContext("webgl", { premultipliedAlpha: false }) | 浏览器合成时按未预乘值解释像素 | WebGL 内数据是未预乘、需与页面透明合成 |
| #2 | getContext("webgl", { alpha: false }) | backbuffer 仅 RGB,浏览器可优化合成 | 全屏不透明渲染(原文推荐的首选) |
| #3 | colorMask(false,false,false,true)+clear | 每帧末尾把 backbuffer alpha 清成 1.0 | 需要保留 alpha 通道但要求每帧输出完全不透明 |
| #4 | 初始化清一次 +colorMask(true,true,true,false) | 之后完全不写 alpha | 类似 #3,但连每帧清屏开销都省掉 |
| #5 | pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, ...) | 控制纹理上传时的预乘/反预乘 | Canvas 2D 数据进 WebGL 时保持预乘以消除有损转换 |
| #6 | blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) | 与预乘纹理匹配的混合方程 | 渲染预乘 alpha 纹理 |
小结
WebGL 与 OpenGL 的 alpha 差异本质上源于一点:OpenGL 的 backbuffer 不参与合成,而 WebGL 的 canvas 由浏览器按 HTML5 的预乘 alpha 约定与页面合成。围绕这一点,仓库文档给出了六个层次的解法——从 context 属性(#1、#2)到每帧状态管理(#3、#4),再到纹理数据转换(#5)与混合方程(#6)。对绝大多数不依赖页面透明合成的应用,alpha: false(方案 #2)是最省心、性能最优的选择;而对需要透明合成或绘制 Canvas 2D 来源纹理的场景,正确组合 #1、#5 与 #6 才能既消除边缘瑕疵,又保持合成颜色正确。仓库中 webgl-text-texture.md 与 webgl-text-texture-premultiplied-alpha.html 提供了这套组合拳的完整可运行示例,可直接参考复现。
- 图形学
【免费下载链接】webgl-fundamentals
WebGL lessons that start with the basics
相关推荐
WebGL 纹理完全指南:纹理坐标、mipmap、NPOT 限制与纹理图集(webgl-fundamentals 实战)
WebGL 纹理完全指南:纹理坐标、mipmap、NPOT 限制与纹理图集(webgl fundamentals 实战) 本文是 webgl fundament
图形学WebGL 纹理完全指南:从纹理坐标、Mipmap 到纹理图集(webgl-fundamentals 实战解析)
WebGL 纹理完全指南:从纹理坐标、Mipmap 到纹理图集(webgl fundamentals 实战解析) 本文基于 webgl fundamentals
图形学webgl-fundamentals 实战:在 WebGL 中使用 2 个以上纹理(纹理单元 Texture Unit 详解)
webgl fundamentals 实战:在 WebGL 中使用 2 个以上纹理(纹理单元 Texture Unit 详解) 在 webgl fundamen
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考