Article contents
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:
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. |
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).
|
|
|
|
|
Knowing the formulae, implementing them as a fragment shader in GLSL is straightforward:
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.
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.
Comments
When the pixel coordinates scale is really different than from your example the center movement won't be scaled properly. To fix this issue I had to replace every occurrence of u_k/2.0 with 0.5/u_k like so:
vec2 c = v_coords * u_k - 0.5/u_k;
c += 0.5/u_k * vec2(sin(u_time/3.0), cos(u_time/2.0));
I know it's a bit scandalous to ask, but could you help me to adapt this code ? I have tried a few other examples, but I find your output really nice.
Thank you in advance,
Philippe (pim.man AT gmail.com)
Especially the relationship between the coordinates and the sinus curve and how this produces the visual effect.
I also don't get why you use xx and yy in your code instead of the real x and y value.
BTW, it would be nice if you could explain how this can be turned into a pre-generated constant table (other plasma programs I've seen use that approach, with the advantage of having to do the costly floating point calculations only once). the only clear part here is how you would prepopulate your palette of 256 colors.
For the purpose of this article I just wanted to recreate the effect so I didn't do any optimizations like precomputing the sines, it is unnecessary for simple JavaScript illustrations like these. As you can probably see the animations run just fine. The actual final implementation was done in GLSL and I don't think that precomputation is viable in this case.
My nostalgia revived with your clarity! :)
Thank you!