A bunch of Shaders from a LD game
An awesome guy wrote a game in LD full of shaders, and he wrote a thing showing how he wrote them, and I would REALLY REALLY want to understand how it works, so I'll try to study it... I'm pastaing it here.
The piece itself: http://ludumdare.com/compo/2015/05/11/a-shader-made-of-whispers/#more-467042
The effects in totality:


Copy pasta:
A Shader Made Of Whispers
Posted by Managore (twitter: @managore)
May 11th, 2015 4:31 am
For Ludum Dare 32 I made a game called A Knife Made Of Whispers. There’s also a video if you’d like to get a quick idea of what the game is like. Quite a few of the 48 hours were spent writing a shader to add a number of visual effects to the game, including chromatic aberration and diffusion.
I’ve since had a number of people express interest in how the shader was developed and what it does, so I’ve decided to write a guide. While I try to explain things as best I can, there is a fair bit of maths and non-intuitive code involved, so be warned!
Here’s a summary of the effects:
Shader Effects
Basics
The shader is written in GLSL, and is comprised of a vertex shader and a fragment shader. It’s important to note that the vertex shader I used is the stock standard, do-nothing-special, passthrough vertex shader in Gamemaker Studio:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
vec4 object_space_pos = vec4(in_Position.x, in_Position.y, in_Position.z, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
}
In other frameworks this might look a little different. While vertex shaders can be very powerful, for my game it’s worth mentioning that this code can safely be ignored completely.
The fragment shader is where all of the special effects in AKMOW come from. Here is the do-nothing-special, passthrough fragment shader in Gamemaker Studio:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
}
Think of this code as the starting point to any shader. The main() function is run for each pixel drawn to the screen. gl_FragColor is the output. It is a vec4, a vector with four values, namely r (red), g (green), b (blue) and a (alpha). In my shader, alpha can be ignored. If you’re unfamiliar with GLSL, note that a vec3, a vector with three values, can have the values referred to as either x, y, z, or r, g, b. So if I have a vec3 called bob, then bob.r and bob.x both refer to the same thing. For a vec4 it’s x, y, z, w, or r, g, b, a.
Inversion
d1
The code is as follows:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec3 u_uSource;
void main()
{
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
Let’s go through this bit by bit.
uniform vec3 u_uSource;
The first thing I did was invert the colours in a radius around the lantern. This means I needed to give the shader the x and y values of the lantern, as well as the light radius, and this is what the uniform variable u_uSource does. u_uSource.x and u_uSource.y correspond to the x and y values of the lantern on (or off) the screen, while u_uSource.z corresponds to the radius, given as a fraction of the screen width.
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
The variable point is the vector from the lantern’s screen position to the pixel’s screen position.
float len = length(point);
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
What happens here is if the current pixel is too far away from the lantern (if len is too large), then gl_FragColor is inverted.
Diffusion
d4x
Here’s the code, including the inversion code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
}
The rand() function is a well known pseudo random function so I won’t cover it here.
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
u_uSource is now a vec4, since I needed to pass through a time value, u_uSource.w. Using xx, yy and u_uSource.w in the rand() function ensures the static effect appears random. As a result, ran is a random value between 0 and 0.3. The 0.3 was hardcoded in so that the diffusion wasn’t too weak or too strong.
if(len+ran>0.7) ...
A distance from the lantern increases, len+ran>0.7 becomes more and more likely, which makes the diffusion a gradual, random thing rather than a solid circle.
... gl_FragColor.rgb = vec3(0.,0.,0.);
When the previous comparison returns true, the output is overwritten with pure black.
else {
...
}
When the comparison returns false, the pixel is drawn as usual.
Chromatic Aberration
d5x
(look especially to the right side of the gif, to where the red and blue separate out)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
}
This part of the shader is easily the messiest, but what is essentially happening here is the shader is sampling the screen at three different magnifications. One for the red value, one for the green, one for the blue. To give you a clearer idea of what I mean, here’s the chromatic aberration turned up by a factor of ten.
d6x
The red value is determined as normal, in the same way that the colour values were determined previously, but the green value is determined by ‘zooming in’ by 0.25% (2.5% in the exaggerated version) and the blue value by ‘zooming in’ by 0.5% (5% in the exaggerated version). Let me highlight and talk about the new pieces of code:
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
The 0.0025 value here determines the strength of the aberration, any number can be used here. r1 and s1 are used for green while r2 and s2 are used for blue. You can see that r2 is simply twice as much as r1.
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
The previously defined values are now used to calculate new sample positions. newcoordx.r and newcoordy.r are still xx and yy; they haven’t changed, but newcoordx.g and newcoordy.g are now slightly closer to the center of the screen, and newcoordx.b and newcoordy.b even more so.
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
The first line might not be necessary but simply initializes gl_FragColor. The next three lines calculate the values of the three colours separately, using the three different coordinate values. There is probably a lot of room for optimizing all of this code, but at the time I used what worked, disregarding efficiency.
Chromatic Ring
d7x
This part of the shader was almost a fluke. For a while I had a much faster diffusion ramp between the light and darkness but was never happy with how it looked, so eventually I tried to create a different sort of chromatic effect and this is what I ended up with.
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
float zz = len+0.01*ran-u_uSource.z;
if(zz>0.) gl_FragColor.r = 1. - gl_FragColor.r;
if(zz>point.x*0.02) gl_FragColor.g = 1. - gl_FragColor.g;
if(zz>point.y*0.02) gl_FragColor.b = 1. - gl_FragColor.b;
}
}
The idea is twofold. Firstly, I added 0.01*ran to the inversion calculation, so that the edge was slightly diffused. Then, I inverted the red, green and blue values separately, allowing me to offset the green horizontally and the blue vertically, which creates the rainbow effect. The green inverse circle is ‘pulled’ slightly to the right and the blue inverse circle is ‘pulled’ slightly down.
float zz = len+0.01*ran-u_uSource.z;
This new variable zz is just a way to tidy the next calculations up slightly. Note that zz > 0 is equivalent to len+0.01*ran > u_uSource.z, which is very similar to the inversion calculation done before, but with 0.01*ran added in.
if(zz>0.) gl_FragColor.r = 1. - gl_FragColor.r;
if(zz>point.x*0.02) gl_FragColor.g = 1. - gl_FragColor.g;
if(zz>point.y*0.02) gl_FragColor.b = 1. - gl_FragColor.b;
Nothing has changed for the red calculation, but the green calculation is now factoring in the horizontal distance of the current pixel from the lantern, and the blue calculation is now factoring in the vertical distance.
And that’s it! I hope this was informative and showed you something of the wonderful world of shaders. My apologies if it was too confusing, I find shader code can be particularly challenging to explain.
Thank you for reading and please let me know if you have any questions, comments or advice! I’m more than happy to answer a few questions where I can.
The piece itself: http://ludumdare.com/compo/2015/05/11/a-shader-made-of-whispers/#more-467042
The effects in totality:


Copy pasta:
A Shader Made Of Whispers
Posted by Managore (twitter: @managore)
May 11th, 2015 4:31 am
For Ludum Dare 32 I made a game called A Knife Made Of Whispers. There’s also a video if you’d like to get a quick idea of what the game is like. Quite a few of the 48 hours were spent writing a shader to add a number of visual effects to the game, including chromatic aberration and diffusion.
I’ve since had a number of people express interest in how the shader was developed and what it does, so I’ve decided to write a guide. While I try to explain things as best I can, there is a fair bit of maths and non-intuitive code involved, so be warned!
Here’s a summary of the effects:
Shader Effects
Basics
The shader is written in GLSL, and is comprised of a vertex shader and a fragment shader. It’s important to note that the vertex shader I used is the stock standard, do-nothing-special, passthrough vertex shader in Gamemaker Studio:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
vec4 object_space_pos = vec4(in_Position.x, in_Position.y, in_Position.z, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
}
In other frameworks this might look a little different. While vertex shaders can be very powerful, for my game it’s worth mentioning that this code can safely be ignored completely.
The fragment shader is where all of the special effects in AKMOW come from. Here is the do-nothing-special, passthrough fragment shader in Gamemaker Studio:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
}
Think of this code as the starting point to any shader. The main() function is run for each pixel drawn to the screen. gl_FragColor is the output. It is a vec4, a vector with four values, namely r (red), g (green), b (blue) and a (alpha). In my shader, alpha can be ignored. If you’re unfamiliar with GLSL, note that a vec3, a vector with three values, can have the values referred to as either x, y, z, or r, g, b. So if I have a vec3 called bob, then bob.r and bob.x both refer to the same thing. For a vec4 it’s x, y, z, w, or r, g, b, a.
Inversion
d1
The code is as follows:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec3 u_uSource;
void main()
{
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
Let’s go through this bit by bit.
uniform vec3 u_uSource;
The first thing I did was invert the colours in a radius around the lantern. This means I needed to give the shader the x and y values of the lantern, as well as the light radius, and this is what the uniform variable u_uSource does. u_uSource.x and u_uSource.y correspond to the x and y values of the lantern on (or off) the screen, while u_uSource.z corresponds to the radius, given as a fraction of the screen width.
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
The variable point is the vector from the lantern’s screen position to the pixel’s screen position.
float len = length(point);
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
What happens here is if the current pixel is too far away from the lantern (if len is too large), then gl_FragColor is inverted.
Diffusion
d4x
Here’s the code, including the inversion code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
}
The rand() function is a well known pseudo random function so I won’t cover it here.
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
u_uSource is now a vec4, since I needed to pass through a time value, u_uSource.w. Using xx, yy and u_uSource.w in the rand() function ensures the static effect appears random. As a result, ran is a random value between 0 and 0.3. The 0.3 was hardcoded in so that the diffusion wasn’t too weak or too strong.
if(len+ran>0.7) ...
A distance from the lantern increases, len+ran>0.7 becomes more and more likely, which makes the diffusion a gradual, random thing rather than a solid circle.
... gl_FragColor.rgb = vec3(0.,0.,0.);
When the previous comparison returns true, the output is overwritten with pure black.
else {
...
}
When the comparison returns false, the pixel is drawn as usual.
Chromatic Aberration
d5x
(look especially to the right side of the gif, to where the red and blue separate out)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
if(len>u_uSource.z) gl_FragColor = 1. - gl_FragColor;
}
}
This part of the shader is easily the messiest, but what is essentially happening here is the shader is sampling the screen at three different magnifications. One for the red value, one for the green, one for the blue. To give you a clearer idea of what I mean, here’s the chromatic aberration turned up by a factor of ten.
d6x
The red value is determined as normal, in the same way that the colour values were determined previously, but the green value is determined by ‘zooming in’ by 0.25% (2.5% in the exaggerated version) and the blue value by ‘zooming in’ by 0.5% (5% in the exaggerated version). Let me highlight and talk about the new pieces of code:
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
The 0.0025 value here determines the strength of the aberration, any number can be used here. r1 and s1 are used for green while r2 and s2 are used for blue. You can see that r2 is simply twice as much as r1.
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
The previously defined values are now used to calculate new sample positions. newcoordx.r and newcoordy.r are still xx and yy; they haven’t changed, but newcoordx.g and newcoordy.g are now slightly closer to the center of the screen, and newcoordx.b and newcoordy.b even more so.
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
The first line might not be necessary but simply initializes gl_FragColor. The next three lines calculate the values of the three colours separately, using the three different coordinate values. There is probably a lot of room for optimizing all of this code, but at the time I used what worked, disregarding efficiency.
Chromatic Ring
d7x
This part of the shader was almost a fluke. For a while I had a much faster diffusion ramp between the light and darkness but was never happy with how it looked, so eventually I tried to create a different sort of chromatic effect and this is what I ended up with.
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 u_uSource;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main()
{
float r1 = 0.0025;
float r2 = r1*2.;
float s1 = 1.-r1*2.;
float s2 = 1.-r2*2.;
float xx = v_vTexcoord.x;
float yy = v_vTexcoord.y;
vec2 point = vec2(xx - u_uSource.x, (yy - u_uSource.y)*9./16.);
float len = length(point);
float ran = 0.3*rand(vec2(u_uSource.w+xx,yy));
if(len+ran>0.7) gl_FragColor.rgb = vec3(0.,0.,0.);
else {
vec3 newcoordx = vec3(xx,xx*s1+r1,xx*s2+r2);
vec3 newcoordy = vec3(yy,yy*s1+r1,yy*s2+r2);
gl_FragColor = vec4(0,0,0,1);
gl_FragColor.r = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.r, newcoordy.r))).r;
gl_FragColor.g = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.g, newcoordy.g))).g;
gl_FragColor.b = (v_vColour * texture2D( gm_BaseTexture, vec2(newcoordx.b, newcoordy.b))).b;
float zz = len+0.01*ran-u_uSource.z;
if(zz>0.) gl_FragColor.r = 1. - gl_FragColor.r;
if(zz>point.x*0.02) gl_FragColor.g = 1. - gl_FragColor.g;
if(zz>point.y*0.02) gl_FragColor.b = 1. - gl_FragColor.b;
}
}
The idea is twofold. Firstly, I added 0.01*ran to the inversion calculation, so that the edge was slightly diffused. Then, I inverted the red, green and blue values separately, allowing me to offset the green horizontally and the blue vertically, which creates the rainbow effect. The green inverse circle is ‘pulled’ slightly to the right and the blue inverse circle is ‘pulled’ slightly down.
float zz = len+0.01*ran-u_uSource.z;
This new variable zz is just a way to tidy the next calculations up slightly. Note that zz > 0 is equivalent to len+0.01*ran > u_uSource.z, which is very similar to the inversion calculation done before, but with 0.01*ran added in.
if(zz>0.) gl_FragColor.r = 1. - gl_FragColor.r;
if(zz>point.x*0.02) gl_FragColor.g = 1. - gl_FragColor.g;
if(zz>point.y*0.02) gl_FragColor.b = 1. - gl_FragColor.b;
Nothing has changed for the red calculation, but the green calculation is now factoring in the horizontal distance of the current pixel from the lantern, and the blue calculation is now factoring in the vertical distance.
And that’s it! I hope this was informative and showed you something of the wonderful world of shaders. My apologies if it was too confusing, I find shader code can be particularly challenging to explain.
Thank you for reading and please let me know if you have any questions, comments or advice! I’m more than happy to answer a few questions where I can.
Comments
The shader above looks awesome.
Custom programmed shaders are awesome!
I have only dabbled with the basics of shaders. (for triplanar shading and grass billboards and such.) But I would love to spend more time mastering GLSL myself.