【问题标题】:Blurring (two-pass shader) an object with a transparent background?模糊(两遍着色器)具有透明背景的对象?
【发布时间】:2017-09-15 19:20:03
【问题描述】:

我想要模糊一个对象。

  1. 将其渲染为透明 (glClear with 1, 1, 1, 0) FBO。
  2. 使用垂直模糊着色器将其渲染到第二个透明 FBO。
  3. 使用水平模糊着色器将其渲染到屏幕上。

这是一个看起来没有模糊,然后用这种技术模糊的例子:

显然问题在于模糊对象周围的白光。

我想我掌握了为什么会发生这种情况的基本概念。虽然 FBO 中对象周围的像素是透明的,但它们仍然保持颜色 (1,1,1),因此,该颜色混合到模糊中。

我只是不知道该怎么做才能解决这个问题?

这是我的水平模糊着色器,垂直方向大致相同:

hBlur.vert

uniform mat4 u_projTrans;
uniform float u_blurPixels;
uniform float u_texelWidth;

attribute vec4 a_position;
attribute vec2 a_texCoord0;
attribute vec4 a_color;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
    v_texCoord = a_texCoord0;
    gl_Position = u_projTrans * a_position;

    float blurDistance6 = u_blurPixels * u_texelWidth;
    float blurDistance5 = blurDistance6 * 0.84;
    float blurDistance4 = blurDistance6 * 0.70;
    float blurDistance3 = blurDistance6 * 0.56;
    float blurDistance2 = blurDistance6 * 0.42;
    float blurDistance1 = blurDistance6 * 0.28;
    float blurDistance0 = blurDistance6 * 0.14;

    v_blurTexCoords[ 0] = v_texCoord + vec2(-blurDistance6, 0.0);
    v_blurTexCoords[ 1] = v_texCoord + vec2(-blurDistance5, 0.0);
    v_blurTexCoords[ 2] = v_texCoord + vec2(-blurDistance4, 0.0);
    v_blurTexCoords[ 3] = v_texCoord + vec2(-blurDistance3, 0.0);
    v_blurTexCoords[ 4] = v_texCoord + vec2(-blurDistance2, 0.0);
    v_blurTexCoords[ 5] = v_texCoord + vec2(-blurDistance1, 0.0);
    v_blurTexCoords[ 6] = v_texCoord + vec2(-blurDistance0, 0.0);
    v_blurTexCoords[ 7] = v_texCoord + vec2( blurDistance0, 0.0);
    v_blurTexCoords[ 8] = v_texCoord + vec2( blurDistance1, 0.0);
    v_blurTexCoords[ 9] = v_texCoord + vec2( blurDistance2, 0.0);
    v_blurTexCoords[10] = v_texCoord + vec2( blurDistance3, 0.0);
    v_blurTexCoords[11] = v_texCoord + vec2( blurDistance4, 0.0);
    v_blurTexCoords[12] = v_texCoord + vec2( blurDistance5, 0.0);
    v_blurTexCoords[13] = v_texCoord + vec2( blurDistance6, 0.0);
}

blur.frag

uniform sampler2D u_texture;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
    gl_FragColor = vec4(0.0);
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 0]) * 0.0044299121055113265;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 1]) * 0.00895781211794;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 2]) * 0.0215963866053;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 3]) * 0.0443683338718;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 4]) * 0.0776744219933;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 5]) * 0.115876621105;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 6]) * 0.147308056121;
    gl_FragColor += texture2D(u_texture, v_texCoord         ) * 0.159576912161;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 7]) * 0.147308056121;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 8]) * 0.115876621105;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[ 9]) * 0.0776744219933;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[10]) * 0.0443683338718;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[11]) * 0.0215963866053;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[12]) * 0.00895781211794;
    gl_FragColor += texture2D(u_texture, v_blurTexCoords[13]) * 0.0044299121055113265;
}

如果我说我完全确定这段代码在做什么,那我就是在撒谎。但总而言之,它只是从u_blurPixels 的半径内采样像素,并使用预先确定的高斯权重对gl_FragColor 的结果颜色求和。

如何修改它以防止由于透明背景而产生的白光?

【问题讨论】:

  • 是的,这是一个预乘 alpha 问题。至少这是我从其他有类似问题的人那里读到的。

标签: opengl-es shader


【解决方案1】:

此模糊过程实际上不适用于透明图像,因此需要进行一些调整。

要查看您在代码中所做的是,当您对它们进行归一化时,根据它们的距离应用周围像素生效,取它们的平均值,不管你想怎么称呼它......我在说的是您的因子0.00442991210551132650.00895781211794 已标准化,因此它们的总和始终为 1。更自然地,这些值可能是例如(仅使用 3 个像素)scales = [1, 5, 1],其中结果为(pix[0]*scales[0] + pix[1]*scales[1] + pix[2]*scales[2])/(scales[0]+scales[1]+scales[2])

因此,如果我们退后一步,您的代码可以转换为:

    int offset = 7; // Maximum range taking effect
    float offsetScales[offset+1] = { // +1 is for the zero offset
        0.159576912161,
        0.147308056121,
        ...
    };

    float sumOfScales = offsetScales[0];
    for(int i=0; i<offset; i++) sumOfScales += offsetScales[i+1]*2.0;

    gl_FragColor = texture2D(u_texture, v_texCoord)*offsetScales[0];
    for(int i=0; i<offset; i++) {
        gl_FragColor += texture2D(u_texture, v_blurTexCoords[6-i]) * offsetScales[i+1];
        gl_FragColor += texture2D(u_texture, v_blurTexCoords[7+i]) * offsetScales[i+1];
    }

    gl_FragColor /= sumOfScales; // sumOfScales in current case is always 1.0

现在除非我犯了一些错误,否则这段代码应该和你的代码完全一样。不过它更灵活一些,因为如果您在范围内添加另一个像素 (offset = 8),您可以简单地添加它并将其值设置为 0.0022,您的颜色将永远不会溢出。在您的情况下,您需要调整所有比例,使其总和等于1.0。但没关系,你的方法更接近最优,所以继续使用它,我解释这一点是为了退后一步,找到解决你问题的方法......

好的,现在代码更易于维护,让我们看看当 Alpha 通道需要生效时会发生什么;当一个像素是透明或半透明时,它对计算整体颜色的影响应该很小或没有影响。不过,它应该在结束 alpha 时生效。这意味着在这些尺度旁边,我们还需要采用 alpha 尺度。但是这样做我们需要调整应用颜色的总和:

    int offset = 7; // Maximum range taking effect
    float offsetScales[offset+1] = { // +1 is for the zero offset
        0.159576912161,
        0.147308056121,
        ...
    };

    highp vec4 summedColor = vec4(0.0); // We best take a large value now
    highp float overallAlpha = 0.0; // The actual end alpha (previously sumOfScales)
    highp float overallScale = 0.0; // We need this one so alpha doesn't overflow. If the sum of original scales is 1.0 then this factor is 1.0 and is not needed at all.

    vec4 fetchedColor = texture2D(u_texture, v_texCoord);
    float scaleWithAlpha = fetchedColor.a * offsetScales[0];
    overallScale += offsetScales[0];
    summedColor += fetchedColor*scaleWithAlpha;
    overallAlpha += scaleWithAlpha;

    for(int i=0; i<offset; i++) {
        vec4 fetchedColor = texture2D(u_texture, v_blurTexCoords[6-i]);
        float scaleWithAlpha = fetchedColor.a * offsetScales[i+1];
        overallScale += offsetScales[i+1];
        summedColor += fetchedColor*scaleWithAlpha;
        overallAlpha += scaleWithAlpha;

        fetchedColor = texture2D(u_texture, v_blurTexCoords[7+i]);
        scaleWithAlpha = fetchedColor.a * offsetScales[i+1];
        overallScale += offsetScales[7+i];
        summedColor += fetchedColor*scaleWithAlpha;
        overallAlpha += scaleWithAlpha;
    }

    overallAlpha /= overallScale;
    summedColor /= overallAlpha; // TODO: if overallAlpha is 0.0 then discard or use clear color
    gl_FragColor = vec4(summedColor.xyz, overallAlpha);

可能仍需要对此代码进行一些调整,但我希望这能让您走上正轨。在你让它工作之后,我建议你再次失去所有循环并像你最初那样做(使用新逻辑)。如果你发布你最终得到的代码也很好。

如有任何问题,请随时提出...

【讨论】:

  • 哇,非常感谢。直到现在,我还没有时间阅读并尝试代码/理解它。我想我在 for 循环的第二部分发现了一个问题 overallScale += offsetScales[7+i]; 应该是 i+1 对吧?无论如何,结果是更好,但仍有发光。这是旧方法与新方法的对比i.imgur.com/h4SlgsR.jpg
  • 第二遍时似乎出现了白光,影响了第一遍中模糊的像素。首先应用水平模糊,然后是垂直模糊。结果,垂直/对角线具有发光(如图所示)。试图自己弄清楚但空白......
  • 我现在可以工作了,还在清理东西,但上面的着色器确实可以工作。问题是预乘 alpha。在我的例子中,我需要在第一次模糊过程中将混合功能更改为setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA),然后在最后一次通过(到屏幕)返回默认的setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA)。再次感谢 Matic 的帮助。
  • @FTLRalph 对不起,我让你一个人完成了这个,但我在其他东西上完全超额预订了。无论如何,这似乎很奇怪,您需要在两个通道之间进行不同的混合操作,这表明问题可能仍然存在于其他地方。虽然您可能只需要使用 glBlendFuncSeparate 并将 alpha 保持为 ONE,ONE 在两者上......这是那些只需要一些时间来玩的程序之一......
  • 没问题,要不是你,我哪儿也去不了。我相信我需要在该 1 个特定通道上进行混合更改的原因是因为对象的初始渲染已经预乘(对象没有半透明像素) - 否则我也需要那里的混合更改。所以 FBO 是 (0,0,0,0),对象是 (r,g,b,1)。现在,第一个模糊通道引入了半透明(模糊)像素,因此需要 OpenGL 混合函数来预乘 alpha,否则第二个通道将读取非预乘像素并因此失败(发光)。
【解决方案2】:

问题在于 OpenGL ES 使用后乘 alpha(硬件成本更低),而“正确”执行则需要预乘 alpha。

您可以在着色器中为您模糊的每个样本进行预乘运算:

premult.rgb = source.rgb * source.a;

...但是你会为你正在混合的每个纹理样本产生运行时成本。通常最好在纹理创建/压缩期间离线预乘您的输入艺术资产。

如果您需要用于光照计算等的后乘数据,您可以通过将对象颜色挤出到相邻的“透明”像素中来使错误不那么明显。例如:

请注意,如果着色器发出预乘 alpha 片段颜色,则需要修正 OpenGL 混合方程(使用 GL_ONE 表示 srcAlpha,而不是 alpha 值)。

【讨论】:

  • 这似乎很相关,但我似乎无法理解它。我投入到模糊着色器中的premult.rgb = source.rgb * source.a; 类型代码似乎并没有做太多事情,只是让事情变得更暗。至于挤压,我尝试了类似的东西。我首先渲染了 FBO 的背景,但是使用像 gl_FragColor = vec4(texture2D(u_texture, v_texCoord).rgb, 0); 这样的着色器所以现在对象(我希望模糊)样本的“透明”像素应该与背景颜色相同。不管什么原因它都不起作用,白光仍然留在模糊的物体上。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 2021-07-02
  • 1970-01-01
  • 2015-03-18
  • 2018-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多