Click or drag to resize
DigitalRuneStep 12: Particles

In this step we will add a particle effect.

Add particle texture
  1. In the Solution Explorer right-click the MyGameContent content project.
  2. Select Add | Existing Item…
  3. Browse to this folder of the DigitalRune Engine:
    • <DigitalRune Engine Folder>\Samples\Content\Particles
  4. Select Smoke.png and click Add.

(No special content processor required. Texture can use the default XNA importers and processors.)

Add the particle system service

In Game1.cs add the particle system manager like this:

Game1.cs
using DigitalRune.Particles;                                                                  // NEWnamespace MyGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        …
        private ParticleSystemManager _particleSystemManager;                                 // NEW
        …

        protected override void Initialize()
        {
            …
            _simulation = new Simulation();
            _simulation.ForceEffects.Add(new Gravity());
            _simulation.ForceEffects.Add(new Damping());
            _services.Register(typeof(Simulation), null, _simulation);

            _particleSystemManager = new ParticleSystemManager();                             // NEW
            _services.Register(typeof(IParticleSystemService), null, _particleSystemManager); // NEW

            Components.Add(new MyGameComponent(this));

            base.Initialize();
        }

        protected override void Update(GameTime gameTime)
        {
            _inputManager.Update(gameTime.ElapsedGameTime);

            base.Update(gameTime);

            _gameObjectManager.Update(gameTime.ElapsedGameTime);

            _simulation.Update(gameTime.ElapsedGameTime);

            _particleSystemManager.Update(gameTime.ElapsedGameTime);                          // NEW

            _animationManager.Update(gameTime.ElapsedGameTime);
            _animationManager.ApplyAnimations();
        }
        …
Add a smoke effect

Add a new game object SmokeObject.cs:

SmokeObject.cs
using System;
using DigitalRune.Game;
using DigitalRune.Geometry;
using DigitalRune.Graphics;
using DigitalRune.Graphics.SceneGraph;
using DigitalRune.Mathematics;
using DigitalRune.Mathematics.Algebra;
using DigitalRune.Mathematics.Statistics;
using DigitalRune.Particles;
using DigitalRune.Particles.Effectors;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace MyGame
{
    public class SmokeObject : GameObject
    {
        private IGraphicsService _graphicsService;
        private ParticleSystemNode _particleSystemNode;

        protected override void OnLoad()
        {
            _graphicsService = ServiceLocator.Current.GetInstance<IGraphicsService>();
            var game = ServiceLocator.Current.GetInstance<Game>();
            var scene = ServiceLocator.Current.GetInstance<IScene>();
            var particleSystemService = ServiceLocator.Current.GetInstance<IParticleSystemService>();

            var ps = new ParticleSystem
            {
                Name = "Smoke",
                MaxNumberOfParticles = 200,
            };
            ps.Parameters.AddUniform<float>(ParticleParameterNames.Lifetime).DefaultValue = 5;
            ps.Effectors.Add(new StreamEmitter 
            {
                DefaultEmissionRate = 10,
            });
            ps.ReferenceFrame = ParticleReferenceFrame.Local;
            ps.Parameters.AddVarying<Vector3F>(ParticleParameterNames.Position);
            ps.Effectors.Add(new StartPositionEffector
            {
                Parameter = ParticleParameterNames.Position,
                DefaultValue = Vector3F.Zero,
            });
            ps.Parameters.AddVarying<Vector3F>(ParticleParameterNames.Direction);
            ps.Effectors.Add(new StartDirectionEffector
            {
                Parameter = ParticleParameterNames.Direction,
                Distribution = new DirectionDistribution { Deviation = 0.5f, Direction = Vector3F.Up },
            });
            ps.Parameters.AddVarying<float>(ParticleParameterNames.LinearSpeed);
            ps.Effectors.Add(new StartValueEffector<float>
            {
                Parameter = ParticleParameterNames.LinearSpeed,
                Distribution = new UniformDistributionF(0.5f, 1),
            });
            ps.Effectors.Add(new LinearVelocityEffector());
            ps.Parameters.AddVarying<float>(ParticleParameterNames.Angle);
            ps.Effectors.Add(new StartValueEffector<float>
            {
                Parameter = ParticleParameterNames.Angle,
                Distribution = new UniformDistributionF(-ConstantsF.Pi, ConstantsF.Pi),
            });
            ps.Parameters.AddVarying<float>(ParticleParameterNames.AngularSpeed);
            ps.Effectors.Add(new StartValueEffector<float>
            {
                Parameter = ParticleParameterNames.AngularSpeed,
                Distribution = new UniformDistributionF(-2, 2),
            });
            ps.Effectors.Add(new AngularVelocityEffector());
            ps.Parameters.AddVarying<float>("StartSize");
            ps.Effectors.Add(new StartValueEffector<float>
            {
                Parameter = "StartSize",
                Distribution = new UniformDistributionF(0.1f, 0.5f),
            });
            ps.Parameters.AddVarying<float>("EndSize");
            ps.Effectors.Add(new StartValueEffector<float>
            {
                Parameter = "EndSize",
                Distribution = new UniformDistributionF(2, 4),
            });
            ps.Parameters.AddVarying<float>(ParticleParameterNames.Size);
            ps.Effectors.Add(new SingleLerpEffector
            {
                ValueParameter = ParticleParameterNames.Size,
                StartParameter = "StartSize",
                EndParameter = "EndSize",
            });
            ps.Parameters.AddVarying<float>(ParticleParameterNames.Alpha);
            ps.Parameters.AddUniform<float>("TargetAlpha").DefaultValue = 1f;
            ps.Effectors.Add(new SingleFadeEffector
            {
                ValueParameter = ParticleParameterNames.Alpha,
                TargetValueParameter = "TargetAlpha",
                FadeInStart = 0f,
                FadeInEnd = 0.2f,
                FadeOutStart = 0.7f,
                FadeOutEnd = 1f,
            });

            ps.Parameters.AddUniform<Texture2D>(ParticleParameterNames.Texture).DefaultValue =
                game.Content.Load<Texture2D>("Smoke");
            
            particleSystemService.ParticleSystems.Add(ps);

            _particleSystemNode = new ParticleSystemNode(ps);
            _particleSystemNode.PoseWorld = new Pose(new Vector3F(-2, 0, -1));
            scene.Children.Add(_particleSystemNode);
        }

        protected override void OnUnload()
        {
            var particleSystemService = ServiceLocator.Current.GetInstance<IParticleSystemService>();
            particleSystemService.ParticleSystems.Remove(_particleSystemNode.ParticleSystem);

            _particleSystemNode.Parent.Children.Remove(_particleSystemNode);
            _particleSystemNode.Dispose(false);
            _particleSystemNode = null;
        }

        protected override void OnUpdate(TimeSpan deltaTime)
        {
            _particleSystemNode.Synchronize(_graphicsService);
        }
    }
}

This game object creates a new ParticleSystem and sets a lot of particle system parameters and effectors, which define the appearance and dynamics of the effect. For a more detailed introduction to particle system parameters and effector, please have a look at the DigitalRune documentation and samples.

At the end of OnLoad, a ParticleSystemNode is generated for the particle system and added to the scene.

It is important to understand that the ParticleSystem is an object of the particle system service. The ParticleSystemNode is a scene node which is managed by the graphics system. SmokeObject.OnUpdate has to copy data from the particle system service to the graphics service – similar to copying the rigid body pose to the model pose in the CrateObject. Particle systems have a more complex state which is copied with ParticleSystemNode.Synchronize.

In MyGameComponent, we create an instance of SmokeObject:

MyGameComponent.cs
namespace MyGame
{
    public class MyGameComponent : Microsoft.Xna.Framework.GameComponent
    {
        …

        public MyGameComponent(Game game)
            : base(game)
        {
            …

            gameObjectService.Objects.Add(new GroundObject());
            gameObjectService.Objects.Add(new LightsObject());
            gameObjectService.Objects.Add(new DudeObject());
            gameObjectService.Objects.Add(new CrateObject());
            gameObjectService.Objects.Add(new SmokeObject());                     // NEW

            _myGraphicsScreen.DebugRenderer.DrawText("MyGame");
            _myGraphicsScreen.DebugRenderer.DrawAxes(Pose.Identity, 1, false);
        }
        …

If we run the game, the particle system is not visible. This happens because, so far, our render pipeline can only handle meshes and debug graphics.

Add particle system renderer

The BillboardRenderer is a renderer which can draw billboards and particle systems. We have to add this renderer to MyGraphicsScreen.cs:

MyGraphicsScreen.cs
namespace MyGame
{
    public class MyGraphicsScreen : GraphicsScreen
    {
        private MeshRenderer _meshRenderer;
        private BillboardRenderer _billboardRenderer;                                         // NEW

        …

        public MyGraphicsScreen(IGraphicsService graphicsService)
            : base(graphicsService)
        {
            _meshRenderer = new MeshRenderer();
            _billboardRenderer = new BillboardRenderer(graphicsService, 2048);                // NEW
            …
        }

        …
         
        protected override void OnRender(RenderContext context)
        {
            var graphicsDevice = GraphicsService.GraphicsDevice;
            graphicsDevice.Clear(Color.CornflowerBlue);

            context.CameraNode = CameraNode;
            context.Scene = Scene;

            // Frustum Culling: Get all the scene nodes that intersect the camera frustum.
            var query = Scene.Query<CameraFrustumQuery>(context.CameraNode, context);

            // Render opaque meshes that are visible from the camera
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.SamplerStates[0] = SamplerState.AnisotropicWrap;
            context.RenderPass = "Default";
            _meshRenderer.Render(query.SceneNodes, context);
            context.RenderPass = null;

            graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;                   // NEW
            _billboardRenderer.Render(query.SceneNodes, context, RenderOrder.BackToFront);    // NEW

            DebugRenderer.Render(context);

            context.Scene = null;
            context.CameraNode = null;
        }
    }
}

Opaque meshes are rendered first, then billboards and particles (which require alpha-blending) and finally debug graphics.

Tutorial-01-20