Shader Toy Tutorial - Glow#

../../_images/cyber_fuji_2020.png

cyber_fuji_2020.glsl Full Listing#

Graphics cards can run programs written in the C-like language OpenGL Shading Language, or GLSL for short. These programs can be easily parallelized and run across the processors of the graphics card GPU.

Shaders take a bit of set-up to write. The ShaderToy website has standardized some of these and made it easier to experiment with writing shaders. The website is at:

https://www.shadertoy.com/

Arcade includes additional code making it easier to run these ShaderToy shaders in an Arcade program. This tutorial helps you get started.

PyCon 2022 Slides#

This tutorial is scheduled to be presented at 2022 PyCon US. Here are the slides for that presentation:


Step 1: Open a window#

This is simple program that just opens a basic Arcade window. We’ll add a shader in the next step.

Open a window#
 1import arcade
 2
 3# Derive an application window from Arcade's parent Window class
 4class MyGame(arcade.Window):
 5
 6    def __init__(self):
 7        # Call the parent constructor
 8        super().__init__(width=1920, height=1080)
 9
10    def on_draw(self):
11        # Clear the screen
12        self.clear()
13
14if __name__ == "__main__":
15    MyGame()
16    arcade.run()

Step 2: Load a shader#

This program will load a GLSL program and display it. We’ll write our shader in the next step.

Run a shader#
 1import arcade
 2from arcade.experimental import Shadertoy
 3
 4# Derive an application window from Arcade's parent Window class
 5class MyGame(arcade.Window):
 6
 7    def __init__(self):
 8        # Call the parent constructor
 9        super().__init__(width=1920, height=1080)
10
11        # Load a file and create a shader from it
12        shader_file_path = "circle_1.glsl"
13        window_size = self.get_size()
14        self.shadertoy = Shadertoy.create_from_file(window_size, shader_file_path)
15
16    def on_draw(self):
17        # Run the GLSL code
18        self.shadertoy.render()
19
20if __name__ == "__main__":
21    MyGame()
22    arcade.run()

Note

The proper way to read in a file to a string is using a with statement. For clarity/brevity our code isn’t doing that in the presentation. Here’s the proper way to do it:

file_name = "circle_1.glsl"
with open(file_name) as file:
    shader_source = file.read()
self.shadertoy = Shadertoy(size=self.get_size(),
                           main_source=shader_source)

Step 3: Write a shader#

Next, let’s create a simple first GLSL program. Our program will:

  • Normalize the coordinates. Instead of 0 to 1024, we’ll go 0.0 to 1.0. This is standard practice, and allows us to work independently of resolution. Resolution is already stored for us in a standardized variable named iResolution.

  • Next, we’ll use a white color as default. Colors are four floating point RGBA values, ranging from 0.0 to 1.0. To start with, we’ll set just RGB and use 1.0 for alpha.

  • If we are greater that 0.2 for our coordinate (20% of screen size) we’ll use black instead.

  • Set our output color, standardized with the variable name fracColor.

GLSL code for creating a shader.#
 1void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 2
 3    // Normalized pixel coordinates (from 0 to 1)
 4    vec2 uv = fragCoord/iResolution.xy;
 5
 6    // How far is the current pixel from the origin (0, 0)
 7    float distance = length(uv);
 8
 9    // Are we are 20% of the screen away from the origin?
10    if (distance > 0.2) {
11        // Black
12        fragColor = vec4(0.0, 0.0, 0.0, 1.0);
13    } else {
14        // White
15        fragColor = vec4(1.0, 1.0, 1.0, 1.0);
16    }
17}

The output of the program looks like this:

../../_images/circle_1.png

Other default variables you can use:

uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;
uniform float iFrame;
uniform float iChannelTime[4];
uniform vec4 iMouse;
uniform vec4 iDate;
uniform float iSampleRate;
uniform vec3 iChannelResolution[4];
uniform samplerXX iChanneli;

“Uniform” means the data is the same for each pixel the GLSL program runs on.

Step 4: Move origin to center of screen, adjust for aspect#

Next up, we’d like to center our circle, and adjust for the aspect ratio. This will give us a (0, 0) in the middle of the screen and a perfect circle.

Center the origin#
 1void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 2
 3    // Normalized pixel coordinates (from 0 to 1)
 4    vec2 uv = fragCoord/iResolution.xy;
 5
 6    // Position of fragment relative to center of screen
 7    vec2 rpos = uv - 0.5;
 8    // Adjust y by aspect ratio
 9    rpos.y /= iResolution.x/iResolution.y;
10
11    // How far is the current pixel from the origin (0, 0)
12    float distance = length(rpos);
13
14    // Default our color to white
15    vec3 color = vec3(1.0, 1.0, 1.0);
16
17    // Are we are 20% of the screen away from the origin?
18    if (distance > 0.2) {
19        // Black
20        fragColor = vec4(0.0, 0.0, 0.0, 1.0);
21    } else {
22        // White
23        fragColor = vec4(1.0, 1.0, 1.0, 1.0);
24    }
25}
../../_images/circle_2.png

Step 5: Add a fade effect#

We can take colors, like our white (1.0, 1.0, 1.0) and adjust their intensity by multiplying them times a float. Multiplying white times 0.5 will give us gray (0.5, 0.5, 0.5).

We can use this to create a fade effect around our circle. The inverse of the distance \(\frac{1}{d}\) gives us a good curve. However the numbers are too large to adjust our white color. We can solve this by scaling it down. Run this, and adjust the scale value to see how it changes.

Add fade effect#
 1void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 2
 3    // Normalized pixel coordinates (from 0 to 1)
 4    vec2 uv = fragCoord/iResolution.xy;
 5
 6    // Position of fragment relative to center of screen
 7    vec2 rpos = uv - 0.5;
 8    // Adjust y by aspect ratio
 9    rpos.y /= iResolution.x/iResolution.y;
10
11    // How far is the current pixel from the origin (0, 0)
12    float distance = length(rpos);
13    // Use an inverse 1/distance to set the fade
14    float scale = 0.02;
15    float strength = 1.0 / distance * scale;
16
17    // Fade our white color
18    vec3 color = strength * vec3(1.0, 1.0, 1.0);
19
20    // Output to the screen
21    fragColor = vec4(color, 1.0);
22}
../../_images/circle_3.png

Step 6: Adjust how fast we fade#

We can use an exponent to adjust how steep or shallow that curve is. If we use 1.0 it will be the same, 0.5 will cause it to fade out slower, 1.5 will fade faster.

We can also change our color to orange.

Adjusts fade speed#
 1void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 2
 3    // Normalized pixel coordinates (from 0 to 1)
 4    vec2 uv = fragCoord/iResolution.xy;
 5
 6    // Position of fragment relative to center of screen
 7    vec2 rpos = uv - 0.5;
 8    // Adjust y by aspect ratio
 9    rpos.y /= iResolution.x/iResolution.y;
10
11    // How far is the current pixel from the origin (0, 0)
12    float distance = length(rpos);
13    // Use an inverse 1/distance to set the fade
14    float scale = 0.02;
15    float fade = 1.5;
16    float strength = pow(1.0 / distance * scale, fade);
17
18    // Fade our orange color
19    vec3 color = strength * vec3(1.0, 0.5, 0.0);
20
21    // Output to the screen
22    fragColor = vec4(color, 1.0);
23}
../../_images/circle_4.png

Step 7: Tone mapping#

Once we add color, the glow looks a bit off. We can do “tone mapping” with a bit of math if you like the look better.

Tone mapping#
 1void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 2
 3    // Normalized pixel coordinates (from 0 to 1)
 4    vec2 uv = fragCoord/iResolution.xy;
 5
 6    // Position of fragment relative to center of screen
 7    vec2 rpos = uv - 0.5;
 8    // Adjust y by aspect ratio
 9    rpos.y /= iResolution.x/iResolution.y;
10
11    // How far is the current pixel from the origin (0, 0)
12    float distance = length(rpos);
13    // Use an inverse 1/distance to set the fade
14    float scale = 0.02;
15    float fade = 1.1;
16    float strength = pow(1.0 / distance * scale, fade);
17
18    // Fade our orange color
19    vec3 color = strength * vec3(1.0, 0.5, 0);
20
21    // Tone mapping
22    color = 1.0 - exp( -color );
23
24    // Output to the screen
25    fragColor = vec4(color, 1.0);
26}
../../_images/circle_5.png

Step 8: Positioning the glow#

What if we want to position the glow at a certain spot? Send an x, y to center on? What if we want to control the color of the glow too?

We can send data to our shader using uniforms. The data we send will be the same (uniform) for each pixel rendered by the shader. The uniforms can easily be set in our Python program:

Run a shader#
 1import arcade
 2from arcade.experimental import Shadertoy
 3
 4# Derive an application window from Arcade's parent Window class
 5class MyGame(arcade.Window):
 6
 7    def __init__(self):
 8        # Call the parent constructor
 9        super().__init__(width=1920, height=1080)
10
11        # Load a file and create a shader from it
12        shader_file_path = "circle_6.glsl"
13        window_size = self.get_size()
14        self.shadertoy = Shadertoy.create_from_file(window_size, shader_file_path)
15
16    def on_draw(self):
17        # Set uniform data to send to the GLSL shader
18        self.shadertoy.program['pos'] = self.mouse["x"], self.mouse["y"]
19        self.shadertoy.program['color'] = arcade.get_three_float_color(arcade.color.LIGHT_BLUE)
20        # Run the GLSL code
21        self.shadertoy.render()
22
23if __name__ == "__main__":
24    MyGame()
25    arcade.run()

Then we can use those uniforms in our shader:

Glow follows mouse, and color can be changed.#
 1uniform vec2 pos;
 2uniform vec3 color;
 3
 4void mainImage(out vec4 fragColor, in vec2 fragCoord) {
 5
 6    // Normalized pixel coordinates (from 0 to 1)
 7    vec2 uv = fragCoord/iResolution.xy;
 8    vec2 npos = pos/iResolution.xy;
 9
10    // Position of fragment relative to specified position
11    vec2 rpos = npos - uv;
12    // Adjust y by aspect ratio
13    rpos.y /= iResolution.x/iResolution.y;
14
15    // How far is the current pixel from the origin (0, 0)
16    float distance = length(rpos);
17    // Use an inverse 1/distance to set the fade
18    float scale = 0.02;
19    float fade = 1.1;
20    float strength = pow(1.0 / distance * scale, fade);
21
22    // Fade our orange color
23    vec3 color = strength * color;
24
25    // Tone mapping
26    color = 1.0 - exp( -color );
27
28    // Output to the screen
29    fragColor = vec4(color, 1.0);
30}
../../_images/circle_6.png

Note

Built-in Uniforms

Shadertoy assumes some built-in values. These can be set during the Shadertoy.render() call. In this example I’m not using those variables because I want to show how to send any value, not just built-in ones. The built-in values:

Python Variable

GLSL Variable

time

iTime

time_delta

iTimeDelta

mouse_position

iMouse

size

This is set by Shadertoy.resize()

frame

iFrame

An example of how they are set:

my_shader.render(time=self.time, mouse_position=mouse_position)

When resizing a window, make sure to always resize the shader as well.

Other examples#

Here’s another Python program that loads a GLSL file and displays it:

Shader Toy Demo#
 1import arcade
 2from arcade.experimental import Shadertoy
 3
 4
 5class MyGame(arcade.Window):
 6
 7    def __init__(self):
 8        # Call the parent constructor
 9        super().__init__(width=1920, height=1080, title="Shader Demo", resizable=True)
10
11        # Keep track of total run-time
12        self.time = 0.0
13
14        # File name of GLSL code
15        # file_name = "fractal_pyramid.glsl"
16        # file_name = "cyber_fuji_2020.glsl"
17        file_name = "earth_planet_sky.glsl"
18        # file_name = "flame.glsl"
19        # file_name = "star_nest.glsl"
20
21        # Create a shader from it
22        self.shadertoy = Shadertoy(size=self.get_size(),
23                                   main_source=open(file_name).read())
24
25    def on_draw(self):
26        self.clear()
27        mouse_pos = self.mouse["x"], self.mouse["y"]
28        self.shadertoy.render(time=self.time, mouse_position=mouse_pos)
29
30    def on_update(self, dt):
31        # Keep track of elapsed time
32        self.time += dt
33
34
35if __name__ == "__main__":
36    MyGame()
37    arcade.run()

You can use this demo with any of the sample code below. Click on the caption below the example shaders here to see the source code for the shader.

Some other sample shaders:

../../_images/star_nest.png

star_nest.glsl Full Listing#

../../_images/flame.png

flame.glsl Full Listing#

../../_images/fractal_pyramid.png

fractal_pyramid.glsl Full Listing#

Additional learning#

On this site:

On other sites: