Replicating Networks Pre-Roll Animations with WebGL Fragment Shaders

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.

Checkout the 3 demos: Netflix Hulu HBO Netflix Hulu HBO





This article is broken down into 4 parts: Intro

We'll go over the basics and project setup.

We'll go over the basics and project setup. Hulu

Next we'll look into drawing shapes and controlling time in animation.

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.

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

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 screen

to map the position of the pixel on the screen Fragment 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 in vec2(XY) format

The size of the canvas in format mouse The mouse coordinates on the canvas in vec2(XY) format

The mouse coordinates on the canvas in format time The time elapsed since the app started as float 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 the vec2 , vec3 and vec4 data types allow to be accessed via both keys, as if they were aliased: x , y , z , w , or alternatively

, , , , or alternatively r , 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:

Check it out on ShaderToy

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.

Check it out on ShaderToy

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); }

Check it out on ShaderToy

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: Hulu

Next we'll look into drawing shapes and controlling time in animation.

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.

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.

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!