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 为参与运算的两个颜色值;c~A~, c~B~ 分别是 A, B 的某一分量(r/g/b/a 之一);F~A~, F~B~ 分别为 A, B 的因数。13 个二元操作之中的每一个,均有其特定的 F~A~, F~B~ 因数。c~O~ 为结果颜色值的分量。

以 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 张图片)中各取一个颜色值(ab),对它们进行 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 只是停留在理论中,现实中要么是一片叶子、又或者一座高山,挡住了我们的视线,让我们没法穿透深空凝视虚无。我想那肯定是一番人类难以言状和承受的恐怖情景。

相关推荐
凌云行者3 天前
OpenGL入门004——使用EBO绘制矩形
c++·cmake·opengl
闲暇部落3 天前
Android OpenGL ES详解——模板Stencil
android·kotlin·opengl·模板测试·stencil·模板缓冲·物体轮廓
凌云行者5 天前
OpenGL入门003——使用Factory设计模式简化渲染流程
c++·cmake·opengl
凌云行者6 天前
OpenGL入门002——顶点着色器和片段着色器
c++·cmake·opengl
闲暇部落7 天前
Android OpenGL ES详解——裁剪Scissor
android·kotlin·opengl·窗口·裁剪·scissor·视口
彭祥.12 天前
点云标注工具开发记录(四)之点云根据类别展示与加速渲染
pyqt·opengl
闲暇部落15 天前
android openGL ES详解——缓冲区VBO/VAO/EBO/FBO
kotlin·opengl·缓冲区·fbo·vbo·vao·ebo
闲暇部落16 天前
android openGL ES详解——混合
android·opengl
离离茶20 天前
【opengles】笔记1:屏幕坐标与归一化坐标(NDC)的转换
opengl·opengles
蒋灵瑜的笔记本20 天前
【OpenGL】创建窗口/绘制图形
开发语言·c++·图形渲染·opengl