I’ve been following ilithya’s shader demos for a while and wanted to write about her work weeks ago, but quickly learned that I was out of my depth with shaders. Instead, ilithya helped me write the Introduction to Shaders issue as a warm-up. Now it’s race day!
Let’s dive deep into how ilithya made this awesome shader, Summer Screen.
There’s a few things going on here. First, we’ve got these vertical line sections that are shifting back and forth. Then we’ve got these gradients coming in from the top and bottom like color waves on a beach. Let’s figure out these two effects.
First we’ll check out a shortened version of the GLSL code so we can see what’s going on. You might see a new language and some math here but don’t worry! We’ll trot through them nice and slow.
// Create three static colors vec3 colorA = vec3(0.5, 0., 0.5); // Purple vec3 colorB = vec3(1.0, 0.41, 0.71); // Pink vec3 colorC = vec3(0.0, 1.0, 1.0); // Teal blue // Create three dynamic colors vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time))); vec3 color2 = mix(colorB, colorC, (y-sin(u_time))*0.5); vec3 color3 = mix(color1, color2, x*cos(u_time+2.)); // Our color output gl_FragColor = vec4(color3, 1.0);
The first thing to address is that GLSL is a typed language, so when you declare a variable you also declare its type.
float for our purposes can be thought of as a number with a decimal.
vec4 has one more value than a
vec3 and is what the output of a shader
main() function needs to be. The four values there align to red, green, blue, alpha.
First ilithya creates
colorC. Each of these are static colors that aren’t going to change.
Then she uses these colors and other variables to create dynamic colors that will change. We’ll look at the vertical lines first.
For the vertical lines let’s zoom in and see what’s really going on here.
The lines are really repeated gradients going from dark to light.
To make gradients with GLSL we use the
mix takes three arguments:
x: the start of the range,
y: the end of the range, and
a: the point between
// vec3 mix(vec3 x, vec3 y, vec3 a) vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time)));
ilithya is making a gradient between the purple and pink, so those are her first two arguments. The colors get mixed in different amounts based on the value of
a, which is a value from
0, we’ll see 100% of the first color. If
1 we’ll see 100% of the second color. Anything in between and we get a blend:
So that can give us one gradient, but how do we get repeating gradients?
To get the gradients to stop and start from
0 again, ilithya is using a GLSL function called
fract. This returns the fraction of a number, dropping the integer.
fract(x) is the same as doing
x - Math.floor(x) or
fract(x) will always be a float between 0 and 1.
I’m going to simplify it beyond what ilithya has in her piece to show how the
fract function works:
As we can see, even though
x keeps increasing, the result of
fract(x) just returns the fraction, or whatever comes after the decimal, giving us that same purplish color. Translate that over the entire piece and you’ve got ilithya’s cool vertical bars!
We’ll touch on how all these pieces animate a bit later.
Now we move to
vec3 color2 = mix(colorB, colorC, (y - sin(u_time)) * 0.5);
Another mix, this time between the pink and blue. No
fract this time, so we’re getting a smooth gradient. The
y value makes it change color from bottom to top. We’ll touch on the
sin(u_time) bit soon.
Then she combines
color2, using a mix amount that gets closer to
1 on the right side. The horizontal change is due to the
x value. The
cos(u_time+2) is for animation.
vec3 color3 = mix(color1, color2, x*cos(u_time+2.));
Here’s a simplified version of the shader’s parts and the final color output:
You can see our simplified version looks similar to the finished shader, it’s just missing animations.
There are three main animations happening in Summer Screen:
- The bars shifting left and right in
- The pink and blue gradient moving up and down in
- The mix value for
color3moving left and right
Each of these are using the value of the current time, and familiar math functions,
vec3 color1 = mix(colorA, colorB, fract(x*fr+cos(u_time))); vec3 color2 = mix(colorB, colorC, (y-sin(u_time))*0.5); vec3 color3 = mix(color1, color2, x*cos(u_time+2.));
time value comes from a Three.js
Clock and is how ilithya animates most of her shaders. It returns the elapsed time since the page loaded.
When ilithya calls either
cos(u_time) she’s going to get back a value between -1 and 1, and it’ll transition smoothly. If you’re familiar with CSS animations, it feels a lot like an
Super Quick Sine Wave Refresher:
As we increase the value along the horizontal axis, the sine wave moves us up and peaks at
1, brings us down to
0 and bottoms out at
-1 before climbing again. No matter high the horizontal value is, the sine of that value will always be between
1. Cosine works similarly but gives a different value.
And there you have it! Three different parts being blended and animated together to make one awesome animation.
As she was helping me understand it, ilithya encouraged me to swap values out in the shader code. I highly recommend you go and do just that.
To see the vertical bars isolated, change the last line of the shader code to:
gl_FragColor = vec4(color1, 1.0);
To see the vertical pink and blue gradient:
gl_FragColor = vec4(color2, 1.0);
Or you can change that to
colorC to see a screen of solid color. Play with it, have fun, figure out how it works.
I won’t be diving into how this shader works, but it’s just so dang soothing to stare at, I had to share it. Let me know if you’d like to read about it in a future issue.