Navigation: Particle Systems

Tutorial 1 - How to Make Fire


Overview

Magical spells or special effects can look good in games. In this tutorial I want to show you how you can include effects like fire and smoke into a game using HLSL.

How it Works


Figure 1.

The burning fire is a particle system that emits particles (the flames). Each particle is a quad that always faces the camera and each particle has an age of 0% to 100%. You can make the particle system a bit more fancy but I will make it so each particle simply moves in an upward direction; You might find experimenting with random movement could make the particle fire more interesting.

We will apply 3 textures, flame coloured(See Figure 2.), to each particle and blend them together over time. Also, we will apply 3 opacity maps, textures, to each particle and blend them together as well. The opacity maps are textures with alpha values of pixels so we can use the alpha values to control how much of the other 3 flame textures are visible. For example, we can have a flame shaped alpha texture to make the red-ish textures appear like flames.


Figure 2.

So that's basically how the fire works. We have to use some vector math to make the quads always face the camera. This method seems to work fine but if you do get a performance hit then you could try doing the same thing but with 2d sprites instead. With the second method you could render the particles to a surface and then draw the surface like an ordinary sprite. I find the method used in this tutorial is simpler to code.

We will use HLSL to do the blending of one texture into another.

The Code

HLSL

Let's start by looking at how the HLSL effect blends the flame textures together.

You can download the HLSL txt file here: Download HLSL1.fx
Here is the pixel shader:
//Pixel Shader
float4 ps_Flames(VS_OUTPUT IN) : COLOR0
{
	float4 color1 = tex2D(FireTex1Sampler, IN.tex0);
	float4 color2 = tex2D(FireTex2Sampler, IN.tex0);
	float4 color3 = tex2D(FireTex3Sampler, IN.tex0);
	
	float4 color4 = tex2D(FireOpTex1Sampler, IN.tex0);
	float4 color5 = tex2D(FireOpTex2Sampler, IN.tex0);
	float4 color6 = tex2D(FireOpTex3Sampler, IN.tex0);
	
	float4 color;
	color.r = color1.r*fBlend1 + color2.r*fBlend2 + color3.r*fBlend3;
	color.g = color1.g*fBlend1 + color2.g*fBlend2 + color3.g*fBlend3;
	color.b = color1.b*fBlend1 + color2.b*fBlend2 + color3.b*fBlend3;
	color.a = color4.a*fBlend1 + color5.a*fBlend2 + color6.a*fBlend3;
	
	return color;
}
In the code above each of the colours, color1, color2 ... color6, each holds the colour of a pixel in the corresponding textures. For example, color1 contains the pixel colour of FireTex1.

The sampler for FireTex1 is defined as:
sampler FireTex1Sampler = sampler_state
{
   Texture = (FireTex1);
   MinFilter = Linear;   MagFilter = Linear;   MipFilter = Linear;
   AddressU  = Wrap;     AddressV  = Wrap;     AddressW  = Wrap;
   MaxAnisotropy = 16;
};
A sampler allows us to access the pixels of a texture; For example FireTex1Sampler, FireTex2Sampler etc. (see ps_Flames() above)

fBlend1 is a float between 0.0f-1.0f that controls the amount to blend FireTex1 with the other textures. If fBlend1 is 0.0f then FireTex1 is entirely invisible (Not blended). fBlend2 is the amount to blend FireTex2 with the other textures.

Each particle needs to tell the shader how much to blend the textures. In other words each particle needs to set the fBlend1, fBlend2 and fBlend3 when it is rendered.

Particle Mesh

It's not necessary to use a separate quad mesh for each particle; Instead each particle can use the same mesh and change it's position and vertices.

I have defined the ParticleMesh class as follows:
class ParticleMesh
{
private:
	LPDIRECT3DVERTEXBUFFER9 vertices;

public:
	ParticleMesh();
	
	void Init(IDirect3DDevice9* pDevice);
	void MakeBillboard(D3DXVECTOR3 Pos, Camera* camera, float quad_width, float quad_height);
	void Render(IDirect3DDevice9* pDevice, ID3DXEffect* pEffect);

private:
	void CreateVertices(IDirect3DDevice9* pDevice);
	void UpdateVertices(D3DXVECTOR3 TopLeft,
						  D3DXVECTOR3 TopRight,
						  D3DXVECTOR3 BottomLeft,
						  D3DXVECTOR3 BottomRight);
};
The Init() method calls CreateVertices() to create the actual quad.

MakeBillboard() re-arranges the 4 vertices to produce a billboard of the given width and height(quad_width and quad_height).

Render() is intended to be called by an individual particle to render the billboard.

You can examine the MakeBillboard() function if you want to know how a billboard is produced. I won't go over the maths but basically you just need to understand normalized vectors and cross products. See this tutorial on vectors.

Particle

The Particle class is defined as:
class PSystem;//For friend reference.

class Particle
{
friend class PSystem;
public:
	Particle();

	//render_depth used to sort order of rendering.
	float render_depth;

	void SetParticleMesh(ParticleMesh* quad, float particle_size);
	void Update(float deltaTime);
	//camera used for billboarding. 
	void Render(IDirect3DDevice9* pDevice, ID3DXEffect* pEffect, Camera* camera);
	bool IsDead();

protected:
	D3DXVECTOR3 Pos3D;

	ParticleMesh* particleMesh;
	float particleSize;
	D3DXVECTOR3 Direction;
	float speed;
	float fAge;
	float fMaxAge;

	//blend amounts in range of 0 to 1
	float fblend1;
	float fblend2;
	float fblend3;

private:
	void SetParticleSize(float size);
};
I've added some comments to help you understand the code.

Particle System

For this tutorial I will use a simple particle emitter(class PSystem). The particle emitter stores particles in a std::vector. If a particle is dead, i.e. age > 100%, then the emitter removes that particle from the std::vector.

The PSystem::Update() function calls Update() on all particles that are alive. This indirectly updates the position of each particle and the fBlend1, fBlend2 and fBlend3 for each particle.

The Emitter emits particles from a circle area.

The particle emitter is defined as follows:
class PSystem
{
public:
	PSystem();

	void Init(ID3DXEffect* pEffect, IDirect3DDevice9* pDevice);

	void Update(IDirect3DDevice9* pDevice, float deltaTime, Camera* camera);//Spawns particles while SpawnTimeElapsed is < TotalSpawnTime.

	void StartSpawn(D3DXVECTOR3 Pos, 
					Mesh* pGround, 
					float Radius, 
					float ParticlesPerSecond, 
					float TotalSpawnTime, 
					float ParticleSpeed, 
					float ParticleLife, 
					float ParticleSize);

	void SortParticlesByDepth(Camera* camera);
	void RenderParticles(IDirect3DDevice9* pDevice, ID3DXEffect* pEffect, Camera* camera);

	void Release();

private:
	ParticleMesh my_particle_quad;

	std::vector Particles;

	float SpawnTimeElapsed;//Set this to zero in a call to StartSpawn().
	float TimeElapsedSinceLastParticle;
	float TimeToCreateParticle;
	float TotalSpawnTime;
	D3DXVECTOR3 SpawnPos;
	Mesh* pGround;//This is for spawning particles from the ground.
	float Radius;
	float ParticleSpeed;
	float ParticleLife;
	float ParticleSize;

	IDirect3DTexture9* fire_tex_1;
	IDirect3DTexture9* fire_tex_2;
	IDirect3DTexture9* fire_tex_3;
	IDirect3DTexture9* fire_tex_opacity_1;
	IDirect3DTexture9* fire_tex_opacity_2;
	IDirect3DTexture9* fire_tex_opacity_3;
};
Again I think the best way to understand the PSystem is to examine the code. Look at all occurences of particle_system1.

Basically it sets the 3 flame textures we want to use in the HLSL Effect(Uploads them to the Effect). And it sets the opacity textures likewise. The PSystem::Init() function loads the textures from files.

You can create an instance of the PSystem like this:
//(In the Game class or a suitable place)
PSystem particle_system1;
Initialize the PSystem and start spawing particles:
//PSystem.
particle_system1.Init(m_pEffect, m_d3dDevice);

particle_system1.StartSpawn(D3DXVECTOR3(-20,-10,20),//Start spawning at origin of world. 
						&platform_mesh, 
						5.0f,//Radius.
						5.0f,//Particles per second. 
						10000.0f,//Total Spawn time. 
						7.0f,//Particle speed.
						1.5f,//Particle life. 
						10.0f);//Particle size.
To view the PSystem in it's entirety, take a look at the whole application code.

Download Complete Source

Download Application

Note: If you download the Application Zip make sure you unzip all the files to the same directory before trying to run the program.


Figure 3.

I hope you enjoyed this tutorial.