LensFlareComponent.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. //-----------------------------------------------------------------------------
  2. // LensFlareComponent.cs
  3. //
  4. // Microsoft XNA Community Game Platform
  5. // Copyright (C) Microsoft Corporation. All rights reserved.
  6. //-----------------------------------------------------------------------------
  7. using System;
  8. using Microsoft.Xna.Framework;
  9. using Microsoft.Xna.Framework.Graphics;
  10. namespace LensFlare
  11. {
  12. /// <summary>
  13. /// Reusable component for drawing a lensflare effect over the top of a 3D scene.
  14. /// </summary>
  15. public class LensFlareComponent : DrawableGameComponent
  16. {
  17. // How big is the circular glow effect?
  18. const float glowSize = 400;
  19. // How big a rectangle should we examine when issuing our occlusion queries?
  20. // Increasing this makes the flares fade out more gradually when the sun goes
  21. // behind scenery, while smaller query areas cause sudden on/off transitions.
  22. const float querySize = 100;
  23. // These are set by the main game to tell us the position of the camera and sun.
  24. public Matrix View;
  25. public Matrix Projection;
  26. public Vector3 LightDirection = Vector3.Normalize(new Vector3(-1, -0.1f, 0.3f));
  27. // Computed by UpdateOcclusion, which projects LightDirection into screenspace.
  28. Vector2 lightPosition;
  29. bool lightBehindCamera;
  30. // Graphics objects.
  31. Texture2D glowSprite;
  32. SpriteBatch spriteBatch;
  33. BasicEffect basicEffect;
  34. VertexPositionColor[] queryVertices;
  35. // Custom blend state so the occlusion query polygons do not show up on screen.
  36. static readonly BlendState ColorWriteDisable = new BlendState
  37. {
  38. ColorWriteChannels = ColorWriteChannels.None
  39. };
  40. // An occlusion query is used to detect when the sun is hidden behind scenery.
  41. OcclusionQuery occlusionQuery;
  42. bool occlusionQueryActive;
  43. float occlusionAlpha;
  44. // The lensflare effect is made up from several individual flare graphics,
  45. // which move across the screen depending on the position of the sun. This
  46. // helper class keeps track of the position, size, and color for each flare.
  47. class Flare
  48. {
  49. public Flare(float position, float scale, Color color, string textureName)
  50. {
  51. Position = position;
  52. Scale = scale;
  53. Color = color;
  54. TextureName = textureName;
  55. }
  56. public float Position;
  57. public float Scale;
  58. public Color Color;
  59. public string TextureName;
  60. public Texture2D Texture;
  61. }
  62. // Array describes the position, size, color, and texture for each individual
  63. // flare graphic. The position value lies on a line between the sun and the
  64. // center of the screen. Zero places a flare directly over the top of the sun,
  65. // one is exactly in the middle of the screen, fractional positions lie in
  66. // between these two points, while negative values or positions greater than
  67. // one will move the flares outward toward the edge of the screen. Changing
  68. // the number of flares, or tweaking their positions and colors, can produce
  69. // a wide range of different lensflare effects without altering any other code.
  70. Flare[] flares =
  71. {
  72. new Flare(-0.5f, 0.7f, new Color( 50, 25, 50), "flare1"),
  73. new Flare( 0.3f, 0.4f, new Color(100, 255, 200), "flare1"),
  74. new Flare( 1.2f, 1.0f, new Color(100, 50, 50), "flare1"),
  75. new Flare( 1.5f, 1.5f, new Color( 50, 100, 50), "flare1"),
  76. new Flare(-0.3f, 0.7f, new Color(200, 50, 50), "flare2"),
  77. new Flare( 0.6f, 0.9f, new Color( 50, 100, 50), "flare2"),
  78. new Flare( 0.7f, 0.4f, new Color( 50, 200, 200), "flare2"),
  79. new Flare(-0.7f, 0.7f, new Color( 50, 100, 25), "flare3"),
  80. new Flare( 0.0f, 0.6f, new Color( 25, 25, 25), "flare3"),
  81. new Flare( 2.0f, 1.4f, new Color( 25, 50, 100), "flare3"),
  82. };
  83. /// <summary>
  84. /// Constructs a new lensflare component.
  85. /// </summary>
  86. public LensFlareComponent(Game game)
  87. : base(game)
  88. {
  89. }
  90. /// <summary>
  91. /// Loads the content used by the lensflare component.
  92. /// </summary>
  93. protected override void LoadContent()
  94. {
  95. // Create a SpriteBatch for drawing the glow and flare sprites.
  96. spriteBatch = new SpriteBatch(GraphicsDevice);
  97. // Load the glow and flare textures.
  98. glowSprite = Game.Content.Load<Texture2D>("glow");
  99. foreach (Flare flare in flares)
  100. {
  101. flare.Texture = Game.Content.Load<Texture2D>(flare.TextureName);
  102. }
  103. // Effect for drawing occlusion query polygons.
  104. basicEffect = new BasicEffect(GraphicsDevice);
  105. basicEffect.View = Matrix.Identity;
  106. basicEffect.VertexColorEnabled = true;
  107. // Create vertex data for the occlusion query polygons.
  108. queryVertices = new VertexPositionColor[4];
  109. queryVertices[0].Position = new Vector3(-querySize / 2, -querySize / 2, -1);
  110. queryVertices[1].Position = new Vector3( querySize / 2, -querySize / 2, -1);
  111. queryVertices[2].Position = new Vector3(-querySize / 2, querySize / 2, -1);
  112. queryVertices[3].Position = new Vector3( querySize / 2, querySize / 2, -1);
  113. // Create the occlusion query object.
  114. occlusionQuery = new OcclusionQuery(GraphicsDevice);
  115. }
  116. /// <summary>
  117. /// Draws the lensflare component.
  118. /// </summary>
  119. public override void Draw(GameTime gameTime)
  120. {
  121. // Check whether the light is hidden behind the scenery.
  122. UpdateOcclusion();
  123. // Draw the flare effect.
  124. DrawGlow();
  125. DrawFlares();
  126. RestoreRenderStates();
  127. }
  128. /// <summary>
  129. /// Mesures how much of the sun is visible, by drawing a small rectangle,
  130. /// centered on the sun, but with the depth set to as far away as possible,
  131. /// and using an occlusion query to measure how many of these very-far-away
  132. /// pixels are not hidden behind the terrain.
  133. ///
  134. /// The problem with occlusion queries is that the graphics card runs in
  135. /// parallel with the CPU. When you issue drawing commands, they are just
  136. /// stored in a buffer, and the graphics card can be as much as a frame delayed
  137. /// in getting around to processing the commands from that buffer. This means
  138. /// that even after we issue our occlusion query, the occlusion results will
  139. /// not be available until later, after the graphics card finishes processing
  140. /// these commands.
  141. ///
  142. /// It would slow our game down too much if we waited for the graphics card,
  143. /// so instead we delay our occlusion processing by one frame. Each time
  144. /// around the game loop, we read back the occlusion results from the previous
  145. /// frame, then issue a new occlusion query ready for the next frame to read
  146. /// its result. This keeps the data flowing smoothly between the CPU and GPU,
  147. /// but also causes our data to be a frame out of date: we are deciding
  148. /// whether or not to draw our lensflare effect based on whether it was
  149. /// visible in the previous frame, as opposed to the current one! Fortunately,
  150. /// the camera tends to move slowly, and the lensflare fades in and out
  151. /// smoothly as it goes behind the scenery, so this out-by-one-frame error
  152. /// is not too noticeable in practice.
  153. /// </summary>
  154. public void UpdateOcclusion()
  155. {
  156. // The sun is infinitely distant, so it should not be affected by the
  157. // position of the camera. Floating point math doesn't support infinitely
  158. // distant vectors, but we can get the same result by making a copy of our
  159. // view matrix, then resetting the view translation to zero. Pretending the
  160. // camera has not moved position gives the same result as if the camera
  161. // was moving, but the light was infinitely far away. If our flares came
  162. // from a local object rather than the sun, we would use the original view
  163. // matrix here.
  164. Matrix infiniteView = View;
  165. infiniteView.Translation = Vector3.Zero;
  166. // Project the light position into 2D screen space.
  167. Viewport viewport = GraphicsDevice.Viewport;
  168. Vector3 projectedPosition = viewport.Project(-LightDirection, Projection,
  169. infiniteView, Matrix.Identity);
  170. // Don't draw any flares if the light is behind the camera.
  171. if ((projectedPosition.Z < 0) || (projectedPosition.Z > 1))
  172. {
  173. lightBehindCamera = true;
  174. return;
  175. }
  176. lightPosition = new Vector2(projectedPosition.X, projectedPosition.Y);
  177. lightBehindCamera = false;
  178. if (occlusionQueryActive)
  179. {
  180. // If the previous query has not yet completed, wait until it does.
  181. if (!occlusionQuery.IsComplete)
  182. return;
  183. // Use the occlusion query pixel count to work
  184. // out what percentage of the sun is visible.
  185. const float queryArea = querySize * querySize;
  186. occlusionAlpha = Math.Min(occlusionQuery.PixelCount / queryArea, 1);
  187. // On mobile platforms, we can't use occlusion queries,
  188. // so just assume the sun is always visible
  189. // TODO ___MOBILE___ occlusionAlpha = 1.0f;
  190. }
  191. // Set renderstates for drawing the occlusion query geometry. We want depth
  192. // tests enabled, but depth writes disabled, and we disable color writes
  193. // to prevent this query polygon actually showing up on the screen.
  194. GraphicsDevice.BlendState = ColorWriteDisable;
  195. GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
  196. // Set up our BasicEffect to center on the current 2D light position.
  197. basicEffect.World = Matrix.CreateTranslation(lightPosition.X,
  198. lightPosition.Y, 0);
  199. basicEffect.Projection = Matrix.CreateOrthographicOffCenter(0,
  200. viewport.Width,
  201. viewport.Height,
  202. 0, 0, 1);
  203. basicEffect.CurrentTechnique.Passes[0].Apply();
  204. // Issue the occlusion query.
  205. occlusionQuery.Begin();
  206. GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, queryVertices, 0, 2);
  207. occlusionQuery.End();
  208. occlusionQueryActive = true;
  209. // On mobile platforms, render the query geometry without occlusion testing
  210. /* TODO GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, queryVertices, 0, 2);
  211. occlusionQueryActive = true; */
  212. }
  213. /// <summary>
  214. /// Draws a large circular glow sprite, centered on the sun.
  215. /// </summary>
  216. public void DrawGlow()
  217. {
  218. if (lightBehindCamera || occlusionAlpha <= 0)
  219. return;
  220. Color color = Color.White * occlusionAlpha;
  221. Vector2 origin = new Vector2(glowSprite.Width, glowSprite.Height) / 2;
  222. float scale = glowSize * 2 / glowSprite.Width;
  223. spriteBatch.Begin();
  224. spriteBatch.Draw(glowSprite, lightPosition, null, color, 0,
  225. origin, scale, SpriteEffects.None, 0);
  226. spriteBatch.End();
  227. }
  228. /// <summary>
  229. /// Draws the lensflare sprites, computing the position
  230. /// of each one based on the current angle of the sun.
  231. /// </summary>
  232. public void DrawFlares()
  233. {
  234. if (lightBehindCamera || occlusionAlpha <= 0)
  235. return;
  236. Viewport viewport = GraphicsDevice.Viewport;
  237. // Lensflare sprites are positioned at intervals along a line that
  238. // runs from the 2D light position toward the center of the screen.
  239. Vector2 screenCenter = new Vector2(viewport.Width, viewport.Height) / 2;
  240. Vector2 flareVector = screenCenter - lightPosition;
  241. // Draw the flare sprites using additive blending.
  242. spriteBatch.Begin(0, BlendState.Additive);
  243. foreach (Flare flare in flares)
  244. {
  245. // Compute the position of this flare sprite.
  246. Vector2 flarePosition = lightPosition + flareVector * flare.Position;
  247. // Set the flare alpha based on the previous occlusion query result.
  248. Vector4 flareColor = flare.Color.ToVector4();
  249. flareColor.W *= occlusionAlpha;
  250. // Center the sprite texture.
  251. Vector2 flareOrigin = new Vector2(flare.Texture.Width,
  252. flare.Texture.Height) / 2;
  253. // Draw the flare.
  254. spriteBatch.Draw(flare.Texture, flarePosition, null,
  255. new Color(flareColor), 1, flareOrigin,
  256. flare.Scale, SpriteEffects.None, 0);
  257. }
  258. spriteBatch.End();
  259. }
  260. /// <summary>
  261. /// Sets renderstates back to their default values after we finish drawing
  262. /// the lensflare, to avoid messing up the 3D terrain rendering.
  263. /// </summary>
  264. void RestoreRenderStates()
  265. {
  266. GraphicsDevice.BlendState = BlendState.Opaque;
  267. GraphicsDevice.DepthStencilState = DepthStencilState.Default;
  268. GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
  269. }
  270. }
  271. }