OpenGL ES 2.0 小试 Porter-Duff 图像混合算法

介绍

在做 OpenGL ES 2.0 练习时,遇到将两个颜色值(形如 (r, g, b, a) 的 vector)相乘的应用。这是 Porter-Duff 算法之一。Porter-Duff 算法是一种对带有 alpha 通道的数字图像的混合(Blend)算法,最早由发表于 1984 年的一篇论文系统性地提出,算法名称即是取自两位作者的姓名。该论文可在 keithp.com/~keithp/por... 阅读。

常见于图像编辑软件(如 Adobe Photoshop)中的图层混合功能,就是 Porter-Duff 算法的实际应用。

Porter-Duff 算法包括共计 13 个二元操作(Binary Operation),以及 3 个一元操作(Unary Operation)。其中 13 个二元操作统一为一个计算公式:

其中,A, B 为参与运算的两个颜色值;cA, cB 分别是 A, B 的某一分量(r/g/b/a 之一);FA, FB 分别为 A, B 的因数。13 个二元操作之中的每一个,均有其特定的 FA, FB 因数。cO 为结果颜色值的分量。

以 Porter-Duff 算法为基础,又发展出一些其他混合算法,也都一并通称为 Porter-Duff。例如,Android API 所提供的 Porter-Duff 算法中,就包含数个原始算法之外的,详见 www.android-doc.com/reference/a... 。

本文将用 GLSL 实现 Porter-Duff 算法,并在 OpenGL ES 2.0 应用程序中演示其效果。演示程序使用的 2 张原始图片如下,都带有 alpha 通道,肉眼显而易见的图像之外的部分都是透明的:

程序完整源码在 gitlab.com/sihokk/lear... 。

Porter-Duff 算法实现在 fragment shader 内。对于前文所示的 Porter-Duff 计算公式,其 GLSL 实现为:

C++ 复制代码
float PorterDuff_Op(float a, float fa, float b, float fb)
{
    return a * fa + b * fb;
}

其中,a, b 为参与运算的两个颜色的分量,fa, fb 则分别为其计算因数。

GLSL 支持函数重载,因此定义一个同名函数,接收 vector 类型的参数,同时返回值亦为 vector:

C++ 复制代码
vec4 PorterDuff_Op(vec4 a, float fa, vec4 b, float fb)
{
    return a * fa + b * fb;
}

对于具体的二元运算的实现,则调用 PorterDuff_Op() 函数,提供其特定因数即可。例如 Clear 操作,Porter-Duff 的定义为:

可见其两个因数均为 0,因此 GLSL 实现为:

C++ 复制代码
vec4 PorterDuff_Clear(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 0., b, 0.);
}

Shader main() 函数定义如下。根据 texture 坐标(v_tex)从 2 个 texture(即 2 张图片)中各取一个颜色值(a 和 b),对它们进行 Porter-Duff 运算,所得结果作为输出,将最终呈现在屏幕上:

C++ 复制代码
varying vec2 v_tex;

uniform sampler2D s_tex;
uniform sampler2D s_tex1;

void main()
{
    vec4 a = texture2D(s_tex, v_tex);
    vec4 b = texture2D(s_tex1, v_tex);
    gl_FragColor = PorterDuff_Clear(a, b);
}

Clear 操作结果固定为 (0, 0, 0, 0),其效果显而易见,故不予演示。下面将分小节分别实现其他操作(包括 Android API 扩展的),并展示其实际效果。

A

A 和 B 实际为同一个操作,交换 A 操作的两个参数就等同于 B 操作。因此只需实现其中之一,我将实现 A。后文还有其他以成对对称形式出现的等效操作,也将仅实现其中之一,不再特别说明。

A 和 B 操作的定义如下:

即其计算因数分别为 1, 0 。根据该定义,A 操作的结果为第一个颜色参数。实现为:

C++ 复制代码
vec4 PorterDuff_A(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 1., b, 0.);
}

程序运行结果表明,只有第一幅图片被显示,符合 A 操作的定义。如下:

Over

Over 操作的定义是

"A over B"与"B Over A" 实为同一操作。按照以上定义,A Over B 操作的结果为 A 加上 B 未被遮挡的部分,即 AB 的简单叠加。GLSL 实现为:

C++ 复制代码
vec4 PorterDuff_A_Over_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 1., b, 1. - a.a);
}

可看到如下运行效果:

In

In 操作定义为

"A In B" 与 "B In A" 实际为同一操作。A In B 操作的结果是 A 在与 B 重叠的部分,GLSL 实现为:

C++ 复制代码
vec4 PorterDuff_A_In_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, b.a, b, 0.);
}

程序运行效果:

Out

Out 操作定义为

"A Out B"与"B Out A" 实为同一操作。A Out B 操作的结果为:A 未被 B 遮挡的部分。GLSL 实现为:

C++ 复制代码
vec4 PorterDuff_A_Out_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 1. - b.a, b, 0.);
}

程序运行效果为:

Atop

Atop 操作定义为:

"A Atop B"与"B Atop A" 实为同一操作。A Atop B 结果为:A 与 B 重叠的部分 + B 未被遮挡的部分。 GLSL 实现为

C++ 复制代码
vec4 PorterDuff_A_Atop_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, b.a, b, 1. - a.a);
}

程序运行效果如下。对照上面 Atop 操作的定义理解 12 秒钟...:

Xor

Xor 操作定义如下。操作结果为 A 与 B 各自未重叠的部分:

Xor 操作的实现为:

C++ 复制代码
vec4 PorterDuff_A_Xor_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 1. - b.a, b, 1. - a.a);
}

嗯这个不需要 12s 来理解;-) 看到两张图片重叠的部分被清除,其余部分则都被保留:

Plus

Plus 操作定义为:

GLSL 实现为:

C++ 复制代码
vec4 PorterDuff_A_Plus_B(vec4 a, vec4 b)
{
    return PorterDuff_Op(a, 1., b, 1.);
}

程序运行效果如下图所示,可看到两个图片重叠部分的图像都被(最大程度地)保留下来:

一元操作

Porter-Duff 定义了 3 个一元操作如下:

Darken 操作保持 alpha 值不变,而 r/g/b 分量则按相同比例降低强度,实际效果是颜色变暗。Dissolve 操作则将 4 个通道值同时降低,实际效果是颜色变浅/褪色。Opaque 操作则仅仅降低 alpha 分量的值,实际效果是渐隐。需要说明的是,前述是针对这三个操作的标量参数为 0, 1 区间的一般情况而言。假如,例如对于 Darken 操作,该参数值 > 1,则为增亮效果。

三个一元操作的实现如下:

C++ 复制代码
vec4 PorterDuff_Darken(vec4 a, float f)
{
    return vec4(vec3(a) * f, a.a);
}
 
vec4 PorterDuff_Dissolve(vec4 a, float f)
{
    return a * f;
}

vec4 PorterDuff_Opaque(vec4 a, float f)
{
    return vec4(vec3(a), a.a * f);
}

现在我们结合 Plus 和 Dissolve 操作,实现在 Porter-Duff 论文中所提及的两张图片的过渡:

C++ 复制代码
vec4 a = ...
vec4 b = ...

float alpha = .1;

gl_FragColor = PorterDuff_A_Plus_B(
    PorterDuff_Dissolve(a, alpha), //
    PorterDuff_Dissolve(b, 1. - alpha) //
);

运行效果如下。可以看到,第一张图片已经消褪到几不可见的程度:

Lighten (Android)

Android API 对 Lighten 操作的定义是:

GLSL 实现:

C++ 复制代码
vec4 PorterDuff_Lighten(vec4 a, vec4 b)
{
    return vec4(a.r * (1. - b.a) + b.r * (1. - a.a) + max(a.r, b.r), // r
        a.g * (1. - b.a) + b.g * (1. - a.a) + max(a.g, b.g), // g
        a.b * (1. - b.a) + b.b * (1. - a.a) + max(a.b, b.b), // b
        a.a + b.a - a.a * b.a // a
    );
}

实际运行效果是下面这样。这个...从算法本身是看不出效果或者说目的了,感觉开始步入玄学的领域了:

Multiply (Android)

Android API 定义的 Multiply 操作:

实现:

C++ 复制代码
vec4 PorterDuff_Multiply(vec4 a, vec4 b)
{
    return a * b;
}

运行:

Screen (Android)

定义:

...

C++ 复制代码
vec4 PorterDuff_Screen(vec4 a, vec4 b)
{
    return a + b - a * b;
}

..

深深的无力感...

唉..

虚无的颜色

究竟是黑可怕,还是没有颜色更可怕?

在 Porter-Duff paper 中特别将 Black (0, 0, 0, 1) 与 Clear (0, 0, 0, 0) 作出了区分。在 OpenGL 程序中执行:

C 复制代码
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);

将得到

,而不是

是 OpenGL 错了,还是我显示器的液晶面板没有如实显示呢??幸好 Clear 只是停留在理论中,现实中要么是一片叶子、又或者一座高山,挡住了我们的视线,让我们没法穿透深空凝视虚无。我想那肯定是一番人类难以言状和承受的恐怖情景。

相关推荐
扶尔魔ocy18 小时前
【qt openGL入门笔记】OSG实现显示飞机模型飞行的点迹(二)
qt·opengl·osg
扶尔魔ocy20 小时前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行(三)
opengl·osg
扶尔魔ocy2 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行
qt·opengl·osg
扶尔魔ocy6 天前
【qt openGL入门笔记】FPS相机
qt·opengl
扶尔魔ocy6 天前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl
扶尔魔ocy8 天前
【qt openGL入门笔记】基础光照
qt·opengl
扶尔魔ocy8 天前
【qt openGL入门笔记】坐标系统和摄像机
qt·opengl
茉莉玫瑰花茶11 天前
OpenGL [ 纹理 ]
开发语言·c++·opengl
mmsx23 天前
Android 地图十万要素不卡顿:空间网格 + 渐进式加载的移动端实践
android·大数据·opengl
mmsx23 天前
Android 地图几万要素卡成幻灯片?顶点缓冲批上传与显存复用
android·opengl·地图·栅格