前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >终端图像处理系列 - OpenGL混合模式的使用

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

作者头像
天天P图攻城狮
发布2018-05-22 12:27:36
4.6K4
发布2018-05-22 12:27:36
举报
文章被收录于专栏:天天P图攻城狮天天P图攻城狮

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 天天P图攻城狮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • OpenGL中的混合模式
  • OpenGL混合模式在Android平台上的使用
    • Bitmap解码时不做预乘。
      • OpenGL混合时不再乘以alpha值
      • OpenGL混合模式对三维渲染的影响
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档