Implementing lens flare within the HiDef profile
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 within a HiDef profile.
Getting ready
This recipe assumes that you've already got a scene, rendering correctly, albeit without a lens flare.
How to do it...
To create a lens flare within the HiDef profile:
1. Create a new class to hold the lens flare behavior:
class HiDefLensFlare {
2. Add some instance-level variables to hold the details of the occlusion test, the lighting, and the glow image:
SpriteBatch spriteBatch; GraphicsDevice graphicsDevice; public BasicEffect ShadowCaptureEffect; OcclusionQuery occlusionQuery; bool occlusionQueryActive; float occlusionAlpha; const float querySize = 50; VertexPositionColor[] queryVertices; public Vector3 LightDirection = Vector3.Normalize(new Vector3(0.5f, -0.1f, 0.5f)); Vector2 lightPosition; bool lightBehindCamera; Texture2D glow; Vector2 glowOrigin; float glowScale = 400f;
3. Next, add a constructor to set up the occlusion test prerequisites and load the glow texture:
public HiDefLensFlare(GraphicsDevice graphicsDevice, ContentManager content) { this.graphicsDevice = graphicsDevice; spriteBatch = new SpriteBatch(graphicsDevice); ShadowCaptureEffect = new BasicEffect(graphicsDevice) { View = Matrix.Identity, VertexColorEnabled = true }; occlusionQuery = new OcclusionQuery(graphicsDevice); 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); glow = content.Load<Texture2D>(@"lensflare/glow"); 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; var viewport = graphicsDevice.Viewport; var projectedPosition = viewport.Project( -LightDirection, projection, infiniteView, Matrix.Identity); 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 (!occlusionQuery.IsComplete) { return; } const float queryArea = querySize * querySize; occlusionAlpha = Math.Min( occlusionQuery.PixelCount / queryArea, 1); }
8. Set up for the next occlusion query:
graphicsDevice.BlendState = ColorWriteDisable; graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; 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:
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: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(); }
How it works...
XNA and the underlying DirectX infrastructure contain a rather handy little diagnostic tool in the form of the 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 example 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.