Shader Programming: Massively parallel art [Episode 4]
PART 4: Classic Plasma
We're ready to make something pretty awesome now :)
Intro
We actually now know enough GLSL to produce a lovely classic plasma effect.
Within our paradigm of a shader only needing to calculate a single pixel value, you'll be amazed how little code is needed to produce something beautiful.
Let's begin
Although the plasma effect looks really impressive, the technique behind it is actually quite simple. It's really made by taking a sum of sines. Let me show you what I mean by that!
Let's start by just outputting a shade of grey where R, G and B are all just the sine of the x coord. Paste this into Shadertoy:
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
float f = sin(uv.x);
fragColor = vec4(f, f, f, 1);
}
You shouldn't have any problem understanding the shader program above; if anything is unfamiliar, look back to the previous episodes (or comment below).
At this point, you should see a black-to-grey horizontal fade:
Make it repeat
As we know, our x coord varies from 0 to 1.0. Let's multiply the x coord by a constant, so that instead of going from 0 to 1.0 across the horizontal we get a value that goes from, say, 0 to 20.0. Modify just the middle line of the shader prog to:
float f = sin(uv.x * 20.);
Because the sine function repeats every 2 * PI units, the horizontal plane of 20 units has enough room for slightly over 3 full repeats, and we get horizontal stripes:
I know I said previously that legit values for colour components vary between 0 and 1.0, and sine outputs values from -1.0 to 1.0, but as you can see GLSL doesn't really mind negative values -- they're equivalent to 0, so we just see black.
Anyway, let's make our stripes move! This is easy enough; we can just use time as an offset to the x coord:
float f = sin((uv.x + iTime) * 20.);
Try it along with me in Shadertoy.
Oops, that's a bit fast! Let's multiply time by a constant to slow things down.
float f = sin((uv.x + iTime * .4) * 20.);
That's better. Currently, because iTime will always increase, our stripes will only move in one direction. Do you remember how to get oscillation from a continous variable? Of course you do -- with sine!
float f = sin((uv.x + sin(iTime * .4)) * 20.);
Run that, and you should see the stripes moving left and right.
That's a complicated looking line, but we've built it up step by step and hopefully you were able to follow along. That's also the most complex thing we'll meet today! From here to plasma is not a long journey :)
Adding another dimension
To make a lovely plasma, we actually just need to add more out of phase stripes. Note that if you change uv.x
in the current shader to uv.y
(try it!), you get horizontal instead of vertical stripes.
Let's make it so that we calculate both vertical and horizontal stripes, and add them together.
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
// Calculate uv
vec2 uv = fragCoord / iResolution.xy;
// Calculate vertical stripes
float f = sin((uv.x + sin(iTime * .4)) * 20.);
// Add in horizontal stripes
f = f + sin((uv.y + sin(iTime * .4)) * 20.);
// Output the result
fragColor = vec4(f, f, f, 1.);
}
If you run that, you'll see diagonally moving diamonds.
You may wonder why, when we add our horizontal and vertical patterns of stripes together, we don't simply see a grid of stripes superimposed on each other. That's because the parts we see as only black when looking at an individual set of stripes don't have a zero value; they're negative, so when we add the two pictures together some of the additions actually result in a subtraction.
To make the movement more interesting, let's have each axis move at an independent speed.
Change the line that adds in the horizontal stripes to:
f = f + sin((uv.y + sin(iTime * .1)) * 13.);
The change of time scalar separates the movement of each axis, while changing the y coord scalar allows more regularly-shaped diamonds.
Adding yet more dimensions
If we add just one more dimension, something jaw-dropping happens in terms of visual interest. Let's add in another set of vertical stripes, with slightly different scalars for time and x coord.
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
float f = sin((uv.x + sin(iTime * .4)) * 20.);
f = f + sin((uv.x + sin(iTime * .23)) * 11.);
f = f + sin((uv.y + sin(iTime * .1)) * 13.);
fragColor = vec4(f, f, f, 1);
}
Please do paste this one into Shadertoy -- the screenshot cannot do the animation justice!
Wow!
Note that these are not particular or specific numbers -- I'm just coming up with them off the top of my head. The only thing that matters is that they're slightly different for each sine term that we introduce, so that they are out of phase with each other. Making some of these scalars prime numbers is a really good idea as it means the overall animation will have a longer repeat time. I'll leave the reason for that as an exercise to the reader ;)
Let's add MORE sine terms with different phases:
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
// x phases (vertical stripes)
float f = sin((uv.x + sin(iTime * .4)) * 20.);
f = f + sin((uv.x + sin(iTime * .23)) * 11.);
f = f + sin((uv.x + sin(iTime * .1)) * 4.);
// y phases (horizontal stripes)
f = f + sin((uv.y + sin(iTime * .11)) * 13.);
f = f + sin((uv.y + sin(iTime * .33)) * 17.);
f = f + sin((uv.y + sin(iTime * .29)) * 7.);
fragColor = vec4(f, f, f, 1);
}
Again, very worth a run in Shadertoy to see the movement.
At this point, we have all the interesting motion we need for our plasma.
In fact, only one thing remains!
Colour it
At the moment, our plasma is monochrome because we use our final f value for R, G and B.
Instead of doing that, we can very easily colour our plasma by using the HSV colour space. We can treat our f value as H (hue), and convert to RGB for final output.
GLSL doesn't have a built-in HSV-to-RGB function, so I'm using an implementation found here.
hsv2rgb() expects a vec3 representing hue, saturation and value, each between 0 and 1.0. We'll hard-code saturation and value to 1.0, and pass in a hue based on our f value.
Since the range of our f value is {-1..1}, we must shift the range to {0..1} by adding 1 and dividing by 2.
// Scale f into range {0..1}
f = (f + 1.) / 2.;
// Convert to RGB
vec3 col = hsv2rgb(vec3(f, 1.0, 1.0));
Adding this into our shader gives us the below.
// HSV-to-RGB helper
// From https://gist.github.com/983/e170a24ae8eba2cd174f
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
// Calculate uv
vec2 uv = fragCoord / iResolution.xy;
// x phases (vertical stripes)
float f = sin((uv.x + sin(iTime * .4)) * 20.);
f = f + sin((uv.x + sin(iTime * .23)) * 11.);
f = f + sin((uv.x + sin(iTime * .1)) * 4.);
// y phases (horizontal stripes)
f = f + sin((uv.y + sin(iTime * .11)) * 13.);
f = f + sin((uv.y + sin(iTime * .33)) * 17.);
f = f + sin((uv.y + sin(iTime * .29)) * 7.);
// Scale f into range {0..1}
f = (f + 1.) / 2.;
// Convert to RGB
vec3 col = hsv2rgb(vec3(f, 1.0, 1.0));
// Outout final colour
fragColor = vec4(col, 1);
}
And that gives us classic plasma, running on the GPU!
Final polish
From the above, you should absolutely tweak in any direction you can think of. Add more dimensions, adjust speeds, scale stuff, generally go wild.
For my final version (which rendered the pic at the start of the article), I've added some circular stripes by using sine and cosine, and slowed everything waaay down for a lava lamp-ish vibe.
Finally, I limit the colour space to a smaller region of the palette, and shift that region over time.
// *** Simple Plasma by Rex ***
// HSV-to-RGB helper
// From https://gist.github.com/983/e170a24ae8eba2cd174f
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
// Calculate uv
vec2 uv = fragCoord / iResolution.xy;
// Scale time
float time = iTime * .2;
// x phases (vertical stripes)
float f = sin((uv.x + sin(time * .4)) * 20.);
f = f + sin((uv.x + sin(time * .23)) * 11.);
f = f + sin((uv.x + sin(time * .1)) * 4.);
// y phases (horizontal stripes)
f = f + sin((uv.y + sin(time * .11)) * 13.);
f = f + sin((uv.y + sin(time * .33)) * 17.);
f = f + sin((uv.y + sin(time * .29)) * 7.);
// Circular phases
f += sin((uv.y + sin(time * .19)) * 3.)
+ cos((uv.x + sin(time * .31)) * 5.);
f += sin((uv.y + sin(time * 1.47)) * 3.3)
+ cos((uv.x + sin(time * .72)) * 10.);
// Scale f into range {0..1}
f = (f + 1.) / 2.;
// Limit colour range and shift palette
f = (f * .1) + sin(time * .5);
// Convert to RGB
vec3 col = hsv2rgb(vec3(f, 1.0, 1.0));
// Outout final colour
fragColor = vec4(col, 1);
}
Please do paste that into Shadertoy, put some suitable music on, and hit fullscreen mode!
It's a little surprising that so much seemingly random motion can be produced from just a few interacting out-of-phase sine waves. In fact there is no randomness here at all, and the pattern is completely deterministic. Every time you hit Rewind and run it from time 0, it will generate the same animation.
This was a mammoth episode! Well done if you've stuck with me all the way through the series so far. I hope you've had fun, and are beginning to see the mighty power of shaders.
If you do feel like some homework: See if you can implement a zoom effect in the plasma by scaling uv with time. It can be done with a single line ;)
Next time we'll learn how to use textures in our shaders, and implement the classic "rotozoom" effect.
Thank you for contributing to #LearnWithSteem ( #tutorial, #lesson, #assignment) theme. This post has been upvoted by @cryptogecko using @steemcurator09 account. We encourage you to keep publishing quality and original content in the Steemit ecosystem to earn support for your content.
Regards,
Team #Sevengers
Thank you very much :) I'm going to keep the series going as long as I have ideas for shaders.