A.M.P.S.

During the development of One Minute Dungeon I wasn’t happy with Unity’s stock particle system so I decided to roll my own. The project was called Another Modular Particle System.

My goal was to create an extensible framework where creating complex particle behaviors is reasonably easy. Fewer but smarter particles combined with more complex shaders to produce advanced special effects.

The concept

The Amps Emitter is a component on a Game Object and references an Amps Blueprint type of asset. Such an asset is edited in the Amps Editor window:

The Amps editor window particle system A.M.P.S. EditorWindow

The left most column is the fixed list of stacks where each item is responsible for a certain aspect of the particle system. The second column shows the variable number of modules in the currently selected stack. Every module has properties, displayed in the third column. The right most area is for value pickers which are used to adjust more complex properties like ranges or curves. Finally at the bottom of the window are the optional parameters which allow other systems to pass values to the emitter and thus modify its behavior.

The stack and module lists are evaluated top to bottom, modifying the internal particle data along the way, no unlike layers in image editing applications (although in reverse order). The final step is creating the particle mesh.

Depending on the modules used the emitter can behave like a stateless particle system, driven by a single input value, “Time” or a scalar changing in an arbitrary manner.

 

The current beta version has the following main shortcomings:

– Haven’t yet found a clean and robust solution for handling collisions.

– There are only some basic performance optimizations, no LODing at all for example.

– The move to Unity 5 introduced some UX issues while editing emitters.

 

For a detailed description of the stacks, modules and properties please visit the Documentation. Now let’s see how the example scenes (included with the source code) work:

Example 1

The emitter uses a very simple blueprint: The quad renderer has the default values, the spawn rate is a constant, particles are positioned right at the emitter and they rotate by 60 degrees a second. However to make rotation more interesting the [Pivot Offset] stack has a single module which animates its output: that offsets the particle representations. The raw position values of the particles remain unchanged, they still revolve around that location, they are just displaced during rendering.

 

To get a feel for how it works just select the “Changing pivot offset” module and switch its [Value] property toConstant” (small icon at the top right corner of the property). While in constant mode you can change the values by dragging on their labels and see how it affects the particles in the viewport.

The scale of the particles is also animated to provide additional visual complexity.

The first module animates the width of the particle (scale on X) over particle time (age): the red curve defines the profile of the change. Its input range is 0 to 5 so the curve is looped every 5 seconds. (Of course the post behavior of the curve is set to “Loop” in the curve editor.) The output range defines what widths the curve produces: the low end is 0.08 while the high end is 0.25 units.

Amps emitter vector curve, normal blend mode particle system A.M.P.S. image10

The next module does pretty much the same except it adjusts the Y scale (height) and the related curve loops every 3.5 seconds. (The different input range is the reason why it’s a separate module.)

It’s blending mode is Multiply: the X and Z curves produce a constant 1 so they won’t overwrite the animated width in the module above. The animated Y curve outputs values between 0.2 and 1 which is then gets multiplied by the constant 1 coming from the previous module. The net result is that the first module only changes width and the second only modifies height.

Amps emitter vector curve, multiply blend mode particle system A.M.P.S. image11

The last module in this stack, “Morph to square“, defines a constant of (0.8, 0.8, 0.8) which makes the quads a square (Z scale is disregarded by the sprite renderer in this case).

The animated property here is the Weight: the curve makes this module fade-in and out periodically, every 7 seconds of emitter time. At the peak of the curve this module totally overwrites the previous two, making the particles squares for a while.

Finally let’s take a look at the [Color] stack.

Amps emitter vector property with weight curve particle system A.M.P.S. image12

The top module defines the color the particles are born with: the older the emitter gets (Emitter Time from 0 to 4) the higher hue value (0.6 -> 1) is assigned to the new particles.

The second module has a looping curve which animates the hue of a particle over particle time. The blending mode is Add so the value what was given at birth will be adjusted. The third module does almost the same thing except affects saturation and value (using the Multiply blend mode).

The last module is a Converter: it treats incoming data as HSV and produces an RGB set because the [Color] stack is interpreted as such by the rest of the system.

A stack of modules particle system A.M.P.S. image13

Otherwise the Particle limit of 64 is set in the [Emitter] stack and the empty [Death Condition] stack makes sure that the particles never die: the value of an empty stack remains 0 while a 1 would kill a particle.

Example 2

A mesh renderer is used here to make the particles look like paper planes. The spawn rate is constant while the death condition is a linear graph: when the particle becomes 10 seconds old it reaches 1 so the particle gets marked for death. It will die in 1 second, as described in the [Death Duration] stack. This 1 second time frame will be used later to gracefully remove the particles from the world.

 

Now let’s see the [Velocity] stack: the first node is a property sampler which queries the current forward vector (the direction of the particle) 10 times a second. This module’s normalized vector output is used as acceleration which happens to be too fast so the next module dials it down a bit: depending on the [Death Condition] (how close the particle is to death) the forward velocity is multiplied by 0.3 to 0.5 using a scalar curve.

 

In the [Position] stack a random vector makes the particles spawn on a horizontal plane, just above the room.

 

The [Rotation] stack tries to keep the planes stay inside the box by rotating them around before they wander too far off. The first module makes the particles start facing backward while the second module animates heading, pitch and bank over the life of the particles (regardless how long that life actually lasts). The last module varies the starting heading a bit in a random fashion so particles don’t follow the exact same path.

 

The 1 second [Death Duration] mentioned earlier is used in the [Scale] stack as a multiplier to the random starting scale: when the particle dies then it is not instantly removed from the world. Instead the Time: Dying timer starts ticking so we have a chance to prepare the particle for removal: in this case the X and Y scales become 0.

Example 3

This particle system uses a quad renderer, the fake 3d balls are created in a shader. That shader needs world normals so the rendering module’s Normal and Tangent modes are set up accordingly. The UV2 Mode is set to Custom Vector (XY) so that stack can control two extra shader parameters (squish and texture rotation).

 

The most tricky part in this blueprint is the [Position] stack where the bouncing is done. The core of this technique is two emitters, “Bottom end” and “Top end“: they are vector samplers coming up with a new vector (a new position for the particle) every 3 seconds. However they are not doing that at the same time as the first node has the Initial delay of 1.5 second. That 1.5 second value also appears in the Weight curve of the second module: it makes that module overwrite the first one every 3 seconds (1.5 curve in ping-pong mode). The net result of this setup is that each sampler generates a new random position at a time when that particular sampler has no effect on the final result of the stack so the sudden change of values is hidden: the balls don’t jump between random position at the floor and in the air, instead they return to a different, random position in a smooth manner.

 

The sole module in the [Scale] stack makes the forever alive particles grow up to their final size when the emitter starts. (The squish on hitting the floor is implemented in the shader and controlled through the [Custom Vector] stack.)

 

The [Color] stack has the now familiar HSV to RGB conversion plus two new ones: “Fake lighting” scales color based on particle position on the up axis (distance from the bright ceiling) while “Bounce flash” adds a color at the right time.

Example 4

Here each card is a two sided quad with normals and tangents calculated. The backsides get their own vertex colors from the [Custom Vector] stack so they look different than the front of the cards because vertex colors are used in the shader to control what portion of the texture is shown.

 

The death condition in this blueprint is a particle’s vertical distance from the emitter: as soon as a particle reaches 0.2 units bellow the emitter it gets marked for death and a 2 second death duration starts ticking. During that time there is a wobble animation coming from the last module in the [Rotation] stack.

The particles are moved by the [Acceleration] stack. The first module describes an Euler rotation with an animated heading value. That is then converted to a direction in the next module because a direction vector is expected from the [Acceleration] stack. The third module fades out the initial push overtime while the last one adds a constant, downward acceleration, simulating gravity.

 

But this alone would just make the cards fall through the floor. Since currently no actual collision system is implemented I had to use a Motion Tweaker module in the [Multi-Function] stack: the Momentum multiplier property zeroes out momentum (the accumulated accelerations) when the particle falls 0.25 bellow the emitter, which is the ground level.

A stack of modules particle system A.M.P.S. image14

The first module in the [Rotation] stack (“Get velocity“) gets the direction a particle is heading by sampling the Velocity property. The Sample Condition property is set up so the module only does the sampling when the Direct Value property is 1 or higher: The curve there falls to 0 after 2.5 seconds so after that the sampling doesn’t run anymore and thus velocity stops affecting the rotation.

 

So we now have a direction vector which is then converted by the next module to Euler rotations, expected from this stack. At this point the cards face the direction they are moving but since I didn’t like that I added the “Adjust Rotation” module which rotates the cards 90 degrees on X and keeps spinning them on the other two axes.

A stack of modules particle system A.M.P.S. image15

The use of the [Scale] and [Color] stacks is straightforward so let’s move onto the [Pivot Offset] modules. The first one moves the pivot from the center of a card to it’s edge over time, so when the particle position is at the floor then the card seems to be standing on its end. Of course this also affects rotation: at the beginning the card spins around its center but when it comes to the ground hit wobble, the rotation happens around the edge touching the ground. The second module pushes the pivot up during dying, making the card look like it’s sinking, although the abstract particle is still located on the ground.

Example 5

This setup involves two emitters using two different blueprints: The first one spawns the photon torpedoes while it’s child produces the sparkle trails.

 

The emitter for the torpedoes is at the middle of the room but the particles start from the Enterprise (using a named emitter parameter), as defined by the second module in the [Position] stack, a Transform sampler. The Vector module above it serves as a fallback position, should the transform sampling fail for any reason (for example the referenced game object doesn’t exist anymore).

 

After a particle is born it should start moving in the direction of the Borg cube, so let’s take a look at the [Velocity] stack. The first two modules produce the position difference of the two spaceships while the third one makes it a unit vector so their distance doesn’t matter. The final module is just for fine tuning speed.

 

So with that the torpedoes are fired toward the Borg but they should react to hitting it or colliding with the walls of the room. Not having real collisions I had to use a workaround: volume samplers. They are in the [Death condition] stack and they produce 1 when inside the Borg cube or outside the room’s volume. (Again, those game objects are referenced through named emitter parameters.)

 

The [Death duration] stack defines a 0.5 seconds time frame during which the particles change their size and color (vertex color controls different shader features in this case). On death the particles are also stopped by a Motion tweaker module, similar to the previous example.

 

Now let’s see the child emitter, creating the sparks behind the torpedoes. In the [Spawn Rate] stack the parent’s particle count is sampled every 0.2 seconds and multiplied by a constant in the next node. This way the number of sparks is directly controlled by the parent emitter, there is no need to sync the two blueprints in any other way: this child blueprint could be used with any other parent, it will adapt automatically.

 

The only other interesting mechanic is in the [Position] stack: The first node puts the sparks inside the Borg cube, simply hides them in case the next module fails at sampling the position of a particle from the parent. If the spark does find a living torpedo particle then it is placed at it’s location at the time of sampling. (Plus some random deviation from the last module.)

 

(Note that currently there is a bug here: if a spark was successfully positioned at the wake of a torpedo but that torpedo dies before the spark does then the spark jumps back to its fallback position, into the Borg cube, instead of keeping its totally valid sample value.)