iOS之深入解析预乘透明度Premultiplied Alpha
生活随笔
收集整理的这篇文章主要介绍了
iOS之深入解析预乘透明度Premultiplied Alpha
小编觉得挺不错的,现在分享给大家,帮大家做个参考.
一、前言
- Premultiplied Alpha 的概念,做过游戏开发的应该都知道,Xcode 的工程选项里有一项 Compress PNG Files,会对 PNG 进行 Premultiplied Alpha,Texture Packer 中也有Premultiplied Alpha 的选项。那么,Premultiplied Alpha 到底是什么呢?
- 在 Alpha Blending: To Pre or Not To Pre 一文中,详情地阐明了 Premultiplied Alpha 的相关解释,如果还需要深入理解的可以阅读《Real Time Rendering》这本书。
二、Alpha Blending
① Alpha Blending 的原理
- 在图形学中,Alpha 指的是除了颜色的三个分量(RGB)外的第四个分量:透明度。因此一个真彩色(指利用 RGB 分量合成颜色)的像素就变成由四个分量组成:R、G、B、A。我们这里讨论,设 R、G、B、A 均为从 0 到 1 的值,其中 Alpha = 0 为完全透明,Alpha = 1 为完全覆盖,中间的数值代表半透明,这样的设定是为了能使本文独立于显示硬件,我们把诸如(R,G,B,A)这样的东西称为四元组。一个这样的四元组代表一个由 RA、GA、B*A 组合而成的颜色。
- 有一点重要的是,要清楚分辨如下两个关键像素的意义:
- 那么,如何根据 Alpha 通道数据进行混合的算法呢?
-
- 简单地,只需要把需要组合的颜色计算出不含 Alpha 分量的原始 RGB 分量然后相加便可,比如现在有两幅图象,分别称为图象 A 和图象 B,由这两幅图象组合而成的图象称为 C,则有如下的四元组:
-
- 以及组合后的 RGB 三元组:
-
- 那么:
-
- 便可得出混合后的颜色。如果有多幅图像需要混合,则按照以上方法两幅两幅地进行混合。
② Alpha Blending 的混合公式
- 最常见的像素表示格式是 RGBA8888 即 (r, g, b, a),每个通道 8 位,0255。例如红色 60% 透明度就是(255, 0, 0, 153),为了表示方便,Alpha 通道一般记成正规化后的 0~1 的浮点数,也就是(255, 0, 0, 0.6)。而 Premultiplied Alpha 则是把 RGB 通道乘以透明度也就是(r * a, g * a, b * a, a),50% 透明红色就变成了(153, 0, 0, 0.6)。
- 透明通道在渲染的时候通过 Alpha Blending 产生作用,如果一个透明度为 as 的颜色 Cs 渲染到颜色 Cd 上,混合后的颜色通过以下公式计算:
- 以 60% 透明的红色渲染到白色背景为例:
- 也就是说,从视觉上(255, 0, 0, 0.6)渲染到白色背景上和(255, 102, 102)是同一个颜色。如果颜色以 Premultiplied Alpha 形式存储,也就是 Cs 已经乘以透明度了,所以混合公式变成:
三、为什么要 Premultiplied Alpha?
- Premultiplied Alpha 后的像素格式变得不直观,因为在画图的时候都是先从调色板中选出一个 RGB 颜色,再单独设置透明度,如果 RGB 乘以透明度就搞不清楚原色是什么。
- 从前面的 Alpha Blending 公式可以看出,Premultiplied Alpha 之后,混合的时候可以少一次乘法,这可以提高一些效率,但这并不是最主要的原因,最主要的原因是:没有 Premultiplied Alpha 的纹理无法进行 Texture Filtering(除非使用最近邻插值)。
- 以最常见的 filtering 方式线性插值为例,一个宽 2px 高 1px 的图片,左边的像素是红色,右边是绿色 10% 透明度,如果把这个图片缩放到 1x1 的大小,那么缩放后 1 像素的颜色就是左右两个像素线性插值的结果,也就是把两个像素各个通道加起来除以2,如果使用没有 Premultiplied Alpha 的颜色进行插值,那么结果就是:
- 如果绿色 Premultiplied Alpha,也就是(0, 255 * 0.1, 0, 0.1),和红色混合后:
- Premultiplied Alpha 最重要的意义是使得带透明度图片纹理可以正常的进行线性插值,这样旋转、缩放或者非整数的纹理坐标才能正常显示,否则就会像上面的例子一样,在透明像素边缘附近产生奇怪的颜色。
四、纹理处理
- 使用的 PNG 图片纹理,一般是不会 Premultiplied Alpha 的。游戏引擎在载入 PNG 纹理后会手动处理,然后再 glTexImage2D 传给 GPU,比如 Cocos2D-x 中的 CCImage::premultipliedAlpha:
- 而 GPU 专用的纹理格式,比如 PVR、ETC 一般在生成纹理都是默认 Premultiplied Alpha 的,这些格式一般是 GPU 硬解码,引擎用 CPU 处理会很慢。
- 总之 glTexImage2D 传给 GPU 的纹理数据最好都是 Multiplied Alpha 的,要么在生成纹理时由纹理工具 Pre-multiplied,要么载入纹理后由游戏引擎或 UI 框架 Post-multiplied。
五、iOS 中的 Premultiplied Alpha
- Core Graphics 的 CGImage.h 对图像透明度信息有如下定义:
- 预乘透明度(Premultiplied Alpha)图像简单地说,即每个颜色分量都乘以 alpha 通道值作为结果值:
- 为什么关注预乘透明度图像?微信团队因 AR 抢红包场景的 OpenGL 混色结果出错引起注意:
六、理解 Premultiplied Alpha 的 Tips
① 理解 Alpha 混合
- 最常见的混合是“over”混合,假设已经有一张 RenderTexture,RT 上像素的 RGB 称其为 RGBdst,Alpha 为 Adst 。现在有一个像素(RGBsrc,Asrc)要和 RT 上的像素混合,那正确的混合会这样进行:
- 最终混合出来的颜色由两部组成:
-
- Asrc * RGBsrc 代表 RGBsrc 对最终颜色的贡献,它受 Alpha 影响,如果 Alpha 为 0 则对最终像素没有影响,如果 Alpha 为 1 则贡献 100% 的 RGBsrc;
-
- Adst * RGBdst 是 RT 中像素原本没有其它像素覆盖时的贡献值,但是现在被一个新来的像素遮挡了,被遮挡了 (1 - Asrc),因此 RT 中像素最终贡献 (Adst * RGBdst)*(1 - Asrc)。
- 由此可见,无论对于 src 还是 dst, A * RGB 才是实际的有效颜色,称其为 premultiplied alpha。令:
- 则颜色混合可以改写为:
- 这么看就更加清楚:
② SrcAlpha,OneMinusSrcAlpha 颜色混合正确有前提
- 在 Unity 中的透明混合,默认采用 SrcAlpha,OneMinusSrcAlpha 方式,用符号表示出来:
- 对比之前给出的计算:
- 加号右侧少乘 Adst ,这是为什么呢?因为 SrcAlpha, OneMinusSrcAlpha 正确的前提是混合目标是不透明的,即 Adst 为 1。
- 平时渲染时,常见的情况是先渲染不透明物体,再渲染不透明的天空盒,最后再渲染半透明物体做 Alpha 混合,在这种情况下渲染目标是不透明的,不透明物体的有效颜色即其颜色本身。
- 在满足这个前提下:
- 才会成立,其本质为:
- 依然是符合上文中给出的结论:
- 只不过此时的 RGB’dst 等于 RGBdst。可能有人会产生疑问,不透明背景上混合半透明后,怎么看待混合后的透明度?我们看看上文中的 Alpha 的计算:
- 发现没有,其本质是以 Asrc 作为参数的 Adst 到 1.0 线性插值。当 Adst 为 1.0 时,无论 Asrc 是何值,最终输出都是 1.0。回到现实中,这很好理解,砖墙前放一块玻璃,当我们将玻璃和墙看作一个整体时,它们是不透明的。
③ SrcAlpha, OneMinusSrcAlpha 混合出来的 Alpha 值是无意义的
- 这种常见混合方式根据上文 ② 中的,其已默认渲染目标的 Alpha 为 1,因此它不关心 Alpha 结果的正确性。根据其表达式:
- 可以清晰的看到,这里没有出现 Adst ,得出正确的 RGB 与 RT 中的 Alpha 存什么没有任何关联。
- 通过 SrcAlpha, OneMinusSrcAlpha 方式计算得到:
- 这个结果没有意义。有些情况下,可以利用这种性质,将 RT 中没有被用到的 Alpha 通道利用起来,例如存储 bloom 系数。
④ 理解预乘 Alpha 混合公式的颜色部分
- Premultiplied alpha 混合采用 One, OneMinusSrcAlpha,其实我们在 ① 中就已经看到:
- 即:
- One 就是这里的 1.0 而 OneMinusSrcAlpha 就是 (1 - Asrc) 。RGB’dst 来自于混合的结果,真正的问题是 RGB’rsc 如何获得,最简单方式就是纹理中的 RGB 预乘好 Alpha,那么采样得到的颜色直接就是有效 RGB。
⑤ 纹理预乘 Alpha 实践上可能有潜在问题
- 在实践中,纹理的数据源大多是 RGBA32,即单通道 8 比特,只能表示 0-255 的整数,同时游戏资产还会根据目标平台做纹理压缩。
- 由于精度问题,原本相近的颜色在预乘后会存储为更相近,甚至相同的颜色,经压缩后很容易产生大量 artifacts。要使用预乘 Alpha 的纹理,一般会建议采用单通道 16 位的存储。
- 由于这种情况,即使预乘有很好的纹理过滤特性,也没有被广泛采用,我所了解 WebGL 由于网页对于 Alpha composition 的天然需求,做了这方面的支持。
⑥ 即使不纹理预乘,采用预乘 Alpha 的混合公式也有好处
- 采用 One, OneMinusSrcAlpha 混合有个很好的特性,可以统一 Blend 和 Additive,减少 BlendState 切换,还能增加效果,推荐阅读:A Mind Forever Programming。
- 简单理一下思路:
-
- 把非预乘纹理的采样到的 RGBA,在 shader 中输出 (RGB*A, A) 就是 Blend 模式;
-
- 把非预乘纹理的采样到的 RGBA,在 shader 中输出 (RGB*A, 0) 就是 Additive 模式。
- 输出的 Alpha 可以定义一个 uniform t 控制,输出 (RGBA, At ),这样通过 t 就是控制 Blend 和 Additive 模式之间的过渡。
- 如果再定义一个 uniform s,输出 (RGBAs, Ats),还可以通过 s 控制其整体透明度,用于淡入淡出,简直就是特效的救星。
- 众所周知,采用 Additive 模式的特效,在亮的场景中几乎看不到效果,而 Blend 模式的特效在暗的场景中提不亮。采用 One OneMinusSrcAlpha 就可以使用中间态来做出适配比较好的特效,而且不需要 framebuffer fetch。
⑦ Premultiplied Alpha 运算是封闭的
- 换言之,预乘 alpha 混合得到的颜色也是预乘 alpha 的。细心的你可能会注意到,在 ① 中:
- 作为运算结果的 RGB’result 是有 prime 符号的,正是想提示这一点。最终输出的有效颜色来自两部分:
-
- 叠加上去的 src 像素贡献的有效颜色;
-
- 背景 dst 像素贡献的有效颜色,它被 src 遮挡掉一部分,遮挡的量是 (1 - Asrc)。
- 观察 ① 中给出的两式:
- (1)(2) 的计算过程是一样的,这就不禁会产生疑问:(1)式混合两个未预乘 alpha 的RGB,结果是预乘 Alpha 的RGB?这没错,未预乘 Alpha 的颜色经混合得到的是预乘 Alpha 的颜色。
- 那平时用 SrcAlpha, OneMinusSrcAlpha 为什么能得到未预乘的结果呢?正是 ② 中的原因,由于 SrcAlpha, OneMinusSrcAlpha 混合隐含了一个假设,渲染目标是不透明的,在这个前提下,用正确的混合公式计算,可以得到:
-
- 预乘 Alpha 的 RGB’result;
-
- Aresult = 1.0。
- 在 ② 中已经讲过,与不透明目标混合得到的 Alpha 恒为 1。显而易见,当 Alpha 为 1 时, RGB’result 等于 RGBresult 。因此(1)式在当渲染目标是不透明时,改成下式是成立的:
⑧ 理解预乘 Alpha 混合公式的 Alpha 部分
- 预乘 Alpha 混合时,颜色分量和 Alpha 分量的运算是一致的,对比一下:
- 都是:
- 因此,不需要额外指定 Alpha 分量的混合公式,就能得到有意义的 Alpha 值,而且无论渲染目标是透明还是不透明,结果都是正确的。
⑨ 仅当必要时 Unmultiply
- 凡是讲 premultiplied alpha 都会告诉你,可以通过以下方式,还原未预乘的颜色值:
- 常见的、未预乘的颜色值也叫 straight alpha 或 unassociated alpha,而预乘好的叫 premultiplied alpha 或 associated alpha。这种还原操作在渲染自己可控的环境下几乎用不到。
- 根据上文中的 Premultiplied Alpha 运算是封闭的,预乘 Alpha 混合时运算封闭,可以多次混合不需要还原 straight alpha。但如果用未预乘 Alpha 混合时,如果渲染目标是半透明的,每次混合完成都要 unmultiply 回 straight alpha 才能继续混合,而且当一个网格有多层透明叠加时结果是错误的。
- 从实践上讲,预乘 Alpha 混合的结果需要 unmultiply 主要就这种情况:三方组件只接受 straight alpha 表示的纹理。Framebuffer 显示到屏幕上输出时,RT 最终总是不透明的,不透明的 Alpha 为 1,预乘和未预乘没有区别,也不用特殊处理。
⑩ Bleed Alpha 与预乘 Alpha 原理不同,目的相同,结果略有不同
- 预乘 alpha 和 bleed alpha 目的都是减少半透明纹理过滤产生的瑕疵,但它们有一些比较显著的区别:
-
- Bleed alpha 不需要修改混合公式;
-
- Bleed alpha 只能优化完全透明和非完全透明像素边缘的过滤瑕疵;
-
- 预乘 alpha 不仅可以达到 bleed alpha 的结果,半透像素之间的过滤效果也能得到优化;
-
- 预乘 alpha 需要修改混合公式,可能产生 tip6 中提到的情况。
- 当不使用 premultiplied alpha 时,预处理贴图 bleed alpha 是一个“免费”替代品。虽然效果上会有折扣,但性价比极高。
⑪ 纹理预乘 Alpha 可以减少纹理过滤带来的 artifacts
- 纹理预乘 alpha 可以减少 downsampling、upsampling、非 pixel perfect 各种情况下半透纹理过滤产生的 artifacts,推荐阅读:
-
- Alpha Blending: To Pre or Not To Pre;
-
- Beware of Transparent Pixels。
总结
以上是生活随笔为你收集整理的iOS之深入解析预乘透明度Premultiplied Alpha的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Python之深入解析如何使用Pytho
- 下一篇: RxSwift之深入解析如何创建观察者O