Resman says hi.

Article contents

Intro

I recently had to write an intro screen for an Android app I'm working on. I decided to go old school and use some plasma. This article is the result of my investigations into recreating this well-known effect. Let's not build unnecessary suspense, here's what the final result looks like:

Building the plasma

The plasma is basically a function on 2D space created by adding together a few sinusoids. By combining different types of sines and adding a time component the illusion of motion is achieved. Below are some examples of different types of sinusoids that we can use, and an illustration with and without the time component:

The first sinusoid is simply taken along the x-axis. The coordinates of the squares on the right go from -0.5 to 0.5 in x and y.

Formula:

Obviously it is also possible to take another sinusoid along the y-axis, or in a diagonal, or at any angle, and to make the angle change with time. Here's what it looks like.

Formula:

The last type of sinusoid we can use is a concentric sinusoid starting from a point, here we can also animate it and move the center point around in a Lissajous figure:

 

I'm adding 1 to the square root term to avoid a visible dot at the center when it "folds" at zero.

We can then mix and match these functions and hopefully we get a nice plasma effect. Here I'm simply adding the 3 together.

Adding color

Now we've got something pretty cool looking, but obviously it's not as good as the one you saw in that demo at Assembly '92. Time to add some color.

To preserve the organic, fluid look of the plasma, the color scheme should not have discontinuities. However after adding our sines together, the plasma value is not necessarily constrained in a nice known interval like [0, 1]. An easy way to solve this problem is to take the sinus again of the value we obtained at the end of our plasma function, and use it to create the RGB components of the color. In the examples below r, g and b are the red, green and blue components of the color, with -1 being the lowest intensity (black), and 1 the highest (fully saturated color component).










GLSL implementation

Knowing the formulae, implementing them as a fragment shader in GLSL is straightforward:

Fragment shader
precision mediump float;
#define PI 3.1415926535897932384626433832795
 
uniform float u_time;
uniform vec2 u_k;
varying vec2 v_coords;
 
void main() {
    float v = 0.0;
    vec2 c = v_coords * u_k - u_k/2.0;
    v += sin((c.x+u_time));
    v += sin((c.y+u_time)/2.0);
    v += sin((c.x+c.y+u_time)/2.0);
    c += u_k/2.0 * vec2(sin(u_time/3.0), cos(u_time/2.0));
    v += sin(sqrt(c.x*c.x+c.y*c.y+1.0)+u_time);
    v = v/2.0;
    vec3 col = vec3(1, sin(PI*v), cos(PI*v));
    gl_FragColor = vec4(col*.5 + .5, 1);
}

The shader has two uniform parameters: u_k, used as a scale factor, and u_time, which is the current time in seconds. To avoid precision issues when using floats, the values should be kept relatively small. I encoutered issues when time went above a few minutes, so I had to periodically reset the time to zero. The plasma is actually a periodic function itself, in this case with a period of , so the looping of u_time back to zero can be made seamlessly at multiples of seconds.

The varying parameter v_coords comes from the vertex shader and contains the interpolated fragment coordinates. In my case I used a full screen quad with coordinates (-0.5,-0.5),(0.5,0.5), but it could also be used in place of a texture on any object for potentially interesting results.

Final words

Thanks to some simple math we achieved a pretty nice old school effect with a lot of variations. Obviously I had it easy with the fast computer graphics we have today, and I just calculated everything at each frame, but in the good old times the various sinusoids would have needed to be precomputed, and displayed with an offset or rotozoomed. For browser compatibility the examples on this page are written using an HTML5 canvas element, but they can also easily and efficiently be written as a fragment shader and rendered using OpenGL. Simply take a look at the source of this page for the JavaScript code.