12

I've found a few places where this has been asked, but I've not yet found a good answer.

The problem: I want to render to texture, and then I want to draw that rendered texture to the screen IDENTICALLY to how It would appear if I skipped the render to texture step and were just directly rendering to the screen. I am currently using a blend mode glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). I have glBlendFuncSeparate to play around with as well.

I want to be able to render partially transparent overlapping items to this texture. I know the blend function is currently messing up the RGB values based on the Alpha. I've seen some vague suggestions to use "premultiplied alpha" but the description is poor as to what that means. I make png files in photoshop, I know they have a translucency bit and you can't easily edit the alpha channel independently as you can with TGA. If necessary I can switch to TGA, though PNG is more convenient.

For now, for the sake of this question, assume we aren't using images, instead I am just using full color quads with alpha.

Once I render my scene to the texture I need to render that texture to another scene, and I need to BLEND the texture assuming partial transparency again. Here is where things fall apart. In the previous blending steps I clearly alter the RGB values based on Alpha, doing it again works a-okay if Alpha is 0 or 1, but if it is in in between, the result is a further darkening of those partially translucent pixels.

Playing with blend modes I've had very little luck. The best I can do is render to texture with:

glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);

I did discover that rendering multiple times with glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) will approximate the right color (unless things overlap). But that's not exactly perfect (as you can see in the following image, the parts where the green/red/blue boxes overlap gets darker, or accumulates alpha. (EDIT: If I do the multiple draws in the render to screen part and only render once to texture, the alpha accumulation issue disappears and it does work, but why?! I don't want to have to render the same texture hundreds of times to the screen to get it to accumulate properly)

Here are some images detailing the issue (the multiple render passes are with basic blending (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), and they are rendered multiple times in the texture rendering step. The 3 boxes on the right are rendered 100% red, green, or blue (0-255) but at alpha values of 50% for blue, 25% for red, and 75% for green: enter image description here

So, a breakdown of what I want to know:

  1. I set blend mode to: X?
  2. I render my scene to a texture. (Maybe I have to render with a few blend modes or multiple times?)
  3. I set my blend mode to: Y?
  4. I render my texture to the screen over an existing scene. (Maybe I need a different shader? Maybe I need to render the texture a few times?)

Desired behavior is that at the end of that step, the final pixel result is identical to if I were to just do this:

  1. I set my blend mode to: (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  2. I render my scene to the screen.

And, for completeness, here is some of my code with my original naive attempt (just regular blending):

    //RENDER TO TEXTURE.
    void Clipped::refreshTexture(bool a_forceRefresh) {
        if(a_forceRefresh || dirtyTexture){
            auto pointAABB = basicAABB();
            auto textureSize = castSize<int>(pointAABB.size());
            clippedTexture = DynamicTextureDefinition::make("", textureSize, {0.0f, 0.0f, 0.0f, 0.0f});
            dirtyTexture = false;
            texture(clippedTexture->makeHandle(Point<int>(), textureSize));
            framebuffer = renderer->makeFramebuffer(castPoint<int>(pointAABB.minPoint), textureSize, clippedTexture->textureId());
            {
                renderer->setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
                SCOPE_EXIT{renderer->defaultBlendFunction(); };

                renderer->modelviewMatrix().push();
                SCOPE_EXIT{renderer->modelviewMatrix().pop(); };
                renderer->modelviewMatrix().top().makeIdentity();

                framebuffer->start();
                SCOPE_EXIT{framebuffer->stop(); };

                const size_t renderPasses = 1; //Not sure?
                if(drawSorted){
                    for(size_t i = 0; i < renderPasses; ++i){
                        sortedRender();
                    }
                } else{
                    for(size_t i = 0; i < renderPasses; ++i){
                        unsortedRender();
                    }
                }
            }
            alertParent(VisualChange::make(shared_from_this()));
        }
    }

Here is the code I'm using to set up the scene:

    bool Clipped::preDraw() {
        refreshTexture();

        pushMatrix();
        SCOPE_EXIT{popMatrix(); };

        renderer->setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        SCOPE_EXIT{renderer->defaultBlendFunction();};
        defaultDraw(GL_TRIANGLE_FAN);

        return false; //returning false blocks the default rendering steps for this node.
    }

And the code to render the scene:

test = MV::Scene::Rectangle::make(&renderer, MV::BoxAABB({0.0f, 0.0f}, {100.0f, 110.0f}), false);
test->texture(MV::FileTextureDefinition::make("Assets/Images/dogfox.png")->makeHandle());

box = std::shared_ptr<MV::TextBox>(new MV::TextBox(&textLibrary, MV::size(110.0f, 106.0f)));
box->setText(UTF_CHAR_STR("ABCDE FGHIJKLM NOPQRS TUVWXYZ"));
box->scene()->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({0, 0, 1, .5})->position({80.0f, 10.0f})->setSortDepth(100);
box->scene()->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({1, 0, 0, .25})->position({80.0f, 40.0f})->setSortDepth(101);
box->scene()->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({0, 1, 0, .75})->position({80.0f, 70.0f})->setSortDepth(102);
test->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({.0, 0, 1, .5})->position({110.0f, 10.0f})->setSortDepth(100);
test->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({1, 0, 0, .25})->position({110.0f, 40.0f})->setSortDepth(101);
test->make<MV::Scene::Rectangle>(MV::size(65.0f, 36.0f))->color({.0, 1, 0, .75})->position({110.0f, 70.0f})->setSortDepth(102);

And here's my screen draw:

renderer.clearScreen();
test->draw(); //this is drawn directly to the screen.
box->scene()->draw(); //everything in here is in a clipped node with a render texture.
renderer.updateScreen();

*EDIT: FRAMEBUFFER SETUP/TEARDOWN CODE:

void glExtensionFramebufferObject::startUsingFramebuffer(std::shared_ptr<Framebuffer> a_framebuffer, bool a_push){
    savedClearColor = renderer->backgroundColor();
    renderer->backgroundColor({0.0, 0.0, 0.0, 0.0});

    require(initialized, ResourceException("StartUsingFramebuffer failed because the extension could not be loaded"));
    if(a_push){
        activeFramebuffers.push_back(a_framebuffer);
    }

    glBindFramebuffer(GL_FRAMEBUFFER, a_framebuffer->framebuffer);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, a_framebuffer->texture, 0);
    glBindRenderbuffer(GL_RENDERBUFFER, a_framebuffer->renderbuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, roundUpPowerOfTwo(a_framebuffer->frameSize.width), roundUpPowerOfTwo(a_framebuffer->frameSize.height));

    glViewport(a_framebuffer->framePosition.x, a_framebuffer->framePosition.y, a_framebuffer->frameSize.width, a_framebuffer->frameSize.height);
    renderer->projectionMatrix().push().makeOrtho(0, static_cast<MatrixValue>(a_framebuffer->frameSize.width), 0, static_cast<MatrixValue>(a_framebuffer->frameSize.height), -128.0f, 128.0f);

    GLenum buffers[] = {GL_COLOR_ATTACHMENT0};
    //pglDrawBuffersEXT(1, buffers);


    renderer->clearScreen();
}

void glExtensionFramebufferObject::stopUsingFramebuffer(){
    require(initialized, ResourceException("StopUsingFramebuffer failed because the extension could not be loaded"));
    activeFramebuffers.pop_back();
    if(!activeFramebuffers.empty()){
        startUsingFramebuffer(activeFramebuffers.back(), false);
    } else {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glBindRenderbuffer(GL_RENDERBUFFER, 0);

        glViewport(0, 0, renderer->window().width(), renderer->window().height());
        renderer->projectionMatrix().pop();
        renderer->backgroundColor(savedClearColor);
    }
}

And my clear screen code:

void Draw2D::clearScreen(){
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}
4

4 回答 4

17

根据我运行的一些计算和模拟,我想出了两个非常相似的解决方案,似乎可以解决问题。一种将预乘颜色与单个(单独的)混合函数结合使用,另一种在没有预乘颜色的情况下工作,但需要在此过程中更改混合函数几次。

选项 1:单一混合函数,预乘

这种方法在整个过程中使用单个混合函数。混合函数为:

glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA,
                    GL_ONE_MINUS_DST_ALPHA, GL_ONE);

它需要预乘颜色,这意味着如果您的输入颜色通常是(r, g, b, a),您可以使用它(r * a, g * a, b * a, a)。您可以在片段着色器中执行预乘。

顺序是:

  1. 将混合功能设置为(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE)
  2. 将渲染目标设置为 FBO。
  3. 使用预乘颜色将要渲染到 FBO 的图层渲染。
  4. 将渲染目标设置为默认帧缓冲区。
  5. 使用预乘颜色在 FBO 内容下方渲染您想要的图层。
  6. 渲染 FBO 附件,无需应用预乘,因为 FBO 中的颜色已经预乘。
  7. 使用预乘颜色在 FBO 内容之上渲染您想要的图层。

选项 2:切换混合函数,无需预乘

这种方法不需要任何步骤的颜色预乘。缺点是在此过程中必须多次切换混合功能。

  1. 将混合功能设置为(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE)
  2. 将渲染目标设置为 FBO。
  3. 将要渲染到 FBO 的图层渲染。
  4. 将渲染目标设置为默认帧缓冲区。
  5. (可选)将混合功能设置为(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  6. 在 FBO 内容下方渲染您想要的图层。
  7. 将混合功能设置为(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
  8. 渲染 FBO 附件。
  9. 将混合功能设置为(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  10. 在 FBO 内容之上渲染您想要的图层。

解释与证明

我认为选项 1 更好,并且可能更有效,因为它不需要在渲染期间切换混合功能。所以下面的详细解释是针对选项 1 的。选项 2 的数学原理几乎相同。唯一真正的区别是选项 2 使用GL_SOURCE_ALPHA混合函数的第一项在必要时执行预乘,其中选项 1 期望预乘颜色进入混合函数。

为了说明这是有效的,让我们看一个渲染 3 层的示例。我将为ra组件进行所有计算。g和的计算b等同于 的计算r。我们将按以下顺序渲染三层:

(r1, a1)  pre-multiplied: (r1 * a1, a1)
(r2, a2)  pre-multiplied: (r2 * a2, a2)
(r3, a3)  pre-multiplied: (r3 * a3, a3)

对于参考计算,我们将这 3 层与标准GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA混合函数混合。我们不需要在这里跟踪生成的 alpha,因为DST_ALPHA它没有在混合函数中使用,而且我们还没有使用预乘颜色:

after layer 1: (a1 * r1)
after layer 2: (a2 * r2 + (1.0 - a2) * a1 * r1)
after layer 3: (a3 * r3 + (1.0 - a3) * (a2 * r2 + (1.0 - a2) * a1 * r1)) =
               (a3 * r3 + (1.0 - a3) * a2 * r2 + (1.0 - a3) * (1.0 - a2) * a1 * r1)

所以最后一项是我们最终结果的目标。现在,我们将第 2 层和第 3 层渲染为 FBO。稍后我们会将第 1 层渲染到帧缓冲区中,然后在其上混合 FBO。目标是获得相同的结果。

从现在开始,我们将应用开头列出的混合功能,并使用预乘颜色。我们还需要计算 alpha,因为DST_ALPHA它用于混合函数。首先,我们将第 2 层和第 3 层渲染到 FBO 中:

after layer 2: (a2 * r2, a2)
after layer 3: (a3 * r3 + (1.0 - a3) * a2 * r2, (1.0 - a2) * a3 + a2)

现在我们渲染到他的主帧缓冲区。由于我们不关心生成的 alpha,我只会r再次计算组件:

after layer 1: (a1 * r1)

现在我们在此之上混合 FBO 的内容。所以我们在 FBO 中为“第 3 层之后”计算的是我们的源颜色/alpha,a1 * r1是目标颜色,并且GL_ONE, GL_ONE_MINUS_SRC_ALPHA仍然是混合函数。FBO 中的颜色已经预乘,因此在混合 FBO 内容时着色器中不会有预乘:

srcR = a3 * r3 + (1.0 - a3) * a2 * r2
srcA = (1.0 - a2) * a3 + a2
dstR = a1 * r1
ONE * srcR + ONE_MINUS_SRC_ALPHA * dstR
    = srcR + (1.0 - srcA) * dstR
    = a3 * r3 + (1.0 - a3) * a2 * r2 + (1.0 - ((1.0 - a2) * a3 + a2)) * a1 * r1
    = a3 * r3 + (1.0 - a3) * a2 * r2 + (1.0 - a3 + a2 * a3 - a2) * a1 * r1
    = a3 * r3 + (1.0 - a3) * a2 * r2 + (1.0 - a3) * (1.0 - a2) * a1 * r1

将最后一项与我们上面计算的标准混合情况下的参考值进行比较,您可以看出它完全一样。

GL_ONE_MINUS_DST_ALPHA, GL_ONEThis answer to a similar question在混合功能方面有更多背景: OpenGL ReadPixels (Screenshot) Alpha

于 2014-06-24T06:57:24.390 回答
2

我达到了我的目标。现在,让我与互联网分享这些信息,因为它存在于我找不到的其他任何地方。

在此处输入图像描述

  1. 创建你的帧缓冲区(blindframebuffer 等)
  2. 将帧缓冲区清除为 0, 0, 0, 0
  3. 正确设置视口。这是我在问题中认为理所当然的所有基本内容,但想包括在这里。
  4. 现在,使用 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 将场景正常渲染到帧缓冲区。确保对场景进行了排序(就像通常那样。)
  5. 现在绑定包含的片段着色器。这将消除通过混合功能对图像颜色值造成的损害。
  6. 使用 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 将纹理渲染到屏幕
  7. 使用常规着色器返回正常渲染。

我在问题中包含的代码基本上保持不变,除了我确保在执行“preDraw”功能时绑定下面列出的着色器,该功能特定于我自己的小框架,但基本上是“绘制到屏幕”调用我的渲染纹理。

我称之为“未混合”着色器。

#version 330 core

smooth in vec4 color;
smooth in vec2 uv;

uniform sampler2D texture;

out vec4 colorResult;

void main(){
    vec4 textureColor = texture2D(texture, uv.st);

    textureColor/=sqrt(textureColor.a);

    colorResult = textureColor * color;
}

为什么我要使用 textureColor/=sqrt(textureColor.a)?因为原始颜色是这样计算的:

结果R = r * a,结果G = g * a,结果B = b * a,结果A = a * a

现在,如果我们想撤销它,我们需要弄清楚 a 是什么。最简单的查找方法是在此处求解“a”:

结果A = a * a

如果最初渲染时 a 是 0.25,我们有:

结果A = .25 * .25

或者:

结果A = 0.0625

但是,当纹理被绘制到屏幕上时,我们不再有“a”了。我们知道 resultA 是什么,它是纹理的 alpha 通道。所以我们可以通过 sqrt(resultA) 得到 0.25。现在有了这个值,我们可以除以撤销乘法:

textureColor/=sqrt(textureColor.a);

这样就解决了所有问题,取消了混合!

*编辑:嗯...至少有点。有一点不准确,在这种情况下,我可以通过渲染与帧缓冲区清除颜色不同的清除颜色来显示它。一些 alpha 信息似乎丢失了,可能在 rgb 通道中。这对我来说仍然足够好,但我想在退出前跟进显示不准确的屏幕截图。如果有人有解决方案,请提供!

我已经打开了一个赏金,将这个答案提升为一个规范的 100% 正确的解决方案。现在,如果我在现有透明度上渲染更多部分透明的对象,则透明度的累积方式与右侧不同,导致最终纹理的亮度超出右侧显示的范围。同样,当在非黑色背景上渲染时,很明显现有解决方案的结果略有不同,如上所示。

适当的解决方案在所有情况下都是相同的。我现有的解决方案不能在着色器校正中考虑目标混合,只有源 alpha。

在此处输入图像描述

于 2014-06-22T03:12:01.707 回答
1

为了在一次通过中执行此操作,您需要支持单独的颜色和 alpha 混合功能。首先,渲染具有存储在 alpha 通道中的前景贡献的纹理(即 1 = 完全不透明,0 = 完全透明)和 RGB 颜色通道中的预乘源颜色值。要创建此纹理,请执行以下操作:

  1. 将纹理清除为 RGBA=[0, 0, 0, 0]
  2. 将颜色通道混合设置为 src_color*src_alpha+dst_color*(1-src_alpha)
  3. 将 alpha 通道混合设置为 src_alpha*(1-dst_alpha)+dst_alpha
  4. 将场景渲染到纹理

要设置 2) 和 3) 指定的模式,您可以执行以下操作:glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE)glBlendEquation(GL_FUNC_ADD)

接下来通过将颜色混合设置为:src_color+dst_color*(1-src_alpha) 将这个纹理渲染到场景中,即glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)glBlendEquation(GL_FUNC_ADD)

于 2014-06-25T03:10:11.577 回答
0

Your problem is older than OpenGL, or personal computers, or indeed any living human. You're trying to blend two images together and make it look like they weren't blended at all. Printing presses face this exact problem. When ink is applied to paper, the result is a blend between the ink color and the paper color.

The solution is the same in paper as it is in OpenGL. You must alter your source image in order to control your final result. This is easy enough to figure out if you examine the math used to blend.

For each of R, G, B, the resultant color is (old * (1-opacity)) + (new * opacity). The basic scenario, and the one you'd like to emulate, is drawing a color directly onto the final back buffer at opacity A.

For example, opacity is 50% and your green channel has 0xFF. The result should be 0x7F on a black background (including unavoidable rounding error). You probably can't assume the background is black, so expect the green channel to vary between 0x7F and 0xFF.

You'd like to know how to emulate that result when you're really rendering to a texture, then rending the texture to the back buffer. It turns out that the "vague suggestions to use 'premultiplied alpha'" were correct. Whereas your solution is to use a shader to unblend a previous blend operation in the last step, the standard solution is to multiply the colors of your original source texture with the alpha channel (aka premultiplied alpha). When composting the intermediate texture, the RGB channels are blended without multiplying by Alpha. When rendering the texture to the back buffer, against the RGB channels are blended without multiplying by Alpha. Thus you neatly avoid the multiple multiplication problem.

Please consult these resources for a better understanding. I and most others are more familiar with this technique in DirectX, so you may have to search for the appropriate OGL flags.

于 2014-06-24T01:14:07.297 回答