Opengl混合算法探究
在我们的实际应用中使用OpenGL进行混合常见的问题有以下三种:
- 使用Opengl自带的混合算法
- 自定义混合算法
- 半透明混合
针对以上三种情况我们具体分析有何不同及如何解决问题。
一、Opengl自带混合算法
OpenGL渲染管线的最后阶段会将源色和底色进行混合,我们大部分情况下只需考虑实现此次drawcall的渲染实现即可,无心过分操心如何与底色进行混合。那么如何使用自带的混合呢?
首先我们需要开启混合模式,开启混合模式之后即可使用OpenGL自带的混合算法。
glEnable(GL_BLEND);
开启混合之后如何混合源颜色与底色呢?混合需要设置两个函数,一是设置混合因子即源颜色与底色各按何种比例进行混合,另一个就是混合函数,决定源颜色与底色使用哪种运算符得到结果。同时RGB颜色也能与Alpha值分别设置不同得混合因子与混合函数。
//设置源颜色混合因子与底色混合因子
glBlendFunc(GLenum sfactor,GLenum defector);
//示例
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//分别设置RGB颜色与Alpha值得混合因子
glBlendFuncSeparate(GLenum srcRGB,GLenum dstRGB,GLenum srcAlpha,GLdstAlpha)
//示例
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
//设置混合函数
glBlendEquation(GLenum mode);
//示例
glBlendEquation(GL_FUNC_ADD);
//分别设置RGB颜色与Alpha值得混合函数
glBlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);
//示例
glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
上面得代码示例表示:源颜色(RGB或RGBA)需要乘源颜色得alpha,底色(RGB或RGBA)需要乘以一减源颜色得alpha,而混合函数为GL_FUNC_ADD
,所以最终混合算法为:
//未分别设置
Color = vec4(Srgb * Sa + Drgb * (1 - Sa), Sa * Sa + Da * (1 - Sa));
//分别设置RGB与Alpha得混合
Color = vec4(Srgb * Sa + Drgb * (1 - Sa), Sa + Da * (1 - Sa));
详细得混合因子与混合函数参考OpenGL混合示例,不再赘述。唯一需要注意得颜色是否预乘alpha得问题,由于默认也是最常用得混合模式结果是预乘的,因此如果想要得到未预乘的结果,自带的混合算法并不适用。
总的来说,自带混合算法比较方便速度也比较快可以满足大部分的使用场景,但功能支持有限有些需求无法满足。
二、自定义混合算法
上面讲到如果要得到未预乘的结果Opengl自带混合算法无法为我们得到正确的结果,此时我们就需要关闭OpenGL自带的混合模式在shader中自己实现混合算法。PhotoShop与AE中都有混合模式这个功能,有很多的混合模式可选,但由于OpenGL自带的混合算法支持的功能有限,这些混合算法都无法直接通过OpenGL自带的混合算法实现。手动实现混合算法比较自由,我们可以自定义一些混合方法,实现一些OpenGL自带混合模式无法实现的复杂混合算法,缺点是在大部分GPU上同一个texture无法既作FBO输出,又作纹理采样输入,如果底图作为输入传入Fragment Shader,则当前FBO需要绑定另一个texture作为输出。如果混合区域覆盖全图,可以用FBO绑定一个空的texture作为输出,同时原始底图传入Fragment Shader作为输入;如果混合区域只占全图的一部分,那么就需要首先复制一份底图纹理并绑定到FBO作为输出,同时原始底图纹理传入Fragment Shader做混合,这两种不同的混合场景下,不管混合区域是全图还是部分区域,都需要申请一块额外的底图大小的纹理存储(空白或复制底图),另外部分区域混合时还需要一次额外的渲染(复制底图),混合所需要的空间和时间都有额外开销。虽然实现了我们的需求,但缺点也是显而易见的,时间与空间的开销都比较大。
一些常用的自定义混合算法可以从这里找到
三、半透明混合
在3D渲染中,深度缓冲去非常重要,深度值决定了当前片元是否需要丢弃,如果透明物体与半透明物体放在一起渲染就可能会出现半透明物体没有与背后的物体进行混合。例如
发生这一现象的原因是,深度测试和混合一起使用的话会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。结果就是不考虑透明度都会进行深度测试。即使透明的部分应该显示背后的物体,但深度测试仍然丢弃了它们。这也是混合变得有些麻烦的部分。要想保证窗户中能够显示它们背后的窗户,我们需要首先绘制背后的这部分窗户。这也就是说在绘制的时候,我们必须先手动将窗户按照最远到最近来排序,再按照顺序渲染。
但这仍然不能解决所有问题,因为物体在3d空间中可能会交叉导出我们无法确定顺序,甚至我们都无法确定物体是否为半透明的。这样看来这也不是一个好的解决办法。那么如果我们能逐像素对所有的渲染目标进行排序,这样我们就能分清远近关系。这就需要我之前一篇文章提到的OIT渲染。
半透明混合是非常棘手的,如果想要得到好的效果必然带来性能或空间的巨大损失,OIT渲染的未来仍需要提出更好的方案及硬件支持。