Monday, August 5, 2013

11: Lens Flare Effect


      The Cyclone Game Engine can now render a lens flare effect. It uses hardware occlusion queries to efficiently detect whether or not the sun is hidden behind the landscape. This will allow me to fade out the sun when the sun is not visible. Modern GPUs are very good at determining whether one set of polygons is obscured by another set. We can use this to our advantage when creating lens flares.


By changing the light direction vector I was able to position the lens flare over the sun on the sky-box. It took a while to position it just right but I eventually got it. 


Source Code below based on the XNA 4.0 Cookbook's sample:

How it Works
       XNA and the underlying DirectX infrastructure contain a handy diagnostic tool in the form of occlusion test. With this test, you can count how many pixels were filled when trying to render a particular portion of a scene.

       We utilize this in the lens flare by attempting to render a small rectangle across the opposite side of the scene from the player's viewpoint, and measuring how much of it is obscured by the scene's meshes. With this number, we adjust the opacity of the lens flare's glow texture up or down to simulate the sun disappearing either partially or completely behind an object.

1. Create a new class to hold the lens flare behavior:
class LensFlare
{


2. Add some instance-level variables to hold the details of the occlusion test,
the lighting, and the glow image:
// Graphics objects
SpriteBatch spriteBatch;
GraphicsDevice graphicsDevice;
public BasicEffect ShadowCaptureEffect;
VertexPositionColor[] queryVertices;
Texture2D glow;

// An occlusion query is used to detect when the sun is hidden behind scenery
OcclusionQuery occlusionQuery;
bool occlusionQueryActive;
float occlusionAlpha;

// Light Direction
public Vector3 LightDirection = Vector3.Normalize(new
Vector3(0.5f, -0.1f, 0.5f));

// Computed on the ContentManger and then updated, which projects the Light Direction into screenspace.
Vector2 lightPosition;
bool lightBehindCamera;

Vector2 glowOrigin;

// How big is the circular glow effect?
float glowScale = 400f

/* How big a rectangle should we examine when issuing our occlusion queries?
Increasing this makes the flare fade our more gradually when the sun goes behind scenery, while smaller query areas cause sudden on/off transitions. */
const float querySize = 50;


3. Next, add a constructor to set up the occlusion test prerequisites and load the
glow texture:
public LensFlare(GraphicsDevice graphicsDevice,
ContentManager content)
{
     this.graphicsDevice = graphicsDevice;


// Create a SpriteBatch for drawing the glow and flare sprites.
spriteBatch = new SpriteBatch(graphicsDevice);


// Effect for drawing occlusion query polygons.
ShadowCaptureEffect = new BasicEffect(graphicsDevice)
{
     View = Matrix.Identity,
     VertexColorEnabled = true
};


// Creat the occlusion query object.
occlusionQuery = new OcclusionQuery(graphicsDevice);


// Create vertex data for the occlusion query polygons
queryVertices = new VertexPositionColor[4];
queryVertices[0].Position = new Vector3(-querySize / 2,
-querySize / 2, -1);
queryVertices[1].Position = new Vector3(querySize / 2,
-querySize / 2, -1);
queryVertices[2].Position = new Vector3(-querySize / 2,
querySize / 2, -1);
queryVertices[3].Position = new Vector3(querySize / 2,
querySize / 2, -1);


// Load the glow and flare textures
glow = content.Load<Texture2D>(@"lensflare/glow");


// Center the sprite texture
glowOrigin = new Vector2(glow.Width, glow.Height) / 2;
}


4. Create a new BlendState instance-level variable so the ocular test can proceed
without changing the visible image:
static readonly BlendState ColorWriteDisable = new BlendState
{
     ColorWriteChannels = ColorWriteChannels.None
};


5. Add a new method to perform the ocular test:
public void Measure(Matrix view, Matrix projection)
{


6. Calculate the position of the lens flare on screen, and exit early if it's behind the
player's viewpoint:
var infiniteView = view;
infiniteView.Translation = Vector3.Zero;

// Project the light position into 2D screen space.
var viewport = graphicsDevice.Viewport;
var projectedPosition = viewport.Project(
-LightDirection, projection,
infiniteView, Matrix.Identity);


// Don't draw any flares if the light is behind the camera
if ((projectedPosition.Z < 0) ||
(projectedPosition.Z > 1))
{
     lightBehindCamera = true;
     return;
}

lightPosition = new Vector2(projectedPosition.X,
projectedPosition.Y);
lightBehindCamera = false;


7. Add the calculation for how much of the lens flare test area is occluded by the scene once the previous occlusion test has completed: 

if (occlusionQueryActive)
{

// If the previous query has not yet completed, wait until it does.
if (!occlusionQuery.IsComplete)
{
     return;
}

// Use the occlusion query pixel count to work
// out what percentages of the sun is visible.
const float queryArea = querySize * querySize;

occlusionAlpha = Math.Min(
occlusionQuery.PixelCount / queryArea, 1);
}


8. Set up for the next occlusion query:

// Set renderstates for drawing the occlusion query geometry. We want depth
// tests enabled, but depth writes disabled, and we disable color writes
// to prevent this query polygon actually showing up on the screen.
graphicsDevice.BlendState = ColorWriteDisable;
graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;


// Set up our Effect to center on the current 2D light position.
ShadowCaptureEffect.World = Matrix.CreateTranslation(
lightPosition.X,
lightPosition.Y, 0);

ShadowCaptureEffect.Projection = Matrix.
CreateOrthographicOffCenter(0,
viewport.Width,
viewport.Height,
0, 0, 1);
ShadowCaptureEffect.CurrentTechnique.Passes[0].Apply();


9. Render the lens flare test vertices inside the occlusion test to determine how many
pixels were rendered:

// Issue the occlusion query.
occlusionQuery.Begin();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
queryVertices, 0, 2);
occlusionQuery.End();
occlusionQueryActive = true;


10. Complete the class by adding a Draw() method to render the glow:

///<summary>
/// Draws a large circular glow sprite, centered on the sun.
/// </summary>
public void Draw()
{
      if (lightBehindCamera || occlusionAlpha <= 0)
      return;

     Color color = Color.White * occlusionAlpha;
     Vector2 origin = new Vector2(glow.Width, glow.Height) / 2;
     float scale = glowScale * 2 / glow.Width;


     spriteBatch.Begin();
     spriteBatch.Draw(glow, lightPosition, null, color, 0,
     origin, scale, SpriteEffects.None, 0);
     spriteBatch.End();
}


You can also download the XNA App Hub Community website's Lens Flare Effect Sample here. If you found this blog post helpful, please comment below.