Shader Toy Tutorial
Contents
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:
Arcade includes additional code making it easier to run these ShaderToy shaders in an Arcade program. This tutorial helps you get started.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import arcade
# Derive an application window from Arcade's parent Window class
class MyGame(arcade.Window):
def __init__(self):
# Call the parent constructor
super().__init__(width=1920, height=1080)
def on_draw(self):
# Clear the screen
self.clear()
if __name__ == "__main__":
MyGame()
arcade.run()
|
Step 2: Load and display a shader
This program will load a GLSL program and display it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import arcade
from arcade.experimental import Shadertoy
# Derive an application window from Arcade's parent Window class
class MyGame(arcade.Window):
def __init__(self):
# Call the parent constructor
super().__init__(width=1920, height=1080)
# Read a GLSL program into a string
file = open("circle_3.glsl")
shader_sourcecode = file.read()
# Create a shader from it
self.shadertoy = Shadertoy(size=self.get_size(),
main_source=shader_sourcecode)
def on_draw(self):
# Run the GLSL code
self.shadertoy.render()
if __name__ == "__main__":
MyGame()
arcade.run()
|
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.
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// How far is the current pixel from the origin (0, 0)
float distance = length(uv);
// Default our color to white
vec3 color = vec3(1.0, 1.0, 1.0);
// If we are more than 20% of the screen away from origin, use black.
if (distance > 0.2)
color = vec3(0.0, 0.0, 0.0);
// Output to the screen
fragColor = vec4(color, 1.0);
}
|
The output of the program looks like this:
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 3: Move origin to center of screen, adjust for aspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// Position of fragment relative to center of screen
vec2 pos = uv - 0.5;
// Adjust y by aspect ratio
pos.y /= iResolution.x/iResolution.y;
// How far is the current pixel from the origin (0, 0)
float distance = length(pos);
// Default our color to white
vec3 color = vec3(1.0, 1.0, 1.0);
// If we are more than 20% of the screen away from origin, use black.
if (distance > 0.2)
color = vec3(0.0, 0.0, 0.0);
// Output to the screen
fragColor = vec4(color, 1.0);
}
|
Note
To Be Done…
The rest of the is TBD
Glow
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Adapted from https://www.shadertoy.com/view/3s3GDn
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
//*********** Basic setup **********
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// Position of fragment relative to centre of screen
vec2 pos = 0.5 - uv;
// Adjust y by aspect for uniform transforms
pos.y /= iResolution.x/iResolution.y;
//********** Glow **********
// Equation 1/x gives a hyperbola which is a nice shape to use for drawing glow as
// it is intense near 0 followed by a rapid fall off and an eventual slow fade
float dist = 1.0/length(pos);
//********** Radius **********
// Dampen the glow to control the radius
dist *= 0.1;
//********** Intensity **********
// Raising the result to a power allows us to change the glow fade behaviour
// See https://www.desmos.com/calculator/eecd6kmwy9 for an illustration
// (Move the slider of m to see different fade rates)
dist = pow(dist, 0.8);
// Knowing the distance from a fragment to the source of the glow, the above can be
// written compactly as:
// float getGlow(float dist, float radius, float intensity){
// return pow(radius/dist, intensity);
// }
// The returned value can then be multiplied with a colour to get the final result
// Add colour
vec3 col = dist * vec3(1.0, 0.5, 0.25);
// Tonemapping. See comment by P_Malin
col = 1.0 - exp( -col );
// Output to screen
fragColor = vec4(col, 1.0);
}
|
Other examples
This short ShaderToy demo loads a GLSL file and displays it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import arcade
from arcade.experimental import Shadertoy
class MyGame(arcade.Window):
def __init__(self):
# Call the parent constructor
super().__init__(width=1920, height=1080, title="Shader Demo", resizable=True)
# Keep track of total run-time
self.time = 0.0
# Read in a GLSL program and create a shadertoy out of it
# file_name = "fractal_pyramid.glsl"
# file_name = "cyber_fuji_2020.glsl"
file_name = "earth_planet_sky.glsl"
# file_name = "flame.glsl"
# file_name = "star_nest.glsl"
file = open(file_name)
shader_sourcecode = file.read()
self.shadertoy = Shadertoy(size=self.get_size(), main_source=shader_sourcecode)
def on_draw(self):
self.clear()
mouse_pos = self.mouse["x"], self.mouse["y"]
self.shadertoy.render(time=self.time, mouse_position=mouse_pos)
def on_update(self, dt):
# Keep track of elapsed time
self.time += dt
if __name__ == "__main__":
MyGame()
arcade.run()
|
You can click on the caption below the example shaders here to see the source code for the shader.
Some other sample shaders:
Writing shaders is beyond the scope of this tutorial. Unfortunately, I haven’t found one comprehensive tutorial on how to write a shader. There are several smaller tutorials out there that are good.
Here is one learn-by-example tutorial:
https://www.shadertoy.com/view/Md23DV
Here’s a video tutorial that steps through how to do an explosion: