终端图像处理系列 - OpenGL混合模式的使用

OpenGL一次渲染过程包含了多个阶段,包括顶点着色器、图元组装、栅格化、片元着色器、测试和混合等,最后将结果输出的FrameBuffer上。渲染管线最后一个阶段就是混合:

混合是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色经过一定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,然后相加。

在OpenGL里做颜色混合一般有两种方式,一种是将要混合的纹理都传入Fragment Shader,在shader里实现算法完成混合,一种就是利用OpenGL渲染管线最后的blending阶段自动对源色和底色进行混合。

在Fragment Shader手动实现混合算法比较自由,我们可以自定义一些混合方法,实现一些OpenGL自带混合模式无法实现的复杂混合算法,缺点是在部分GPU上同一个texture无法既作FBO输出,又作纹理采样输入,如果底图作为输入传入Fragment Shader,则当前FBO需要绑定另一个texture作为输出,否则会出现黑色和黑块的兼容性问题。如果混合区域覆盖全图,可以用FBO绑定一个空的texture作为输出,同时原始底图传入Fragment Shader作为输入;如果混合区域只占全图的一部分,那么就需要首先复制一份底图纹理并绑定到FBO作为输出,同时原始底图纹理传入Fragment Shader做混合,这两种不同的混合场景下,不管混合区域是全图还是部分区域,都需要申请一块额外的底图大小的纹理存储(空白或复制底图),另外部分区域混合时还需要一次额外的渲染(复制底图),混合所需要的空间和时间都有额外开销。

作为对比,OpenGL渲染管线自带的混合模式包含的混合算法是有限的,不过基本可以满足大部分的使用场景。优点是渲染时不用将底图作为采样纹理输入,定义好混合模式后,在Fragment Shader里只需要对源图纹理进行采样,然后由OpenGL驱动自动完成混合算法。这种方法对全图和部分区域的混合同样适用,都不用额外申请纹理存储空间,渲染时不用切换FBO,只需渲染一次,渲染的效率比在Fragment Shader里手动实现混合算法要高。

本文主要介绍OpenGL渲染管线自带的混合模式的用法和实例,同时简要介绍一下天天P图里用到的一些混合算法及效果,以及3D渲染时使用混合模式需要注意的一些问题。

OpenGL中的混合模式

前面提到,OpenGL渲染管线的最后阶段会将源色和底色进行混合。这里的源色和底色分别指什么呢?我们可以把OpenGL的一次渲染过程形象地比作画家拿画笔在画布上作画,假如画家拿着黄色的画笔在红色的画布上作画,最后画出一幅绿色的图,这里画笔的黄色就是源色,画布上的红色就是底色,又叫目标色,绿色就是混合以后的结果。对应到OpenGL的一次渲染过程里,源色就是Fragment Shader处理结束后给gl_FragColor的赋值,底色就是当前FBO绑定的纹理的颜色值,混合后的结果会更新底色纹理的颜色值,就好比是红色的画布在用黄色的笔画完后变成了绿色,绿色变成了画布新的颜色。OpenGL里的混合就是将源色和底色以某种方式自动混合的技术,通常用来绘制半透明物体(不透明物体颜色直接覆盖,无需混合)。不同的混合模式算法其实就是定义了源色和底色不同的混合比例,最后达到不同程度的混合效果。需要注意的是,物体的绘制顺序可能会影响到OpenGL混合的最终处理效果。

OpenGL API提供了相关接口来开启/关闭混合模式以及设置源色和底色混合因子,以Android Java层系统接口为例,相关调用如下:

其中开启和关闭混合模式的调用很简单,在此不再赘述。下面着重介绍一下源色和目标色混合因子。OpenGL在做混合时,会把源颜色和目标颜色各乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加得到新的颜色。

下面用数学公式来表达一下这个运算方式。假设源颜色的四个分量(指红色,绿色,蓝色,alpha值)是(Rs, Gs, Bs, As),目标颜色的四个分量是(Rd, Gd, Bd, Ad),又设源因子为(Sr, Sg, Sb, Sa),目标因子为(Dr, Dg, Db, Da)。则混合产生的新颜色可以表示为:(Rs*Sr+Rd*Dr, Gs*Sg+Gd*Dg, Bs*Sb+Bd*Db, As*Sa+Ad*Da)。如果颜色的某一分量超过了1.0,则它会被自动截取为1.0,不需要考虑越界的问题。

新版本的OpenGL可以设置运算方式,包括加、减、取两者中较大的、取两者中较小的、逻辑运算等,本文中不做过多讨论,只介绍相加的方式。

源因子和目标因子可以通过glBlendFunc函数来进行设置。glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。这两个参数的所有可选值如下图所示:

混合比例

GL_DST_ALPHA

( Ad , Ad , Ad , Ad )

GL_DST_COLOR

( Rd , Gd , Bd , Ad )

GL_ONE

(1,1,1,1)

GL_ONE_MINUS_DST_ALPHA

(1,1,1,1) - (Ad,Ad,Ad,Ad)

GL_ONE_MINUS_DST_COLOR

(1,1,1,1) - (Rd,Gd,Bd,Ad)

GL_ONE_MINUS_SRC_ALPHA

(1,1,1,1) - (As,As,As,As)

GL_SRC_ALPHA

( As , As , As , As )

GL_SRC_ALPHA_SATURATE

(f,f,f,1) : f = min(As,1-Ad)

GL_ZERO

( 0 , 0 , 0 , 0 )

我们举个例子来说明混合颜色值是怎么算出来的。以最常用的glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA )为例:

若源色为 ( 1.0 , 0.9 , 0.7 , 0.8 ),源色使用 GL_SRC_ALPHA,所以源色配比值为 ( 0.8 * 1.0 , 0.8 * 0.9 , 0.8 * 0.8 , 0.8 * 0.7 ) ,即 ( 0.8 , 0.72 , 0.64 , 0.56 )

目标色为 ( 0.6 , 0.5 , 0.4 , 0.3 ),目标色使用GL_ONE_MINUS_SRC_ALPHA,即配比比例为 1 - 0.8 = 0.2,目标色配比值为( 0.2 * 0.6 , 0.2 * 0.5 , 0.2 * 0.4 , 0.2 * 0.3 ),即 ( 0.12 , 0.1 , 0.08 , 0.06 )

最后混合后的颜色值为 ( 0.8 , 0.72 , 0.64 , 0.56 ) + ( 0.12 , 0.1 , 0.08 , 0.06 ) = ( 0.92 , 0.82 , 0.72 , 0.62 )

使用这种混合参数的意义也很明显,源色的alpha值决定了结果颜色中源色和目标色的百分比。这里源色的alpha值为0.8,即结果颜色中源色占80%,目标色占20%。

OpenGL混合模式在Android平台上的使用

在Android上使用OpenGL ES时,纹理上传最常用的方式就是先把图片解码成Bitmap后调用GLUtils.texImage2D(int target, int level, Bitmap bitmap, int border)接口将Bitmap上传至GPU显存。

这里需要注意的是,对于有alpha通道的Bitmap,Android系统解码API会自动执行预乘操作,即Bitmap每个像素的RGB值在解码时会自动乘以当前像素的alpha值,也就意味着Bitmap中存储的RGB值与原始图片的RGB值是不同的。预乘机制为Android系统View System和Canvas绘制提供了更好的性能。

在图片为完全不透明的情况下(像素点alpha值为255),预乘机制其实对原始图像没有影响,但是在半透明、渐变等情况下,预乘机制会对OpenGL混合因子的选择产生影响。我们举个简单的例子,假设我们设置了OpenGL混合模式为glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA ),我们希望源色的占比为alpha,即RGB_new = RGB * alpha,但是因为Bitmap在解码时已经做了一次预乘,所以最后源色的比例实际为RGB_new = RGB * alpha * alpha,比如在白色的透明度为0.5的地方,原来的 RGB 为255,预乘机制的影响导致最终得到的结果是63.75,与期望值128.5相比会更偏向于黑色,下面是两种结果的对比图,第一张是正确的结果,第二张是预乘以后的结果。这也是在做天天P图动效SDK第一个版本时遇到的坑。

了解了Bitmap的解码预乘机制,解决这个问题的思路其实就有两个方向了:

  1. Bitmap解码时不做预乘。
  2. 考虑到Bitmap预乘的影响,OpenGL混合时不再乘以alpha值。

下面分别介绍一下这两种方式:

Bitmap解码时不做预乘。

在Android平台上,解码一个Bitmap时,BitmapFactory.Options的参数inPremultiplied控制是否预乘,这个值默认为true,如果设为false则在解码时不做预乘。需要注意的是,如果是Android View System或者Canvas会默认此值为true进行绘制,如果Bitmap该值为false进行绘制会报RuntimeException。所以在这种情况下inPremultiplied值为false的Bitmap只能用作OpenGL上传纹理。另外Bitmap的createBitmapcreateScaledBitmap方法接受输入Bitmap的接口,传入的Bitmap的inPremultiplied值也必须为true,因为这些接口调用也需要绘制源Bitmap。另外inPremultiplied值的设置需要API level 19及以上才支持。

OpenGL混合时不再乘以alpha值

在没有做预乘时,我们设置的OpenGL混合模式因子为glBlendFunc( GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA ),即源色RGB值会乘以alpha值,但是因为Bitmap在解码时已经做了预乘操作,所以源色混合因子不需要再乘以alpha值,此时我们可以设置OpenGL混合模式为glBlendFunc( ONE , GL_ONE_MINUS_SRC_ALPHA )。这种方式也是目前天天P图Android端动效SDK渲染贴纸采用的方式。

OpenGL混合模式对三维渲染的影响

三维物体和二维图片渲染不同的一点就是物体的遮挡关系,OpenGL渲染多个三维物体时一般情况下都需要判断它们之间的前后关系,此时需要用到深度缓冲。

深度缓冲记录了每一个像素距离观察者有多近。在启用深度测试的情况下,如果将要绘制的像素比原来的像素更近,则像素将被绘制。否则,像素就会被忽略掉,不进行绘制。这在绘制不透明的物体时非常有用——不管是先绘制近的物体再绘制远的物体,还是先绘制远的物体再绘制近的物体,或者干脆以混乱的顺序进行绘制,最后的显示结果总是近的物体遮住远的物体。

然而在实现半透明效果时,我们会发现一些问题。如果我们先绘制了一个近距离的半透明物体,则它在深度缓冲区内保留了一些半透明物体的深度信息,此时再绘制远处的不透明物体,因为不透明物体比当前深度缓冲区内的深度值远,则会导致远处的物体将无法再被绘制出来。虽然半透明的物体仍然半透明,但透过它却看不到远处的不透明物体了。

深度缓冲区可以设置为只读或可写,要解决以上问题,我们可以在绘制半透明物体时将深度缓冲区设置为只读,这样虽然半透明物体被绘制上去了,但深度缓冲区还保持在原来的状态。如果再有一个物体需要渲染在半透明物体之后,在不透明物体之前,则它也可以被绘制(因为此时深度缓冲区中记录的是那个不透明物体的深度)。以后再要绘制不透明物体时,只需要再 将深度缓冲区设置为可读可写的形式即可。如果需要绘制一个一部分半透明一部分不透明的物体怎么办?只需要把物体分为两个部分,一部分全是半透明的,一部分全是不透明的,分别绘制就可以了。

需要注意的是,即使使用了以上技巧,我们仍然不能随心所欲的按照混乱顺序来进行绘制。必须是先绘制不透明的物体,然后再绘制透明的物体。举个例子,假设背景为蓝色,近处有一块红色玻璃,中间有一个绿色物体。我们首先绘制了蓝色背景,然后绘制红色半透明玻璃,它会先和蓝色背景进行混合,最后再绘制中间的绿色物体时,因为绿色物体在蓝色背景前面,此时绿色物体会被绘制,但是因为它是不透明的,所以绿色物体会直接覆盖掉红色玻璃和蓝色背景混合的效果,我们想要的绿色物体单独与红色玻璃混合的效果已经不能实现了。

所以总结起来,我们在绘制三维物体时,绘制顺序需要首先绘制所有不透明的物体。如果两个物体都是不透明的,则谁先谁后都没有关系。然后,将深度缓冲区设置为只读。接下来,绘制所有半透明的物体。如果两个物体都是半透明的,则谁先谁后可以根据自己的意愿。不过需要注意的是,先绘制的将成为“目标颜色”,后绘制的将成为“源颜色”,所以绘制的顺序将会对最后的渲染结果造成一些影响。所有物体全都绘制完成后,再将深度缓冲区设置为可读可写形式。OpenGL提供了一些接口来设置深度缓冲区的是否可读写:

目前天天P图Android端动效SDK渲染3D素材使用了开源的GamePlay引擎,目前线上的一些眼镜类素材都有半透明的镜片效果,透过半透明的镜片需要能够看到后面的镜架等其他3D物体,所以我们目前的3D素材的混合效果就是采用了上面介绍的三维渲染的技术方案。

总结

OpenGL混合模式避免了直接在Fragment Shader中做混合时纹理空间和渲染时间的额外开销,所以我们在开发中对于简单的混合算法可以尽量使用OpenGL混合模式。

OpenGL混合模式的源因子和目标因子可以设置多种模式。在Android平台上因为Bitmap解码时预乘的影响有时需要调整源因子的混合模式。

在进行三维物体绘制和混合时,绘制的顺序十分重要,不仅要考虑源因子和目标因子,还应该考虑深度缓冲区。必须先绘制所有不透明的物体,再绘制半透明的物体。在绘制半透明物体时前,还需要将深度缓冲区设置为只读形式,否则可能出现绘制结果错误。


作者简介:kevinxing(邢雪源),天天P图AND工程师

文章后记: 天天P图是由腾讯公司开发的业内领先的图像处理,相机美拍的APP。欢迎扫码或搜索关注我们的微信公众号:“天天P图攻城狮”,那上面将陆续公开分享我们的技术实践,期待一起交流学习! 加入我们: 天天P图技术团队长期招聘 (1)图像处理算法工程师,(2)Android/iOS开发工程师,期待对我们感兴趣或者有推荐的技术牛人加入我们(base在上海)!联系方式:kesenhu@tencent.com

原文发布于微信公众号 - 天天P图攻城狮(ttpic_dev)

原文发表时间:2018-05-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏算法+

自动曝光修复算法 附完整C代码

AF自动对焦(Automatic Focus) 自动对焦即调节摄像头焦距自动得到清晰的图像的过程

5264
来自专栏WOLFRAM

如何利用图形基元?

1233
来自专栏极客猴

Python 绘图,我只用 Matplotlib(二)

上篇文章,我们了解到 Matplotlib 是一个风格类似 Matlab 的基于 Python 的绘图库。它提供了一整套和matlab相似的命令API,十分适合...

1321
来自专栏前端那些事

过渡与动画 - 逐帧动画&steps调速函数

上一篇中我们熟悉五种内置的缓动曲线和(三次)贝塞尔曲线,并且基于此完成了缓动效果.

900
来自专栏前端那些事

过渡与动画 - 逐帧动画&steps调速函数

写在前面 上一篇中我们熟悉五种内置的缓动曲线和(三次)贝塞尔曲线,并且基于此完成了缓动效果. 但是如果我们想要实现逐帧动画,基于贝塞尔曲线的调速函数就显得有些无...

2537
来自专栏HTML5学堂

一步步教你弹性框架-上篇

HTML5学堂:本系列主要在于跟大家分享弹性运动框架的制作方式。弹性运动框架的运动方式类似于弹簧,有一种回弹的效果,在网站中的一些特效当中还是有一些应用的。实现...

3648
来自专栏Spring相关

Css权重解析

关于CSS权重,我们需要一套计算公式来去计算,这个就是 CSS Specificity,我们称为CSS 特性或称非凡性,它是一个衡量CSS值优先级的一个标准 具...

1362
来自专栏工科狗和生物喵

【机器视觉与图像处理】基于MATLAB+Hough的圆检测

本次文章,没有太多好写的,就是最近做的一个机器视觉的课程设计作业,是要做一个流水线的生产线建模以及对于产品的检测识别,我个人承包了圆心半径检测的内容,熬了好几天...

2921
来自专栏邹成卓的专栏

Unity3D WebCamTexture 取帧渲染、像素读取的终端适配

由于Win/Mac/Adnroid/iOS等各系统平台和硬件环境下,WebCamTexture 用于渲染和图像计算时表现不完全一致,很容易造成图像渲染或者计算不...

5200
来自专栏较真的前端

深入研究CSS字体度量及CSS 盒子

1653

扫码关注云+社区

领取腾讯云代金券