基于OpenGL实现PS部分混合模式

混合模式介绍

1.什么是混合模式?

为了让不同色彩的图片叠加后能够实现更多种色彩组合,从而渲染出各式各样的画面,PS 提供了各式各样规则的混合模式(这里就不具体一一介绍了,提供一个传送门,有兴趣的可自行了解:https://zhuanlan.zhihu.com/p/94081709)

2.如何实现混合模式?

我们知道,我们在使用 OpenGL 进行图片效果开发时,将两张图片叠加,如果上层的图片是半透明的,如果我们想在不改变原图色彩的情况下透过上层图片看到底图,有两种实现方法,第一是使用opengl中为我们提供的混合模式的接口glBlendFunc(),第二是我们再片段着色器里使用 mix()函数,而这两种方法改变的都是图片的 alpha值(图片透明度), 可 ps 里的混合模式大多数是基于将两张图片的rgb 和 alpha做各种运算得出的,因此要在 OpenGL 中实现 PS的混合模式,更关键的是依赖图形相关算法,本文参照 Photoshop blend算法 ,介绍如何通过shader,在OpenGL中实现混合效果。

第一步,如何在OpenGL 中开启混合模式?

glEnable( GL_BLEND );
glDisable( GL_BLEND );

第二步,如何设置混合模式?

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
glBlendEquation(GLenum mode)

glBlendFunc(GLenum sfactor, GLenum dfactor)

函数接受两个参数,来设置源和目标因子。OpenGL为我们定义了很多个选项,我们将在下面列出大部分最常用的选项。注意常数颜色向量C¯constant可以通过glBlendColor函数来另外设置。

GL_ZERO 因子等于0
GL_ONE  因子等于1
GL_SRC_COLOR    因子等于源颜色向量C¯source
GL_ONE_MINUS_SRC_COLOR  因子等于1−C¯source
GL_DST_COLOR    因子等于目标颜色向量C¯destination
GL_ONE_MINUS_DST_COLOR  因子等于1−C¯destination
GL_SRC_ALPHA    因子等于C¯source的alpha分量
GL_ONE_MINUS_SRC_ALPHA  因子等于1− C¯source的alpha分量
GL_DST_ALPHA    因子等于C¯destination的alpha分量
GL_ONE_MINUS_DST_ALPHA  因子等于1− C¯destination的alpha分量
GL_CONSTANT_COLOR   因子等于常数颜色向量C¯constant
GL_ONE_MINUS_CONSTANT_COLOR 因子等于1−C¯constant
GL_CONSTANT_ALPHA   因子等于C¯constant的alpha分量
GL_ONE_MINUS_CONSTANT_ALPHA

glBlendEquation(GLenum mode):

GL_FUNC_ADD:默认选项,将两个分量相加:C¯result=Src+Dst。
GL_FUNC_SUBTRACT:将两个分量相减: C¯result=Src−Dst。
GL_FUNC_REVERSE_SUBTRACT:将两个分量相减,但顺序相反:C¯result=Dst−Src。

通常我们都可以省略调用glBlendEquation,因为GL_FUNC_ADD对大部分的操作来说都是我们希望的混合方程,但如果你真的想打破主流,其它的方程也可能符合你的要求。

第三步,如何用 OpenGL 实现 PS 中的混合模式?

这里先给出一张网上收集来的 PS 算法公式图:

基于OpenGL实现PS部分混合模式
理论上来说,我们只要在片段着色器里,根据如图所示的公式,将采样后的图片的 rgba 如法炮制进行变换是不是就可以如法炮制 ps 中各种花里胡哨的效果了呢?我一开始也是这么想的,但是坑就在这里!这种方式只能实现非透明图的效果混合,对于带透明度或者阴影的图片来说,会出现巨大的问题!(这里的坑被我踩爆了,研究了好久才知道没有想象的那么简单)

话不多说,先列出本次实现的共计九种混合模式算法,然后依次介绍实现:

1.正片叠底

2.叠加

3.线性减淡

4.线性加深

5.滤色

6.柔光

7.高光

8.线性光

9.差值

正片叠底:

理论上公式是 a * b 即可,本着control c+v的精神,我迅速实现了效果,代码如下

vec4 result = blendColor * baseColor

上机一看,虽然对比 PS 有一些差别,但是看起来问题不大,内心开始暗自窃喜,原来这么简单(手动狗头),效果如图(左图为开发效果,右边为 PS 上效果对比图):

基于OpenGL实现PS部分混合模式
然而,当换了一张透明底图的贴纸之后,发现周围的黑边开始越来越重
基于OpenGL实现PS部分混合模式
于是我换了一张透明度更高的贴纸进行测试,却发现效果黑边更重了
基于OpenGL实现PS部分混合模式
此时,我的想法是,先将所有混合模式都试了下,己或许只有正片叠底会出现这种情况,其他的或许不会,结果好家伙,每个都会出现该问题!

此刻,我终于发现事情不对劲,开始思考导致问题的原因,在使用了各种类型的贴纸图片后,我发现,带 alpha 度的图片的黑边问题会更严重,我开始意识到应该是 alpha 度混合出现了问题

想到这里,我先是熟练的打开了谷歌浏览器,熟练的输入了如何用 opengl 实现带透明度的 PS 混合算法,然而我得到的所有答案,都是 a * b!没办法,看来白piao 的方式是行不通了,只能自己动手了,

首先我们需要分析一下产生黑边的原因

1.图片出现黑边的程度与图片自身的透明度有关,由此以正片叠加为例,结合目前实现方式可以推断出产生黑边的原因,是由于公式中使用的是 a*b,当图片周围的为透明时,该透明处的 rgba 均为 0,导致相乘后结果值为 0,而我在外部混合调用的是
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);由于拿到的是结果图的 rgb 是 0,导致渲染上屏幕的这一块就是黑色,因此针对这一块的 alpha,需要进行改进,那么 alpha 该如何调整呢?调整的公式又是什么呢?

于是我开始不断推导公式,不断试错,开始观察 PS 上的具体效果变化,然后对正片叠底公式进行改进,得出如下结果:

vec4 result = blendColor * baseColor + blendColor * (1.0 - baseColor.a) + baseColor * (1.0 - blendColor.a);

其他的模式不多做赘述,直接 po 出代码:


vec4 blendColor(int blendMode, vec4 baseColor, vec4 blendColor) {
    if (blendMode  0 || blendMode > 9) {
        return blendColor;
    } else if (blendMode == 1) {
            return  blendColor * baseColor + blendColor * (1.0 - baseColor.a) + baseColor * (1.0 - blendColor.a);
    } else if (blendMode == 2) {
            float ra;
            if (2.0 * baseColor.r < baseColor.a) {
                ra = 2.0 * blendColor.r * baseColor.r + blendColor.r * (1.0 - baseColor.a) + baseColor.r * (1.0 - blendColor.a);
            } else {
                ra = blendColor.a * baseColor.a - 2.0 * (baseColor.a - baseColor.r) * (blendColor.a - blendColor.r) + blendColor.r * (1.0 - baseColor.a) + baseColor.r * (1.0 -                 blendColor.a);
            }
           float ga;
            if (2.0 * baseColor.g < baseColor.a) {
                ga = 2.0 * blendColor.g * baseColor.g + blendColor.g * (1.0 - baseColor.a) + baseColor.g * (1.0 - blendColor.a);
            } else {
                ga = blendColor.a * baseColor.a - 2.0 * (baseColor.a - baseColor.g) * (blendColor.a - blendColor.g) + blendColor.g * (1.0 - baseColor.a) + baseColor.g * (1.0 - blendColor.a);
            }
           float ba;
            if (2.0 * baseColor.b < baseColor.a) {
                ba = 2.0 * blendColor.b * baseColor.b + blendColor.b * (1.0 - baseColor.a) + baseColor.b * (1.0 - blendColor.a);
            } else {
                ba = blendColor.a * baseColor.a - 2.0 * (baseColor.a - baseColor.b) * (blendColor.a - blendColor.b) + blendColor.b * (1.0 - baseColor.a) + baseColor.b * (1.0 - blendColor.a);
            }
            return vec4(ra, ga, ba, 1.0);
    } else if (blendMode == 3) {
            return  vec4(clamp((baseColor.rgb + baseColor.rgb * (1.0 - blendColor.a)+ blendColor.rgb * (1.0 - baseColor.a) + blendColor.rgb) , vec3(0.0), vec3(1.0)) , blendColor.a);
    } else if (blendMode == 4) {
            return vec4(clamp(baseColor.rgb + blendColor.rgb  - vec3(1.0) * blendColor.a, vec3(0.0), vec3(1.0)), baseColor);
    } else if (blendMode == 5) {
            vec4 whiteColor = vec4(1.0);
            return whiteColor - ((whiteColor - blendColor) * (whiteColor - baseColor));
    } else if (blendMode == 6) {
            float alphaDivisor = baseColor.a + step(baseColor.a, 0.0);
            return baseColor * (blendColor.a * (baseColor / alphaDivisor) + (2.0 * blendColor * (1.0 - (baseColor / alphaDivisor)))) + blendColor * (1.0 - baseColor.a) + baseColor * (1.0 - blendColor.a);
    } else if (blendMode == 7) {
            return vec4(clamp(baseColor.rgb + baseColor.rgb * (1.0 - blendColor.a)+ blendColor.rgb * (1.0 - baseColor.a) + baseColor.rgb * (2.0*blendColor.rgb - vec3(1.0))/ (2.0*(vec3(1.0)-blendColor.rgb)),vec3(0.),vec3(1.)),blendColor.a);
    } else if (blendMode == 8) {
            return vec4(clamp(baseColor.rgb  + baseColor.rgb * (1.0 - blendColor.a)+ blendColor.rgb * (1.0 - baseColor.a) + 2.0 * blendColor.rgb - vec3(1.0),vec3(0.0), vec3(1.0)),blendColor.a);
    } else if (blendMode == 9) {
            return vec4(abs(clamp(blendColor.rgb + baseColor.rgb * (1.0 - blendColor.a)+ blendColor.rgb * (1.0 - baseColor.a) - baseColor.rgb,vec3(-1.),vec3(1.))),blendColor.a);
    }
}

每种效果改进的方式因具体实现公式而有部分变化,主要是要注意两点:

1.当向量进行相加减的时候,需要使用 clamp()函数进行限制
2.在 rgb 的值大于 0.5(即比灰度图亮)的时候需要进行公式变化
3.根据 公式原理判断结果图的明暗度,进行最后混合使用原图还是混合图的 alpha
4.每个公式其实都是由 blendColor * (1.0 – baseColor.a) + baseColor * (1.0 – blendColor.a)变换而来

改进之后对比竞品效果如图:

PS:

基于OpenGL实现PS部分混合模式
醒图:
基于OpenGL实现PS部分混合模式
基于OpenGL实现PS部分混合模式
可以发现,我们开发的效果和 PS 上几乎是保持一致的,而醒图的效果由于 alpha 度没有处理好,导致曝光非常严重,严重影响效果

本文到这里就结束了,如果大家有兴趣,我再写一篇关于具体推导公式的文章,创作和分享不易,觉得有所收益的可以关注一下,不定期会分享一些 opengl 开发的学习过程和经验

Original: https://blog.csdn.net/weixin_42407126/article/details/124253497
Author: 每天都多努力一点
Title: 基于OpenGL实现PS部分混合模式

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/641590/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球