Main Site Links Resources Tutorials
News VB Gaming Code Downloads DirectX 7
Contact Webmaster VB Programming Product Reviews DirectX 8
  General Multimedia Articles DirectX 9
      Miscellaneous

Point Sprites for Particle Effects
Author : Jack Hoxley
Written : 26th January 2001
Contact : [Web] [Email]
Download : Graph_11.Zip [316 Kb]


Contents of this lesson:
1. Introduction
2. Setting up point sprites
3. A simple particle engine


1. Introduction

Welcome to lesson 11, or part 4 of the advanced geometry section - the last part. You should by this point in time be perfectly capable of using all the important features of geometry in DirectX8; but I wanted to cover this interesting new feature called point sprites.

Point sprites have been designed to make generating particle systems and special effects much easier than they used to be. Several new features in DirectX8 have been included just for future games - so they can look better and better; this is one of them. Particle effects, good ones at least, have often been quite hard to do - purely on the basis that you need to use up 3-4 vertices for each particle, and have several thousand particles before it looked good (depending on various things of course).

Unfortunately, point sprites being a new feature dont have much support yet - the enumeration program back in lesson 02 will tell you if they're supported or not (but we'll cover that again in a minute); despite this you can still get them to work with most hardware - just not fully featured though.


2. Setting up point sprites

The actual particle itself is going to be one vertex, nothing more than that. Through various render states we can set them up so that they use textures and are of different sizes. First we need to sort out the actual vertex type:

Private Type PARTICLEVERTEX
    v As D3DVECTOR
    Color As Long
    tu As Single
    tv As Single
End Type

Const FVF_PARTICLEVERTEX = (D3DFVF_XYZ Or D3DFVF_DIFFUSE Or D3DFVF_TEX1)

Const nParticles As Long = 750
Dim PrtVertList(0 To nParticles - 1) As PARTICLEVERTEX

Not too complicated really, the only new thing here is the use of a D3DVECTOR instead of explicitly having an X, Y and Z property, Direct3D expects 3 singles in a row that tell it the coordinates, this can either be 3 different variables X, Y and Z or it can be a D3DVECTOR - which is 3 singles under one type definition.

Next we need to set up the render states that allow us to use point sprites:

'//Create the texture, note that the texture format includes an alpha channel, and that we are
'  specifying a colour key...
Set ParticleTex = D3DX.CreateTextureFromFileEx(D3DDevice, App.Path & "\particle.bmp", 32, 32, _
						 D3DX_DEFAULT, 0, D3DFMT_A1R5G5B5, D3DPOOL_DEFAULT, _
						 D3DX_FILTER_LINEAR, D3DX_FILTER_LINEAR, &HFFFF00FF, _
						 ByVal 0, ByVal 0)

'These next two calls define how we make colours transparent, for a specific colour
'leave them as they are
D3DDevice.SetRenderState D3DRS_SRCBLEND, D3DBLEND_SRCALPHA
D3DDevice.SetRenderState D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA
'Enable the usage of transparencies
D3DDevice.SetRenderState D3DRS_ALPHABLENDENABLE, 1
'Enable point sprite rendering
D3DDevice.SetRenderState D3DRS_POINTSPRITE_ENABLE, 1 '//Enable point sprite rendering
'Allow Direct3D to size the points
D3DDevice.SetRenderState D3DRS_POINTSCALE_ENABLE, 1 '//Allow Direct3D to set/alter the size of the Psprites
'Set the size
D3DDevice.SetRenderState D3DRS_POINTSIZE, FtoDW(ParticleSize)

'// The above code uses this function to pass a floating point number (0.01 for example) 
'as a long integer (1 for example)
Function FtoDW(f As Single) As Long
    Dim buf As D3DXBuffer
    Dim l As Long
    Set buf = D3DX.CreateBuffer(4)
    D3DX.BufferSetData buf, 0, 4, 1, f
    D3DX.BufferGetData buf, 0, 4, 1, l
    FtoDW = l
End Function

After all these render states have been successfully set any vertex we render as a point list will become a point sprite, simple as that. If you've noticed, the SetRenderState call only allows data of type LONG to be passed; this is a problem if we want to pass 0.08 for example, visual basic will chop off the .08 part and pass 0 as the value, but using the clever code from the SDK we can pack a decimal number into an integer value.

Whilst this section isn't dealing with setting up the actual vertices (see the next section) we need to know how to render point sprites. Whilst it may well seem easy, I had several problems getting the transparencies to work properly (all solved thanks to MetalWarrior), the important parts being that we need to disable the depth buffer before the calls - otherwise you get some odd artifacts appearing. Here's an excerpt from the example code for rendering the point sprites:

D3DDevice.SetRenderState D3DRS_ALPHABLENDENABLE, 1
D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 0 '//Stops Particles screwing things up :)
D3DDevice.SetTexture 0, ParticleTex

     D3DDevice.DrawPrimitiveUP D3DPT_POINTLIST, nParticles, PrtVertList(I), Len(PrtVertList(0))
   
D3DDevice.SetRenderState D3DRS_ZWRITEENABLE, 1

First we enable alpha blending (so that the transparent parts of the textures appear correctly) then we disable writing to the z-buffer. The next part is normal rendering, we set the texture then render all the vertices in our array with one call; note that they are rendered as a POINTLIST - this is important. Finally we re-enable writing to the z-buffer, otherwise any other geometry drawn will overwrite what we just rendered...

Because the pointsprites are rendered using alpha blending it is CRUCIAL that you draw them in the correct order. You may have noticed that you can render geometry in any order, and as long as the depth buffer is present it'll all appear correctly, this is not the case here. The transparent areas are blended with whatever is currently underneath it, if that's just a black screen (because they're first rendered) then you'll get a black area around them, and then when you draw the sky later on you'll still be left with a black square - as the transparent areas are not updated. To sort this out make sure you draw all objects from back to front, ie, objects furthest away are drawn first, closest objects are drawn last; with any point sprites somewhere in the middle.


3. A simple particle engine

As a quick taster - here's a screenshot from the sample program for you to look at, it's this particle engine that I'm about to explain to you:

It looks like a bit of a mess here, but that's just the way I decided to set it up, the engine will use a set of constants that controls the shape, speed and density of the particle stream, the one above is setup like an explosion, but you can easily set it to other types. The motion blur on the white particles is an optical illusion, and isn't actually designed to work like this - wait for the alpha blending article for more information on this...

A particle engine isn't actually very complicated at all. We only need to design the maths for one particle, then add a little random variation and apply it to 100's of particles in order to get the effect seen in the picture. There are only 3 things that we really need to keep track of as well - position, velocity and colour, you could count lifetime as well - but I dont. Other particle engines can get extremely complicated, using extensive mathematical and physics algorithms, but for this example we'll keep it simple - should you want to be more adventurous you can modify this base.

The first thing we need to do is design some data storage for each particle, we'll keep the vertex data seperately so as not to confuse Direct3D (or slow it down).

0
Private Enum PARTICLE_STATUS
    Alive = 0
    Dead = 1
End Enum


Private Type PARTICLE
    X As Single     'World Space Coordinates
    Y As Single
    Z As Single
    vX As Single    'Speed and Direction
    vY As Single
    vZ As Single
    StartColor As D3DCOLORVALUE
    EndColor As D3DCOLORVALUE
    CurrentColor As D3DCOLORVALUE
    LifeTime As Long    'How long Mr. Particle Exists
    Created As Long 'When this particle was created...
    Status As PARTICLE_STATUS 'Does he even exist?
End Type

Dim PrtData(0 To nParticles - 1) As PARTICLE

The first part is an enumeration for keeping track of whether the particle is dead or alive (when it's dead we recreate it at the source), the next part is the bulk part of the data required:
X, Y, Z = The particles current position
vX, vY, vZ = The velocity that the particle is travelling at, respective to each axis
StartColor = The colour of the particle at the source
EndColor = The colour of the particle as it's life runs out
CurrentColor = What the current colour is, this will be an interpolated value between the start and end colours
Lifetime = How long the particle should live for, in milliseconds. It is possible that the particle dies earlier than this - if it collides with anything for example
Created = In order to know when the particle should die we need to know when it was created.
Status = If this becomes 1 (Dead) then we need to recreate the particle back at the source

Now that we have a way of storing information about the particle we need to initialise the particles; this is where we need to setup the variation. We'll set each particle going in the same general direction, but slightly different each time - this is why we get the nice effect of it mushrooming out over time.

Private Function InitParticles() As Boolean
On Error GoTo BailOut:

Dim I As Integer

For I = 0 To nParticles - 1
    PrtData(I).Status = Alive
    PrtData(I).LifeTime = 10000 + ((Rnd * 5000) - 2500)
    PrtData(I).Created = GetTickCount
    PrtData(I).X = 0
    PrtData(I).Y = -0.5
    PrtData(I).Z = 0
    PrtData(I).vX = (Rnd * XVariation) - (XVariation / 2)
    PrtData(I).vY = (Rnd * YVariation) - (YVariation / 3)
    PrtData(I).vZ = (Rnd * ZVariation) - (ZVariation / 2)
        Randomize
    PrtData(I).StartColor = CreateColorVal(1, 0.7, 0.7, 1)
    PrtData(I).EndColor = CreateColorVal(0, 0.7, 0.7, 1)
    PrtData(I).CurrentColor = PrtData(I).StartColor
Next I


'//Setup the particle vertices...
Call GenerateVertexDataFromParticles

InitParticles = True
Exit Function

BailOut:
Debug.Print "Could not initialise particles", Err.Number, Err.Description
InitParticles = False
End Function

All we're doing here is going through each particle and setting all it's members to similiar, but not identical, values. This code will be mirrored again later on, when we need to recreate particles. You'll also notice that there's a call to the function "GenerateVertexDataFromParticles", this function copies the current position and colour to the relevent vertex; this is because we cant pass this structure to Direct3D, we need to copy it into a valid vertex structure first...

The next part is to work out how we are going to update the particles. As all Direct3D applications tend to be on a loop (the ones we've done are) we just need to work out how to update the particles smoothly on every loop that the program makes. In order to do this we're going to use time based animations, which were covered in the animation lesson previously - so I wont go into any great detail...

Private Sub UpdateParticles()
'//0. Any variables required
    Dim I As Long
    

'//1. Loop through all particles
    For I = 0 To nParticles - 1
        If PrtData(I).Status = Alive Then
'//Update the positions
                    PrtData(I).X = PrtData(I).X + ((PrtData(I).vX / 500) * (GetTickCount - _
LastUpdatedParticles))
                    PrtData(I).Y = PrtData(I).Y + ((PrtData(I).vY / 500) * (GetTickCount - _
LastUpdatedParticles))
                    PrtData(I).Z = PrtData(I).Z + ((PrtData(I).vZ / 500) * (GetTickCount - _
LastUpdatedParticles))
                    
'//Update the velocities
                    PrtData(I).vX = PrtData(I).vX + ((XWind / 500) * (GetTickCount - _
LastUpdatedParticles))
                    PrtData(I).vY = PrtData(I).vY + ((Gravity / 500) * (GetTickCount - _
LastUpdatedParticles))
                    PrtData(I).vZ = PrtData(I).vZ + ((ZWind / 500) * (GetTickCount - _
LastUpdatedParticles))
                    
'//Update The color values
                    D3DXColorLerp PrtData(I).CurrentColor, PrtData(I).StartColor, PrtData(I).EndColor, _
(GetTickCount - PrtData(I).Created) / PrtData(I).LifeTime
                    
'//Check if the particle has gone below ground level...
                    If PrtData(I).Y < -1 Then PrtData(I).Status = Dead
                    
'//Check if it's lifetime has expired
                    If GetTickCount - PrtData(I).Created >= PrtData(I).LifeTime Then PrtData(I).Status = Dead
        Else
'//We need to recreate our particle...
                PrtData(I).Status = Alive
                PrtData(I).LifeTime = 10000 + ((Rnd * 5000) - 2500)
                PrtData(I).Created = GetTickCount
                PrtData(I).X = 0
                PrtData(I).Y = -0.5
                PrtData(I).Z = 0
                PrtData(I).vX = (Rnd * XVariation) - (XVariation / 2)
                PrtData(I).vY = (Rnd * YVariation) - (YVariation / 3)
                PrtData(I).vZ = (Rnd * ZVariation) - (ZVariation / 2)
                    Randomize
                PrtData(I).StartColor = CreateColorVal(1, 0.7, 0.7, 1)
                PrtData(I).EndColor = CreateColorVal(0, 1, 1, 0.1)
                PrtData(I).CurrentColor = PrtData(I).StartColor
        End If
    Next I

LastUpdatedParticles = GetTickCount

'//2. Update the raw vertex data
    Call GenerateVertexDataFromParticles
End Sub

The above code is all fairly explanatory, but the general idea is that we go through each property and update it, finally checking if the particle is dead or not - if it is dead then on the next loop it'll be recreated. As you may have noticed this part of the code is identical to the code covered just before this...

If we now put a call to this function on every loop that the application makes the particles will act like a proper particle system, combined with the rendering method described in the previous section you'll be presented with your own working particle system.

The only thing not covered is the constants that we've used - a set of values that we can change to alter the way the particle system operates. You can be as imaginative as you want in altering these, or adding new ones - but for a basic particle system these will work fine:

Const ParticleSize As Single = 0.03
Const Gravity As Single = -0.05
Const XWind As Single = 0
Const ZWind As Single = 0
Const XVariation As Single = 0.5 
Const YVariation As Single = 0.85
Const ZVariation As Single = 0.5 

Be careful when altering these, any big changes could result in absolute chaos.... Only put small amounts of wind on, anything more than 0.1 will tend to pull the particles away from the source very very quickly - and look more like a hurricane.


I strongly suggest that you download the source code for this example, all the major aspects have been covered, but to keep things short I've left out some of the obvious things... The source code can be downloaded from the top of the page you can also send any comments, questions or complaints to the email address at the top of the page as well... Next - Lesson 12: Texture Blending for special effects

DirectX 4 VB © 2000 Jack Hoxley. All rights reserved.
Reproduction of this site and it's contents, in whole or in part, is prohibited,
except where explicitly stated otherwise.
Design by Mateo
Contact Webmaster