Devlog. Part 5 — Using ParticleSystem to create complex effects

Devblog

We’ve recently introduced particle systems to EdenSpark, which was a highly requested feature.

It features GPU-accelerated simulation of large numbers of particles sharing a lot of common properties, making it a big optimization over making a similar simulation by hand using scene nodes. Besides, you don’t need to write any code to make an effect this way.

We’d like to talk a bit about their background and then make a tutorial about making a rather complex effect.

Background

Given we’re a game development company, we of course have our in-house effect system.

The system we’re talking about is ModFX, which is publicly available in the Dagor Engine repo.

It’s quite complex, has a vast amount of parameters with a lot of rather game-specific ones along them, has a dedicated creation tool and is generally made for performance in our games’ conditions.

As EdenSpark isn’t constrained to any game genre and aims to be easily accessible to newcomers, this a system needed adapting. We used it as a basis to make use of years of its previous development, and shaped it to be as user-friendly as possible.

We’ve done a rather big job correcting naming to make it clear what each parameter or enum value does from just its name. We also did parameter categorization and got rid of a lot of game-specific or too niche ones to keep a good balance between ease of use and expressive power.

Finally, we’ve added outlines, pixel perfect selection and the ability to edit it all at runtime.

Parameter changes still reset the particles for now, with live editing on the way, along with distortion effects and particle collisions.

The current system already allows for creation of various effects, which we’ll show in the following tutorial.

Firework effect tutorial

We’ll be making a 3-stage fireworks effect with a projectile ascension, a primary burst and a secondary burst at the end, which looks like this:

Let’s start with the projectile ascension. For that we need to create a trail effect, and a script that moves the emitter, since emitter motion itself is part of your game logic.

Ascending projectile

From a moving projectile we want a bright uniform sparkling trail.

To achieve that, we can first set up a distance-based emission with a zero-sized emitter. In our case we use a sphere with zero radius. This gets us a trail of particles that spawn as the node moves:

Now in order to make them behave like sparks they need to start moving. For that we’ll enable initial velocity through motion.velocity, which by default spreads particles around the emitter center, and apply gravity so they start falling down.

To make it look realistic we can add air resistance through motion.drag parameters. Also, to not constantly move the emitter around, we can add an idle period to our distance-based emission, so particles will constantly spawn even when it’s static.

With that we get this particle behavior:

In order to turn it into actual sparks, we need to create a smooth round shape for them, add color and make them emissive.

For the shape we’ll use a simple Gaussian texture:

Now we need to make them glow and fade over time. For the glow we’ll use a strong emission which gets reduced over the particle’s lifetime. To make particles themselves disappear, we’ll also add a color curve that would linearly reduce color alpha. For spark rendering, we’ll use additive blending: it ensures independence of draw order (particles within a single system are rendered in spawn or reverse-spawn order) and corresponds to what we try to simulate.

With some additional particle radius and color tuning we get such an effect, which will be enough to produce a convincing trail:

Now we need to make an actual projectile out of it. For that we’ll add a new component that would move the node, simulate a little bit of physics and spawn secondary prefabs which we’ll need for creating a multi-stage effect. We also make its lifetime randomizable right away because we’ll need randomized timing for the secondary burst.

require engine.core

def random_point_on_sphere() : float3 {
    let z = 1.0 - 2.0 * random_float()
    let r = sqrt(max(0.0, 1.0 - z * z))
    let phi = 2.0 * PI * random_float()
    return float3(r * cos(phi), r * sin(phi), z)
}

struct SubFireworkSetup {
    prefab : PrefabId
    count : int
}

class FireworkBehavior : Component {
    subFireworks : array<SubFireworkSetup>

    lifeTimeRange : float2
    speed : float
    drag : float // Speed reduction coefficient.
    acceleration : float3 // Gravity or other constant acceleration in world space.

    private velocity : float3
    private lifeTime : float

    def override on_initialize() {
        velocity = nodeId.worldRotation * float3(0, speed, 0)
        lifeTime = lerp(lifeTimeRange.x, lifeTimeRange.y, random_float())
    }

    def override on_update() {
        let dt = get_delta_time()

        lifeTime -= dt
        if (lifeTime <= 0.0) {
            launch_sub_fireworks()
            remove_node(nodeId)
            return
        }

        velocity += acceleration * dt - velocity * drag * dt
        nodeId.worldPosition += velocity * dt
    }

    def private launch_sub_fireworks() {
        for (sub in subFireworks) {
            for (i in range(sub.count)) {
                let dir = random_point_on_sphere()
                sub.prefab.instantiate_prefab(NodeData(
                    position = nodeId.worldPosition,
                    rotation = quat4(UP, dir)
                ))
            }
        }
    }
}

After adding it to our firework prefab and setting up its parameters we get something like this:

You can notice that as the emitter dies, the whole trail instantly disappears. To fix that, we can change the stopBehavior property of the particle system to ParticleStopBehavior.DeleteEmitter. Now as the node dies, the emission stops but the effect is kept alive until all particles are exhausted:

Primary burst

As we now have the firework behavior component, we can just create sub-firework prefabs and add them to the corresponding list to launch:

One thing we can add here is more apparent trails that we can simulate using ribbon rendering mode, which is enabled through the render.shape.kind parameter and set up using a sibling ribbon field.

Let’s also add some interesting coloring through a gradient over the particle’s lifetime to make it more versatile.

For texturing, we’ll use a flat version of the Gaussian we used previously (the texture in fact could have just one pixel height):

With some other parameters adjusted we get such a trail:

Combined into our effect, it looks like this:

Secondary burst

Now the only thing missing is the secondary burst effect that would end our sequence. Here we will use the burst spawn mode and also add some delay to it through spawn.delay:

Now let’s add this new effect to the spawn list so it appears at the end. Let’s also randomize the secondary projectile lifetime in order to randomize the secondary burst. Now we finally get our 3-stage firework:

Exploring further

The whole project created for this tutorial will be available as a sample, so you could play with it yourself.

There are many other parameters available in ParticleSystem that we haven’t covered in this tutorial, which you can explore on your own. The effect of most of them is visible right away and when something is unclear you can always refer to the documentation. Now it’s easier than ever, as every field and enum value has a hint in the editor.

We hope this tutorial was helpful as a starting point for creating your own beautiful effects!