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

原文:https://blog.csdn.net/weixin_42407126/article/details/124253497

混合模式介绍

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);//为RGB和alpha通道分别设置不同的混合选项
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 算法公式图:
请添加图片描述

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

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

1.正片叠底
2.叠加
3.线性减淡
4.线性加深
5.滤色
6.柔光
7.高光
8.线性光
9.差值

正片叠底:

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

vec4 result = blendColor * baseColor

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

//混合模式函数 总共9种
vec4 blendColor(int blendMode, vec4 baseColor, vec4 blendColor) {
    if (blendMode <= 0 || blendMode > 9) { //判空 返回贴纸素材
        return blendColor;
    } else if (blendMode == 1) { // 1.正片叠底 Multiply
            return  blendColor * baseColor + blendColor * (1.0 - baseColor.a) + baseColor * (1.0 - blendColor.a);
    } else if (blendMode == 2) { // 2.叠加 Overlay
            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) { // 3. 线性减淡 Linear Dodage
            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) { // 4.线性加深 Linear Burn
            return vec4(clamp(baseColor.rgb + blendColor.rgb  - vec3(1.0) * blendColor.a, vec3(0.0), vec3(1.0)), baseColor);
    } else if (blendMode == 5) { // 5.滤色 Screen
            vec4 whiteColor = vec4(1.0);
            return whiteColor - ((whiteColor - blendColor) * (whiteColor - baseColor));
    } else if (blendMode == 6) { // 6.柔光 SoftLight
            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) { // 7.亮光 Vivid Light
            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) { // 8.线性光 Linear Light
            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) { // 9.差值 Different
            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:
在这里插入图片描述
醒图:
在这里插入图片描述
我的效果:在这里插入图片描述
可以发现,我们开发的效果和 PS 上几乎是保持一致的,而醒图的效果由于 alpha 度没有处理好,导致曝光非常严重,严重影响效果

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值