top of page

A proper GPU-based particle system: a quick overview

In my last graphics article I talked about a way to make a CPU-based particle system; however, modern game engines would do a GPU-based approach.

We are going to do this with the help of Geometry Shaders. I will not go into an in-depth explanation of them, this post assumes working knowledge of Geometry Shaders. For those fuzzy with the concept, you can read up on it here.

Now that we have a stage added that takes a single point and turns it into a quad on the fly, we can make a better particle system. Each particle can be represented by a single vertex in the vertex buffer, so we can offload the creation of triangles onto the GS. So now that we're not storing 4 vertices for each particle, there is less updating. The definition of a ParticleVertex will no longer need a UV (handled by GS) or normal (no lighting).

Before I go into the process though, one can still rely entirely on the CPU if using a geometry shader. So how do we make it GPU-reliant? Stream Output! The stream output stage writes the output of the GS back into a vertex buffer, and the changes made to vertices persist on the GPU. The particles on the GPU are able to be reused in future frames if need be.

SO is an optional stage in the pipeline that comes after the GS, and is not programmable. You need a special geometry shader for it, though, and that can be created with the CreateGeometryShaderWithStreamOutput() method.

The next step is to make a vertex buffer that handles the stream output result, using the bind flag D3D11_BIND_STREAM_OUTPUT. The vertex buffer must be bound to the stream output stage. By using "device->SOSetTargets()", you can set the stream output stage.

So, now the process, which will use a single geometry shader. There will be a ParticleGS, and SpawnGS. ParticleGS will manage a single particle. It takes input from the ParticleVS vertex shader, such as color, size, and position. ParticleGS will: caculate offsets for the smaller triangles, calculate the WVP matrix, and then for each vert, spit out a small triangle:

[unroll] for (int o = 0; o < 4; o++) { // Create a single vertex and add to the stream output.position = mul(float4(input[0].position, 1.0f), wvp); // Depth stuff float depthChange = output.position.z / output.position.w * 5.0f;

// Adjust based on depth output.position.xy += offsets[o] * depthChange * input[0].size; output.color = input[0].color; output.uv = saturate(offsets[o] * 10);

// Done outStream.Append(output); }

The output is used as the input for our ParticlePS pixel shader, which will sample the particle texture with trilinear filtering. Our spawner only has a GS and a VS, there is no pixel shader because the particle spawner will not be visible.

We then move to the overhauled Emitter class. The emitter will set up several properties in its constructor: startPosition, startVeloicty, start- mid- and end-color, start- mid- and end-size, ageToSpawn, maxLifetime, and an XMFLOAT3 representing constant acceleration.

Then, for particle geometry, we set the properties of vertices[0] to the values we just made. Then, just make a regular vertex buffer that will hold that initial vertex data.

After that, we come to some SO code. It's very simple:

// Create SO buffers spawnGS->CreateCompatibleStreamOutBuffer(&soBufferRead, 1000000); spawnGS->CreateCompatibleStreamOutBuffer(&soBufferWrite, 1000000); spawnFlip = false; frameCount = 0;

The spawner we want to draw is a 1D texture that the user will never see, and will be done in a DrawSpawn() method. Then, of course, make sure all the shaders are set correctly. After that, we do a check: if we are on the first frame, draw using the particleVB's seed vertex (and advance the frame); else, draw using the SO buffers. After all that, unbind the SO targets and shader, and swap the SORead and SOWrite buffers after draw.

Lastly is our Draw() method, which will be called for the emitter in our main class's draw method:

void Emitter::Draw(ID3D11DeviceContext * context, Camera* camera, float deltaTime, float totalTime) { // Spawn particles DrawSpawn(context, deltaTime, totalTime);

// Draw particles, start by setting identity matrix ----------------------------------------------------

particleGS->SetMatrix4x4("world", XMFLOAT4X4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)); particleGS->SetMatrix4x4("view", camera->GetView()); particleGS->SetMatrix4x4("projection", camera->GetProjection()); particleGS->CopyAllBufferData();

particleVS->SetFloat3("acceleration", particleConstantAccel); particleVS->SetFloat("maxLifetime", particleMaxLifetime); particleVS->CopyAllBufferData();

particlePS->SetSamplerState("trilinear", particleSampler); particlePS->SetShaderResourceView("particleTexture", particleTexture); particlePS->CopyAllBufferData();

particleVS->SetShader(); particlePS->SetShader(); particleGS->SetShader();

// Set up states float factor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; context->OMSetBlendState(particleBlendState, factor, 0xffffffff); context->OMSetDepthStencilState(particleDepthState, 0);

// Set buffers UINT particleStride = sizeof(ParticleVertex); UINT particleOffset = 0; context->IASetVertexBuffers(0, 1, &soBufferRead, &particleStride, &particleOffset);

// Draw auto - draws based on current stream out buffer context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST); context->DrawAuto();

// Unset Geometry Shader for next frame and reset states context->GSSetShader(0, 0, 0); context->OMSetBlendState(0, factor, 0xffffffff); context->OMSetDepthStencilState(0, 0);

// Reset topology context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); }

Note: Wix does not allow one to upload videos to blog posts directly from a computer, and I have been locked out of my Wix account for now. The results are visually similar to the previous particle system, however.

Featured Posts
Check back soon
Once posts are published, you’ll see them here.
Recent Posts
Search By Tags
No tags yet.
Follow Us
  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square
bottom of page