glsl functions

glsl contain nice built-in function that we explor now

Continues hello shaders.

GLSL stands for OpenGL Shading Language.

Setup

Same p5 setup as the previous part — the online editor works just as well.

html · setup
<link rel="stylesheet" href="/demo/reset.css">
<script src="https://cdn.jsdelivr.net/npm/p5@2.3.0/lib/p5.min.js"></script>

Same runner:

runner · js file
async function setup(){
  createCanvas(200,200, WEBGL) 
    myShader = await loadShader("shader.vert", "shader.frag")
}

function draw(){
  shader(myShader)
  rect(-width/2, -height/2, width,height)
}

And the same two GLSL files:

shader.vert · glsl file
#version 300 es 
precision mediump float;

in vec3 aPosition;
out vec2 vPos;
  
void main(){ 
  vec4 position = vec4(aPosition, 1.0);
  position.xy = position.xy * 2.0 - 1.0;
  vPos = position.xy;
  gl_Position = position;
}
shader.frag · glsl file
#version 300 es 
precision mediump float;

in vec2 vPos;
out vec4 fragColor;

void main(){
 fragColor = vec4(1.0, 1.0, .0, 1.0);
}
js · run runner

Let's start

Let's begin with what we already have: the position of each fragment, painted as grey.

shader.frag · glsl file · update
void main(){
+ float x = vPos.x;
+ float y = vPos.y;
- fragColor = vec4(1.0, 1.0, .0, 1.0);
+ fragColor = vec4(x, x, x, 1.0);
}
shader.frag · glsl file · update
- fragColor = vec4(x, x, x, 1.0);
+ fragColor = vec4(y, y, y, 1.0);

It isn't a strictly accurate picture — everything below zero clamps to black — but it makes the point: x runs left to right, y runs bottom to top, 0.0 is black and 1.0 is white. Like math class, and unlike the screen coordinates you're used to in JS.

length(vec)

length(vec) returns the distance of a vector from the origin — the centre of our coordinate space.

shader.frag · glsl file · update
void main(){
- float x = vPos.x;
- float y = vPos.y;
- fragColor = vec4(y, y, y, 1.0);
+ vec2 uv = vPos;
+ float d = length(uv);
+ fragColor = vec4(d, d, d, 1.0);
}

In GLSL a scalar applies to the whole vector, so subtracting 0.5 moves both components at once — and the dark centre moves with them:

shader.frag · glsl file · update
- vec2 uv = vPos;
+ vec2 uv = vPos - 0.5;

So far we've been fetching the fragment position the long way around: the CPU hands the square's vertices to the vertex shader, we pass them out as vPos, and the fragment shader reads them back in. That was a good way to watch data travel down the pipeline — but for reading screen position, none of it is necessary.

The GPU already knows where each fragment lives, and it hands us the coordinate in a built-in variable: gl_FragCoord.

To see where that number comes from, look at the stage called rasterization — the step that turns a shape into the 2D grid of pixels that ends up on the screen.


So in shader.frag we drop vPos and use gl_FragCoord instead:

shader.frag · glsl file · update
- in vec2 vPos;
out vec4 fragColor;

void main(){
- vec2 uv = vPos - 0.5;
+ vec2 uv = gl_FragCoord.xy - 0.5;

Everything is white, and the reason is the range: vPos arrived between 0 and 1, while gl_FragCoord is measured in pixels — 0 to 200 on our canvas. Past 1.0 every channel clamps to white.

So we need to normalize it, which means the shader has to know the canvas size. We send it from the CPU as a uniform:

runner · js file
function draw(){
-  shader(myShader)
+  shader(myShader)
+  myShader.setUniform('uResolution',[width,height])
shader.frag · glsl file
#version 300 es 
precision mediump float;

out vec4 fragColor;
uniform vec2 uResolution;   

void main(){
vec2 uv = (gl_FragCoord.xy/uResolution.xy) - 0.5;
float d = length(uv);
fragColor = vec4(d, d, d, 1.0);
}

If the circle isn't centred on your screen, you probably have a high-DPI display: the drawing buffer is larger than width/height, so the division doesn't land where we expect. Add pixelDensity(1) to p5's setup().

runner · js file
async function setup(){
  createCanvas(200,200, WEBGL) 
  myShader = await loadShader("shader.vert", "shader.frag")
  pixelDensity(1)
}

function draw(){
   shader(myShader)
   myShader.setUniform('uResolution',[width,height])
  rect(-width/2, -height/2, width,height)
}

Now, how do we make an effect that repeats? Meet fract().

fract()

fract() strips the whole part and leaves the fraction: fract(11.2) == 0.2. You'd expect fract(-11.4) == -0.4, but that's not what happens — the actual definition is fract(x) = x - floor(x):

math
fract(-0.3)=-0.3 - floor(-0.3) 
= -0.3 - (-1.0)
= 0.7

So the function always climbs from 0 to 1 and starts over, negative numbers included — a free repeating ramp.


shader.frag · glsl file
#version 300 es 
precision mediump float;

out vec4 fragColor;
uniform vec2 uResolution;   

void main(){
vec2 uv = (gl_FragCoord.xy/uResolution.xy) - 0.5;
uv = fract(uv);
float d = length(uv);
fragColor = vec4(d, d, d, 1.0);
}

The seam comes from that wrap-around: on the left edge fract(-0.5) is 0.5, and just before the origin fract(-0.001) is 0.999. On the positive side the ramp grows normally.

Multiply before wrapping and we get more cells:

shader.frag · glsl file · variant
- uv = fract(uv);
+ uv = fract(uv * 2.0);

Subtracting 0.5 after the wrap re-centres each cell:

shader.frag · glsl file · variant
- uv = fract(uv);
+ uv = fract(uv * 2.0) -0.5;

Multiply the range by 3.0:

shader.frag · glsl file · variant
- uv = fract(uv);
+ uv = fract(uv * 3.0) -0.5;

By 4.0:

shader.frag · glsl file · variant
- uv = fract(uv);
+ uv = fract(uv * 4.0) -0.5;

By 5.0:

shader.frag · glsl file · variant
- uv = fract(uv);
+ uv = fract(uv * 5.0) -0.5;

shader.frag · glsl file · update
- uv = fract(uv);
+ uv = fract(uv * 3.0) - 0.5;

Back to three cells. Now what happens if I stretch the canvas to 600×200?

runner · js file
async function setup(){
-  createCanvas(200,200, WEBGL) 
+  createCanvas(600,200, WEBGL)

The cells stretch with it. Dividing by the resolution maps each axis to 0–1 no matter how many pixels it holds, so one unit on x is now three times wider than one unit on y. The fix is to correct for the aspect ratio:

shader.frag · glsl file · variant
- vec2 uv = (gl_FragCoord.xy/uResolution.xy) - 0.5;
+ vec2 uv = (gl_FragCoord.xy/uResolution.xy);
+ uv.x *= uResolution.x / uResolution.y;

Multiplying x back by width / height restores square units: y still runs 0 to 1, while x runs 0 to the aspect ratio — 3.0 on a 600×200 canvas. Circles are round again, and the wider axis simply fits more of them.


One last thing. Open almost any example on shadertoy and you'll meet a line like this:

glsl
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;

Nothing new is hiding in there — we've already written its cousin in shader.vert:

glsl
  position.xy = position.xy * 2.0 - 1.0;

Same trick, different input range. In the vertex shader the values arrive between 0 and 1, so multiplying by 2 and subtracting 1 maps them to −1…1.

In the fragment shader gl_FragCoord runs from 0 to the canvas width and 0 to the canvas height. Multiplying by 2 and subtracting the resolution gives -width…width and -height…height; dividing both by the height then gives y a clean −1…1 and x a range of -w/h…w/h — the aspect ratio correction baked into the same expression.