尝试优化复杂的GLSL片段着色器时遇到困难

piwo6bdm  于 2022-09-26  发布在  其他
关注(0)|答案(1)|浏览(192)

因此,首先,我要说的是,虽然代码从视觉Angular 来看运行得很好,但它遇到了非常严重的性能问题,随着您添加更多的灯光,这些问题会变得越来越糟糕。在目前的形式下,它可以很好地作为概念验证或技术演示,但在其他方面无法使用。

长话短说,我正在编写一款RimWorld风格的游戏,使用实时的自上而下的2D照明。我实现渲染的方式是使用三层技术,如下所示:

首先,我将遮挡渲染到Map到帧缓冲区的单通道R8遮挡纹理。这部分是 lightning 般的速度,不会随着更多的灯光而减速,所以这不是问题的一部分:

然后,我通过在Map到另一个帧缓冲区的光照贴图纹理上绘制一个巨大的矩形来调用照明着色器。灯光数据存储在UBO中的数组中,并在其计算中使用遮挡贴图。这就是经济放缓的地方:

最后,光照贴图纹理被相乘并添加到常规世界渲染器中,这也不受灯光数量的影响,因此这不是问题的一部分:

因此,问题出在光照贴图着色器中。The first iteration有许多分支,当我第一次尝试它时,这些分支立即冻结了我的图形驱动程序,但在删除其中大多数分支后,我在1440p和3个灯的情况下得到了稳定的144 fps,在1440p和20个灯的情况下得到了~58 fps。这是一个进步,但它的可伸缩性非常差。着色器代码如下所示,带有其他注解:


# version 460 core

// per-light data
struct Light
{
    vec4 location;
    vec4 rangeAndstartColor;
};
const int MaxLightsCount = 16;    // I've also tried 8 and 32, there was no real difference

layout(std140) uniform ubo_lights
{
    Light lights[MaxLightsCount];
};

uniform sampler2D occlusionSampler;  // the occlusion texture sampler

in vec2 fs_tex0;                     // the uv position in the large rectangle
in vec2 fs_window_size;              // the window size to transform world coords to view coords and back

out vec4 color;

void main()
{
    vec3 resultColor = vec3(0.0);
    const vec2 size = fs_window_size;
    const vec2 pos = (size - vec2(1.0)) * fs_tex0;

    // process every light individually and add the resulting colors together
    // this should be branchless, is there any way to check?
    for(int idx = 0; idx < MaxLightsCount; ++idx)
    {
        const float range = lights[idx].rangeAndstartColor.x;
        const vec2 lightPosition = lights[idx].location.xy;
        const float dist = length(lightPosition - pos);    // distance from current fragment to current light

        // early abort, the next part is expensive
        // this branch HAS to be important, right? otherwise it will check crazy long lines against occlusions
        if(dist > range)
            continue;

        const vec3 startColor = lights[idx].rangeAndstartColor.yzw;

        // walk between pos and lightPosition to find occlusions
        // standard line DDA algorithm
        vec2 tempPos = pos;
        int lineSteps = int(ceil(abs(lightPosition.x - pos.x) > abs(lightPosition.y - pos.y) ? abs(lightPosition.x - pos.x) : abs(lightPosition.y - pos.y)));
        const vec2 lineInc = (lightPosition - pos) / lineSteps;

        // can I get rid of this loop somehow? I need to check each position between 
        // my fragment and the light position for occlusions, and this is the best I 
        // came up with
        float lightStrength = 1.0;
        while(lineSteps --> 0)
        {
            const vec2 nextPos = tempPos + lineInc;
            const vec2 occlusionSamplerUV = tempPos / size;
            lightStrength *= 1.0 - texture(occlusionSampler, vec2(occlusionSamplerUV.x, 1 - occlusionSamplerUV.y)).x;

            tempPos = nextPos;
        }

        // the contribution of this light to the fragment color is based on 
        // its square distance from the light, and the occlusions between them
        // implemented as multiplications
        const float strength = max(0, range - dist) / range * lightStrength;
        resultColor += startColor * strength * strength;
    }

    color = vec4(resultColor, 1.0);
}

我会根据需要多次调用此着色器,因为结果是累加的。它适用于大批次的灯光或逐个灯光。在性能方面,我没有注意到尝试不同的批号时有任何真正的变化,这可能有点奇怪。

所以我的问题是,有没有更好的方法来查找遮挡纹理中我的碎片位置和灯光位置之间的任何(布尔)遮挡,而不是手动迭代每个像素?渲染缓冲区在这里可能会有帮助(从我读到的情况来看,它们用于将数据读回系统内存,但我需要在另一个着色器中使用它)。

也许,对于我在这里做的事情,有没有更好的算法?

zte4gxcn

zte4gxcn1#

我可以想出几条优化的路线:

1.精确:在遮挡贴图上应用distance transform:这将给出每个像素到最近遮挡的距离。在此之后,您可以在循环中安全地按该距离进行步进,而不是进行小步走。这将大大减少开放地区的步数。

有一个非常简单的CPU端算法来计算DT,如果你的遮挡是静态的,它可能适合你。但是,如果您的场景每帧都更改,您将需要搜索文献中的GPU端算法,这些算法似乎更复杂。
1.不准确:求助于柔和的阴影--这可能是你愿意做出的妥协,甚至被视为一种艺术选择。如果可以,可以从遮挡贴图创建mipmap,然后随着距离着色点的距离越远,逐渐增加步长和对较低级别进行采样。

您可以进一步构建发射器贴图(到与遮挡相同的4通道贴图中)。然后,您的整个着色过程将独立于灯光数量。这等同于应用于2D的体素圆锥体跟踪GI。

相关问题