Starry Sky In GLSL

[Editor’s note: Hi all! Elburz here. This week we’ve got a special guest post from Davide Santini, also known as Deltacut. He’s a fantastic member of the community, TouchDesigner developer, and artist. He was responsible for creating the Italian translation of our Introduction To TouchDesigner book, and he’s just released his own new book “TouchDesigner Introduction to GLSL”. This blog post touches on some of the basic elements of drawing with GLSL that give you a taste of his new book (links at the bottom!)]

What about GLSL?

Facing GLSL code for the first time can be quite overwhelming. Uniforms, vectors, functions, space coordinates, lots of math everywhere…

If you are not into coding, and sometimes even if you are, your first encounter with this coding language can look like a crash against a wall, with consequent frustration and refuse to keep learning it. And, even if you have never encountered GLSL, you probably have heard of it being particularly difficult and painful to master!

Yet, GLSL is one of the most useful tricks in the sleeves of a video artist. Deferring all the work to the GPU, thus lowering the load on the CPU, it can be crucial in some projects, while in others it can open up many new possibilities.

But what exactly is GLSL?

GLSL, short for OpenGL Shading Language, is a high-level programming language modeled after C and C++.

It is mainly used to create snippets of code called shaders that will be executed by the GPU of the system in use. The shaders thus created can be enormously optimized to make the best out of modern graphic cards, allowing the user to manipulate geometries, generate particle systems or even create post-processing effects, with as little impact as possible on system-performance.

There are different kinds of fragment shaders, but in this introductory-level article, we’ll only deal with the Pixel shaders, the pipeline section responsible for assigning the desired colors and transparency values to each pixel.

A minimal set of mathematical skills can help you follow and understand better all of the inner workings of the shaders, but don’t be afraid. If you know how to add and multiply numbers, and have an idea of what a sine function is, you know enough to follow along with the explanations.

So, don’t be scared, and follow me into this mysterious world!

Draw a polygon

Let’s start by opening an empty Network, and creating a “GLSL TOP” node. We’ll need to write everything related to code inside the “Pixel Shader” DAT attached to this node.

If we want to draw a polygon, the first thing to do is to create a variable stating how many sides we want our polygon to have. We can call it nSides. With this information, and some basic math, we can calculate the angle at which every vertex is located: knowing that a complete circle spans for Pi*2 radians, we can simply divide this by the number of sides:

float angles = PI * 2.0 / nSides;

where float allows our variable to store floating-point values.

Next, we need to define theta, the angle that any point creates with the positive x-axis, using the arctangent function:

float theta = atan(vUV.s, vUV.t);

Here, vUV.s and vUV.t are the x and y components of the space coordinates, that span from 0.0 to 1.0 from left to right, and from 0.0 to 1.0 from bottom to top of our output image.

The last operation we need to perform is calculating the distance of every point from a vertex, and we can do this with a bit of trigonometry:

float dist = cos(round(theta / angles) * angles – theta) * length(vUV.st);

In this expression, we used the round function to round every value to the closest integer number, straightening the sides of our shape.

We can now put everything together, writing:

#define PI 3.14159265359

float nSides = 5.0;
out vec4 fragColor;

void main(){
  float theta = atan(vUV.s, vUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round(theta / angles) * angles - theta) * length(vUV.st);
  vec3 myPoly = vec3(dist);
  vec4 color = vec4(myPoly, 1.0);

  fragColor = TDOutputSwizzle(color);
}

In this code, you can see a couple of lines that we have not explained yet. Briefly, the first line is simply a definition of the Pi value; line 5 defines an output (later used in line 15) so that we can show our result; the main function, opening at line 7, is the container where our calculations happen. We also treated our polygon as a three-dimensional vector, accounting for red, green and blue values, and only later we added a constant alpha value of 1.0 (line 13).

Yet, this doesn’t look like a polygon.

To solve this problem, we can use the step function, that will “cut” our gradient at a certain radius, returning 0 when inside of this radius, and 1 outside. Also, if we want a white polygon on a black background, we will need to invert the result of the step function, writing:

1.0 - step()

This will result in the following code:

#define PI 3.14159265359

float nSides = 5.0;
float radius = 0.3;

out vec4 fragColor;

void main(){
  float theta = atan(vUV.s, vUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round(theta / angles) * angles - theta) *
length(vUV.st);
  vec3 myPoly = vec3(1.0 - step(radius, dist));

  vec4 color = vec4(myPoly, 1.0);

  fragColor = TDOutputSwizzle(color);
}

That’s already better, but our pentagon is centered in the bottom-left corner of the canvas. What if we want to place it in another position?

We can move the plane in which our figure is placed by changing the space coordinates, from vUV.st to our own myUV:

#define PI 3.14159265359

float nSides = 5.0;
float radius = 0.3;
vec2 center = vec2(0.4, 0.6);

out vec4 fragColor;

void main(){
  vec2 myUV = vUV.st - center;
  float theta = atan(myUV.s, myUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round(theta / angles) * angles - theta) *
length(myUV);
  vec3 myPoly = vec3(1.0 - step(radius, dist));
  
  vec4 color = vec4(myPoly, 1.0);
  
  fragColor = TDOutputSwizzle(color);
}

Colors

That’s a polygon! Adding colors is now super easy.

First of all, we can create two variables storing the red, green and blue values of both the polygon and background colors:

vec3 myPolyColor = vec3(0.7, 0.7, 0.0);
vec3 myBackgroundColor = vec3(0.2, 0.1, 0.5);

Next, we must choose how to combine these two colors. The easiest way is applying the background color everywhere, and add the polygon color only inside the white region we already created.

The code should look like this:

#define PI 3.14159265359

float nSides = 5.0;
float radius = 0.3;

vec2 center = vec2(0.4, 0.6);
vec3 myPolyColor = vec3(0.7, 0.7, 0.0);
vec3 myBackgroundColor = vec3(0.2, 0.1, 0.5);

out vec4 fragColor;

void main(){
  vec2 myUV = vUV.st - center;
  float theta = atan(myUV.s, myUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round(theta / angles) * angles - theta) *
length(myUV);
  vec3 myPoly = vec3(1.0 - step(radius, dist));

  vec4 color = vec4(myBackgroundColor + myPolyColor * myPoly,
1.0);

  fragColor = TDOutputSwizzle(color);
}

Spice it up!

Now, this works, but we can definitely make it more interesting.

Manipulating the mathematical operations we used, we can deform the shape in different ways. Try to add or subtract some floating values to the first term called theta in line 16 of the code.

This will create some “spikes” in every vertex of the polygon.

As the last step, we can animate these spikes.

Create a “LFO” CHOP. This will drive our animation.

In its parameters, in the “LFO” page”, lower the “frequency” field to 0.4, as well as the “Amplitude” value.

For better management of the Network, wire a “Null” CHOP to the “LFO” CHOP. Next, in the parameters of the “GLSL” TOP, in the “Vectors” page, write “uSpike” in the Uniform Name field. Next, in the first “Value” field write:

op('null1')['chan1']

This way we linked the LFO animation to a uniform that we called “uSpikes”, and that we can now use inside of our code.

To do this we must declare a float uniform called the same.

We can now write the new code:

#define PI 3.14159265359

uniform float uSpikes;

float radius = 0.2;
float nSides = 5.0;
vec2 center = vec2(0.4, 0.6);

vec3 myPolyColor = vec3(0.7, 0.7, 0.0);
vec3 myBackgroundColor = vec3(0.2,0.1,0.5);

out vec4 fragColor;

void main(){
  vec2 myUV = vUV.st - center;
  float theta = atan(myUV.s, myUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round((theta + uSpikes) / angles) * angles
theta) * length(myUV);
  vec3 myPoly = vec3(1.0 - step(radius, dist));

  vec4 color = vec4(myBackgroundColor + myPolyColor * myPoly,
1.0);

  fragColor = TDOutputSwizzle(color);
}

Patterns

Let’s say we want to iterate our “star” in a 5×5 grid layout. The easiest method involves manipulating the coordinate space.

In the main function loop, we need to multiply the coordinate space by 5.0. This would scale our drawing, and place it in the bottom-left corner. The coordinate space would, in fact, span from 0.0 to 5.0, both horizontally and vertically, while our drawing fills the 1.0×1.0 square that goes from 0.0 to 1.0 in both directions. Now we need to tell the software to treat the other 1.0×1.0 squares the same as our bottom-left one.

The fract function is what works best for us. Since it returns the fractional part of the numbers we feed it, it will interpret all the numbers from 1.0 to 2.0 as if they were from 0.0 to 1.0. The same behavior will be applied for numbers from 2.0 to 3.0, from 3.0 to 4.0 and from 4.0 to 5.0, interpreted once again as 0.0-1.0.

Therefore, the code becomes something like this:

#define PI 3.14159265359

uniform float uSpikes;

float radius = 0.2;
float nSides = 5.0;
vec2 center = vec2(0.4, 0.6);
vec3 myPolyColor = vec3(0.7, 0.7, 0.0);
vec3 myBackgroundColor = vec3(0.2,0.1,0.5);

out vec4 fragColor;

void main(){
  vec2 myUV = vUV.st;
  myUV *= 5.0;
  myUV = fract(myUV);
  myUV = myUV - center;

  float theta = atan(myUV.s, myUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round((theta + uSpikes) / angles) * angles
theta) * length(myUV);
  vec3 myPoly = vec3(1.0 - step(radius, dist));

  vec4 color = vec4(myBackgroundColor + myPolyColor * myPoly,
1.0);

  fragColor = TDOutputSwizzle(color);
}

A further step we can take is to offset the grid creating more interesting patterns.

What we want to achieve is to move one every two rows of our grid of a determined value. We can use the mod function to loop the vertical coordinate of the space between 0.0 and 2.0, with the following expression:

mod(myUV.y, 2.0);

Next, we can use the step function to output 1.0 if we are in the 1.0-2.0 range, and 0.0 if we are in the 0.0-1.0 range.

step(1.0, mod(myUV.y, 2.0))

In our drawing, these two passages will give us a value of 0.0 for every odd row, and a value of 1.0 for every even row, that we can simply sum to the horizontal direction of our coordinate system. We can also decide the amount of offset we want, just by multiplying the step function for the needed value (for example 0.5 will offset our drawing of half a grid cell). The code now becomes something like this:

#define PI 3.14159265359

uniform float uSpikes;

float radius = 0.2;
float nSides = 5.0;
vec2 center = vec2(0.4, 0.6);
vec3 myPolyColor = vec3(0.7, 0.7, 0.0);
vec3 myBackgroundColor = vec3(0.2,0.1,0.5);

out vec4 fragColor;

void main(){
  vec2 myUV = vUV.st;
  myUV *= 5.0;
  myUV.x += step(1.0, mod(myUV.y, 2.0)) * 0.5;
  myUV = fract(myUV);
  myUV = myUV - center;

  float theta = atan(myUV.s, myUV.t);
  float angles = PI * 2.0 / nSides;
  float dist = cos(round((theta + uSpikes) / angles) * angles
theta) * length(myUV);
  vec3 myPoly = vec3(1.0 - step(radius, dist));

  vec4 color = vec4(myBackgroundColor + myPolyColor * myPoly,
1.0);

  fragColor = TDOutputSwizzle(color);
}

And that’s what we wanted to create!

Of course, you can further expand this example, by trying to change some of the values we used, or introducing some more uniforms, freely experimenting with our code.

Also, if you think you want to learn something more about GLSL, don’t miss my new book “TouchDesigner Introduction to GLSL”, available on Amazon worldwide!

Project file download

Here is the template project file you can download and use as reference:

Wrap up

Editor’s note: Elburz here, Thanks again to Davide for his great guest post and a big congrats on his new book. The examples in this blog are simple but show you that even with a tiny bit of code you can start generating content from scratch in a shader. You should definitely check out Davide’s new book if you’re interested in doing generative graphics in GLSL in TouchDesigner. Enjoy!