Monday, 2 July 2012

3D PointSprite Particle Tutorial I



Was browsing the boards as I do and came across a thread on Ziggyware where a member was looking for a Particle Engine tutorial, at first I did not quite get what he wanted, so tried to clarify what he wanted with a post and suggested I could create a simple particle library, to which another member and the original poster said it would be a good idea and so I though the best thing would be a very simple tutorial describing what I know of particle system.

Now, I am no expert but I think I have an idea of the basic principles of a particle system and so I am going to put up a series of tutorials describing how to create a simple particle system. I know I have already got 2 samples up here, but it it just holds code samples and no explanation of what is going on under the hood.
In this series I am going to do both a 3D and 2D tutorial. As I already have a 3D sample I will start with the 3D system. In this first tutorial I am going to cover a PonitSprite particle system.

So, where do I start when creating a particle system, well I guess the best place is the particle it's self, to me this means the data structure I am going to use to hold the data required to draw and move my particle, think about what you want the particle to represent, what data you will want to store to draw the particle. So I create my own vertex element, this describes the primitive to be used to draw the particle. Now this is a tutorial so I am going to be quite generic in my particle description, it is going to contain what I think are the basic elements for any particle, you will probably want to add more to this structure for your system(s) or this might just do the job. It is going to hold position, colour, sprite size and an alpha value.

So I create a new source file in my project and name it after the structure, in this case I have called it PointSpriteElement.cs. In the file I create a new structure definition

public struct PointSpriteElement
{
}


I now add my elements
public struct PointSpriteElement
{
Vector3 position;
Color color;
Vector2 data;
}

So, my position is a vector 3 and will store this particles position in space, colour (I am in the habit of using the American spelling now) will store the color I want to draw the particle in and data is a vector 2 that is going to hold my sprite size in the X and the alpha value in the Y.

I now give this structure some properties so I can access the particle elements.
public struct PointSpriteElement
{
Vector3 position;
Color color;
Vector2 data;

public Vector3 Position
{
get { return position; }
set { position = value; }
}

public Vector2 Data
{
get { return data; }
set { data = value; }
}

public Color Color
{
get { return color; }
set { color = value; }
}

public float SpriteSize
{
get { return data.X; }
set { data.X = value; }
}
public float AlphaValue
{
get { return data.Y; }
set { data.Y = value; }
}
}

I now need to create the definition of the particle that will be used by my shader, this is done using a VertexElement array.
public static readonly VertexElement[] VertexElements = new VertexElement[]
{
new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
new VertexElement(0, sizeof(float)*3, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0),
new VertexElement(0, sizeof(float)*4, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
};


Right, as you can see this looks a bit messy, but all it is doing is describing my vertex input structure, or I guess a simpler description is how the particle is passed to the shader. So the first line describes the vector 3 that is used for the position, the first parameter is the stream to be used and can be used for stuff like multi texturing as it saves the duplication of vertex data (see this link for a better description), I have only ever used 0. As this is the first element in the element array it has an offset of 0, a format of Vector 3 as that is the data type, the method is default, usage is Position and the channel is 0. So in the shader this would be
described as half3 Position : Position0; And so the other to elements are described in the same vein, you can see the offset (second parameter) increased by the last elements size.

I also include a property so I don't have to keep typing in the particle element size.
public static int SizeInBytes
{
get
{
return sizeof(float) * 6;
}
}
So this just returns the size if the structure, which in this case is the size of 6 floats. This could also be described like this (3 + 1 + 2) * 4.

The whole structure now looks like this:
public struct PointSpriteElement
{
Vector3 position;
Color color;
Vector2 data;

public Vector3 Position
{
get { return position; }
set { position = value; }
}

public Vector4 Data
{
get { return data; }
set { data = value; }
}

public Color Color
{
get { return color; }
set { color = value; }
}

public float SpriteSize
{
get { return data.X; }
set { data.X = value; }
}
public float AlphaValue
{
get { return data.Y; }
set { data.Y = value; }
}

public static readonly VertexElement[] VertexElements = new VertexElement[]
{
new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0),
new VertexElement(0, sizeof(float)*3, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0),
new VertexElement(0, sizeof(float)*4, VertexElementFormat.Vector2, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0),
};

public static int SizeInBytes
{
get
{
return sizeof(float) * 6;
}
}
}

Right we now have a definition for our particle that is going to be used in the shader to draw our particle in the game world. We now need an array of these particles and a class to manage this particle array. In my project I create a new source file called PointSpriteParticleEmitter.cs, this is going to hold my particle emitter class. Again, you need to think about what kind of particle emitter you want to create, in this tutorial I am going to create a simple trail.

So we start by creating our class
public class PointSpriteParticleEmitter : DrawableGameComponent
{
}

Now, like any other object in the game world we will need a position a scale and rotation. I am also going to give it a color so I can set all the particle colors if I wish. We will also need a variable to store the number of particles to have in this instance. I also have in this emitter some variables for my particle physics, I will go into more detail in the Update override. Also, we need an Effect object to load the shader into. Oh, and a Game object to bind to the calling game class.

So the class now looks like this
public class PointSpriteParticleEmitter : DrawableGameComponent
{
public PointSpriteElement[] particleArray;

public Vector3 myPosition;
public Vector3 myScale;
public Quaternion myRotation;

public Color particleColor;

public int partCount;

Effect shader;

Game game;

int nextParticle = 0;
Vector3 targetPos;
Vector3 myLastpos;
}



Now for the constructor (ctor), very simple, the calling game and the particle count are passed and used to build the emitter and all values are set to arbitrary default values.

public PointSpriteParticleEmitter(Game game, int particleCount) : base(game)
{
this.game = game;
myPosition = Vector3.Zero;
myScale = Vector3.One;
myRotation = new Quaternion(0, 0, 0, 1);

partCount = particleCount;

particleColor = Color.White;
}

We are inheriting from DrawableGameComponent so we now need to override some of the base methods to load, update and draw our particles. Lets start with LoadContent. In this method we are going to load the shader, texture and load our particle array. I will go into more detail regarding the shader and the particle texture later on, but for now I will concentrate on the loading of our particles. We are also setting up some of the particle physics variables ready for use. Also, at the top of the method we will setup the vertex declaration on the graphics device.

protected override void LoadContent()
{
game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(game.GraphicsDevice, PointSpriteElement.VertexElements);

shader = game.Content.Load<Effect>("Shaders/PointSpriteParticleShader");
shader.Parameters["particleTexture"].SetValue(game.Content.Load<Texture2D>("Textures/particle"));

targetPos = myPosition;
myLastpos = targetPos;

LoadParticles();
}

I have put the loading of particles into a separate method as you might want to call it again at a later date. So our method to load the particle array is going define the size of out particle array, and set each particle up ready for use in the system. Now in here if you were using it for say a weather effect like rain or snow you would want to set the positions to random values in the scene, as I say it all depends on what you want the effect to achieve.

My LoadParticles method looks like this
private void LoadParticles()
{
particleArray = new PointSpriteElement[partCount];

for (int i = 0; i < particleArray.Length; i++)
{
particleArray[i].Position = myPosition;
particleArray[i].Color = Color.Black;
particleArray[i].SpriteSize = 1f;
}
}

Pretty simple stuff, we are setting the position of each particle to the emitters position, then (as in this example we are going to do additive blending and so the alpha value is redundant, but will show that in the next tutorial) setting the particle color to black and it's size to 1.

Now for the fun bit, this is where the behaviour of your particle emitter will be defined. In this tutorial we are doing a trail so we have a target position we are chasing, we need to know what the targets last position was, so that too is stored as well as the current particle we want to work with. I have been terming this particle physics, this is a very lose term, what I really mean is particle behaviour. In this sample we are doing all the particle physics on the CPU, in a later tutorial I will show how you can move this to the GPU.

So here is the work horse of the emitter, the Update override.
public override void Update(GameTime gameTime)
{
for (int p = 0; p < particleArray.Length; p++)
{
if (p == nextParticle && myLastpos != myPosition)
{
particleArray[p].Position = myLastpos;
particleArray[p].Color = particleColor;
}
particleArray[p].SpriteSize = 1 - (Vector3.Distance(particleArray[p].Position, targetPos) / 25);
}
nextParticle++;

if (nextParticle >= particleArray.Length)
nextParticle = 0;

myLastpos = targetPos;
targetPos = new Vector3((float)Math.Cos(gameTime.TotalGameTime.TotalSeconds) * 10, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 10, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds));
}

All we are doing here is moving through our particle array, finding the particle we want to work with and setting it's new position and color, also we are sizing the particle based on how far it is from the target so giving a tapering effect to the trail. We then store the last position of the target and (as this is a tutorial and I don't want to code another object to use as a target) we move the target.
And now onto the draw method. This is where we are going to define out blending method, in this tutorial, additive blending. We are also setting up a few parameters in the shader, but I will explain those in the shader code it's self in a short while.

public override void Draw(GameTime gameTime)
{
bool PointSpriteEnable = game.GraphicsDevice.RenderState.PointSpriteEnable;

float PointSize = game.GraphicsDevice.RenderState.PointSize;

bool AlphaBlendEnable = game.GraphicsDevice.RenderState.AlphaBlendEnable;
Blend DestinationBlend = game.GraphicsDevice.RenderState.DestinationBlend;
Blend SourceBlend = game.GraphicsDevice.RenderState.SourceBlend;
bool DepthBufferWriteEnable = game.GraphicsDevice.RenderState.DepthBufferWriteEnable;
BlendFunction BlendFunc = game.GraphicsDevice.RenderState.BlendFunction;

game.GraphicsDevice.RenderState.PointSpriteEnable = true;

game.GraphicsDevice.RenderState.AlphaBlendEnable = true;
game.GraphicsDevice.RenderState.SourceBlend = Blend.One;
game.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

Matrix wvp = (Matrix.CreateScale(myScale) * Matrix.CreateFromQuaternion(myRotation) * Matrix.CreateTranslation(myPosition)) * Camera.myView * Camera.myProjection;

shader.Parameters["Projection"].SetValue(Camera.myProjection);
shader.Parameters["ViewportHeight"].SetValue(Camera.myViewport.Height);
shader.Parameters["WorldViewProj"].SetValue(wvp);

shader.Begin();
for (int ps = 0; ps < shader.CurrentTechnique.Passes.Count; ps++)
{
shader.CurrentTechnique.Passes[ps].Begin();
game.GraphicsDevice.DrawUserPrimitives<PointSpriteElement>(PrimitiveType.PointList, particleArray, 0, particleArray.Length);
shader.CurrentTechnique.Passes[ps].End();
}
shader.End();

game.GraphicsDevice.RenderState.PointSpriteEnable = PointSpriteEnable;

game.GraphicsDevice.RenderState.AlphaBlendEnable = AlphaBlendEnable;

game.GraphicsDevice.RenderState.DestinationBlend = DestinationBlend;
game.GraphicsDevice.RenderState.SourceBlend = SourceBlend;

game.GraphicsDevice.RenderState.BlendFunction = BlendFunc;

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = DepthBufferWriteEnable;

base.Draw(gameTime);
}

So our emitter class now looks like this
public class PointSpriteParticleEmitter : DrawableGameComponent
{
public PointSpriteElement[] particleArray;

public Vector3 myPosition;
public Vector3 myScale;
public Quaternion myRotation;

public Color particleColor;

public int partCount;

Effect shader;

Game game;

int nextParticle = 0;
Vector3 targetPos;
Vector3 myLastpos;

public PointSpriteParticleEmitter(Game game, int particleCount)
: base(game)
{
this.game = game;
myPosition = Vector3.Zero;
myScale = Vector3.One;
myRotation = new Quaternion(0, 0, 0, 1);

partCount = particleCount;

particleColor = Color.White;
}

protected override void LoadContent()
{
game.GraphicsDevice.VertexDeclaration = new VertexDeclaration(game.GraphicsDevice, PointSpriteElement.VertexElements);

shader = game.Content.Load<Effect>("Shaders/PointSpriteParticleShader");
shader.Parameters["particleTexture"].SetValue(game.Content.Load<Texture2D>("Textures/particle"));

targetPos = myPosition;
myLastpos = targetPos;

LoadParticles();
}

private void LoadParticles()
{
particleArray = new PointSpriteElement[partCount];

for (int i = 0; i < particleArray.Length; i++)
{
particleArray[i].Position = myPosition;
particleArray[i].Color = Color.Black;
particleArray[i].SpriteSize = 1f;
}
}

public override void Update(GameTime gameTime)
{
for (int p = 0; p < particleArray.Length; p++)
{
if (p == nextParticle && myLastpos != myPosition)
{
particleArray[p].Position = myLastpos;
particleArray[p].Color = particleColor;
}
particleArray[p].SpriteSize = 1 - (Vector3.Distance(particleArray[p].Position, targetPos) / 25);
}
nextParticle++;

if (nextParticle >= particleArray.Length)
nextParticle = 0;

myLastpos = targetPos;
targetPos = new Vector3((float)Math.Cos(gameTime.TotalGameTime.TotalSeconds) * 10, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds) * 10, (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds));
}
public override void Draw(GameTime gameTime)
{
bool PointSpriteEnable = game.GraphicsDevice.RenderState.PointSpriteEnable;

float PointSize = game.GraphicsDevice.RenderState.PointSize;

bool AlphaBlendEnable = game.GraphicsDevice.RenderState.AlphaBlendEnable;
Blend DestinationBlend = game.GraphicsDevice.RenderState.DestinationBlend;
Blend SourceBlend = game.GraphicsDevice.RenderState.SourceBlend;
bool DepthBufferWriteEnable = game.GraphicsDevice.RenderState.DepthBufferWriteEnable;
BlendFunction BlendFunc = game.GraphicsDevice.RenderState.BlendFunction;

game.GraphicsDevice.RenderState.PointSpriteEnable = true;

game.GraphicsDevice.RenderState.AlphaBlendEnable = true;
game.GraphicsDevice.RenderState.SourceBlend = Blend.One;
game.GraphicsDevice.RenderState.DestinationBlend = Blend.One;

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = false;

Matrix wvp = (Matrix.CreateScale(myScale) * Matrix.CreateFromQuaternion(myRotation) * Matrix.CreateTranslation(myPosition)) * Camera.myView * Camera.myProjection;

shader.Parameters["Projection"].SetValue(Camera.myProjection);
shader.Parameters["ViewportHeight"].SetValue(Camera.myViewport.Height);
shader.Parameters["WorldViewProj"].SetValue(wvp);

shader.Begin();
for (int ps = 0; ps < shader.CurrentTechnique.Passes.Count; ps++)
{
shader.CurrentTechnique.Passes[ps].Begin();
game.GraphicsDevice.DrawUserPrimitives<PointSpriteElement>(PrimitiveType.PointList, particleArray, 0, particleArray.Length);
shader.CurrentTechnique.Passes[ps].End();
}
shader.End();

game.GraphicsDevice.RenderState.PointSpriteEnable = PointSpriteEnable;

game.GraphicsDevice.RenderState.AlphaBlendEnable = AlphaBlendEnable;

game.GraphicsDevice.RenderState.DestinationBlend = DestinationBlend;
game.GraphicsDevice.RenderState.SourceBlend = SourceBlend;

game.GraphicsDevice.RenderState.BlendFunction = BlendFunc;

game.GraphicsDevice.RenderState.DepthBufferWriteEnable = DepthBufferWriteEnable;

base.Draw(gameTime);
}
}

Now we have a particle system we need to implement it, as we have derived our emitter from DrawableGameComponent this is pretty easy. We just create an instance of the object and add it to the game components like this

public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

PointSpriteParticleEmitter emitter1 = new PointSpriteParticleEmitter(this, 100);
emitter1.myPosition = new Vector3(0, 0, -20);
Components.Add(emitter1);
}

As you can see I have created an emitter with 100 particles in it and placed it 20 units in front of the Camera (see sample download for Camera code)


HLSL

OK, now it's time for the shader. I have adapted this from the shader that can be found in my previous 3D particle sample. It is pretty simple, we have a texture to use for the particle, two matrices for world view projection and projection, and view port height, used to calculate the particles size.
In the other particle sample the shader is written so it can be used on the PC and the XBox 360, as this it a tutorial, I have removed the XBox 360 elements so the shader is easier to read.
I have created one data structure for passing data from the application to the Vertex Shader and one to pass data from the Vertex Shader to the Pixel Shader. As you can see the former matches the structure we created in our PointSpriteElement structure (Now I am still pretty new to this, but you will notice the color element in out application structure is represented by a single float, yet in the shader it is 4, if I set the application structure up so the color is size 4*4 rather 1*4 then I get some odd results...)

So the structures looks like this
struct VS_OUTPUT
{
half4 Position : POSITION0;
half2 TexCoord : TEXCOORD0;
half4 Color : COLOR0;
half psize : PSIZE0;
};

struct VS_INPUT
{
half4 pos : POSITION0;
half4 color : COLOR0;
half2 data : TEXCOORD0;
};

Our Vertex Shader looks like this
VS_OUTPUT VertexShader(VS_INPUT input)
{
VS_OUTPUT Output = (VS_OUTPUT)0;

Output.Position = mul(input.pos, WorldViewProj);

Output.Color = input.color;

Output.Color.a = input.data.y;

Output.psize = input.data.x * Projection._m11 / Output.Position.w * ViewportHeight / 2;

return Output;
}

So in here we tell the Vertex Shader to expect out VS_INPUT structure as the incoming parameters and that it is going to return a VS_OUTPUT structure. We create a VS_OUTPUT structure and initalize it setting all the values to 0. We then calculate the position of the vertex in the world and load it into our return structure, then load the color, color alpha and the particle size and finally return the structure to the calling pass.

Our Pixel Shader looks like this
half4 PixelShader(VS_OUTPUT input) : COLOR0
{
half4 Color = tex2D(Sampler, input.TexCoord.xy);
Color *= input.Color;
return Color;
}


In this Pixel Shader we tell it to expect a VS_OUITPUT structure as the input from the Vertex Shader and that is will return a half4 and that this half4 is a COLOR. We get the color of the pixel ath this TexCoord, multiply it by the color we want to tint the texture by and then pass it out to the calling pass.

So the complete shader looks like this
texture particleTexture;
sampler Sampler = sampler_state
{
Texture = <particleTexture>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
};

half4x4 WorldViewProj : WorldViewProjection;
half4x4 Projection : Projection;

half ViewportHeight;

struct VS_OUTPUT
{
half4 Position : POSITION0;
half2 TexCoord : TEXCOORD0;
half4 Color : COLOR0;
half psize : PSIZE0;
};

struct VS_INPUT
{
half4 pos : POSITION0;
half4 color : COLOR0;
half2 data : TEXCOORD0;
};

VS_OUTPUT VertexShader(VS_INPUT input)
{
VS_OUTPUT Output = (VS_OUTPUT)0;

Output.Position = mul(input.pos, WorldViewProj);

Output.Color = input.color;

Output.Color.a = input.data.y;

Output.psize = input.data.x * Projection._m11 / Output.Position.w * ViewportHeight / 2;

return Output;
}
half4 PixelShader(VS_OUTPUT input) : COLOR0
{
half4 Color = tex2D(Sampler, input.TexCoord.xy);

Color *= input.Color;

return Color;
}
technique PointSpriteTechnique
{
pass P0
{
vertexShader = compile vs_2_0 VertexShader();
pixelShader = compile ps_2_0 PixelShader();
}
}

I guess that brings this first tutorial to a close, hope you find it of help and has made particles a little clearer. As I say at the top, I am no expert, but this is how I am implementing PointSprite particles and it seems to be doing the job for me.

If you want the example source for this tutorial you can find it here.

4 comments:

  1. Hey, i always wanted to create a particle system, i am pretty new to programming, but i know that the first thing i want to do is create a basic particle system, anyways, i look far and wide trying to find a good step by step tutorial, but the only thing i can't seems to understand is what template you use to create it, like? would it be the "windows game" template or something else?

    ReplyDelete
    Replies
    1. This was done with XNA, but by your question I would suggest you learn C# or C++ as soon as possible, once you have a programming language under your belt, then you will be able to write the particles system you want :)

      Good luck.

      Delete
    2. I would like to learn c#, and I believe that making a particle system would be a great achievement, my previous question was what you used to make it, such as Microsoft visual c# 2010 express or such, that is the program that i am using to try to teach myself how to program, visual express has several Templates to choose from, such as "Console Application" and "Windows Game (4.0)". so I am just confused on what template to use for a good environment to make a basic particle engine

      Delete
    3. Well if you have the XNA framework installed then you select a windows game project. If you are that new to this, I started a set of posts here: [http://xnauk-randomchaosblogarchive.blogspot.co.uk/2012/07/programming-in-c-with-xna-preamble.html] to help out those just starting out. Hope this helps.

      Delete