Advanced Particle System Effects

To go over the basics of a particle system, see the Introduction to Particle Systems tutorial.

Weather

snow rain

Setup

To make a snow effect, start by adding snowflake images for each particle and define the movement behavior and other dynamic elements of the particles in the updateParticle function.

The images

The following three images are used in this tutorial. On the left is the rain particle; the center image is a snow particle; and the right image is used for fire effects.

all images

The update function

The update function is used to define movement, arrangement, and visualization of the particles. Modify things like the particle’s color, imageSize, and particleLife. We can even modify the particles based on their distance to the camera (as done below), to an imported model, or to the earth itself.

Here is our update function for snow:

Copy to clipboard. Data copied clipboard.
// snow
var snowGravityVector = new Cesium.Cartesian3();
var snowUpdate = function(particle, dt) {
    Cesium.Cartesian3.normalize(particle.position, snowGravityVector);
    Cesium.Cartesian3.multiplyByScalar(snowGravityVector,
                                                            Cesium.Math.randomBetween(-30.0, -300.0),
                                                            snowGravityVector);
    particle.velocity = Cesium.Cartesian3.add(particle.velocity, snowGravityVector, particle.velocity);

    var distance = Cesium.Cartesian3.distance(scene.camera.position, particle.position);
    if (distance > (snowRadius)) {
        particle.endColor.alpha = 0.0;
    } else {
        particle.endColor.alpha = snowSystem.endColor.alpha / (distance / snowRadius + 0.1);
    }
};

The first part of the function makes the particles fall downward as if by gravity. The update function also includes a distance check so particles disappear when they are far away from the camera.

disappearing particles

Additional weather effects

Use fog and atmosphere effects to enhance the visualization and match the type of weather we’re trying to replicate.

hueShift changes the color along the color spectrum, saturationShift changes how much color versus black and white the visual actually entails, and brightnessShift changes how vivid the colors are.

Fog density changes how opaque the overcover on the earth is with the fog’s color. The fog minimumBrightness is used to darken the fog.

Copy to clipboard. Data copied clipboard.
// snow
scene.skyAtmosphere.hueShift = -0.8;
scene.skyAtmosphere.saturationShift = -0.7;
scene.skyAtmosphere.brightnessShift = -0.33;

scene.fog.density = 0.001;
scene.fog.minimumBrightness = 0.8;

The systems

snow and rain

Snow

The snow system uses the snowflake_particle image and uses a minimumImageSize and maximumImageSize to create snowflakes at random sizes in that range.

Copy to clipboard. Data copied clipboard.
var snowParticleSize = scene.drawingBufferWidth / 100.0;
var snowRadius = 100000.0;

var snowSystem = new Cesium.ParticleSystem({
    modelMatrix : new Cesium.Matrix4.fromTranslation(scene.camera.position),
    minimumSpeed : -1.0,
    maximumSpeed : 0.0,
    lifetime : 15.0,
    emitter : new Cesium.SphereEmitter(snowRadius),
    startScale : 0.5,
    endScale : 1.0,
    image : "../../SampleData/snowflake_particle.png",
    emissionRate : 7000.0,
    startColor : Cesium.Color.WHITE.withAlpha(0.0),
    endColor : Cesium.Color.WHITE.withAlpha(1.0),
    minimumImageSize : new Cartesian2(snowParticleSize, snowParticleSize),
    maximumImageSize : new Cartesian2(snowParticleSize * 2.0, snowParticleSize * 2.0),
    updateCallback : snowUpdate
});
scene.primitives.add(snowSystem);

Rain

The rain system uses circular_particle.png for the rain drops. imageSize is used to stretch the image vertically to give the rain an elongated look.

Copy to clipboard. Data copied clipboard.
rainSystem = new Cesium.ParticleSystem({
    modelMatrix : new Cesium.Matrix4.fromTranslation(scene.camera.position),
    speed : -1.0,
    lifetime : 15.0,
    emitter : new Cesium.SphereEmitter(rainRadius),
    startScale : 1.0,
    endScale : 0.0,
    image : "../../SampleData/circular_particle.png",
    emissionRate : 9000.0,
    startColor :new Cesium.Color(0.27, 0.5, 0.70, 0.0),
    endColor : new Cesium.Color(0.27, 0.5, 0.70, 0.98),
    imageSize : new Cesium.Cartesian2(rainParticleSize, rainParticleSize * 2),
    updateCallback : rainUpdate
});
scene.primitives.add(rainSystem);

The rain update function is slightly different because rain falls much faster than snow.

Copy to clipboard. Data copied clipboard.
// rain
rainGravityScratch = Cesium.Cartesian3.normalize(particle.position, rainGravityScratch);
rainGravityScratch = Cesium.Cartesian3.multiplyByScalar(rainGravityScratch,
                                                        -1050.0,
                                                        rainGravityScratch);

particle.position = Cesium.Cartesian3.add(particle.position, rainGravityScratch, particle.position);

To make the environment match the mood of the scene, modify the atmosphere and fog to match the rain. The below code makes a dark blue sky with a thin fog cover.

Copy to clipboard. Data copied clipboard.
// rain
scene.skyAtmosphere.hueShift = -0.97;
scene.skyAtmosphere.saturationShift = 0.25;
scene.skyAtmosphere.brightnessShift = -0.4;

scene.fog.density = 0.00025;
scene.fog.minimumBrightness = 0.01;

For additional help, see the Sandcastle example for both snow and rain.

Comet and rocket tails

Comet Comet tail. Rocket Rocket tail.

Using multiple particle systems

To create comet and rocket trails, we will need multiple particle systems. Each location on a ring of particles created by the example is a completely separate particle system. This allows us to more uniformly control the direction of the systems’ movement. An easy way to visualize this effect is to limit cometOptions.numberOfSystems to 2 and cometOptions.colorOptions to include just two colors, as shown in the image below.

demo of 2

To streamline the different sets of systems, create arrays to carry the separate systems associated with the comet versus those associated with the rocket example.

Copy to clipboard. Data copied clipboard.
var rocketSystems = [];
var cometSystems = [];

Create two different options for objects; one for the comet version and one for the rocket version. This allows for a varied look between the two with different initial number of systems, offset values, etc.

Copy to clipboard. Data copied clipboard.
var cometOptions = {
    numberOfSystems : 100.0,
    iterationOffset : 0.003,
    cartographicStep : 0.0000001,
    baseRadius : 0.0005,

    colorOptions : [{
        red : 0.6,
        green : 0.6,
        blue : 0.6,
        alpha : 1.0
    }, {
        red : 0.6,
        green : 0.6,
        blue : 0.9,
        alpha : 0.9
    }, {
        red : 0.5,
        green : 0.5,
        blue : 0.7,
        alpha : 0.5
    }]
};

var rocketOptions = {
    numberOfSystems : 50.0,
    iterationOffset :  0.1,
    cartographicStep : 0.000001,
    baseRadius : 0.0005,

    colorOptions : [{
        minimumRed : 1.0,
        green : 0.5,
        minimumBlue : 0.05,
        alpha : 1.0
    }, {
        red : 0.9,
        minimumGreen : 0.6,
        minimumBlue : 0.01,
        alpha : 1.0
    }, {
        red : 0.8,
        green : 0.05,
        minimumBlue : 0.09,
        alpha : 1.0
    }, {
        minimumRed : 1,
        minimumGreen : 0.05,
        blue : 0.09,
        alpha : 1.0
    }]
};

colorOptions is an array of colors used for a randomized visual. Rather than having a set color, each system starts with one specific color dependent on the current system being created. In the below example, i represents the current iteration.

Copy to clipboard. Data copied clipboard.
var color = Cesium.Color.fromRandom(options.colorOptions[i % options.colorOptions.length]);

Setup

Use the below function as the initializer for each system:

Copy to clipboard. Data copied clipboard.
function createParticleSystems(options, systemsArray) {
    var length = options.numberOfSystems;
    for (var i = 0; i < length; ++i) {
        scratchAngleForOffset = Math.PI * 2.0 * i / options.numberOfSystems;
        scratchOffset.x += options.baseRadius * Math.cos(scratchAngleForOffset);
        scratchOffset.y += options.baseRadius * Math.sin(scratchAngleForOffset);

        var emitterModelMatrix = Cesium.Matrix4.fromTranslation(scratchOffset, matrix4Scratch);
        var color = Cesium.Color.fromRandom(options.colorOptions[i % options.colorOptions.length]);
        var force = forceFunction(options, i);

        var item = viewer.scene.primitives.add(new Cesium.ParticleSystem({
            image : getImage(),
            startColor : color,
            endColor : color.withAlpha(0.0),
            particleLife : 3.5,
            speed : 0.00005,
            imageSize : new Cesium.Cartesian2(15.0, 15.0),
            emissionRate : 30.0,
            emitter : new Cesium.CircleEmitter(0.1),
            bursts : [ ],
            lifetime : 0.1,
            forces : force,
            modelMatrix : particlesModelMatrix,
            emitterModelMatrix : emitterModelMatrix
        }));
        systemsArray.push(item);
    }
}

Since both tail versions are similar, the same createPrarticleSystems function can be used to create either. Pass in cometOptions or rocketOptions for the options parameter to create the different effects.

Create the particle image from scratch

Instead of loading an image from a URL, the getImage function creates an image using an HTML canvas. This makes the image creation more flexible.

Copy to clipboard. Data copied clipboard.
var particleCanvas;
function getImage() {
    if (!Cesium.defined(particleCanvas)) {
        particleCanvas = document.createElement('canvas');
        particleCanvas.width = 20;
        particleCanvas.height = 20;
        var context2D = particleCanvas.getContext('2d');
        context2D.beginPath();
        context2D.arc(8, 8, 8, 0, Cesium.Math.TWO_PI, true);
        context2D.closePath();
        context2D.fillStyle = 'rgb(255, 255, 255)';
        context2D.fill();
    }
    return particleCanvas;
}

The force function

Here is our updateCallback function:

Copy to clipboard. Data copied clipboard.
var scratchCartesian3 = new Cesium.Cartesian3();
var scratchCartographic = new Cesium.Cartographic();
var forceFunction = function(options, iteration) {
    var iterationOffset = iteration;
    var func = function(particle) {
        scratchCartesian3 = Cesium.Cartesian3.normalize(particle.position, new Cesium.Cartesian3());
        scratchCartesian3 = Cesium.Cartesian3.multiplyByScalar(scratchCartesian3, -1.0, scratchCartesian3);

        particle.position = Cesium.Cartesian3.add(particle.position, scratchCartesian3, particle.position);

        scratchCartographic = Cesium.Cartographic.fromCartesian(particle.position,
                                                                Cesium.Ellipsoid.WGS84,
                                                                scratchCartographic);

        var angle = Cesium.Math.PI * 2.0 * iterationOffset / options.numberOfSystems;
        iterationOffset += options.iterationOffset;
        scratchCartographic.longitude += Math.cos(angle) * options.cartographicStep;
        scratchCartographic.latitude += Math.sin(angle) * options.cartographicStep;

        particle.position = Cesium.Cartographic.toCartesian(scratchCartographic);
    };
    return func;
};

Notice forceFunction is returning a function. The returned func is the actual updateCallback function. For each iteration, the update function creates a different spinning offset based on the angle and iterationOffset. Smaller iteration offsets adjust the angle only a little, allowing the radius to grow steadily larger as the system continues, as shown in the comet example. Larger iteration offsets will change the angle much faster; this will make a much tighter, jittery, cylindrical output as in the rocket example.

This tutorial uses sine and cosine functions for circular effects. For other effects, try making shapes such as the Lissajous curve, the Gibbs phenomenon, or a square wave.

Relative positioning

rocket and comet

Use a modelMatrix to position the particle systems in the proper location behind the plane. Since these systems are vertical, we need to do a slight offset by using our particleOffset value. As shown in the createParticleSystems function, emitterModelMatrix is calculated for each system with the offset depending on the iteration.

Copy to clipboard. Data copied clipboard.
// positioning the plane
var planePosition = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883, 800.0);
var particlesOffset = new Cesium.Cartesian3(-8.950115473940969, 34.852766731753945, -30.235411095432937);

// creating the particles model matrix
var transl = Cesium.Matrix4.fromTranslation(particlesOffset, new Cesium.Matrix4());
var translPosition = Cesium.Matrix4.fromTranslation(planePosition, new Cesium.Matrix4());
var particlesModelMatrix = Cesium.Matrix4.multiplyTransformation(translPosition, transl, new Cesium.Matrix4());

Resources

For additional help see the Sandcastle example for both tails examples.

For more example code, see: