ParticleSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. //-----------------------------------------------------------------------------
  2. // ParticleSystem.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.Content;
  10. using Microsoft.Xna.Framework.Graphics;
  11. using Microsoft.Xna.Framework.Graphics.PackedVector;
  12. namespace Particle3DSample
  13. {
  14. /// <summary>
  15. /// The main component in charge of displaying particles.
  16. /// </summary>
  17. public class ParticleSystem : DrawableGameComponent
  18. {
  19. // Name of the XML settings file describing this particle system.
  20. string settingsName;
  21. // Settings class controls the appearance and animation of this particle system.
  22. ParticleSettings settings;
  23. // For loading the effect and particle texture.
  24. ContentManager content;
  25. // Custom effect for drawing particles. This computes the particle
  26. // animation entirely in the vertex shader: no per-particle CPU work required!
  27. Effect particleEffect;
  28. // Shortcuts for accessing frequently changed effect parameters.
  29. EffectParameter effectViewParameter;
  30. EffectParameter effectProjectionParameter;
  31. EffectParameter effectViewportScaleParameter;
  32. EffectParameter effectTimeParameter;
  33. // An array of particles, treated as a circular queue.
  34. ParticleVertex[] particles;
  35. // A vertex buffer holding our particles. This contains the same data as
  36. // the particles array, but copied across to where the GPU can access it.
  37. DynamicVertexBuffer vertexBuffer;
  38. // Index buffer turns sets of four vertices into particle quads (pairs of triangles).
  39. IndexBuffer indexBuffer;
  40. // The particles array and vertex buffer are treated as a circular queue.
  41. // Initially, the entire contents of the array are free, because no particles
  42. // are in use. When a new particle is created, this is allocated from the
  43. // beginning of the array. If more than one particle is created, these will
  44. // always be stored in a consecutive block of array elements. Because all
  45. // particles last for the same amount of time, old particles will always be
  46. // removed in order from the start of this active particle region, so the
  47. // active and free regions will never be intermingled. Because the queue is
  48. // circular, there can be times when the active particle region wraps from the
  49. // end of the array back to the start. The queue uses modulo arithmetic to
  50. // handle these cases. For instance with a four entry queue we could have:
  51. //
  52. // 0
  53. // 1 - first active particle
  54. // 2
  55. // 3 - first free particle
  56. //
  57. // In this case, particles 1 and 2 are active, while 3 and 4 are free.
  58. // Using modulo arithmetic we could also have:
  59. //
  60. // 0
  61. // 1 - first free particle
  62. // 2
  63. // 3 - first active particle
  64. //
  65. // Here, 3 and 0 are active, while 1 and 2 are free.
  66. //
  67. // But wait! The full story is even more complex.
  68. //
  69. // When we create a new particle, we add them to our managed particles array.
  70. // We also need to copy this new data into the GPU vertex buffer, but we don't
  71. // want to do that straight away, because setting new data into a vertex buffer
  72. // can be an expensive operation. If we are going to be adding several particles
  73. // in a single frame, it is faster to initially just store them in our managed
  74. // array, and then later upload them all to the GPU in one single call. So our
  75. // queue also needs a region for storing new particles that have been added to
  76. // the managed array but not yet uploaded to the vertex buffer.
  77. //
  78. // Another issue occurs when old particles are retired. The CPU and GPU run
  79. // asynchronously, so the GPU will often still be busy drawing the previous
  80. // frame while the CPU is working on the next frame. This can cause a
  81. // synchronization problem if an old particle is retired, and then immediately
  82. // overwritten by a new one, because the CPU might try to change the contents
  83. // of the vertex buffer while the GPU is still busy drawing the old data from
  84. // it. Normally the graphics driver will take care of this by waiting until
  85. // the GPU has finished drawing inside the VertexBuffer.SetData call, but we
  86. // don't want to waste time waiting around every time we try to add a new
  87. // particle! To avoid this delay, we can specify the SetDataOptions.NoOverwrite
  88. // flag when we write to the vertex buffer. This basically means "I promise I
  89. // will never try to overwrite any data that the GPU might still be using, so
  90. // you can just go ahead and update the buffer straight away". To keep this
  91. // promise, we must avoid reusing vertices immediately after they are drawn.
  92. //
  93. // So in total, our queue contains four different regions:
  94. //
  95. // Vertices between firstActiveParticle and firstNewParticle are actively
  96. // being drawn, and exist in both the managed particles array and the GPU
  97. // vertex buffer.
  98. //
  99. // Vertices between firstNewParticle and firstFreeParticle are newly created,
  100. // and exist only in the managed particles array. These need to be uploaded
  101. // to the GPU at the start of the next draw call.
  102. //
  103. // Vertices between firstFreeParticle and firstRetiredParticle are free and
  104. // waiting to be allocated.
  105. //
  106. // Vertices between firstRetiredParticle and firstActiveParticle are no longer
  107. // being drawn, but were drawn recently enough that the GPU could still be
  108. // using them. These need to be kept around for a few more frames before they
  109. // can be reallocated.
  110. int firstActiveParticle;
  111. int firstNewParticle;
  112. int firstFreeParticle;
  113. int firstRetiredParticle;
  114. // Store the current time, in seconds.
  115. float currentTime;
  116. // Count how many times Draw has been called. This is used to know
  117. // when it is safe to retire old particles back into the free list.
  118. int drawCounter;
  119. // Shared random number generator.
  120. static Random random = new Random ();
  121. /// <summary>
  122. /// Constructor.
  123. /// </summary>
  124. public ParticleSystem (Game game,ContentManager content,string settingsName)
  125. : base(game)
  126. {
  127. this.content = content;
  128. this.settingsName = settingsName;
  129. }
  130. /// <summary>
  131. /// Loads graphics for the particle system.
  132. /// </summary>
  133. protected override void LoadContent ()
  134. {
  135. //return;
  136. settings = content.Load<ParticleSettings> (settingsName);
  137. // Allocate the particle array, and fill in the corner fields (which never change).
  138. particles = new ParticleVertex[settings.MaxParticles * 4];
  139. for (int i = 0; i < settings.MaxParticles; i++) {
  140. particles [i * 4 + 0].Corner = new Short2 (-1, -1);
  141. particles [i * 4 + 1].Corner = new Short2 (1, -1);
  142. particles [i * 4 + 2].Corner = new Short2 (1, 1);
  143. particles [i * 4 + 3].Corner = new Short2 (-1, 1);
  144. }
  145. LoadParticleEffect ();
  146. // Create a dynamic vertex buffer.
  147. vertexBuffer = new DynamicVertexBuffer (GraphicsDevice, ParticleVertex.VertexDeclaration,
  148. settings.MaxParticles * 4, BufferUsage.WriteOnly);
  149. // Create and populate the index buffer.
  150. ushort[] indices = new ushort[settings.MaxParticles * 6];
  151. for (int i = 0; i < settings.MaxParticles; i++) {
  152. indices [i * 6 + 0] = (ushort)(i * 4 + 0);
  153. indices [i * 6 + 1] = (ushort)(i * 4 + 1);
  154. indices [i * 6 + 2] = (ushort)(i * 4 + 2);
  155. indices [i * 6 + 3] = (ushort)(i * 4 + 0);
  156. indices [i * 6 + 4] = (ushort)(i * 4 + 2);
  157. indices [i * 6 + 5] = (ushort)(i * 4 + 3);
  158. }
  159. indexBuffer = new IndexBuffer (GraphicsDevice, typeof(ushort), indices.Length, BufferUsage.WriteOnly);
  160. indexBuffer.SetData (indices);
  161. }
  162. /// <summary>
  163. /// Helper for loading and initializing the particle effect.
  164. /// </summary>
  165. void LoadParticleEffect ()
  166. {
  167. Effect effect = content.Load<Effect> ("ParticleEffect");
  168. // If we have several particle systems, the content manager will return
  169. // a single shared effect instance to them all. But we want to preconfigure
  170. // the effect with parameters that are specific to this particular
  171. // particle system. By cloning the effect, we prevent one particle system
  172. // from stomping over the parameter settings of another.
  173. //particleEffect = effect.Clone ();
  174. // No cloning for now so we will just create a new effect for now
  175. particleEffect = effect;
  176. EffectParameterCollection parameters = particleEffect.Parameters;
  177. // Look up shortcuts for parameters that change every frame.
  178. effectViewParameter = parameters ["View"];
  179. effectProjectionParameter = parameters ["Projection"];
  180. effectViewportScaleParameter = parameters ["ViewportScale"];
  181. effectTimeParameter = parameters ["CurrentTime"];
  182. // Set the values of parameters that do not change.
  183. parameters ["Duration"].SetValue ((float)settings.Duration.TotalSeconds);
  184. parameters ["DurationRandomness"].SetValue (settings.DurationRandomness);
  185. parameters ["Gravity"].SetValue (settings.Gravity);
  186. parameters ["EndVelocity"].SetValue (settings.EndVelocity);
  187. parameters ["MinColor"].SetValue (settings.MinColor.ToVector4 ());
  188. parameters ["MaxColor"].SetValue (settings.MaxColor.ToVector4 ());
  189. parameters ["RotateSpeed"].SetValue (
  190. new Vector2 (settings.MinRotateSpeed, settings.MaxRotateSpeed));
  191. parameters ["StartSize"].SetValue (
  192. new Vector2 (settings.MinStartSize, settings.MaxStartSize));
  193. parameters ["EndSize"].SetValue (
  194. new Vector2 (settings.MinEndSize, settings.MaxEndSize));
  195. // Load the particle texture, and set it onto the effect.
  196. if (settings.TextureName != null)
  197. {
  198. Texture2D texture = content.Load<Texture2D> (settings.TextureName);
  199. var textureParam = parameters ["Texture"];
  200. if (textureParam != null)
  201. {
  202. textureParam.SetValue (texture);
  203. }
  204. }
  205. }
  206. /// <summary>
  207. /// Updates the particle system.
  208. /// </summary>
  209. public override void Update (GameTime gameTime)
  210. {
  211. if (gameTime == null)
  212. throw new ArgumentNullException ("gameTime");
  213. currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
  214. RetireActiveParticles ();
  215. FreeRetiredParticles ();
  216. // If we let our timer go on increasing for ever, it would eventually
  217. // run out of floating point precision, at which point the particles
  218. // would render incorrectly. An easy way to prevent this is to notice
  219. // that the time value doesn't matter when no particles are being drawn,
  220. // so we can reset it back to zero any time the active queue is empty.
  221. if (firstActiveParticle == firstFreeParticle)
  222. currentTime = 0;
  223. if (firstRetiredParticle == firstActiveParticle)
  224. drawCounter = 0;
  225. }
  226. /// <summary>
  227. /// Helper for checking when active particles have reached the end of
  228. /// their life. It moves old particles from the active area of the queue
  229. /// to the retired section.
  230. /// </summary>
  231. void RetireActiveParticles ()
  232. {
  233. float particleDuration = (float)settings.Duration.TotalSeconds;
  234. while (firstActiveParticle != firstNewParticle) {
  235. // Is this particle old enough to retire?
  236. // We multiply the active particle index by four, because each
  237. // particle consists of a quad that is made up of four vertices.
  238. float particleAge = currentTime - particles [firstActiveParticle * 4].Time;
  239. if (particleAge < particleDuration)
  240. break;
  241. // Remember the time at which we retired this particle.
  242. particles [firstActiveParticle * 4].Time = drawCounter;
  243. // Move the particle from the active to the retired queue.
  244. firstActiveParticle++;
  245. if (firstActiveParticle >= settings.MaxParticles)
  246. firstActiveParticle = 0;
  247. }
  248. }
  249. /// <summary>
  250. /// Helper for checking when retired particles have been kept around long
  251. /// enough that we can be sure the GPU is no longer using them. It moves
  252. /// old particles from the retired area of the queue to the free section.
  253. /// </summary>
  254. void FreeRetiredParticles ()
  255. {
  256. while (firstRetiredParticle != firstActiveParticle) {
  257. // Has this particle been unused long enough that
  258. // the GPU is sure to be finished with it?
  259. // We multiply the retired particle index by four, because each
  260. // particle consists of a quad that is made up of four vertices.
  261. int age = drawCounter - (int)particles [firstRetiredParticle * 4].Time;
  262. // The GPU is never supposed to get more than 2 frames behind the CPU.
  263. // We add 1 to that, just to be safe in case of buggy drivers that
  264. // might bend the rules and let the GPU get further behind.
  265. if (age < 3)
  266. break;
  267. // Move the particle from the retired to the free queue.
  268. firstRetiredParticle++;
  269. if (firstRetiredParticle >= settings.MaxParticles)
  270. firstRetiredParticle = 0;
  271. }
  272. }
  273. /// <summary>
  274. /// Draws the particle system.
  275. /// </summary>
  276. public override void Draw (GameTime gameTime)
  277. {
  278. GraphicsDevice device = GraphicsDevice;
  279. // Restore the vertex buffer contents if the graphics device was lost.
  280. if (vertexBuffer.IsContentLost) {
  281. vertexBuffer.SetData (particles);
  282. }
  283. // If there are any particles waiting in the newly added queue,
  284. // we'd better upload them to the GPU ready for drawing.
  285. if (firstNewParticle != firstFreeParticle) {
  286. AddNewParticlesToVertexBuffer ();
  287. }
  288. // If there are any active particles, draw them now!
  289. if (firstActiveParticle != firstFreeParticle) {
  290. device.BlendState = settings.BlendState;
  291. device.DepthStencilState = DepthStencilState.DepthRead;
  292. // Set an effect parameter describing the viewport size. This is
  293. // needed to convert particle sizes into screen space point sizes.
  294. effectViewportScaleParameter.SetValue (new Vector2 (0.5f / device.Viewport.AspectRatio, -0.5f));
  295. // Set an effect parameter describing the current time. All the vertex
  296. // shader particle animation is keyed off this value.
  297. effectTimeParameter.SetValue (currentTime);
  298. // Set the particle vertex and index buffer.
  299. device.SetVertexBuffer (vertexBuffer);
  300. device.Indices = indexBuffer;
  301. // Activate the particle effect.
  302. foreach (EffectPass pass in particleEffect.CurrentTechnique.Passes) {
  303. pass.Apply ();
  304. if (firstActiveParticle < firstFreeParticle) {
  305. // If the active particles are all in one consecutive range,
  306. // we can draw them all in a single call.
  307. device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
  308. firstActiveParticle * 4, (firstFreeParticle - firstActiveParticle) * 4,
  309. firstActiveParticle * 6, (firstFreeParticle - firstActiveParticle) * 2);
  310. } else {
  311. // If the active particle range wraps past the end of the queue
  312. // back to the start, we must split them over two draw calls.
  313. device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
  314. firstActiveParticle * 4, (settings.MaxParticles - firstActiveParticle) * 4,
  315. firstActiveParticle * 6, (settings.MaxParticles - firstActiveParticle) * 2);
  316. if (firstFreeParticle > 0) {
  317. device.DrawIndexedPrimitives (PrimitiveType.TriangleList, 0,
  318. 0, firstFreeParticle * 4,
  319. 0, firstFreeParticle * 2);
  320. }
  321. }
  322. }
  323. // Reset some of the renderstates that we changed,
  324. // so as not to mess up any other subsequent drawing.
  325. device.DepthStencilState = DepthStencilState.Default;
  326. }
  327. drawCounter++;
  328. }
  329. /// <summary>
  330. /// Helper for uploading new particles from our managed
  331. /// array to the GPU vertex buffer.
  332. /// </summary>
  333. void AddNewParticlesToVertexBuffer ()
  334. {
  335. int stride = ParticleVertex.SizeInBytes;
  336. if (firstNewParticle < firstFreeParticle) {
  337. // If the new particles are all in one consecutive range,
  338. // we can upload them all in a single call.
  339. // vertexBuffer.SetData (firstNewParticle * stride * 4, particles,
  340. // firstNewParticle * 4,
  341. // (firstFreeParticle - firstNewParticle) * 4,
  342. // stride, SetDataOptions.NoOverwrite); } else {
  343. // If the new particle range wraps past the end of the queue
  344. // back to the start, we must split them over two upload calls.
  345. // vertexBuffer.SetData (firstNewParticle * stride * 4, particles,
  346. // firstNewParticle * 4,
  347. // (settings.MaxParticles - firstNewParticle) * 4,
  348. // stride, SetDataOptions.NoOverwrite);
  349. if (firstFreeParticle > 0) {
  350. // vertexBuffer.SetData (0, particles,
  351. // 0, firstFreeParticle * 4,
  352. // stride, SetDataOptions.NoOverwrite);
  353. }
  354. }
  355. // Move the particles we just uploaded from the new to the active queue.
  356. firstNewParticle = firstFreeParticle;
  357. }
  358. /// <summary>
  359. /// Sets the camera view and projection matrices
  360. /// that will be used to draw this particle system.
  361. /// </summary>
  362. public void SetCamera (Matrix view, Matrix projection)
  363. {
  364. effectViewParameter.SetValue (view);
  365. effectProjectionParameter.SetValue (projection);
  366. }
  367. /// <summary>
  368. /// Adds a new particle to the system.
  369. /// </summary>
  370. public void AddParticle (Vector3 position, Vector3 velocity)
  371. {
  372. // Figure out where in the circular queue to allocate the new particle.
  373. int nextFreeParticle = firstFreeParticle + 1;
  374. if (nextFreeParticle >= settings.MaxParticles)
  375. nextFreeParticle = 0;
  376. // If there are no free particles, we just have to give up.
  377. if (nextFreeParticle == firstRetiredParticle)
  378. return;
  379. // Adjust the input velocity based on how much
  380. // this particle system wants to be affected by it.
  381. velocity *= settings.EmitterVelocitySensitivity;
  382. // Add in some random amount of horizontal velocity.
  383. float horizontalVelocity = MathHelper.Lerp (settings.MinHorizontalVelocity,
  384. settings.MaxHorizontalVelocity,
  385. (float)random.NextDouble ());
  386. double horizontalAngle = random.NextDouble () * MathHelper.TwoPi;
  387. velocity.X += horizontalVelocity * (float)Math.Cos (horizontalAngle);
  388. velocity.Z += horizontalVelocity * (float)Math.Sin (horizontalAngle);
  389. // Add in some random amount of vertical velocity.
  390. velocity.Y += MathHelper.Lerp (settings.MinVerticalVelocity,
  391. settings.MaxVerticalVelocity,
  392. (float)random.NextDouble ());
  393. // Choose four random control values. These will be used by the vertex
  394. // shader to give each particle a different size, rotation, and color.
  395. Color randomValues = new Color ((byte)random.Next (255),
  396. (byte)random.Next (255),
  397. (byte)random.Next (255),
  398. (byte)random.Next (255));
  399. // Fill in the particle vertex structure.
  400. for (int i = 0; i < 4; i++) {
  401. particles [firstFreeParticle * 4 + i].Position = position;
  402. particles [firstFreeParticle * 4 + i].Velocity = velocity;
  403. particles [firstFreeParticle * 4 + i].Random = randomValues;
  404. particles [firstFreeParticle * 4 + i].Time = currentTime;
  405. }
  406. firstFreeParticle = nextFreeParticle;
  407. }
  408. }
  409. }