Teach Your Effects A New Trick

The Effects Framework is a pretty damn awesome tool.  However I’m afraid that’s not totally obvious to a lot of newbies, who either just don’t what it can do or haven’t been exposed to some of the situations where Effect’s can really come in handy.

One neat thing Effect’s can do that isn’t obvious from the documentation or samples is auto-generate variants of shaders for you based on the value of uniform parameters.  For instance let’s take a common scenario: lets say you have a shader for model, and you need it to work for either a point light, a spot light, or a directional light one-at-a-time.  You might write your shader code like this:

int g_iLightType;

float4 ModelPixelShader(in PSInput input) : COLOR0
{
    float4 vColor;
    if (g_iLightType == LIGHT_TYPE_POINT)
        vColor = DoPointLighting(input);
    else if (g_iLightType == LIGHT_TYPE_SPOT)
        vColor = DoSpotLighting(input);
    else
        vColor = DoDirectionalLighting(input);

    return vColor;
}

Alright, so this works.  The app sets the  g_iLightType shader parameter, and the right calculations get used. However is it optimal?  We’ve got these if statements in there, and maybe we’re not sure what they’ll get compiled into depending on the shader profile we’re targetting.  And maybe we’re not sure what the heck the driver is going to do once it gets the compiled shader.  Wouldn’t it be nice if we could avoid that?  Of course it would.  So let’s make some small changes:

float4 ModelPixelShader(in PSInput input, uniform int iLightType) : COLOR0

{   
    float4 vColor;
    if (iLightType == LIGHT_TYPE_POINT)
        vColor = DoPointLighting(input);
    else if (iLightType == LIGHT_TYPE_SPOT)
        vColor = DoSpotLighting(input);
    else
        vColor = DoDirectionalLighting(input);
}


technique PointLight
{
    pass p0
    {
        VertexShader = compile vs_2_0 ModelVertexShader();
        PixelShader = compile ps_2_0 ModelPixelShader(LIGHT_TYPE_POINT);       
    }
}

technique SpotLight
{
    pass p0
    {
        VertexShader = compile vs_2_0 ModelVertexShader();
        PixelShader = compile ps_2_0 ModelPixelShader(LIGHT_TYPE_SPOT);       
    }
}

technique DirectionalLight
{
    pass p0
    {
        VertexShader = compile vs_2_0 ModelVertexShader();
        PixelShader = compile ps_2_0 ModelPixelShader(LIGHT_TYPE_DIRECTIONAL);       
    }
}

Very similar, but one big difference: the HLSL code branches on a uniform int parameter to the pixel shader function, whose value is set in our technique declaration.  This means that the Effect knows that this parameter has a constant value for that entire technique, which allows it to generate a seperate shader for each technique where the parameter is a constant and not a variable.  Since it’s a constant for each shader variant, no branching of any sort is necessary.  Now our app just picks the technique it wants for each light source it’s handling, rather than setting a shader parameter.

Now keep in mind that using separate shaders like this will have performance implications:  switching vertex or pixel shaders has an associated overhead, and if you auto-generate different variants like we did above you’ll be switching shaders more than if you used one big shader.  Whether or not it’s a performance win will depend on what you’re doing.  However, it’s always good to be aware of all the neat tricks your tools can pull off.