HLSL - 组织着色器和技术而不迷路

HLSL - organizing shaders and techniques and not getting lost

我在组织游戏中的着色器方面需要帮助。

该游戏使用顶点和像素着色器进行纹理和照明。有些对象是有纹理的,有些只是着色的,然后有几种光照算法——光照贴图、漫反射和镜面光照、一些只有一些对象才能获得的阴影生成,等等。

我的第一个想法是拥有一个大而漂亮的着色器技术,它可以根据参数处理这一切。这很好维护(提供想法的伪代码):

float4 PixelShaderFunction(input)
{
    if (UseTexture)
        objectColor = tex2D(...)
    else
        objectColor = ObjectColor;

    if (UseAmbient)
        ...

    if (UseDiffuseLight)
        ...

    // etc...
}

然而表现不佳。

然后我创建了多种技术来避免 if 分支,因此每种技术只使用特定对象需要的东西。因此,如果对象不接受阴影,则那里没有代码。如果它不产生镜面光,则没有代码。另外,我将常用功能分组为函数。像这样(现在是真正的代码):

float4 Textured_PixelShader(Textured_VsOut input) : COLOR0
{
    float4 lightLevel = 0;
    float4 objectColor = GetObjColorTex(input.TexUV);

    // calculate main table lighting
    PsAddTableLight(input.WorldPosition, input.Normal, lightLevel);

    // calculate cloth lighting
    PsAddClothLight(input.WorldPosition, input.Normal, lightLevel);

    // calculate fill light - do we need it?
    PsAddFillLight(input.Normal, lightLevel);

    // add ambient light
    PsAddAmbientLight(lightLevel);

    // apply light
    PsApplyDiffuseLight(lightLevel, objectColor);

    // add speculars
    PsAddSpecularBlurred(input.WorldPosition, input.Normal, objectColor);

    // final work
    return PsFinalize(objectColor);
}

性能提升巨大。

但是我在维护这个着色器时迷失了方向。每隔一天,我就需要添加一项新技术,因为还没有可以满足 texture + lightmap + this_kind_of_shadow + specular 或任何我需要的组合。它们中的一些得到这样的名字,其他的以它们所使用的对象命名,因为只有一个对象具有这样的组合。它变得一团糟。

我有两个问题:

Why cannot I have those if statements? I read a lot about how conditional execution hurts GPUs, but my ifs only depend on shader parameters which have same value for all rendered pixels (or verts). Why cannot they be fast? I really miss them.

想法:

主要问题在这里,编译器只看到一个布尔表达式,而不是你的变量是着色器常量的语义信息。这只是一种特殊情况并且可能会变得复杂,例如,如果您使用更复杂的布尔表达式和函数,它也只使用着色器常量。我认为为了防止着色器编译器开发人员的很多麻烦,他们选择让 if 尽可能通用。

事实:

HLSL-Documentation for if所述,if有两种模式,flattenbranch。使用 flatten 时,编译器会推出 if 的两边,因此首先计算它们,然后从右侧获取结果。使用 branch 时,只执行右侧,因为首先计算布尔值,但这种模式只能在您不使用任何梯度函数(如 tex2D)时使用,因为它们依赖于相邻片段,因此需要在每个片段上执行,不得跳过。在你的情况下,我很确定你正在使用这样的函数,所以编译器为你的 ifs 选择 flatten,导致完全执行和缓慢的着色器。

What is the best way to divide this code into different shaders / techniques / files. Are there any good standards or rules?

据我所知,没有好的标准,但我认为像 Unity 或 Unreal 这样的引擎是深入了解它们如何管理着色器的好地方。上次我查看 Unreal 时,他们会在需要时动态生成每个着色器,因此着色器代码是从他们的着色器构建器生成的,然后进行编译。

在我自己的小引擎中,我使用了与您类似的方法,但我使用的不是动态分支,而是预处理器指令 #if, #elif, #else, and #endif. If the engine encounters a needeed shaders, it sets the right defines and then compiles them on the fly with D3DCompile。为了防止卡顿,我将编译好的着色器保存到光盘,在编译之前,我正在寻找着色器,如果它之前已经编译过的话。