Particle Effects

Particle effects include raindrops, smoke, and sparks, and are typically implemented using Point Sprites or 3D meshes. A particle is an object that is mathematically represented by a single point in XYZ space. Typically used to represent very small objects in a game world, particles are used in large quantities to create a particle effect. The particles all behave in a similar manner, with some element of randomness to create a more convincing effect. The particles all originate from a source, known as a particle emitter.

Particle effects may be implemented in the following situations:

The following sections describe how to render particle effects on the Xbox One dev kit:

Structure of a particle

Every individual particle requires at least the following data: Position, Velocity, and a Termination Condition. A termination condition is the circumstances under which the particle is destroyed - most commonly, a countdown timer. You can add other data members, such as weight, size or rotation, but keep in mind that particles should have a small memory footprint. Particles are built for speed, not robustness.

C++

//--------------------------------------------------------------------------------------
// Name: ParticleMotionData
// Desc: struct used in the buffer storing the particle's current state.
//--------------------------------------------------------------------------------------
struct ParticleMotionData
{
    float3 LastPosition;
    float3 Velocity;
    float  RemainingLife;
    float  Mass;
};  

Creating new particles with randomly initialized data is a time consuming process, and can negatively affect performance. To preserve performance, it is advisable to create a secondary array that has pre-initialized random data that you can copy when generating a new particle.

C++

//--------------------------------------------------------------------------------------
// Name: ParticleResetData
// Desc: struct used in the buffer storing the data used to reset a particle.
//--------------------------------------------------------------------------------------
struct ParticleResetData
{
    float AllottedLife;
    float Speed;
    float InitLife;
    float3 Direction;
};  

To save space, some data should be shared between particles in a constant buffer. This is data that is always the same for all particles, like camera position, particle emitter data, collision geometry, or render data.

C++

//--------------------------------------------------------------------------------------
// Name: ParticleUpdateConstants
// Desc: Constants used to advance particle simulation one frame.
//--------------------------------------------------------------------------------------
cbuffer ParticleUpdateConstants
{
    float4x4 CameraTransform;
    float4   EmitterRotation;
    float4   ViewFrustum[ 6 ];
    float4   ParticleData;
    float4   EmitterPosition;
    float4   Plane;
    float4   Spheres[ g_SphereCount ];
    uint     ActiveCount;
    uint     pad[3];    // Pad to multiple of 16 bytes
};  

Particle arrays

Every particle emitter should have its own finite array of particles to work with, to avoid frequent memory allocation, and to prevent the emitter from spawning more than the desired amount of particles. As particles are destroyed, the vacant space in the array can be reused for a new particle.

Constantly generating new random values for recycled particles is very time-consuming. Create a secondary array of particle reset values, and initialize that array with random values. When a particle is recycled, copy the initialization data from the corresponding element of the reset array into the actual particle array. This technique allows you to maintain the appearance of randomized particle spawning, while vastly reducing the compute time required per frame.

C++

// Create and initialize the particle "reset" buffers. These buffers enable the user to rapidly spawn new particles
//without having to calculate random numbers in real time
ParticleResetData* pReset = new ParticleResetData[ g_MaxParticles ];                //create pointer to new array of ParticleResetData structs
ParticleMotionData *pInitialMotionData = new ParticleMotionData[ g_MaxParticles ];    //create pointer to new array of ParticleMotionData structs
for( UINT i = 0; i < g_MaxParticles; ++i )
{
    pReset[ i ].AllottedLife = FloatRand( g_ParticleLifeMin, g_ParticleLifeMax );    //get a random value for alloted life
    pReset[ i ].Speed = FloatRand( g_ParticleSpeedMin, g_ParticleSpeedMax );        //get a random value for the speed of the particle
    //Generate a semi-random direction value. Orientation is set relative to particle spawner orientation
    XMVECTOR Direction = XMVectorSet( g_ParticleDirectionHorizontalStrength, FloatRand( -1.0f, 1.0f ), FloatRand( -1.0f, 1.0f ), 0 );
    XMStoreFloat3( &pReset[ i ].Direction, XMVector3NormalizeEst( Direction ) );    //Copy direction value to ParticleResetData array. w is truncated

    //populate initial motion data
    pInitialMotionData[ i ].RemainingLife = pReset[ i ].InitLife = FloatRand( 0, g_ParticleLifeMin );//also sets InitLife value of pReset[i]
    XMStoreFloat3( &pInitialMotionData[ i ].LastPosition, XMVector3NormalizeEst(m_EmitterPos));
    pInitialMotionData[ i ].Mass = FloatRand( g_ParticleMinMass, g_ParticleMaxMass );
    XMStoreFloat3( &pInitialMotionData[ i ].Velocity, XMLoadFloat3( &pReset[ i ].Direction ) * pReset[ i ].Speed );//init vel = init dir * init spd
}  

Updating the particle array

Every frame, you must walk through the array of particles and update each one. If the termination condition for a particle is met, destroy the particle and replace it in the array with a new particle. Otherwise, update the position of the particle.

Whenever the termination condition for a particle is met, that particle is recycled into a new particle. This is a relatively simple process that occurs during the update phase of the program. Simply overwrite the data for the old particle with new data copied from the corresponding element of the particle reset array. And thus, a new particle is born.

If a particle is not being recycled within this frame, update position and velocity as expected. How you approach this depends on your particular implementation. For example, although smoke rises while raindrops fall, you may choose to ignore collision resolution altogether. For this sample, we chose to update our particle positions using the compute shader; other options include utilizing a physics engine or defining preset paths for your particles.

C++

//--------------------------------------------------------------------------------------
// Name: AdvanceParticles_CS()
// Desc: Compute shader to advance particle physics by one frame. 128 threads per group.
//       For input we only care about the DispatchThreadID, which gives us an absolute
//       index from 0-NumParticles. Each thread deals with only one particle.
//--------------------------------------------------------------------------------------

[ numthreads( 128, 1, 1 ) ]
void AdvanceParticles_CS( uint3 DispatchThreadID : SV_DispatchThreadID )
{
    // Grab the particle ID. Each thread works on one particle. We use the Dispatch Thread ID
    // to give us the particle index.
    uint ParticleID = DispatchThreadID.x;

    // Grab the current state of the particle
    ParticleMotionData md = g_MotionData[ ParticleID ];
    
    // Update the life value.
    float Life = md.RemainingLife + ParticleData.x;
    float4 Instance;

    // Grab the particle's lifetime
    float Lifetime = g_ResetData[ ParticleID ].AllottedLife;

    // The particle instance value's w-component stores the particle's life as a normalized
    // value from 1 (fully alive) to 0 (expired).
    Instance.w = min( 1, ( Lifetime - Life ) / Lifetime );

    // If the particle has met its termination condition, recycle the data
    if( Life > Lifetime )
    {
        // Reset the particle's data to the initial state....
        // First, save out the start position (the emitter position)
        g_MotionData[ ParticleID ].LastPosition = EmitterPosition.xyz;

        // Calculate the initial direction by rotating the default direction by the quaternion containing the 
        // emitter direction.
        float3 EmitDir = RotateVectorByQuaternion( EmitterRotation, g_ResetData[ ParticleID ].Direction.xyz );

        // Now the velocity is just that direction times the default (initial) speed.
        g_MotionData[ ParticleID ].Velocity = EmitDir * g_ResetData[ ParticleID ].Speed;

        // Restore life value to zero - this particle is starting over.
        g_MotionData[ ParticleID ].RemainingLife = 0;
        Instance.xyz = EmitterPosition.xyz;
    }
    else//The particle was not recycled this frame. Update particle motion instead.
    {
        // Update the velocity by apply "gravity". 
        float3 UpdatedVelocity = md.Velocity - float3( 0, g_GravitationalConstant * g_MotionData[ ParticleID ].Mass * ParticleData.x, 0 );

        // Update the position based on the new velocity
        float3 ParticlePosition = md.LastPosition + UpdatedVelocity * ParticleData.x; // = frame time

        // Check against the ground plane.
        if( ParticlePosition.y < g_ParticleRadius && 
            abs( ParticlePosition.x ) <= Plane.w + g_ParticleRadius && 
            abs( ParticlePosition.z ) <= Plane.w + g_ParticleRadius &&
            md.LastPosition.y >= g_ParticleRadius )
        {
            // Collide with plane. Bounce
            UpdatedVelocity.y = -UpdatedVelocity.y * ParticleData.y;
            ParticlePosition = md.LastPosition + UpdatedVelocity * ParticleData.x;
        }

        // Check against the spheres.
        // Ensure we loop in this case, since otherwise, our GPR usage becomes significant and impacts performance.
        [loop]
        for( uint i = 0; i < g_SphereCount; ++i )
        {
            // Check against the spheres.
            float3 d = ParticlePosition - Spheres[ i ].xyz;
            float Speed = length( UpdatedVelocity );
            float3 v = UpdatedVelocity / Speed;
            if( length( d ) < Spheres[ i ].w )
            {
                // Once we find a sphere intersection, we reflect our velocity vector in the sphere's normal, and 
                // multiply the result by our "bounciness" factor.
                float3 n = normalize( d );
                float3 reflectDir = normalize( v - ( 2 * dot( n, v ) * n ) );
                UpdatedVelocity = reflectDir * Speed * ParticleData.y;
                ParticlePosition = md.LastPosition + UpdatedVelocity * ParticleData.x + g_ParticleRadius * n * 1.3f;

                // Don't let the bounce push the particle through the floor (it looks bad!).
                if( ParticlePosition.y < g_ParticleRadius )
                    ParticlePosition.y = g_ParticleRadius;
            }
        }

        // Store new particle info.
        g_MotionData[ ParticleID ].LastPosition = ParticlePosition;
        g_MotionData[ ParticleID ].Velocity = UpdatedVelocity;
        g_MotionData[ ParticleID ].RemainingLife = Life;

        // Set the final particle position for this frame.
        Instance.xyz = ParticlePosition.xyz;
    }

    // Frustum cull the particle.
    // Now, this isn't a huge performance win (if at all), but it serves to demonstrate the benefits of using 
    // AppendStructuredBuffer in this case. We can cull geometry in the compute shader (fast!) and have a variable size
    // buffer we use for instancing later in the pipeline. 
    bool inFrustum = true;
    for( uint i = 0; i < 6; ++i )
    {
        float distance = dot( ViewFrustum[ i ], float4( Instance.xyz, 1 ) );
        inFrustum = inFrustum && ( distance >= -g_ParticleRadius );
    }

    // If the particle is visible, add to our AppendStructuredBuffer.
    if( inFrustum )
    {
        // Make sure we store out the instance data, and append to our AppendStructuredBuffer.
        g_ParticleInstance.Append( Instance );
    }
}  

Particle rendering

Similar to particle physics, how you choose to render the particles is entirely up to you. Point Sprites are the most common method, but 3D meshes can also be used. This sample uses Point Sprites rendered by the Vertex Shader.

Render Code:

C++

//Render particles!
// Set up input layout, shaders, etc.
pCtx->PSSetShaderResources( 0, 1, m_spParticleSRV.GetAddressOf() );
pCtx->VSSetShader( m_spVSParticle, nullptr, 0 );
pCtx->PSSetShader( m_spPSParticle, nullptr, 0 );
pCtx->IASetIndexBuffer( nullptr, DXGI_FORMAT_R16_UINT, 0 );
pCtx->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );

// Set up appropriate states for particle rendering (depth to no-write, additive alpha blending)
XSF::StockRenderStates::GetStates().ApplyBlendState( pCtx, StockBlendStates::AdditiveBlendIgnoreSrcAlphaKeepDestAlpha );
XSF::StockRenderStates::GetStates().ApplyDepthStencilState( pCtx, StockDepthStencilStates::DepthLessThanNoZWriteNoStencil );

// Set shader resources for vertex shader to hold the blackbody radiation lookup and the particle positions buffer.
ID3D11ShaderResourceView* pInstanceSrvs[] = { m_spSRVParticleInstance, m_spBlackbodySRV };
pCtx->VSSetShaderResources( 0, _countof( pInstanceSrvs ), pInstanceSrvs );

// Now do the draw. Use the indirect parameters buffer, that contains the count from our compute shader invocation in the
// Update function.
pCtx->DrawInstancedIndirect( m_spDrawIndirectParams, 0 );

// Restore default depth and blend states to appropriately render scene next frame
pCtx->OMSetBlendState( nullptr, nullptr, D3D11_DEFAULT_SAMPLE_MASK );
pCtx->OMSetDepthStencilState( nullptr, 0xffffffff );

 //reset shader resource view pointers to null
ID3D11ShaderResourceView* pInstanceSrvNull[] = { nullptr, nullptr };
pCtx->VSSetShaderResources( 0, _countof( pInstanceSrvNull ), pInstanceSrvNull );  

HLSL Shader Code:

C++

//--------------------------------------------------------------------------------------
// Name: VSParticle()
// Desc: Vertex shader that transforms a particle to its instanced position (based
//       on the AppendBuffer we filled in the compute step). 
//--------------------------------------------------------------------------------------

ParticleInterpolants VSParticle( uint BillboardVertex : SV_VertexID, uint ParticleIdx : SV_InstanceID )
{
    ParticleInterpolants Output = ( ParticleInterpolants )0;

    // Get the particle's world position.
    float4 Particle = g_ParticlePositions[ ParticleIdx ];
    float3 WorldPosition = Particle.xyz;

    // Transform the world position into clip space.
    float4 ClipSpacePos = mul( float4( WorldPosition, 1 ), matClipSpace );

    // Now expand (in clip-space) based on the vertex in the quad we are processing. g_ParticleScale
    // contains the _11 and _22 components of the projection matrix, which we use to ensure we
    // expand by the right amount in clip-space. 
    ClipSpacePos.xy += ClipSpaceScale.xy * g_BillboardPositions[ BillboardVertex ] * g_ParticleScale;

    // Output final position...
    Output.Position = ClipSpacePos;

    // And pass through texture UV...
    Output.TextureUV = g_BillboardUVs[ BillboardVertex ];

    // Finally, grab the particle color based on the normalized life value looking up into our blackbody 
    // 1D lookup.
    Output.Color = g_texBlackbody.SampleLevel( g_sampLinear, max( 0.01, Particle.w - 0.5f ), 0 );
    
    return Output;
}

//--------------------------------------------------------------------------------------
// Name: PSParticle()
// Desc: Pixel shader for rendering a particle.
//--------------------------------------------------------------------------------------

float4 PSParticle( in ParticleInterpolants input ) : SV_Target
{
    float4 SampleColor = g_texParticle.Sample( g_sampLinear, input.TextureUV );
    return SampleColor * SampleColor.a * input.Color * ( min( 1, input.Color.a + 0.5f ) );
}  

Additional thoughts

The particle effect described here is constantly deleting and recycling particles. This is useful in certain situations, like using particles to represent a waterfall, or smoke from a fire. But not all particle effects need to work in this manner. Consider making a particle effect that only generates new particles when triggered. For example, a metal barrel that emits sparks when shot, or a faucet that can be turned on/off.

Consider what rendering/physics methods work best for your scenario. Not every particle emitter is going to be the same. For example, a fire may generate smoke particles represented by 2D Point Sprites, while a chainsaw may emit sparks that generate light and are rendered as 3D models.

Experiment with different termination conditions. The sample above uses a timer to determine when particles are destroyed. Alternatively, you could destroy the particles as soon as they collide with a solid object (like raindrops), or destroy them when their velocity reaches zero.

Sample about particle effects

For a working sample that demonstrates coding for particle effects, download ‘ComputeParticles’ from the XDK Samples page at the Xbox Game Developer (XGD) site. For additional instructions, see Running the XDK Samples

See also

DirectX