Particle System Overview
Particle systems are large collections of small objects that create pseudo-3D visual effects such as clouds and beams. Since Left 4 Dead, Source's system has also covered fullscreen post-processing effects.
A particle system often includes a number of nested child effects, each of which are made up of plug-in components that control the behavior of the system. This allows for each particle effect to be as simple or as complex as needed.
- Memory Fields
- Particle systems are designed to conserve memory with the trade off of some extra computation. To this end each particle within a system stores a limited number of fields that describe it, such as position, lifespan, color, rotation, etc. Based on these properties, the components that control the particle system compute each particle's current state each frame.
- Control Points
- Control Points (CPs) are the basic external input mechanism for a particle system, and each system can have up to 64. Control points have a position, orientation, and an entity they can reference. By default control point 0 is the particle system's origin and orientation.
- If a CP is associated with an entity it can be parented to the entity or an attachment point. In this same way, each additional control point in a system can be assigned a position in space, an orientation, and an entity. This allows systems such as the TF Medic's heal beam to travel between the weapon and the player it is healing.
- This can also allow a single system to emit across multiple point or model sources if necessary, to provide an effect such as a growing fire, for example.
- As the various elements of a particle system can access CP data, this can allow multiple dynamic elements to influence a system. While control points act as a position in space by default, other data can be stored in each point if needed, which allows external code or entities to pass generic data into a system that can then be remapped to properties of the system.
- Base Properties
- In its base state, a particle system has no components, just a few basic properties. These are the generic properties that every system has. They include things which affect a whole system, which don't vary on a per-particle basis and don't fit into the other sockets which make up a system.
- Beyond the base properties, a particle system is made up of component pieces which are socketed in. In the general case, you can have as many of these elements in each category as you wish, even socketing in multiple copies of the same component when it makes sense to do so. These act upon the fields that each particle has in order to set up or modify it.
- Renders define how your particles get drawn. These include sprites, ropes, streaks, and so forth. Each particle can be drawn multiple times in multiple ways if you wish to do so.
- Emitters define how many particles are created over what period of time. Again a system can have multiple emitters which define different types of emission which come together to form the overall pattern to the particle emission.
- Initializers set up the starting state for each particle that is created, initializing the fields within each particle. For example, a particle's initial location in space, it's color, size, or alpha. Initializers only set up starting properties of each particle as it is created, after which they have no effect on each particle.
- Once the initial state of a particle has been set up by Initializers, operators take up the act and carry out a function upon each particle for every frame that it exists.
- Forces are a variation on operators that affect motion of the particle.
- Constraints define movement limits on a particle, such as collision or maximum distance from a control point.
- Children are simply other particle systems which are linked to this system. Some data, such as control points, can be passed down to children, and they can be nested multiple levels deep.
Creating your first particle system
Often times you'll end up taking an existing system, copying it, and simply modifying it to fit your needs. However, to create a system, just hit the "Create" button and enter a name for your system.
You've now created an entirely empty system. Without any of the plugin components it will do nothing. There are a few basic components you'll have to add. So let's get started.
- Add an Animated Sprite Renderer to allow it to render.
- We want to specify where they are created, so go to the Initializers and add a Position Within Sphere Initializer. Let's give them a min and max speed of 64. Without setting up an initial position, particles won't know where to spawn properly.
- Add a Continuous Emitter. You should see a white square appear. You're actually emitting a ton of particles with the default texture.
- You'll notice that even though you've given them an initial speed, they're not moving. There are also a ton building up. That's because they have no operators. So even though you're setting up an initial position and speed for them, nothing is being processed after that except for rendering. So we'll need two operators:
- One is the Movement Basic operator. This allows particles to move.
- The other is a Lifespan Decay operator. This kills particles once their lifespan is over.
- Now you should see your particles moving and disappearing after a second. By default particles have a one second lifespan.
- A particle's lifespan is initialized on its creation, so in order to change that, let's add a Lifespan Random Initializer. Almost all Initializers and operators have randomized ranges for their settings. So if we want our particles to last between 2 and 4 seconds, we can enter and min and max of 2 and 4.
- Now change the texture to not be a white square. Go to the base properties and click on the material to bring up the browser and select a more appropriate particle material.
- Finally, add a Fade Out Random operator. This will cause the particles to fade out over a random range of their lifespan, which by default is the final 25%. It's important to note that since this is determined by the lifespan of the particle, the final 25% will be different depending on what the particle was initialized at in the range between 2 and 4 seconds (i.e. anywhere between the last .5 and 1 seconds).
- Max Particles
- By default each system is set to 1000 max particles (even though it says 1004 max particles). This much memory is allocated for the system regardless of whether it's used or not. So after setting up your system, you'll want to take a look at the counter to see how many particles you're using and set the max particles to this number. This will help to keep the memory usage to a sane amount.
- Particles are multithreaded by system. So if you have multiple systems, they'll be distributed over as many threads as are available. So on its face, it's good to split complex systems up into multiple systems to take advantage of this feature. That said, there's overhead to each system, so there's a limited benefit to the usefulness of this approach, especially on simpler systems. Splitting up a system that only has a few dozen particles will almost certainly be a net loss. However, if you system has in the thousand+ range of particles, it's worth treating it as multiple lower count particle systems which can be multi-threaded. A system and it's children are always on the same thread because parents and children can pass data to each other and as such need to be be grouped together.
- Right now, most particle operators, Initializers, etc. work in SIMD. This means on current hardware they're generally doing all math on groups of four particles simultaneously. Theoretically in the future this will scale up to wider numbers on different kinds of hardware. The point here is that if you stick to multiples of four, you'll be making better use of the system. Scaling a system down to the closest multiple of four will make it slightly more optimal. This isn't a huge gain, but it's something to be aware of.
- Overdraw is caused by having to redraw the same pixel many times over, due to many layers on top of one another. Particles are a common culprit, and it's relatively easy to run into a worst case scenario of many sprites directly on top of one another taking up the entire screen. There's a few ways to combat this within the particle system.
- The starting point to avoid this is general good practices, such as using fewer, smaller, more opaque particles, rather than many very translucent ones. However, depending on the usage case, that's not always possible.
- So there are some useful .vmt material parameters which can be helpful. A pair of them are $minfadesize and $maxfadesize. These cause a particle to begin/end to fade based on its percentage of screensize. So for example a material with a $minfadesize of .25 and $maxfadesize of .5 would start to fade if it were 25% of the screen size and be entirely faded (and not render) once it hit 50% of the screensize. In the case of walking through dense particle based smoke this could make the difference between fast framerates and overdraw death.
- Another useful parameter is $maxsize. This is a maximum size, in screenspace, that a particle can reach. It is capped at this size. So for example, if you need to see blood for hit registration purposes, you can't have it fade, but you can cap how much of the screen it'll take up, thus reducing the potential overdraw.
- Conversely there's a $minsize parameter. This is less of a performance option as a visual one. It can limit the minimum size, in screenspace, of a particle. So to use the example of blood again, you may want it to always be at least some minimum size, so enemies shot a very far way away will still have a visible effect. Setting the minsize will accomplish this.
- The two can be used together for other interesting effects. For example, we use dust motes in a few areas which are limited to a very small min-max range. So at a distance you see these sparkling pixel sized motes floating in the air, but they don't grow to be golf ball sized bits of fuzz in the air when you approach them.
- Depthblending makes particles look nice and smooth when interpenetrating surfaces. However, it's also more expensive for fillrate. So setting $depthblend 0 in the .vmt can help to improve performance where needed.
- One additional useful .vmt parameter for animated texture sheets is $blendframes 0/1. By default animated textures blend between any given two animation frames to increase the apparent animation rate. However, this adds additional overdraw to the scene. Disabling the frameblend will result in less smooth animation, but increased performance.
- Sharing data
- There are a variety of ways to share data between parent and children systems. Some of the more complex Initializers or operators can minimize their performance impact by writing out their results to a control point, which can then be read by children and use the same data without having to do any of the work.
- In general, collision is an expensive operation. The default mode 0 does traces for each particle every frame. This is expensive and doesn't scale well. However, the Collision constraint allows for a few different collision modes which allow for much faster collisions that trade off accuracy for speed. Collision Mode 3 is the best accuracy/performance tradeoff for dynamic collisions with particles in unpredictable locations/movements.
Particles must be added to the GAME\particles\particles_manifest.txt file. To make dx8-level fall backs for particle systems, "_dx80" must be appended to the filename, for example "rockettrail.pcf" becomes "rockettrail_dx80.pcf". On DirectX 8 machines, this is loaded instead. This is unlikely to be worth the effort, as almost all hardware in use will support DX9 or higher.