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.
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. */
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 (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.
No comments:
Post a Comment