使用MSAA实现无序透明度。

4
我已经根据《OpenGL编程指南》第8版(红宝书)中的演示实现了基于OIT的功能。现在我需要添加MSAA。但是,启用MSAA会破坏透明度,因为分层像素的解析次数等于样本级别的数量。我读过这篇关于使用DirectX实现的文章,其中他们说像素着色器应该按样本而不是按像素运行。在OpenGL中如何实现呢?
我不会在这里公布整个实现,但是这是最终分辨率解决分层像素的片段着色器代码块:
vec4 final_color = vec4(0,0,0,0);
for (i = 0; i < fragment_count; i++)
{
    /// Retrieving the next fragment from the stack:
    vec4 modulator = unpackUnorm4x8(fragment_list[i].y) ;
    /// Perform alpha blending:
    final_color =   mix(final_color, modulator, modulator.a);
}

color = final_color ;

更新:

我已经尝试了这里提出的解决方案,但仍然无法工作。这是用于列表构建和解析通道的完整片段着色器:

列表构建通道:

#version 420 core
layout (early_fragment_tests) in;
layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
layout (binding = 1, rgba32ui) uniform writeonly uimageBuffer list_buffer;
layout (binding = 0, offset = 0) uniform atomic_uint list_counter;
layout (location = 0) out vec4 color;//dummy output

in vec3 frag_position;
in vec3 frag_normal;
in vec4 surface_color;
in int gl_SampleMaskIn[];
uniform vec3 light_position = vec3(40.0, 20.0, 100.0);

void main(void)
{
    uint index;
    uint old_head;
    uvec4 item;
    vec4 frag_color;
    index = atomicCounterIncrement(list_counter);
    old_head = imageAtomicExchange(head_pointer_image, ivec2(gl_FragCoord.xy), uint(index));

    vec4 modulator =surface_color;
    item.x = old_head;
    item.y = packUnorm4x8(modulator);
    item.z = floatBitsToUint(gl_FragCoord.z);
    item.w = int(gl_SampleMaskIn[0]);
    imageStore(list_buffer, int(index), item);
    frag_color = modulator;
    color = frag_color;
}

列表解析:

#version 420 core
// The per-pixel image containing the head pointers
layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
// Buffer containing linked lists of fragments
layout (binding = 1, rgba32ui) uniform uimageBuffer list_buffer;
// This is the output color
layout (location = 0) out vec4 color;
// This is the maximum number of overlapping fragments allowed
#define MAX_FRAGMENTS 40

// Temporary array used for sorting fragments
uvec4 fragment_list[MAX_FRAGMENTS];

void main(void)
{
    uint current_index;
    uint fragment_count = 0;
    current_index = imageLoad(head_pointer_image, ivec2(gl_FragCoord).xy).x;

    while (current_index != 0 && fragment_count < MAX_FRAGMENTS )
    {   
        uvec4 fragment = imageLoad(list_buffer, int(current_index));
        int coverage = int(fragment.w);
        //if((coverage &(1 << gl_SampleID))!=0) {

            fragment_list[fragment_count] = fragment;
            current_index = fragment.x;

        //}

        fragment_count++;
    }

    uint i, j;

    if (fragment_count > 1)
    {
        for (i = 0; i < fragment_count - 1; i++)
        {
            for (j = i + 1; j < fragment_count; j++)
            {
                uvec4 fragment1 = fragment_list[i];
                uvec4 fragment2 = fragment_list[j];

                float depth1 = uintBitsToFloat(fragment1.z);
                float depth2 = uintBitsToFloat(fragment2.z);

                if (depth1 < depth2)
                {
                    fragment_list[i] = fragment2;
                    fragment_list[j] = fragment1;
                }
            }
        }
    }

    vec4 final_color = vec4(0,0,0,0);

    for (i = 0; i < fragment_count; i++)
    {  
        vec4 modulator = unpackUnorm4x8(fragment_list[i].y);
        final_color =  mix(final_color, modulator, modulator.a);      
    }

    color = final_color;
}

不了解您的完整代码很难回答,但您不能像链接的DX11演示那样做吗?只需在存储通道中为每个片段存储样本覆盖率以及颜色和深度,然后让最终的排序和渲染着色器为每个样本运行,而不仅仅是为每个片段运行,仅添加实际被片段覆盖的样本即可。 - Christian Rau
然后让最终的排序和渲染着色器为每个样本运行,而不仅仅是为每个片段运行,只添加实际被片段覆盖的样本。这就是我不明白如何做到的。我该如何检查片段是否覆盖了样本? - Michael IV
和 DX demo 一样,我猜。GLSL 在片段着色器中也有覆盖掩码和采样 ID 作为输入。 - Christian Rau
2个回答

5

不知道你的代码实际上是如何工作的,但你可以像链接的DX11演示那样做,因为OpenGL提供了相同的所需功能。

因此,在第一个着色器中,你只需存储所有渲染的片段,同时还存储每个片段的采样覆盖掩码(当然还有颜色和深度)。这作为片段着色器输入变量 int gl_SampleMaskIn [] 给出,对于id为32*i+j的每个采样,如果片段覆盖该采样,则设置glSampleMaskIn [i] 的位 j(由于您可能不会使用 > 32xMSAA,因此通常只需使用glSampleMaskIn [0] 并且只需要存储单个int作为覆盖掩码)。

...
fragment.color = inColor;
fragment.depth = gl_FragCoord.z;
fragment.coverage = gl_SampleMaskIn[0];
...

然后,针对每个样本而不仅仅是每个片段运行最终的排序和渲染着色器。这是通过隐式利用输入变量int gl_SampleID来实现的,该变量给出了当前样本的ID。因此,在这个着色器中(除了非MSAA版本之外),我们所做的是排序步骤只考虑样本,仅当当前样本实际上被该片段覆盖时,才将一个片段添加到最终(要排序)的片段列表中。
原先的代码大致如下(请注意,这是根据您的小片段和DX-link推断出来的伪代码):
while(fragment.next != 0xFFFFFFFF)
{
    fragment_list[count++] = vec2(fragment.depth, fragment.color);
    fragment = fragments[fragment.next];
}

现在

while(fragment.next != 0xFFFFFFFF)
{
    if(fragment.coverage & (1 << gl_SampleID))
        fragment_list[count++] = vec2(fragment.depth, fragment.color);
    fragment = fragments[fragment.next];
}

或者类似这样的东西。

编辑:根据您更新的代码,您必须仅在if(covered)块内递增fragment_count,因为如果未覆盖样本,则不希望将片段添加到列表中。始终递增它可能会导致您在边缘看到的伪影,这些伪影是MSAA(因此是覆盖范围)发挥作用的区域。

另一方面,在每个循环迭代中必须向前移动列表指针(current_index = fragment.x),而不仅仅是在样本被覆盖时才移动,否则可能会导致无限循环,例如在您的情况下。因此,您的代码应如下所示:

while (current_index != 0 && fragment_count < MAX_FRAGMENTS )
{
    uvec4 fragment = imageLoad(list_buffer, int(current_index));
    uint coverage = fragment.w;
    if((coverage &(1 << gl_SampleID))!=0)
        fragment_list[fragment_count++] = fragment;
    current_index = fragment.x;
}

如果(fragment.coverage & (1 << gl_SampleID)),这个会抛出"uint to bool"转换错误的错误。 - Michael IV
@MichaelIV 好的,找到了无限循环的原因。 - Christian Rau
是的,你说得对,但是把计数器放在“if”语句里会导致无限循环... - Michael IV
@MichaelIV:“是的,你说得对,但将计数器留在“if”语句内会导致无限循环。”- 请查看最近的更新。 - Christian Rau
@MichaelIV:“我只需要在第二遍渲染中启用采样着色吗?” - 是的,是的,是的! 两个渲染步骤都使用多重采样(因此第一步中的虚拟渲染缓冲器需要是多重采样渲染缓冲器),但只有第二次渲染使用采样着色。根本不需要启用 GL_SAMPLE_SHADING,第二个通道会自动使用它,因为它使用 gl_SampleID,而第一次渲染未使用该参数。 - Christian Rau
显示剩余9条评论

1

OpenGL 4.3 规范在7.1节中提到了gl_SampleID内置变量:

在片元着色器中静态使用该变量会导致整个着色器每个样本都被评估。

(这已经是ARB_sample_shading的情况,并且也适用于gl_SamplePosition或使用sample限定符声明的自定义变量)

因此,这是相当自动化的,因为您可能需要SampleID。


1
注意:要手动启用每个样本的着色,您需要使用 glEnable(GL_SAMPLE_SHADING) - Nicol Bolas
当然,你只需要在解析通道中使用样本着色,之前的列表构建通道像往常一样每个片段只运行一次,并记录由gl_SampleMaskIn给出的覆盖掩码。请参考链接的DX11演示和我的(非常相似的)答案。 - Christian Rau
@MichaelIV 构建过程也不会写入 MSAA 缓冲区,因为它使用一些自定义数据结构来存储片段。构建过程只需要为每个片段调用一次,但启用 MSAA 以便得到光栅化器正确计算的 gl_SampleMaskIn。然后,它只是记录每个片段的样本掩码以及其他每个片段的值。它不会计算或写出任何样本。 - Christian Rau
@MichaelIV 哦,等等,是的,我想你仍然需要在构建通道中使用MSAA渲染缓冲区才能实际使用MSAA,但当然你不会向该缓冲区写入任何内容。排序通道随后也使用MSAA并实际写入到MSAA渲染缓冲区中,就像任何普通的MSAA应用程序一样(只是它使用采样着色)。 - Christian Rau
@MichaelIV "我以为在MSAA中,片段着色器会根据每个MSAA级别被调用一次" - 实际上,MSAA的整个目的及其相对于实际超采样(以更高分辨率渲染完整场景并进行降采样)的性能改进之处在于,重型片段着色器仍然只运行一次每个片段。然后,该片段的每个样本颜色只是从每个片段颜色中获取,并与光栅化器计算的覆盖掩码进行遮罩处理。但是,通过新的每个样本着色技术,您可以获得真正的超采样效果。 - Christian Rau
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接