In this tutorial you’ll learn how to create video-like animations, using WebGL fragment shaders and HTML canvas.
Keep in mind these animations are not videos and are generated in real time with WebGL in your GPU.
This article is broken down into 4 parts:
Intro
We'll go over the basics and project setup.Hulu
Next we'll look into drawing shapes and controlling time in animation.HBO
We'll reiterate these techniques and take a dive into noise functions.Netflix
Finally we'll learn techniques to texturize shapes while under scale, rotation and timing constraints.
1 - Intro
What are Fragment Shaders?
My favorite definition of a fragment shader is a quote from The Book of Shaders which is probably the most known resource out there:
"Shaders are a set of instructions, but the instructions are executed all at once for every single pixel on the screen. That means the code you write has to behave differently depending on the position of the pixel on the screen. Like a type press, your program will work as a function that receives a position and returns a color, and when it's compiled it will run extraordinarily fast"
Another great resource to learn more about fragment shaders is the list of SDF functions by the most amazing pioneer GPU artist: Inigo Quilez.
In order to write and run fragment shader code, we need to display a 3D WegGL context to the screen on a canvas element. For that we use the library THREE.js
Read more details on how to run your shader code:
Can we get coding now?
The demo is built using the Vue CLI
which is super nice and allows me to easily deploy my projects, eventhough the project does not use Vue. Whichever is your favorite workflow, the code is in ES6 and can be reused in other environments.
Next thing we want to do is define our shaders. Here we define them in Javascript using strings with the back quote `
to allow us line breaks.
The language we use in these shader is the OpenGL Shading Language
more often referred as GLSL
.
We need to define two types of shaders:
Vertex Shader
to map the position of the pixel on the screenFragment Shader
to draw something onto the pixel
First the vertex shader:
The rendering technique we are going to use relies on WebGL which is usually more associated with 3D.
Here we are simply drawing a plane on the screen and working later on each pixel (in the fragment shader) so we don't really have much to do in the vertex shader.
No normals to calculate, no geometry transformations, our vertex
shader is going to be very tiny:
const vertexShader = `
void main() {
gl_Position = vec4( position, 1.0 );
}`
Of course there is more to it but THREE
hides and does a lot of the work for us so we can focus on the shaders only.
Next we look at the Fragment Shader
which has a bit more going on:
While we're going to discuss the code chunk by chunk, you can see the full code here
Defines
You can declare constant values in your shader programs by using the keyword #define
, for example we define the value of PI
.
#define PI 3.14159265359
Uniforms
Uniforms are a dynamic way of communicating data from Javascript to the GPU. Here we define the following variables with the prefix u_
as in... unicorn you guess it.
resolution
The size of the canvas invec2(XY)
formatmouse
The mouse coordinates on the canvas invec2(XY)
formattime
The time elapsed since the app started asfloat
number
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
Each of these uniforms are available in the shader and can be updated in real time by Javascript.
The Main Function
This part of your program gets executed every time the shader is rendered.
In its context of execution it receives a few native variables.
The two fundamental ones are:
gl_FragCoord
gl_FragColor
gl_FragCoord
is the position of the pixel, because it's in a 3D space it comes as a vec3(XYZ)
type. And because we're drawing on a 2D plane we will only use X
and Y
while Z
won't be needed here.
gl_FragColor
is the output color. It comes as a vec4(RGBA)
for its Red
, Green
, Blue
, Alpha
values that will define the color of the pixel on your screen.
It's interesting to note that in
GLSL
thevec2
,vec3
andvec4
data types allow to be accessed via both keys, as if they were aliased:
x
,y
,z
,w
, or alternativelyr
,g
,b
,a
.
Now with that in mind, let's take a look at our very basic hello world:
With the code below we are painting the screen green. Indeed, we're setting the output gl_FragColor
to the following RGBA values:
- Red: 0.0
- Green: 1.0
- Blue: 0.0
- Alpha: 1.0
void main() {
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
And the result would look like this:
Next say we want to paint the screen with red gradiently from left to right, starting at opacity 0
and finishing at opacity 1
all the way to the right.
In order to do that we divide gl_FragCoord
(provided in the function scope) by u_resolution
(the uniform we set earlier to get the screen size). From this division we get normalized coordinates of our pixels position on the screen, where vec2(0.0, 0.0)
is the top left of the screen and vec2(1.0, 1.0)
is the bottom right of the screen.
void main() {
// Normalized pixel coordinates (from 0 to 1)
vec2 st = gl_FragCoord / u_resolution.xy;
// Output to screen
gl_FragColor = vec4(st.x, 1.0, 0.0, 1.0);
}
The image below shows the new result. On the left we start green as the red opacity is low, and we end up yellow on the right side as mixing red with green results in yellow.
Next we do the same on the Y axis:
void main() {
// Normalized pixel coordinates (from 0 to 1)
vec2 st = gl_FragCoord / u_resolution.xy;
// Output to screen
gl_FragColor = vec4(st.x, 1.0, st.y, 1.0);
}
If you clicked on the Shadertoy links, you may have noticed native variable names are slightly different there for UI reasons, but don't let that confuse you, the concepts and language are the same.
You should now have a basic understanding of the fragment shader pipeline and be ready to go deeper with our next part on drawing shapes and animation timing
Also keep in mind that the entire code is available here
.
Next checkout the follow up articles, where i describe how i replicated famous networks pre-roll animation using shaders only:
Hope you enjoyed this tutorial! The stuff I write about is a way for me to improve my learnings - probably just like you right now. So please if you catch any issue or want to suggest an edit, use the section below or reach out on twitter. Cheers!