Introduction to Shaders

Updated: 15 April 2024

These notes and snippets are based/derived from The Book of Shaders by Patricio Gonzalez Vivo & Jen Lowe

Setup

For using these samples you will need some way to view shaders. I am using the glslCanvas VSCode extension for previewing shaders but you can also use another glslCanvas directly

Introduction

Fragment Shaders

Shaders are a set of instructions that are executed simultaneously for each pixel. Effectively, a fragment shader receives a position and returns a colour

Why are Shaders Fast

Shaders run on the GPU and are executed in parallel for each pixel on the screen. Since they run on the GPU they can take advantage of special hardware for speeding up matrix and trigonometric functions. In order to make this possible, there are some limitations imposed on them:

  • Threads are “blind” to other threads - they cannot be dependent on each other
  • Threads are “memoryless” - they do not have access to information about previous computations

GLSL

GLSL stands for “openGL Shading Language” and is one of the languages available for writing shaders

Hello World

THe hello world example for a shader looks like so:

001_helloWorld.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform float u_time;
6
7
void main(){
8
gl_FragColor=vec4(0.,1.,1.,1.);
9
}

This example renders a simple colour to the screen. In the above we can see the following:

  • A main function that is the entry point for the program
  • Builtin variables like gl_FragColor which is the output color for the shader
  • Functions like vec4 which take in floats
  • The vec4 function takes in R,G,B,A channels as floats between 0 and 1
  • Using floats between 0 and 1 is called normalizing and makes it easy for us to map vectors to different output spaces, e.g. color
  • A preprocessor macro (#ifdef ... #endif) which checks if GL_ES is defined (which mostly happens when on mobile browsers)
  • The level of floating point precision to be used precision mediump float which sets the float prevision, this can also be highp or lowp
  • Types are not cast and must be defined explicitly, so 1 will not be automatically cast to the float 1.0

Uniforms

Uniforms are data that is passed to our program from the CPU. Some of the uniforms taht are sent can be seen below:

1
uniform vec2 u_resolution; // Canvas size (width,height)
2
uniform vec2 u_mouse; // mouse position in screen pixels
3
uniform float u_time; // Time in seconds since load

The uniform names and types may differ based on implementation, for example in ShaderToy they are as follows:

1
uniform vec3 iResolution; // viewport resolution (in pixels)
2
uniform vec4 iMouse; // mouse pixel coords. xy: current, zw: click
3
uniform float iTime; // shader playback time (in seconds)

Using Uniforms

We can use uniforms just as any other variable, for example we can use the u_time uniform to set the color based on time:

002_uniformTime.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;// Canvas size (width,height)
6
uniform vec2 u_mouse;// mouse position in screen pixels
7
uniform float u_time;// Time in seconds since load
8
9
void main(){
10
float variation=abs(sin(u_time));
11
gl_FragColor=vec4(variation,0.,0.,1.);
12
}

We can also make use of the GPU accelerated functions like abs and sin. Some other GPU accelerated functions are sin(), cos(), tan(), asin(), acos(), atan(), pow(), exp(), log(), sqrt(), sign(), floor(), ceil(), fract(), mod(), min(), max(), and clamp()

gl_FragCoord

Another thing that is provided similar to gl_FragColor is gl_FragCoord which is the coordinates of the pixel or “screen fragment” that the current thread is working on.

Thegl_ variables are called “varying” and are different to uniforms which are the same across all threads

We can make use of this coordinate in the following code:

003_fragCoord.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
// Normalize the coordinate relative to resolution
11
vec2 st=gl_FragCoord.xy/u_resolution;
12
13
gl_FragColor=vec4(st.x,st.y,1.,1.);
14
}

Algorithmic Drawing

Shaping Functions

There are a few builtin shaping functions that we can use for creating a value that is varied based on some input parameter.

The simplest of these is the step function that returns a float that is either 0 if the value is less that the parameter, or 1 otherwise

004_step.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
float y=step(.5,st.x);
13
vec3 color=vec3(y);
14
15
gl_FragColor=vec4(color,1.);
16
}

There is also a smoothstep function that varies a value across the start and end ranges which can be used as follows:

005_smoothstep.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
float y=smoothstep(.3,.7,st.x);
13
vec3 color=vec3(y);
14
15
gl_FragColor=vec4(color,1.);
16
}

We can also use a combination of two smoothsteps that are slightly offset to create a line as follows:

1
float plot(vec2 st,float x){
2
return
3
smoothstep(x-.01,x,st.y)
4
-smoothstep(x,x+.01,st.y);
5
}

We can then call this with the variable of interest to get an output value at the given point:

1
float y=abs(sin(st.x));
2
float plt=plot(st,y);

And we can then multiply the result by a color to actually view something:

1
vec3 color=plt*vec3(1.,0.,0.);
2
gl_FragColor=vec4(color,1.);

The result full code that renders a moving abs(sin(x)) for example can be seen below:

006_plot.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
void main(){
16
vec2 st=gl_FragCoord.xy/u_resolution;
17
18
float y=abs(sin(st.x+u_time));
19
20
float plt=plot(st,y);
21
22
vec3 color=plt*vec3(0.,1.,1.);
23
24
gl_FragColor=vec4(color,1.);
25
}

It’s also possible to combine the lower level GLSL functions to get more complex mathematical functions, for example the below from Kynd

Complex math functions in GLSL

Additionally, the Lygia library also has a huge set of predefined shaping functions that you can use

Colors

Vectors

In general, we have been using colours using vec3 or vec4 vectors. We have mostly being accessing vector values using .x or .y, however there are a few different syntaxes that are equivalent

1
vec4 vector;
2
vector[0] = vector.r = vector.x = vector.s;
3
vector[1] = vector.g = vector.y = vector.t;
4
vector[2] = vector.b = vector.z = vector.p;
5
vector[3] = vector.a = vector.w = vector.q;

Furthermore, it’s also possible to create new vectors out of vectors we and organize their parts as we want. For example:

1
vec4 color = vec4(1.,1.,0.,1.);
2
3
vec3 rgb = color.rgb;
4
vec3 rbg = color.rbg;
5
vec3 bgr = color.bgr;
6
vec3 bga = color.bga;

The above idea is called “swizzling” and can be applied to all vectors based on their components

Mixing Colors

Colors can be mixed using the mix function which can take a percentage value of how to mix the two colors. This can be seen below where we are using a sin wave based on the x coordinate and time to mix two colors:

007_mix.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
float y=abs(sin(st.x+u_time));
13
14
vec3 colorA=vec3(1.,1.,0.);
15
vec3 colorB=vec3(0.,1.,1.);
16
17
vec3 color=mix(colorA,colorB,y);
18
19
gl_FragColor=vec4(color,1.);
20
}

HSB

There are multiple different ways that we can represent color. Using a function for converting from HSB to RGB we can render the colour space:

008_hsb.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
vec3 hsb2rgb(in vec3 c){
10
vec3 rgb=clamp(abs(mod(c.x*6.+vec3(0.,4.,2.),6.)-3.)-1.,0.,1.);
11
12
rgb=rgb*rgb*(3.-2.*rgb);
13
return c.z*mix(vec3(1.),rgb,c.y);
14
}
15
16
void main(){
17
vec2 st=gl_FragCoord.xy/u_resolution;
18
19
float hue=st.x;
20
float saturation=abs(sin(u_time));
21
float brightness=st.y;
22
23
vec3 color=hsb2rgb(vec3(hue,saturation,brightness));
24
25
gl_FragColor=vec4(color,1.);
26
}

Since HSB is meant to be displayed as a polar color space, we can also display this as follows:

009_hsbPolar.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
#define TWO_PI 6.28318530718
6
7
uniform vec2 u_resolution;
8
uniform vec2 u_mouse;
9
uniform float u_time;
10
11
vec3 hsb2rgb(in vec3 c){
12
vec3 rgb=clamp(abs(mod(c.x*6.+vec3(0.,4.,2.),6.)-3.)-1.,0.,1.);
13
14
rgb=rgb*rgb*(3.-2.*rgb);
15
return c.z*mix(vec3(1.),rgb,c.y);
16
}
17
18
void main(){
19
vec2 st=gl_FragCoord.xy/u_resolution;
20
21
vec2 toCenter=vec2(.5)-st;
22
float angle=atan(toCenter.y,toCenter.x);
23
float radius=length(toCenter)*2.;
24
25
float hue=(angle/TWO_PI)+.5;
26
float saturation=radius;
27
28
vec3 color=hsb2rgb(vec3(hue,saturation,1.));
29
30
gl_FragColor=vec4(color,1.);
31
}

Function Arguments

When defining functions we can define inputs as read only, write only, or read-write:

1
int newFunction(in vec4 aVec4, // read-only
2
out vec3 aVec3, // write-only
3
inout int aInt); // read-write

Shapes

Rectangle

If we wanted to fill a rectangle in a shader, we need to think about how to determine the value for a single pixel, the general idea is as follows:

1
if (startX < x < endX) and (startY < y < endY)
2
paint white
3
else
4
paint black

When thinking about this from a shader standpoint, we can implement this kind of conditional using a step function

In our case, we need to do this in a few steps:

  1. Create the boundry for the left side, this is painting all content that is greater the left and bottom values:
1
float left=step(.1,st.x);// returns 1 when st.x > 0.1
2
float bottom=step(.1,st.y);// returns 1 when st.y > 0.1
  1. Create the boundary for right right and top sides by setting these back to white
1
float right=step(.1,1.-st.x);//returns 1 when (1-st.x) > 0.1
2
float top=step(.1,1.-st.y);//returns 1 when (1-st.x) > 0.1
  1. Multiplying the color components functions as a logical AND:
1
float pct = left * bottom * right * top;
2
vec3 color = vec3(pct);

Furthermore, we can simplify part 1. and 2. to use vectors directly instead of working with components, so the updated version for 1. and 2. combined looks like so:

1
vec2 bl=step(vec2(.1),st);
2
vec2 tr=step(vec2(.1),vec2(1.)-st);

Using the above, the final result can be seen below:

010_rectangle.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution.xy;
11
12
vec2 bl=step(vec2(.1),st);
13
vec2 tr=step(vec2(.1),vec2(1.)-st);
14
15
float pct=bl.x*bl.y*tr.x*tr.y;
16
vec3 color=vec3(pct);
17
18
gl_FragColor=vec4(color,1.);
19
}

Circle

The approach for drawing a circle is a bit less confusing. Basically we define the distance of a point from a given location, in our case the center of the circle, and we use a step to include everything within that distance space:

011_circle.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution.xy;
11
12
vec2 center=vec2(.5);
13
14
float dist=distance(center,st);
15
16
float pct=step(.5,1.-dist);
17
vec3 color=vec3(pct);
18
19
gl_FragColor=vec4(color,1.);
20
}

Distance Fields

The above implementation makes use of a distance field. A distance field tells us how far all the points in the field are from some reference point.

Note that the distance function uses the sqrt function underneath - this function can be computationally intensive so it can sometimes be more useful to define our operations without using the sqrt function in any way. For this example, it is also possible to implement a circle distance field using a smoothstep and the vector dot product which can be seen below:

012_circleDistanceField.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution.xy;
11
12
vec2 center=vec2(.5);
13
float r=.5;
14
15
vec2 dist=st-center;
16
17
float pct=1.-smoothstep(r-(r*.01),r+(r*.01),dot(dist,dist)*4.);
18
19
vec3 color=vec3(pct);
20
21
gl_FragColor=vec4(color,1.);
22
}

Properties of Distance Fields

We can draw almost anything using distance fields. Once you have a formula to draw a specific shape using a distance field it becomes relatively easy to apply effects to it

We can visualize a distance field as the distance from the center to any given input point:

013_distanceField.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution.xy;
11
12
vec2 center=vec2(.5);
13
float dist=length(st-center);
14
15
vec3 color=vec3(dist);
16
17
gl_FragColor=vec4(color,1.);
18
}

Polar Shapes

Polar shapes depend on changing the distance of a circle. We can map catresian to polar coordinates using the following:

1
vec2 pos = vec2(0.5)-st;
2
float r = length(pos)*2.0;
3
float a = atan(pos.y,pos.x);

We can use these shapes along with the idea of using a smoothstep/step as a cutoff value to draw more complex shapes in combination with the polar coordinates:

014_polarCoordinates.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution.xy;
11
12
// get the polar coordinates
13
vec2 pos=vec2(.5)-st;
14
float r=length(pos)*2.;
15
float a=atan(pos.y,pos.x);
16
17
float f=sin(a*3.);
18
19
vec3 color=vec3(1.-smoothstep(f,f+.01,r));
20
21
gl_FragColor=vec4(color,1.);
22
}

Matrices

Once we know how to define specific shapes, we can use vector transformations to move them to different locations

Translation

The way we do this at the implementation level is by actually transforming the entire output coordinate space. We can see this by defining a new vector to move the space with below:

015_translation.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float circle(vec2 st,float r){
10
vec2 center=vec2(.5);
11
float dist=distance(center,st);
12
float pct=step(1.-r,1.-dist);
13
return pct;
14
}
15
16
void main(){
17
vec2 st=gl_FragCoord.xy/u_resolution;
18
19
vec2 translate=vec2(cos(u_time),sin(u_time*2.))*.3;
20
21
st+=translate;
22
23
float c=circle(st,.1);
24
25
vec3 gradient=vec3(0.,st.x,st.y);
26
vec3 color=gradient+vec3(c);
27
28
gl_FragColor=vec4(color,1.);
29
}

If you observe the example above, you can see that it’s not just the shape that moves but the entire color space. This is due to the coordinate system translation

2D Matrices

Doing more complex operations requires a matrix, for example we can translate as above by doing the dot product:

[10tx01ty001][xy1]=[x+txy+ty1]\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \cdot{} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}

Rotation

For rotation, this is a bit more interesting

[cosθsinθ0sinθcosθ0001][xy1]=[x.cosθy.sinθx.sinθy.cosθ1]\begin{bmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot{} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x.cos\theta - y.sin\theta \\ x.sin\theta - y.cos\theta \\ 1 \end{bmatrix}

Using the above, we can implement the rotation as follows:

016_rotation.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float rectangle(vec2 st,float x,float y){
10
float b=step(.5-y*.5,st.y);
11
float l=step(.5-x*.5,st.x);
12
13
float t=step(.5-y*.5,1.-st.y);
14
float r=step(.5-x*.5,1.-st.x);
15
16
float pct=b*l*t*r;
17
return pct;
18
}
19
20
vec2 rotate2d(vec2 st,in float theta){
21
mat2 matrix=mat2(cos(theta),-sin(theta),
22
sin(theta),cos(theta));
23
24
vec2 trans=vec2(.5);
25
26
// translate to the origin center
27
st-=trans;
28
29
// rotate
30
st*=matrix;
31
32
// move back to origin
33
st+=trans;
34
35
return st;
36
}
37
38
void main(){
39
vec2 st=gl_FragCoord.xy/u_resolution;
40
41
st=rotate2d(st,sin(u_time));
42
43
float c=rectangle(st,.1,.1);
44
45
vec3 gradient=vec3(0.,st.x,st.y);
46
vec3 color=gradient+vec3(c);
47
48
gl_FragColor=vec4(color,1.);
49
}

Scale

Similarly, we can define a matrix operation for scaling:

[Sx000Sy000Sz][xyz]=[Sx.xSy.ySz.z]\begin{bmatrix} S_x & 0 & 0 \\ 0 & S_y & 0 \\ 0 & 0 & S_z \end{bmatrix} \cdot{} \begin{bmatrix} x \\ y \\ z \end{bmatrix} = \begin{bmatrix} S_x.x \\ S_y.y \\ S_z.z \end{bmatrix}

And the implementation of this can be seen below:

017_scale.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float circle(vec2 st,float r){
10
vec2 center=vec2(.5);
11
float dist=distance(center,st);
12
float pct=step(1.-r,1.-dist);
13
return pct;
14
}
15
16
vec2 scale(vec2 st,vec2 scale){
17
mat2 matrix=mat2(
18
scale.x,0.,
19
0.,scale.y);
20
21
vec2 trans=vec2(.5);
22
23
st-=trans;
24
st*=matrix;
25
st+=trans;
26
27
return st;
28
}
29
30
void main(){
31
vec2 st=gl_FragCoord.xy/u_resolution;
32
33
vec2 s=vec2(abs(sin(u_time)));
34
st=scale(st,s);
35
36
float c=circle(st,.1);
37
38
vec3 gradient=vec3(0.,st.x,st.y);
39
vec3 color=gradient+vec3(c);
40
41
gl_FragColor=vec4(color,1.);
42
}

YUV Color

YUV is a color space for analog encoding of photos and videos that uses the human perception range.

We can define the conversions from YUV and RGB using a matrix and we can apply this to our input space to view the color range

018_yuv.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
mat3 yuv2rgb=mat3(1.,0.,1.13983,
10
1.,-.39465,-.58060,
11
1.,2.03211,0.);
12
13
mat3 rgb2yuv=mat3(.2126,.7152,.0722,
14
-.09991,-.33609,.43600,
15
.615,-.5586,-.05639);
16
17
void main(){
18
vec2 st=gl_FragCoord.xy/u_resolution;
19
20
// Remap our space since YUV goes from -1 to 1
21
st-=.5;
22
st*=2.;
23
24
vec3 color=yuv2rgb*vec3(abs(sin(u_time)),st.x,st.y);
25
26
gl_FragColor=vec4(color,1.);
27
}

Patterns

Since shaders execute once per pixel, no matter how much we repeat a shape the complexity is constant - this is a useful property for defining patterns

When creating patterns, we commonly use the fract function which gives us the decimal part of a number. Since our numbers are always between 1 and 0 this isn’t instantly useful but becomes interesting when multiplying by some value

We can use this for displaying color:

019_fractColor.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
st*=3.;
13
st=fract(st);
14
15
vec3 color=vec3(0,st.x,st.y);
16
gl_FragColor=vec4(color,1.);
17
}

And we can do something similar using a circle:

020_fractCircle.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float circle(vec2 st,float r){
10
vec2 center=vec2(.5);
11
float dist=distance(center,st);
12
float pct=step(1.-r,1.-dist);
13
return pct;
14
}
15
16
void main(){
17
vec2 st=gl_FragCoord.xy/u_resolution;
18
19
st*=3.;
20
st=fract(st);
21
22
float c=circle(st,.1);
23
vec3 color=vec3(c);
24
25
gl_FragColor=vec4(color,1.);
26
}

Since each subsection that we create above is a small sub-coordinate space, we can apply the other methods we’ve used to do stuff like rotate the space:

021_fractRotate.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
#define PI 3.14159265358979323846
10
11
float rectangle(vec2 st,float x,float y){
12
float b=step(.5-y*.5,st.y);
13
float l=step(.5-x*.5,st.x);
14
15
float t=step(.5-y*.5,1.-st.y);
16
float r=step(.5-x*.5,1.-st.x);
17
18
float pct=b*l*t*r;
19
return pct;
20
}
21
22
vec2 rotate2d(vec2 st,in float theta){
23
mat2 matrix=mat2(cos(theta),-sin(theta),
24
sin(theta),cos(theta));
25
26
vec2 trans=vec2(.5);
27
28
st-=trans;
29
st*=matrix;
30
st+=trans;
31
32
return st;
33
}
34
35
void main(){
36
vec2 st=gl_FragCoord.xy/u_resolution;
37
38
st=rotate2d(st,PI*.25);
39
40
st*=3.;
41
st=fract(st);
42
43
float c=rectangle(st,.5,.5);
44
45
vec3 color=vec3(0.,1.,1.)*vec3(c);
46
gl_FragColor=vec4(color,1.);
47
}

Or we can make use of the mod and step functions to identify which row we are in and translate that value halfway to the right like so:

022_fractModulo.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
float rectangle(vec2 st,float x,float y){
16
float b=step(.5-y*.5,st.y);
17
float l=step(.5-x*.5,st.x);
18
19
float t=step(.5-y*.5,1.-st.y);
20
float r=step(.5-x*.5,1.-st.x);
21
22
float pct=b*l*t*r;
23
return pct;
24
}
25
26
void main(){
27
vec2 st=gl_FragCoord.xy/u_resolution;
28
29
st*=5.;
30
31
// returms 1. if we are in an odd numbered row
32
float odd=step(1.,mod(st.y,2.));
33
34
// shift the coordinate space by 0.5 if we are in an odd row
35
st+=vec2(odd*.5,0.);
36
37
// get the new fractional coordinate space
38
st=fract(st);
39
40
float c=rectangle(st,.5,.5);
41
vec3 color=c*vec3(1.);
42
43
gl_FragColor=vec4(color,1.);
44
}

Randomness

If we consider the function y = fract(sin(x)*n) we will notice that as we increase n at a certain point we get what looks like randomness:

023_randomness.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
void main(){
16
vec2 st=gl_FragCoord.xy/u_resolution;
17
18
float y=fract(sin(st.x)*1000000.);
19
vec3 color=vec3(y);
20
21
gl_FragColor=vec4(color,1.);
22
}

THe problem with using this kind of randomness is that while it is somewhat chaotic, it is not truly random since the underlying function is not random

Regardless, since we have a method of defining randomness, we can implement this in two dimensions as follows:

024_randomness2d.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(vec2 st){
10
float a=dot(st.xy,vec2(1000,1000));
11
return fract(sin(a)*1000000.);
12
}
13
14
void main(){
15
vec2 st=gl_FragCoord.xy/u_resolution;
16
17
vec3 color=vec3(random(st));
18
gl_FragColor=vec4(color,1.);
19
}

We do this by getting the dot product of the input vector and some other large vector and then using that to get the value that we pass to our pseudo random function

We can implement something interesting by combining this with the patterning/fraction method we learnt previously do get blocks of random colour instead of just noise:

025_randomnessBlocks.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
#define TWO_PI 6.28318530718
10
11
float random(vec2 st){
12
float a=dot(st.xy,vec2(74.61,20.53));
13
return fract(sin(a)*83737.);
14
}
15
16
void main(){
17
vec2 st=gl_FragCoord.xy/u_resolution;
18
19
st*=10.;
20
st=floor(st);
21
22
vec3 color=vec3(random(st));
23
gl_FragColor=vec4(color,1.);
24
}

This works since by getting the integer value (via the floor function) we effectively group our st.x and st.y values into buckets over a specified range

Next, we can use these values more directly by creting more complex patterns

026_randomnessPatterns.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
#define TWO_PI 6.28318530718
10
11
float random(vec2 st){
12
float a=dot(st.xy,vec2(74.61,20.53));
13
return fract(sin(a)*83737.);
14
}
15
16
vec2 truchetPattern(in vec2 st,in float index){
17
index=fract(((index-.5)*2.));
18
if(index>.75){
19
st=vec2(1.)-st;
20
}else if(index>.5){
21
st=vec2(1.-st.x,st.y);
22
}else if(index>.25){
23
st=1.-vec2(1.-st.x,st.y);
24
}
25
return st;
26
}
27
28
void main(){
29
vec2 st=gl_FragCoord.xy/u_resolution;
30
31
st*=10.;
32
vec2 i=floor(st);
33
34
st=fract(st);
35
36
vec2 tile=truchetPattern(st,random(i));
37
38
// sharp triangles
39
// float c=step(tile.x,tile.y);
40
41
// lines
42
float c=smoothstep(tile.x,tile.x,tile.y)-
43
smoothstep(tile.x,tile.x+.4,tile.y);
44
45
vec3 color=vec3(c);
46
47
gl_FragColor=vec4(color,1.);
48
}

Noise

Since very few things in nature are actually random but are a sort random with some sense of order. An example of a function that does something like this is ther Perlin noise algorithm and is a method for generating noise

Perlin Noise

This algorithm works by mixing the random values with other nearby noise values to create some kind of continuiy. There are of course different ways we can mix these values

For example, we can just use a plain mix:

027_perlin.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(float x){
10
return fract(sin(x)*1000000.);
11
}
12
13
float plot(vec2 st,float x){
14
return
15
smoothstep(x-.01,x,st.y)
16
-smoothstep(x,x+.01,st.y);
17
}
18
19
float perlin(float steps,float x){
20
x*=steps;
21
float i=floor(x);
22
float f=fract(x);
23
24
return mix(random(i),random(i+1.),f);
25
}
26
27
void main(){
28
vec2 st=gl_FragCoord.xy/u_resolution;
29
30
float y=perlin(10.,st.x);
31
32
vec3 color=vec3(plot(st,y));
33
gl_FragColor=vec4(color,1.);
34
}

Or mixing using a smoothstep as well:

028_perlinSmooth.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
float random(float x){
16
return fract(sin(x)*999999.);
17
}
18
19
float perlin(float steps,float x){
20
x*=steps;
21
float i=floor(x);
22
float f=fract(x);
23
24
return mix(random(i),random(i+1.),smoothstep(0.,1.,f));
25
}
26
27
void main(){
28
vec2 st=gl_FragCoord.xy/u_resolution;
29
30
float y=perlin(st.x,10.);
31
32
vec3 color=vec3(plot(st,y));
33
gl_FragColor=vec4(color,1.);
34
}

It is also possible to calculate your own custom curve instead of using something like smoothstep

029_perlinCustom.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
float random(float x){
16
return fract(sin(x)*1000000.);
17
}
18
19
float perlin(float steps,float x){
20
x*=steps;
21
float i=floor(x);
22
float f=fract(x);
23
24
// this is the same a as smoothstep
25
float u=f*f*(3.-2.*f);
26
27
return mix(random(i),random(i+1.),u);
28
}
29
30
void main(){
31
vec2 st=gl_FragCoord.xy/u_resolution;
32
33
float y=perlin(st.x,10.);
34
35
vec3 color=vec3(plot(st,y));
36
gl_FragColor=vec4(color,1.);
37
}

2D Noise

When doing 1D noise we interpolated between random values of x and x+1. For 2D noise however, we need to consider a plane with a point at each edge offset randomly

030_perlin2d.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(in vec2 st){
10
float a=dot(st.xy,vec2(9281.29,174.291));
11
return fract(sin(a)*91287.);
12
}
13
14
float perlin(in vec2 st){
15
vec2 i=floor(st);
16
vec2 f=fract(st);
17
18
// position 0,0
19
float oo=random(i);
20
21
// position 1,0
22
float lo=random(i+vec2(1.,0.));
23
24
// position 0,1
25
float ol=random(i+vec2(0.,1.));
26
27
// position 1,1
28
float ll=random(i+vec2(1.,1.));
29
30
vec2 u=smoothstep(vec2(0),vec2(1.),f);
31
32
// mix the corner percentages
33
return mix(oo,lo,u.x)+(ol-oo)*u.y*(1.-u.x)+
34
(ll-lo)*u.x*u.y;
35
}
36
37
void main(){
38
vec2 st=gl_FragCoord.xy/u_resolution;
39
40
float y=perlin(st*5.);
41
42
vec3 color=vec3(y);
43
gl_FragColor=vec4(color,1.);
44
}

We can combine these ideas along with the methodology for working with polar coordinates and the user’s mouse positon to do something kinda cool

Go ahead and try moving your mouse around on the image below

031_perlinCircle.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
#define PI 3.14159265358979323846
10
11
float circle(vec2 st,float r){
12
vec2 center=vec2(.5);
13
float dist=distance(center,st);
14
float pct=step(1.-r,1.-dist);
15
return pct;
16
}
17
18
float angle(in vec2 v){
19
vec2 pos=vec2(.5)-v;
20
float r=length(pos)*2.;
21
float a=atan(pos.y,pos.x);
22
23
return a;
24
}
25
26
vec2 rotate(vec2 st,in float theta){
27
mat2 matrix=mat2(cos(theta),-sin(theta),
28
sin(theta),cos(theta));
29
30
vec2 trans=vec2(.5);
31
32
st-=trans;
33
st*=matrix;
34
st+=trans;
35
36
return st;
37
}
38
39
float random(float x){
40
return fract(sin(x)*1000000.);
41
}
42
43
float perlin(float steps,float x){
44
x*=steps;
45
float i=floor(x);
46
float f=fract(x);
47
48
return mix(random(i),random(i+1.),smoothstep(0.,1.,f));
49
}
50
51
vec2 shift(float a,float offset){
52
vec2 dir=normalize(vec2((a),(a)));
53
54
vec2 norm=dir;
55
56
return norm*.1;
57
}
58
59
void main(){
60
vec2 st=gl_FragCoord.xy/u_resolution;
61
62
st=rotate(st,-PI/2.);
63
64
float x=abs((u_mouse.x/u_resolution.x)-.5);
65
float y=abs((u_mouse.y/u_resolution.y)-.5);
66
float time=sin(u_time/5.)*10.;
67
68
float r=.3+(x*.01);
69
float sides=time;
70
71
float a=abs(angle(st));
72
float offset=perlin(sides,a)-.5;
73
74
r+=offset*y*.3;
75
vec3 color=vec3(circle(st,r+.005))-vec3(circle(st,r));
76
77
gl_FragColor=vec4(color,1.);
78
}

Improving 2D Noise

Perlin noticed that in with the above noise method, it’s possible to replace the smoothstep (cubic Hermite curve) f(x)=3x22x3f(x) = 3x^2 - 2x^3 with a quintic interpolation curve f(x)=6x515x4+10x3f(x) = 6x^5 - 15x^4 + 10x^3 which maoes both ends of the curve more flat so that each border can stick more gracefully with the other enabling us to transition more smoothly between cells

Simplex Noise

Simplex noise improves upon the previous noise algorithms by accomplishing the following:

  • Lower computational complexity and fewer multiplications
  • Scales to higher dimensions with less computational cost
  • No directional artifacts
  • Well defined gradients
  • Easy to implement in hardware

The simplex shape defined for a space with NN dimensions is N+1N + 1. So for 2D noise we only need points, and for 3D noise we only need 4 points

Creating a simplex grid can be done by splitting a normal 4 cornered grid into two isosceles triangles and then skewing until the triangles are equilateral

We can see an example of skewing and subdividing this space below:

032_simplex.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
// Skew the X and Y Coordinates
10
vec2 skew(in vec2 st){
11
float x=1.1547*st.x;
12
float y=st.y+.5*x;
13
14
return vec2(x,y);
15
}
16
17
// Subdivide a cell on the line x = y
18
vec2 subdivide(in vec2 st){
19
vec2 result=vec2(0.);
20
21
if(st.x>st.y){
22
result=1.-vec2(st.x,st.y-st.x);
23
}else{
24
result=1.-vec2(st.x-st.y,st.y);
25
}
26
27
return fract(result);
28
}
29
30
void main(){
31
vec2 st=gl_FragCoord.xy/u_resolution;
32
33
st*=5.;
34
st=skew(st);
35
st=fract(st);
36
st=subdivide(st);
37
38
vec3 color=vec3(.3,st.x,st.y);
39
gl_FragColor=vec4(color,1.);
40
}

This subdivision idea can be combined to create more powerful implementations of noise than the simple perlin case we have covered

Cellular Noise

Celllar noise is based on iterations. In GLSL we can iterate using loops but we must ensure that the number of loops we have is constant

Distance Field

Cellular Noise is based on distance fields. For each pixel we calculate the distance to the center of a cell, this means that we need to iterate through all our cells for a given pixel to find it’s closest center

033_cellularDistance.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
vec2 cells[5];
13
cells[0]=vec2(.33,.25);
14
cells[1]=vec2(.75,.25);
15
cells[2]=vec2(.25,.75);
16
cells[3]=vec2(.75,.75);
17
cells[4]=u_mouse/u_resolution;
18
19
float min_dist=1.;
20
for(int i=0;i<5;i++){
21
float dist=distance(st,cells[i]);
22
min_dist=min(min_dist,dist);
23
}
24
25
vec3 color=vec3(min_dist);
26
gl_FragColor=vec4(color,1.);
27
}

Tiling and iterations

For loops are not ideal for working with GLSL since we can’t use dynamic exit limits and iterating significantly reduces the performance of a shader so this isn’t really practical when using a large number of cells

We need to instead find a method that makes better use of the GPU

In order to do this, we can define a grid of 9 spaces around our current pixel, and check the value relative to that neighbor position since we don’t need to evaluate non-neighboring grid members. Something like this:

1
for (int y= -1; y <= 1; y++) {
2
for (int x= -1; x <= 1; x++) {
3
// Neighbor place in the grid
4
vec2 neighbor = vec2(float(x),float(y));
5
...
6
}
7
}

We can use this to define a tiled cellular grid:

034_cellularGrid.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
void main(){
10
vec2 st=gl_FragCoord.xy/u_resolution;
11
12
float grid=5.;
13
14
st*=grid;
15
vec2 i_st=floor(st);
16
vec2 f_st=fract(st);
17
18
float min_dist=1.;
19
for(int y=-1;y<=1;y++){
20
for(int x=-1;x<=1;x++){
21
vec2 neighbor=i_st+vec2(float(x),float(y));
22
float dist=distance(st,neighbor);
23
min_dist=min(min_dist,dist);
24
}
25
}
26
27
vec3 color=vec3(min_dist);
28
gl_FragColor=vec4(color,1.);
29
}

Next, we can apply a random offset to the center point of each cell which will allow us to get something more interesting:

035_cellularGridRandom.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
// Do some random stuff to get a 2d random value
10
vec2 random2d(vec2 p){
11
return fract(vec2(dot(p*sin(p)*926000.,vec2(.6125,.222))));
12
}
13
14
void main(){
15
vec2 st=gl_FragCoord.xy/u_resolution;
16
17
float grid=5.;
18
19
st*=grid;
20
vec2 i_st=floor(st);
21
vec2 f_st=fract(st);
22
23
float min_dist=1.;
24
for(int y=-1;y<=1;y++){
25
for(int x=-1;x<=1;x++){
26
// Get the root of the neighbor
27
vec2 neighbor=vec2(float(x),float(y));
28
29
// Define a center as the integer part + the neighbor
30
// This will be the same for all values in a grid
31
vec2 point=random2d(i_st+neighbor)+neighbor;
32
33
// Get the distance from the neighbor's center the tile's center
34
float dist=distance(f_st,point);
35
36
// Store the distance to the closest centroid
37
// This also compares with our current block's center
38
min_dist=min(min_dist,dist);
39
}
40
}
41
42
vec3 color=vec3(min_dist);
43
gl_FragColor=vec4(color,1.);
44
}

Veronoi Algorithm

Instead of considering the algorithm from the perspective of the pixels as we have above, we can also take it from the perspectve of the point such that each point grows until it bumps nito another points - the algorithm for this is named after Georgy Veronoi

Creating veronoi diagrams from cellular noise involves keeping track of some additional information about the point which is closest to a pixel.

We will keep a reference to the center by storing a reference to the distance between our current point and the resolved center:

036_cellularGridVoronoi.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
// do some random stuff to get a 2d random value
10
vec2 random2d(vec2 p){
11
return fract(vec2(dot(p*sin(p)*926000.,vec2(.6125,.222))));
12
}
13
14
void main(){
15
vec2 st=gl_FragCoord.xy/u_resolution;
16
17
float grid=5.;
18
19
st*=grid;
20
vec2 i_st=floor(st);
21
vec2 f_st=fract(st);
22
23
float min_dist=1.;
24
vec2 min_point=vec2(0.);
25
26
for(int y=-1;y<=1;y++){
27
for(int x=-1;x<=1;x++){
28
vec2 neighbor=vec2(float(x),float(y));
29
30
vec2 point=random2d(i_st+neighbor)+neighbor;
31
32
float dist=distance(f_st,point);
33
34
// store min point and distance
35
if(dist<min_dist){
36
min_dist=dist;
37
min_point=point;
38
}
39
}
40
}
41
42
// make the min point relative to absolute coords
43
min_point+=i_st;
44
45
// color for cell
46
vec2 cell_color=min_point/grid;
47
vec3 color=cell_color.yxx;
48
49
// Show color distance lines
50
color-=abs(sin(50.*min_dist))*.05;
51
52
gl_FragColor=vec4(color,1.);
53
}

Fractal Brownian Motion

A wave is a fluctuation of some property over time. Important aspects of a wave are it’s amplitude and frequency

In the case of a sinsin wave, this is as follows: y=a.sin(x.f)y = a.sin(x.f)

Additionally, we can add waves up to create interference. This changes how the overall wave looks

037_waveInterference.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float plot(vec2 st,float x){
10
return
11
smoothstep(x-.01,x,st.y)
12
-smoothstep(x,x+.01,st.y);
13
}
14
15
void main(){
16
vec2 st=gl_FragCoord.xy/u_resolution;
17
st=st*6.-3.;
18
19
float x=st.x;
20
float t=u_time;
21
22
float y=sin(x);
23
24
// adding waves to create interference
25
y+=sin(x*3.3+t*.125)*.12;
26
y+=sin(x*4.4+t*.45)*.342;
27
y+=sin(x*2.2+t*1.6)*.563;
28
29
float plt=plot(st,y);
30
31
vec3 color=plt*vec3(0.,1.,1.);
32
33
gl_FragColor=vec4(color,1.);
34
}

We can use this idea to create noise based on this kind of interference to create something called fractal noise. for our case we’ll use perlin noise instead of a sin wave:

038_fractalBrownianMotion.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(float x){
10
return fract(sin(x)*1000000.);
11
}
12
13
float plot(vec2 st,float x){
14
return
15
smoothstep(x-.01,x,st.y)
16
-smoothstep(x,x+.01,st.y);
17
}
18
19
float perlin(float x){
20
float i=floor(x);
21
float f=fract(x);
22
23
return mix(random(i),random(i+1.),f);
24
}
25
26
void main(){
27
vec2 st=gl_FragCoord.xy/u_resolution;
28
st=st*4.-1.;
29
30
float x=st.x;
31
float t=u_time;
32
33
float y=perlin(x);
34
y+=perlin(x*3.3+t*.125)*.12;
35
y+=perlin(x*4.4+t*.45)*.342;
36
y+=perlin(x*2.2+t*1.6)*.563;
37
38
float plt=plot(st,y);
39
40
vec3 color=plt*vec3(0.,1.,1.);
41
42
gl_FragColor=vec4(color,1.);
43
}

Fractal Brownian Motion

We can extend on this further by introducing a concept of octaves (iterations of noise), lacunarity (increments of frequency), and gain (amplitide) of the noise by which we apply variants of the same wave on top of itself

We can implement this using perlin noise as the base instead of a sin wave, our result is as follows:

039_perlinFractalNoise.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(float x){
10
return fract(sin(x)*999999.);
11
}
12
13
float plot(vec2 st,float x){
14
return
15
smoothstep(x-.01,x,st.y)
16
-smoothstep(x,x+.01,st.y);
17
}
18
19
float perlin(float x){
20
x*=5.;
21
float i=floor(x);
22
float f=fract(x);
23
24
return mix(random(i),random(i+1.),smoothstep(0.,1.,f));
25
}
26
27
void main(){
28
vec2 st=gl_FragCoord.xy/u_resolution;
29
30
const int octaves=8;
31
float lacunarity=2.;
32
float gain=.5;
33
34
float x=st.x;
35
float t=u_time*.2;
36
37
// initial values
38
float amplitude=.5;
39
float freq=1.;
40
float y=0.;
41
42
for(int i=0;i<octaves;i++){
43
y+=amplitude*perlin(freq*x+t);
44
freq*=lacunarity;
45
amplitude*=gain;
46
}
47
48
float plt=plot(st,y);
49
50
vec3 color=plt*vec3(0.,1.,1.);
51
52
gl_FragColor=vec4(color,1.);
53
}

We can also implement something like this in two dimensions:

040_perlinFractalNoise2d.glsl
1
#ifdef GL_ES
2
precision mediump float;
3
#endif
4
5
uniform vec2 u_resolution;
6
uniform vec2 u_mouse;
7
uniform float u_time;
8
9
float random(in vec2 st){
10
float a=dot(st.xy,vec2(12.29,1.291));
11
return fract(sin(a)*1125163.);
12
}
13
14
float perlin(in vec2 st){
15
vec2 i=floor(st);
16
vec2 f=fract(st);
17
18
float oo=random(i);
19
float lo=random(i+vec2(1.,0.));
20
float ol=random(i+vec2(0.,1.));
21
float ll=random(i+vec2(1.,1.));
22
23
vec2 u=smoothstep(vec2(0),vec2(1.),f);
24
25
return mix(oo,lo,u.x)+(ol-oo)*u.y*(1.-u.x)+
26
(ll-lo)*u.x*u.y;
27
}
28
29
float fbm(in vec2 st,float lacunarity,float gain){
30
const int octaves=8;
31
32
float t=u_time*.5;
33
34
// initial values
35
float amplitude=.5;
36
float freq=4.;
37
float r=0.;
38
39
for(int i=0;i<octaves;i++){
40
r+=amplitude*perlin(freq*st+t);
41
freq*=lacunarity;
42
amplitude*=gain;
43
}
44
45
return r;
46
}
47
48
void main(){
49
vec2 st=gl_FragCoord.xy/u_resolution;
50
51
float r=fbm(st,2.,.5);
52
53
vec3 color=vec3(r);
54
55
gl_FragColor=vec4(color,1.);
56
}