【问题标题】:Opengl Render To Texture With Partial Transparancy (Translucency) And Then Rendering That To The ScreenOpengl 渲染到具有部分透明度(半透明)的纹理,然后将其渲染到屏幕上
【发布时间】:2014-08-12 08:26:31
【问题描述】:

我找到了几个地方问过这个问题,但我还没有找到一个好的答案。

问题:我想渲染到纹理,然后我想将渲染的纹理绘制到屏幕上完全相同如果我跳过渲染到纹理步骤并且直接渲染到屏幕上。我目前正在使用混合模式 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)。我也有 glBlendFuncSeparate 可以玩。

我希望能够将部分透明的重叠项目渲染到此纹理。我知道混合功能目前正在弄乱基于 Alpha 的 RGB 值。我已经看到了一些使用“预乘 alpha”的模糊建议,但关于这意味着什么的描述很差。我在 Photoshop 中制作 png 文件,我知道它们具有半透明位,并且您无法像使用 TGA 那样轻松地独立编辑 Alpha 通道。如果需要,我可以切换到 TGA,虽然 PNG 更方便。

现在,为了这个问题,假设我们不使用图像,而我只是使用带 alpha 的全彩色四边形。

一旦我将我的场景渲染到纹理,我需要将该纹理渲染到另一个场景,并且我需要再次混合纹理假设部分透明。这就是事情分崩离析的地方。在前面的混合步骤中,我清楚地根据 Alpha 更改了 RGB 值,如果 Alpha 为 0 或 1,则再次执行此操作可以正常工作,但如果介于两者之间,则结果是那些部分半透明的像素进一步变暗。

玩混合模式我运气不佳。我能做的最好的就是渲染到纹理:

glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE);

我确实发现使用 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 多次渲染将接近正确的颜色(除非重叠)。但这并不完全完美(如下图所示,绿色/红色/蓝色框重叠的部分变得更暗,或积累了 alpha。(编辑:如果我在渲染到屏幕部分进行多次绘制,并且仅渲染一次纹理,alpha 累积问题就消失了,它确实有效,但是为什么?!我不想在屏幕上渲染数百次相同的纹理才能使其正确累积)

这里是一些详细说明该问题的图像(多个渲染通道使用基本混合(GL_SRC_ALPHA、GL_ONE_MINUS_SRC_ALPHA),它们在纹理渲染步骤中被多次渲染。右侧的 3 个框被渲染为 100% 红色、绿色,或蓝色 (0-255),但蓝色的 alpha 值为 50%,红色为 25%,绿色为 75%:

那么,我想知道的细分:

  1. 我将混合模式设置为:X
  2. 我将场景渲染为纹理。 (也许我必须使用几种混合模式或多次渲染?)
  3. 我将混合模式设置为:Y
  4. 我将纹理渲染到现有场景的屏幕上。 (也许我需要不同的着色器?也许我需要渲染纹理几次?)

期望的行为是在该步骤结束时,最终的像素结果与我只是这样做的结果相同:

  1. 我将混合模式设置为:(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  2. 我将场景渲染到屏幕上。

并且,为了完整起见,这里是我最初天真的尝试的一些代码(只是常规混合):

    //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()));
        }
    }

这是我用来设置场景的代码:

    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.
    }

以及渲染场景的代码:

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);

这是我的屏幕绘制:

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();

*编辑:帧缓冲设置/拆卸代码:

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);
    }
}

还有我的清屏代码:

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

【问题讨论】:

  • 我认为您的问题是,您很难理解您在说什么,因为您不完全了解混合的工作原理。如果您在其他部分透明的东西之上渲染部分透明的东西,那么您的显卡绘制的最终颜色将不会与您渲染的任何东西具有相同的 alpha,除非您使用 1 或 0 的 alpha 值。解决这个问题的一种方法(我认为,还没有测试过)是使对象的 alpha 颜色成为渲染次数的函数
  • 对,我不知道怎么实现我想要的,因为我对混合函数的理解不完全。它们对我来说不是很直观。这方面的援助正是这个问题的意义所在。 :)
  • 我也可以影响我用来渲染这个问题的任何一部分的着色器。 IE:我可以更改在将纹理渲染到屏幕时使用的着色器。
  • 一件事可能是您没有在每次渲染纹理时都清除它。每次渲染到纹理时使用 glClear(GL_COLOR_BUFFER_BIT) 或用于清除屏幕的任何函数清除它。我相信这将在您的 refreshTexture 方法中
  • @M2tM 不,不用担心。下次(:

标签: c++ opengl blending render-to-texture glblendfunc


【解决方案1】:

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

选项 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 组件进行所有计算。 gb 的计算将等同于 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_ONE 部分有更多背景:OpenGL ReadPixels (Screenshot) Alpha

【讨论】:

  • 谢谢!我今晚去看看。这是我正在寻找的建议/故障。非常容易理解和可操作。
  • 出于好奇和完整性,您能否概述一下涉及切换混合功能的其他方法?对于遇到此问题的其他人来说,这将是有益的。
  • 是的,我今晚可以添加。
  • 好的,我添加了关于我在原始版本中提到的替代方法的详细信息。
  • 太棒了!两者都有效。一百万分给你,先生。我选择使用您的第二个选项(切换混合模式),因为我可以在我的“剪辑”类中完全实现它,并且着色器选项需要更改我的默认着色器和所有内容的混合功能。但两者都有效!
【解决方案2】:

为了一次性完成此操作,您需要支持单独的颜色和 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)

【讨论】:

    【解决方案3】:

    您的问题比 OpenGL、个人计算机或任何活人都早。您正在尝试将两个图像混合在一起,使其看起来根本没有混合。印刷机面临着这个确切的问题。当墨水应用到纸张上时,结果是墨水颜色和纸张颜色之间的混合。

    论文中的解决方案与 OpenGL 中的解决方案相同。您必须更改源图像才能控制最终结果。如果您检查用于混合的数学,这很容易弄清楚。

    对于 R、G、B 中的每一个,结果颜色为 (old * (1-opacity)) + (new * opacity)。基本场景,也是您想要模拟的场景,是直接在不透明度 A 的最终后台缓冲区上绘制颜色。

    例如,不透明度为 50%,您的绿色通道为 0xFF。结果应该是黑色背景上的0x7F(包括不可避免的舍入误差)。您可能无法假设背景是黑色的,因此预计绿色通道会在 0x7F0xFF 之间变化。

    您想知道当您真正渲染到纹理时如何模拟该结果,然后将纹理渲染到后台缓冲区。事实证明,“使用‘预乘 alpha’的模糊建议”是正确的。虽然您的解决方案是使用着色器在最后一步中取消混合先前的混合操作,但标准解决方案是将原始源纹理的颜色与 Alpha 通道(也称为预乘 Alpha)相乘。在合成中间纹理时,RGB 通道被混合不乘以 Alpha。将纹理渲染到后台缓冲区时,将针对 RGB 通道进行混合不乘以 Alpha。因此,您巧妙地避免了乘法问题。

    consulttheseresources更好地理解。我和其他大多数人都更熟悉 DirectX 中的这种技术,因此您可能需要搜索适当的 OGL 标志。

    【讨论】:

    • 我的问题是,如果我从场景图中以不同的透明度呈现 A、B、C、D 和 E 矩形,我不了解如何以通用方式进行预乘 alpha。我读过的大多数预乘 alpha 资源都假设正在渲染一个源纹理......但我想将它应用到场景中。也许我需要重新阅读您的答案,但我不够聪明,无法仅从这个描述中整理出来,而没有可能通过这些参考资料再次深入研究。为我的具体问题提供更精确的步骤将不胜感激。
    • 真的感谢您抽出时间来回答,但是,无论如何我都会投赞成票。
    【解决方案4】:

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

    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”功能时绑定了下面列出的着色器,这是我自己的小框架特有的,但基本上是“draw筛选”调用我的渲染纹理。

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

    #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。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-16
      • 2010-11-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-07
      相关资源
      最近更新 更多