Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
6845 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

The rendering process is very simple.

  • Render the scene to a floating point render target
  • Downsample that render target and suppress LDR values
  • Take the downsampled target and blur it horizontally
  • Blur this vertically
  • Tone map the original and blurred targets to get a displayable image

So, now we get to the interesting part, how things exactly work. We begin by rendering the textured "quad vertices" into a floating point render target. Now, we need to downsample this to 1/4th its size, suppressing the LDR values, so that we don't blur the entire scene. To do this, we simply use the original render target as a texture and render this onto another render target 1/4th the "original" size. To suppress the LDR values, the below function "SuppressLDR" is used. Kd is the material's diffuse color.

Downsample.fx
////////////////////////////////////////////////////////////
// Vertex shader
////////////////////////////////////////////////////////////

struct VS_OUT
{
  float4 Pos:  POSITION;
  float2 Tex:  TEXCOORD0;
};

VS_OUT vs_main( float3 inPos: POSITION, float2 inTex: TEXCOORD0 )
{
  VS_OUT OUT;

  // Output the transformed vertex
  OUT.Pos = mul( matMVP, float4( inPos, 1 ) );

  // Output the texture coordinates
  OUT.Tex = inTex + ( PixelOffset * 0.5 );

  return OUT;
}

////////////////////////////////////////////////////////////
// Pixel shader
////////////////////////////////////////////////////////////

float4 SuppressLDR( float4 c )
{
   if( c.r > 1.0f || c.g > 1.0f || c.b > 1.0f )
      return c;
   else
      return float4( 0.0f, 0.0f, 0.0f, 0.0f );
}

float4 ps_main( float2 inTex: TEXCOORD0 ) : COLOR0
{
  float4 color = tex2D( DownSampler, inTex ) * Kd;

  return SuppressLDR( color );
}

Next, we need to blur the downsampled image in order to "bleed" colors from the bright pixels into neighboring pixels. To do this, as mentioned earlier, we use a separable Gaussian filter. In the first pass, we blur pixels along the x-axis, we then take this image and blur it along the y-axis. Pretty simple, huh? The below code fragment is from BlurX.fx.

BlurX.fx
////////////////////////////////////////////////////////////
// Vertex shader
////////////////////////////////////////////////////////////

struct VS_OUT
{
  float4 Pos:  POSITION;
  float2 Tex:  TEXCOORD0;
};

VS_OUT vs_main( float3 inPos: POSITION, float2 inTex: TEXCOORD0 )
{
  VS_OUT OUT;

  // Output the transformed vertex
  OUT.Pos = mul( matMVP, float4( inPos, 1 ) );

  // Output the texture coordinates
  OUT.Tex = inTex + ( PixelOffset * 0.5 );

  return OUT;
}

////////////////////////////////////////////////////////////
// Pixel shader
////////////////////////////////////////////////////////////

float4 ps_main( float2 inTex: TEXCOORD0 ) : COLOR0
{
  float4 color = tex2D( BlurXSampler, inTex );

  // Sample pixels on either side
  for( int i = 0; i < 8; ++i )
  {
    color += tex2D( BlurXSampler, inTex + ( BlurOffset * i ) )
             * PixelWeight[i];
    color += tex2D( BlurXSampler, inTex - ( BlurOffset * i ) )
             * PixelWeight[i];
  }

  return color;
}

We just create a loop that adds weighted neighboring pixels. We sample 8 pixels from either side. BlurOffset is actually the per-pixel (or texel) width, and we multiply it by the iteration number "i" to get the coordinates of the "i"th pixel.

Blurring along the y-axis is the same, only we provide the per pixel height this time.

Now comes the most important part, compositing the original and blurred images and tone mapping them to get a displayable image. The below code sample is from ToneMapping.fx, which shows how to combine the 2 images.

Tonemapping.fx
////////////////////////////////////////////////////////////
// Vertex shader
////////////////////////////////////////////////////////////

struct VS_OUT
{
  float4 Pos:  POSITION;
  float2 Tex:  TEXCOORD0;
};

VS_OUT vs_main( float3 inPos: POSITION, float2 inTex: TEXCOORD0 )
{
  VS_OUT OUT;

  // Output the transformed vertex
  OUT.Pos = mul( matMVP, float4( inPos, 1 ) );

  // Output the texture coordinates
  OUT.Tex = inTex + ( PixelOffset * 0.5 );

  return OUT;
}

////////////////////////////////////////////////////////////
// Pixel shader
////////////////////////////////////////////////////////////

float4 ps_main( float2 inTex: TEXCOORD0 ) : COLOR0
{
  float4 original = tex2D( FullSampler, inTex );
  float4 blur     = tex2D( BlurSampler, inTex );

  float4 color    = lerp( original, blur, 0.4f );

  inTex       -= 0.5;
  float vignette  = 1 - dot( inTex, inTex );
  color       *= pow( vignette, 4.0 );

  color       *= fExposureLevel;

  return pow( color, 0.55 );
}

First, we simply lerp between the original and blurred colors. Then, we calculate the vignette (which is the square of the distance of the current pixel from the center of the screen) and multiply the lerped color by vignette raised to the fourth power. Finally, we multiply by the exposure level, which determines how "exposed" your finally image should be, just like you can set the exposure in a camera, and add a gamma correction.

Shown above are images of the light probe at different exposures.

Left-Top - Under exposed (0.5)

Right-Top - Properly exposed (2.5)

Left-Bottom - Over exposed (10.0)

Right-Bottom - Extremely over exposed (20.0)

To properly observe "glow" effects, you'll need either the grace-cube texture or the grace light probe from www.debevec.org.

Voila! Your first HDR app is done. This may not be the most efficient way to do things, since I only wanted to show the basics. So I leave the optimizing part to you. Masaki Kawase has implemented a different method for post-processing, and after implementing it I found out that it actually gives better performance, albeit not the same quality. There are other types of glows, like star, afterimage, etc... which I didn't discuss. You could try and implement them as well. Now let's see if your game comes out with HDR Rendering before Half-Life 2 does!

If you have any doubts, questions or suggestions, you can mail me at anidex@yahoo.com. Here is the source code RNL.zip.

References

  • Real-Time 3D Scene Post-processing. Jason L. Mitchell.

  • DirectX®9 Shading. Jason L. Mitchell.

  • Frame Buffer Post-processing Effects in DOUBLE-S.T.E.A.L (Wreckless). Masaki Kawase.

  • Light probes courtesy of www.debevec.org.





Contents
  Introduction
  Page 2

  Source code
  Printable version
  Discuss this article