More Post-Processing Tricks: Lens Flare

I was playing Killzone 2 the other day, which reminded me of the lens flare trick they used.  Unlike most games, which use some sprites controlled by an occlusion query, they applied the effect as a post-process similar to bloom.  The upside is that it works for all bright areas and not pre-defined areas (the sun), and you don’t have to do occlusion queries or anything like that since that’s handled automatically.  Plus it’s really easy to fit it into a post-processing chain, since you can use your bloom results as the input.  The downside is that it’s pretty far from realistic…I’m not sure that most would like the end result.  This screen here shows the effect pretty clearly (it’s the orange and purple blobby areas by the left bad guy’s head, on the opposite side of the screen from the bright light source).

I haven’t seen anyone duplicate or even discuss the technique since before the game out, so I figured I’d take a crack at deciphering it myself.  After some experimenting I came up with the following basic approach:

1.  Render a bloom buffer using standard downscale + threshold + blur 2.  Flip the texture coordinates by doing float2(1, 1) - texCoord 3.  Blur both towards the center of the screen and away from it 4.  Combine additively with the bloom buffer

To fake a chromatic aberration, Killzone 2 uses a strong orange tint for areas closer to the center of the screen and a purple tint on the periphery.  Upon some further close analysis it started to look like they were doing it in two passes with a different tint and different texture coordinate scaling for each pass.  I decided to make my implementation the same,  so I could produce similar results.  This is the shader code I came up with:

const static float4 vPurple = float4(0.7f, 0.2f, 0.9f, 1.0f);
const static float4 vOrange = float4(0.7f, 0.4f, 0.2f, 1.0f);
const static float fThreshold = 0.1f;

float4 LensFlarePS (    in float2 in_vTexCoord    : TEXCOORD0,
uniform int NumSamples,
uniform float4 vTint,
uniform float fTexScale,
uniform float fBlurScale)    : COLOR0
{
    // The flare should appear on the opposite side of the screen as the
    // source of the light, so first we mirror the texture coordinate.
    // Then we normalize so we can apply a scaling factor.
    float2 vMirrorCoord = float2(1.0f, 1.0f) - in_vTexCoord;
    float2 vNormalizedCoord = vMirrorCoord * 2.0f - 1.0f;
    vNormalizedCoord *= fTexScale;

    // We'll blur towards the center of screen, and also away from it.
    float2 vTowardCenter = normalize(-vNormalizedCoord);
    float2 fBlurDist = fBlurScale * NumSamples;
    float2 vStartPoint = vNormalizedCoord + ((vTowardCenter / g_vSourceDimensions) * fBlurDist);
    float2 vStep = -(vTowardCenter / g_vSourceDimensions) * 2 * fBlurDist;

    // Do the blur and sum the samples
    float4 vSum = 0;
    float2 vSamplePos = vStartPoint;
    for (int i = 0; i < NumSamples; i++)
    {
        float2 vSampleTexCoord = vSamplePos * 0.5f + 0.5f;

        // Don't add in samples past texture border
        if (vSampleTexCoord.x >= 0 && vSampleTexCoord.x <= 1.0f
            && vSampleTexCoord.y >=0 && vSampleTexCoord.y <= 1.0f)
        {
            float4 vSample = tex2D(PointSampler0, vSampleTexCoord);
            vSum +=  max(0, vSample - fThreshold) * vTint;
        }

        vSamplePos += vStep;
    }

    return vSum / NumSamples;
}

float4 CombinePS (in float2 in_vTexCoord    : TEXCOORD0) : COLOR0
{
    float4 vColor = tex2D(PointSampler0, in_vTexCoord);
    vColor += tex2D(PointSampler1, in_vTexCoord);
    vColor += tex2D(PointSampler2, in_vTexCoord);
    return vColor;
}

technique LensFlareFirstPass
{
    pass p0
    {
        VertexShader = compile vs_3_0 PostProcessVS();
        PixelShader = compile ps_3_0 LensFlarePS(12, vOrange, 2.00f, 0.15f);

        ZEnable = false;
        ZWriteEnable = false;
        AlphaBlendEnable = false;
        AlphaTestEnable = false;
        StencilEnable = false;
    }
}

technique LensFlareSecondPass
{
    pass p0
    {
        VertexShader = compile vs_3_0 PostProcessVS();
        PixelShader = compile ps_3_0 LensFlarePS(12, vPurple, 0.5f, 0.1f);

        ZEnable = false;
        ZWriteEnable = false;
        AlphaBlendEnable = false;
        AlphaTestEnable = false;
        StencilEnable = false;
    }
}

Obviously the code is severely unoptimized, but it’s late and I’m tired.  Here’s a screen of what it looks like (ignore the obnoxious brightness and bloom, please):




Comments:

moorx -

looks great! thanks for sharing!


#### [gjoel]( "") -

Hey - the technique was taken from the kawase gdc-presentation from 2003 http://www.daionet.gr.jp/~masa/column/2003-03-21.html ( http://www.daionet.gr.jp/~masa/column/index.html ) ( http://www.daionet.gr.jp/~masa/rthdribl/index.html ) Cheers!


#### [Post Processing Trick from MJP « Sgt. Conker](http://www.sgtconker.com/2009/12/post-processing-trick-from-mjp/ "") -

[…] Matt Pettineo took a crack at a trick used in Killzone 2 for Lens Flare, and decided to share his new shader with the world. […]


#### [Lens And Flair « Sgt. Conker](http://www.sgtconker.com/2009/12/lens-and-flair/ "") -

[…] Shows some code. […]