244Chapter 14
isDead—True if every particle in the system is dead, and false otherwise. Note that every particle being dead doesn’t imply that the system is empty. Empty means that we have no particles in the system. Dead means that we have particles in the system, but they are all marked dead.
removeDeadParticles—Searches the attribute list _particle and removes any dead particles from the list:
|
|
|
Y |
void PSystem::removeDeadParticles() |
{ |
|
|
L |
std::list<Attribute>::iterator i; |
{ |
|
F |
i = _particles.begin(); |
|
while( i != _particles.end() ) |
{ |
|
|
|
if( i-> isAlive == false ) |
|
|
// erase returns the next iterator, so no need |
|
|
// to incrememnt to the next one ourselves. |
T |
|
|
|
i = particles.erase(i);M |
} |
|
A |
|
else |
|
{ |
Ei++; // next in list |
|
}
}
}
Remark: This method is usually called in a subclass’s update method to remove any particles that have been killed (marked as dead). However, for some particle systems, it may be advantageous to recycle dead particles rather than remove them. That is, instead of allocating and deallocating particles from the list as they are born and killed, we simply reset a dead particle to create a new one. The snow system we implement in section 14.3 demonstrates this technique.
14.2.1 Drawing a Particle System
Since the particle system is dynamic, we need to update the particle in the system every frame. An intuitive but inefficient approach to rendering the particle system is as follows:
Create a vertex buffer large enough to hold the maximum number of particles.
For each frame:
A.Update all particles.
B.Copy all living particles to the vertex buffer.
C.Draw the vertex buffer.
Particle Systems 245
This approach works, but it is not the most efficient. For one, the vertex buffer must be big enough to hold all the particles in the system. But more significant is that the graphics card is idling while we copy all the particles from the list to the vertex buffer (step B). For example, suppose our system has 10,000 particles; first we need a vertex buffer that can hold 10,000 particles, which is quite a bit of memory. In addition the graphics card will sit and do nothing until all 10,000 particles in the list are copied to the vertex buffer and we call DrawPrimitive. This scenario is a good example of the CPU and graphics card not working together.
A better approach (and the approach that the Point Sprite sample on the SDK uses) goes something like this:
Note: This is a simplified description, but it illustrates the idea. It assumes that we will always have 500 particles to fill an entire segment, which in reality doesn’t happen because we are constantly killing and creating particles so the number of particles existing varies from frame to frame. For example, suppose we only have 200 particles left to copy over and render in the current frame. Because 200 particles won’t fill an entire segment, we handle this scenario as a special case in the code. This scenario can only happen on the last segment being filled for the current frame because if it’s not the last segment, that implies there must be at least 500 particles to move onto the next segment.
Create a fair-sized vertex buffer (say, one that can hold 2,000 particles). We then divide the vertex buffer into segments; as an exam-
|
ple, we set the segment size to 500 particles. |
III |
|
Part |
|
|
|
|
|
|
|
|
|
|
|
Figure 14.1: Vertex buffer with segments labeled |
|
|
Then create the global variable i = 0 to keep track of the segment |
|
|
that we’re in. |
|
For each frame:
A.Update all particles.
B.Until all living particles have been rendered:
1.If the vertex buffer is not full, then:
a.Lock segment i with the D3DLOCK_NOOVERWRITE flag.
b.Copy 500 particles to segment i.
2.If the vertex buffer is full, then:
a.Start at the beginning of the vertex buffer: i = 0.
b.Lock segment i with the D3DLOCK_DISCARD flag.
c.Copy 500 particles to segment i.
3.Render segment i.
4.Next segment: i++
Remark: Recall that our vertex buffer is dynamic, and therefore we can take advantage of the dynamic locking flags D3DLOCK_NOOVERWRITE and D3DLOCK_DISCARD. These flags allow us to lock parts of the vertex buffer that are not being rendered while other parts of the vertex buffer are being rendered. For instance, suppose that we are rendering segment 0; using the D3DLOCK_NOOVERWRITE flag, we can lock and fill segment 1 while we are rendering segment 0. This prevents a rendering stall that otherwise would incur.
This approach is more efficient. First, we have reduced the size of the vertex buffer needed. Secondly, the CPU and graphics card are now working in unison; that is, we copy a small batch of particles to the vertex buffer (CPU work), and then we draw the small batch (graphics card work). Then we copy the next batch of particles to the vertex buffer and draw that batch. This continues until all the particles have been rendered. As you can see, the graphics card is no longer sitting idle waiting for the entire vertex buffer to be filled.
We now turn our attention to the implementation of this rendering scheme. To facilitate the rendering of a particle system using this scheme, we use the following data members of the PSystem class:
_vbSize—The number of particles that our vertex buffer can hold at a given time. This value is independent of the number of particles in the actual particle system.
_vbOffset—This variable marks the offset (measured in particles, not bytes) into the vertex buffer into which we should begin copying the next batch of particles. For instance, if batch one resides in entries 0 to 499 of the vertex buffer, the offset to start copying batch two would be 500.
_vbBatchSize—The number of particles that we define to be in a batch.
We now present the code for the rendering method:
void PSystem::render()
{
if( !_particles.empty() )
{
// set render states preRender();
_device->SetTexture(0, _tex); _device->SetFVF(Particle::FVF); _device->SetStreamSource(0, _vb, 0, sizeof(Particle));
// start at beginning if we're at the end of the vb if(_vbOffset >= _vbSize)
_vbOffset = 0;
Particle* v = 0;
_vb->Lock(
_vbOffset * sizeof( Particle ), _vbBatchSize * sizeof( Particle ), (void**)&v,
_vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD);
DWORD numParticlesInBatch = 0;
//
// Until all particles have been rendered.
//
std::list<Attribute>::iterator i;
for(i = _particles.begin(); i != _particles.end(); i++)
{
if( i->_isAlive )
{
//
//Copy a batch of the living particles to the
//next vertex buffer segment
//
v->_position = i->_position; v->_color = (D3DCOLOR)i->_color; v++; // next element;
numParticlesInBatch++; //increase batch counter
// is this batch full? if(numParticlesInBatch == _vbBatchSize)
{
//
//Draw the last batch of particles that was
//copied to the vertex buffer.
//
_vb->Unlock();
_device->DrawPrimitive( D3DPT_POINTLIST, _vbOffset, _vbBatchSize);
//
//While that batch is drawing, start filling the
//next batch with particles.
//
// move the offset to the start of the next batch
_vbOffset += _vbBatchSize;
//don't offset into memory thats outside the vb's
//range. If we're at the end, start at the beginning. if(_vbOffset >= _vbSize)
_vbOffset = 0;
_vb->Lock(
_vbOffset * sizeof( Particle ), _vbBatchSize * sizeof( Particle ), (void**)&v,
_vbOffset ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD);
numParticlesInBatch = 0; // reset for new batch }//end if
}//end if }//end for
_vb->Unlock();
//it’s possible that the LAST batch being filled never
//got rendered because the condition
//(numParticlesInBatch == _vbBatchSize) would not have
//been satisfied. We draw the last partially filled batch now.
if( numParticlesInBatch )
{
_device->DrawPrimitive( D3DPT_POINTLIST, _vbOffset, numParticlesInBatch);
}
// next block
_vbOffset += _vbBatchSize;
postRender();
}//end if }// end render()
14.2.2 Randomness
There is a sort of randomness to the particles of a system. For example, if we are modeling snow, we do not want all the snowflakes to fall in exactly the same way. We want them to fall in a similar way but not exactly the same way. To facilitate the randomness functionality required for particle systems, we add the following two functions to the d3dUtility.h/cpp files.
This first function returns a random float in the interval [lowBound, highBound]:
Particle Systems 249
float d3d::GetRandomFloat(float lowBound, float highBound)
{
if( lowBound >= highBound ) // bad input return lowBound;
//get random float in [0, 1] interval float f = (rand() % 10000) * 0.0001f;
//return float in [lowBound, highBound] interval. return (f * (highBound - lowBound)) + lowBound;
}
This next function outputs a random vector in the box defined by its minimum point min and maximum point max.
void d3d::GetRandomVector( D3DXVECTOR3* out, D3DXVECTOR3* min, D3DXVECTOR3* max)
{
out->x = GetRandomFloat(min->x, max->x); out->y = GetRandomFloat(min->y, max->y); out->z = GetRandomFloat(min->z, max->z);
}
Note: Remember to seed the random number generator using srand().
14.3 Concrete Particle Systems:
Snow, Firework, Particle Gun
Now let’s derive several concrete particle systems from PSystem. These systems have been kept simple by design for illustration purposes and do not take advantage of all the flexibility that the PSystem class provides. We implement Snow, Firework, and Particle Gun systems. These systems’ names pretty much sum up the system that they model. The Snow system models falling snowflakes. The Firework system models an explosion that looks like a firework. The Particle Gun system fires particles out from the camera’s position in the direction that the camera is looking based on a keypress; this makes it look like we are firing “particle bullets” and could be used as a foundation for a gun system in a game.
Note: As usual, the complete code projects illustrating these systems can be found in the companion files for this chapter.
14.3.1 Sample Application: Snow
Figure 14.2: A screen shot of the Snow sample
The Snow system’s class is defined as:
class Snow : public PSystem
{
public:
Snow(d3d::BoundingBox* boundingBox, int numParticles); void resetParticle(Attribute* attribute);
void update(float timeDelta);
};
Remark: Notice how simple the interface is for the Snow system because the parent class takes care of most of the work. In fact, all three of the particle systems that we implement in this section have simple interfaces and are relatively easy to implement.
The constructor takes a pointer to a bounding box structure and the number of particles the system will have. The bounding box describes the volume that the snowflakes will fall in. If the snowflakes go outside this volume, they are killed and respawned. This way, the Snow system always has the same amount of particles active. The constructor is implemented as follows:
Snow::Snow(d3d::BoundingBox* boundingBox, int numParticles)
{
_boundingBox |
= *boundingBox; |
_size |
= 0.8f; |
_vbSize |
= 2048; |
_vbOffset |
= 0; |
_vbBatchSize |
= 512; |
14.3.2 Sample Application: Firework
Figure 14.3: A screen shot of the Firework sample
The Firework system’s class is defined as:
class Firework : public PSystem
{
public:
Firework(D3DXVECTOR3* origin, int numParticles); void resetParticle(Attribute* attribute);
void update(float timeDelta); void preRender();
void postRender();
};
The constructor takes a pointer to the origin of the system and the number of particles that the system has. In this case, the origin of the system refers to where the firework will explode.
The resetParticle method initializes a particle at the origin of the system and creates a random velocity in a sphere. Each particle in the Firework system is given a random color. Finally, we define that the particle will live for two seconds.
void Firework::resetParticle(Attribute* attribute)
{
attribute->_isAlive = true; attribute->_position = _origin;
D3DXVECTOR3 min = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
D3DXVECTOR3 max = D3DXVECTOR3( 1.0f, 1.0f, 1.0f);
d3d::GetRandomVector( &attribute->_velocity, &min,
&max);