2

我有一个足够简单的着色器,它支持多个点光源。
灯光存储为 Light 结构数组(最大大小),当它发生变化时,我会传入活动灯光的数量。
问题出在 PixelShader 函数中:
它是基本的东西,从纹理中获取基色,循环从 0 到 numActiveLights 的灯光数组并添加效果,它工作正常,但性能很糟糕!
但是,如果我用相同值的常量替换对全局变量 numActiveLights 的引用,则性能很好。
我只是无法理解为什么引用变量会产生 30+ fps 的差异。

谁能解释一下?

完整的着色器代码:

#define MAX_POINT_LIGHTS 16

struct PointLight
{
    float3      Position;
    float4      Color;
    float       Radius;
};

float4x4    World;
float4x4    View;
float4x4    Projection;
float3  CameraPosition;

float4  SpecularColor;
float   SpecularPower;
float   SpecularIntensity;
float4      AmbientColor;
float   AmbientIntensity;
float   DiffuseIntensity;   

int     activeLights;
PointLight  lights[MAX_POINT_LIGHTS];

bool    IsLightingEnabled;
bool    IsAmbientLightingEnabled;
bool    IsDiffuseLightingEnabled;
bool    IsSpecularLightingEnabled;


Texture Texture;
sampler TextureSampler = sampler_state
{
    Texture = <Texture>;

    Magfilter = POINT;
    Minfilter = POINT;
    Mipfilter = POINT;

    AddressU = WRAP;
    AddressV = WRAP;
};

struct VS_INPUT
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : NORMAL0;
};

struct VS_OUTPUT
{
    float3 WorldPosition : TEXCOORD0;
    float4 Position : POSITION0;
    float3 Normal : TEXCOORD1;
    float2 TexCoord : TEXCOORD2;
    float3 ViewDir : TEXCOORD3;

};

VS_OUTPUT VS_PointLighting(VS_INPUT input)
{
    VS_OUTPUT output;

    float4 worldPosition = mul(input.Position, World);
    output.WorldPosition = worldPosition;

    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.Normal = normalize(mul(input.Normal, World));
    output.TexCoord = input.TexCoord;
    output.ViewDir = normalize(CameraPosition -  worldPosition);

    return output;
}

float4 PS_PointLighting(VS_OUTPUT IN) : COLOR
{
    if(!IsLightingEnabled) return tex2D(TextureSampler,IN.TexCoord);

    float4 color = float4(0.0f, 0.0f, 0.0f, 0.0f);

    float3 n = normalize(IN.Normal);
    float3 v = normalize(IN.ViewDir);
    float3 l = float3(0.0f, 0.0f, 0.0f);
    float3 h = float3(0.0f, 0.0f, 0.0f);

    float atten = 0.0f;
    float nDotL = 0.0f;
    float power = 0.0f;

    if(IsAmbientLightingEnabled) color += (AmbientColor*AmbientIntensity);

    if(IsDiffuseLightingEnabled || IsSpecularLightingEnabled)
    {
        //for (int i = 0; i < activeLights; ++i)//works but perfoemnce is terrible
        for (int i = 0; i < 7; ++i)//performance is fine but obviously isn't dynamic
        {
            l = (lights[i].Position - IN.WorldPosition) / lights[i].Radius;
            atten = saturate(1.0f - dot(l, l));

            l = normalize(l);

            nDotL = saturate(dot(n, l));

            if(IsDiffuseLightingEnabled) color += (lights[i].Color * nDotL * atten);
            if(IsSpecularLightingEnabled) color += (SpecularColor * SpecularPower * atten);
        }
    }

    return color * tex2D(TextureSampler, IN.TexCoord);
}

technique PerPixelPointLighting
{
    pass
    {
        VertexShader = compile vs_3_0 VS_PointLighting();
        PixelShader = compile ps_3_0 PS_PointLighting();
    }
}
4

2 回答 2

2

我的猜测是将循环约束更改为编译时常量允许 HLSL 编译器展开循环。也就是说,而不是这样:

for (int i = 0; i < 7; i++)
    doLoopyStuff();

它正在变成这样:

doLoopyStuff();
doLoopyStuff();
doLoopyStuff();
doLoopyStuff();
doLoopyStuff();
doLoopyStuff();
doLoopyStuff();

循环和条件分支可能会对着色器代码内部的性能造成重大影响,应尽可能避免。

编辑

这只是我的想法,但也许你可以尝试这样的事情?

for (int i = 0; i < MAX_LIGHTS; i++)
{
    color += step(i, activeLights) * lightingFunction();
}

通过这种方式,您可以计算所有可能的灯光,但对于不活动的灯光,总是得到 0 值。当然,好处取决于照明功能的复杂性;你需要做更多的分析。

于 2013-03-07T18:53:04.670 回答
1

尝试使用 PIX 对其进行分析。http://wtomandev.blogspot.com/2010/05/debugging-hlsl-shaders.html

或者,阅读这个漫无边际的猜测:

也许是因为有了常量,编译器可以解开和折叠循环的指令。当您将其替换为变量时,编译器将无法做出相同的假设。

尽管与您的实际问题有些无关,但我会将许多这些条件/计算推向软件级别。

if(IsDiffuseLightingEnabled || IsSpecularLightingEnabled)

^- 就这样。

另外,我认为您也可以在调用着色器程序之前预先计算一些东西。就像l = (lights[i].Position - IN.WorldPosition) / lights[i].Radius;传递一个预先计算好的数组,而不是每次都计算每个像素。

我可能对 HLSL 编译器所做的优化有误解,但我认为你在像素着色器上所做的每个计算都会在屏幕上执行 w*h 次(尽管这是疯狂并行完成的),而且我隐约记得有一些限制到你可以在着色器中拥有的指令数量(比如 72 条?)。(尽管我认为该限制在 HLSL 的更高版本中放宽了很多)。也许是因为你的着色器生成了这么多指令——也许它会破坏你的程序并在编译时将它变成一个多通道像素着色器。如果是这种情况,那可能会增加大量开销。

实际上,这里还有一个可能很愚蠢的想法:将变量传递给着色器让它将数据传输到 GPU。这种传输发生在有限的带宽内。也许编译器足够聪明,以至于当您仅静态索引数组中的前 7 个元素时,仅传输 7 个元素。当编译器没有进行优化时(因为您没有使用常量进行迭代),它会每帧推送整个数组,并且您正在淹没总线。如果是这样的话,那么我之前提出的推出计算,并传递更多结果的建议只会让问题变得更糟,呵呵。

于 2013-03-07T19:05:11.200 回答