Building a bridge with code
The bridge's function
First let's start with an empty bridge function:
precision highp float;
varying vec2 UV;
uniform float ratio;
vec4 bridge(vec2 p){
vec4 col = vec4(0.0);
// We will code the bridge here
return col;
}
void main(void){
float x = UV.x * ratio;
float y = UV.y;
vec2 pos = vec2(x, y);
vec4 col = vec4(0.0);
col += bridge(pos);
col.a = 1.0;
gl_FragColor = col;
}
You should now see that (nothing):
Marvelous! But let's actually add things!
In the bridge function, you can begin by plotting a line. Even easier: draw the area below a line:
float f = 0.5;
if(p.y < f){
col.rgb += vec3(0.2, 0.4, 0.0);
}
We now have this:
But let's say we only want to draw the line of the function.
Here we go, let's replace the function's content with:
float f = 0.5;
if(distance(p.y, f) < 0.01){
col.rgb += vec3(0.2, 0.4, 0.0);
}
We could also use abs(p.y - f) < 0.01
, you get the idea. If the point is too far away from the function, we don't draw it.
Result:
Let's say we want that line to be a curve:
float f = 0.5 + 0.2 * cos(2.0 * p.x);
Result:
Now is the time I realize I have not centered my position at (0,0)
. Screen UV coordinates go from 0 to 1. I prefer working with -0.5 to 0.5 and have 0,0 at the middle of the screen.
In the main, pos
becomes:
vec2 pos = vec2(x, y) - vec2(0.5);
In the bridge function, f
becomes:
float f = 0.0 + 0.2 * cos(2.0 * p.x);
Result:
We got it! We now have a nice bridge. But let's add some detail...
Bridge supports
In the bridge's function, we could draw many bars in the screen like that:
if(cos(p.x * 40.0) > 0.9){
col.rgb += vec3(0.4, 0.4, 0.2);
}
The cos
function with gives cyclic values from -1 to 1 and we can use this fact to draw lines by selecting only places where the result is bigger than 0.9.
Now I want my supports to be over 0, which will be the horizon, and below f
, the bridge. So we'll replace our previous bars solution with this:
if(p.y < f && p.y > 0.0 && cos(p.x * 40.0) > 0.9){
col.rgb += vec3(0.4, 0.4, 0.2);
}
Which gives us:
Actually, I want my supports to be taller, because I'd like to code a suspension bridge.
if(p.y < f + 0.14 && p.y > 0.0 && cos(p.x * 40.0) > 0.9){
col.rgb += vec3(0.4, 0.4, 0.2);
}
I decided to make my supports tinner and draw them only if I did not already draw the bridge's deck:
vec4 bridge(vec2 p){
vec4 col = vec4(0.0);
float f = 0.0 + 0.2 * cos(2.0 * p.x);
if(distance(p.y,f) < 0.01){
col.rgb += vec3(0.2, 0.4, 0.0);
} else if ( p.y < f + 0.14 &&
p.y > 0.0 &&
cos(p.x * 40.0) > 0.97 ){
col.rgb += vec3(0.4, 0.4, 0.2);
}
return col;
}
Result:
Now we could go for some cables.
The cables
For now, I pretty much copy pasted the bridge's deck function and I replaced f
with cable_f
. I also put an offset of 0.14
to be at the top of the towers.
In the bridge
function:
float cable_f = 0.14 + 0.2 * cos(2.0 * p.x);
if(distance(p.y, cable_f) < 0.01){
col.rgb += vec3(0.8, 0.4, 0.0);
}
Result:
Now we want a function that is synchronised with the supports, so why not add this to cable_f
:
cable_f += 0.06 * cos(40.0 * p.x) - 0.06;
Honestly, to find this out, I just put a cos and tried many numbers to multiply x
until I realised it worked perfectly when it is the same number as in the cos
for the supports. Sometimes it is faster to randomly type on the keyboard instead of doing math. I also set both 0.06 factors by trial/error, please don't judge me.
Result:
Wow, this somewhat looks like a bridge!
If you got there, this is the point where you put a paypal link on your page to get paid for your content (no).
Here is the whole code for now:
precision highp float;
varying vec2 UV;
uniform float ratio;
vec4 bridge(vec2 p){
vec4 col = vec4(0.0);
float f = 0.0 + 0.2 * cos(2.0 * p.x);
if(distance(p.y,f) < 0.01){
col.rgb += vec3(0.2, 0.4, 0.0);
} else if ( p.y < f + 0.14 &&
p.y > 0.0 &&
cos(p.x * 40.0) > 0.97 ){
col.rgb += vec3(0.4, 0.4, 0.2);
}
float cable_f = 0.14 + 0.2 * cos(2.0 * p.x);
cable_f += 0.06 * cos(40.0 * p.x) - 0.06;
if(distance(p.y, cable_f) < 0.01){
col.rgb += vec3(0.8, 0.4, 0.0);
}
return col;
}
void main(void){
float x = UV.x * ratio;
float y = UV.y;
vec2 pos = vec2(x, y) - vec2(0.5);
vec4 col = vec4(0.0);
col += bridge(pos);
col.a = 1.0;
gl_FragColor = col;
}
Suspender cables
First, a little refactoring. At this point I did some google image search for suspension bridge parts
. I know the deck is called a deck, the cables are cables, the supports are actually called towers and the smaller cables are suspender cables. Praise the lord for the existence of the internet.
So I renamed f
to deck_f
in the function bridge
.
To build the suspenders I added this:
if(cos(p.x * 270.0) > 0.9 && p.y > deck_f && p.y < cable_f){
col.rgb += vec3(0.9);
}
As you can see, I use the same method as I used for the supports (towers). Only, the cos' factor is higher and I limit the visibility to be only between the deck and the cables.
Result:
Scene & reflection
Now, I would like to add a background. Later, I would like to reflect the entire scene to create water. To accomplish this, I will create a new function called scene
. Instead of calling bridge
, the main function will call scene
, which will manage the bridge and background.
Here is scene
:
vec4 scene(vec2 p){
vec4 col = vec4(0.0);
col += bridge(p);
return col;
}
Note that scene
has to be placed after bridge
(because scene
uses bridge
and the compiler needs every function in a function to be already defined).
Don't forget to change bridge
to scene
in the main!
Now for the background, I will go with a sunset-ish blend of colors. This is pretty much pseudo random typing on my keyboard, with the general Idea that I use the y
position to create a nice gradient.
Here is what I ended up with, placed just before the call to bridge
:
col.r += 0.6 - 0.6 * p.y;
col.g += 0.2 - 0.3 * p.y;
col.b += 0.3;
Result:
Then, for the water, I will at this at the end of my main
function:
if(pos.y < 0.0){
col.b += 0.2;
}
Result:
To create the reflection I simply have to call the scene
, but with the y axis flipped. I do this by multiplying by a vector (1,-1):
In the main
:
col += scene(pos * vec2(1.0, -1.0));
Result:
Note that the result is much too bright. This is because the background gradient is applied 2 times. A solution would be to multiply the color by 0.0
for y < 0
in scene
:
if(p.y < 0.0){
col *= 0.0;
}
Result:
I want the reflection to be a bit transparent, so I changed my reflection line to this:
col += 0.4 * scene(pos * vec2(1.0, -1.0));
Also, to add some waves, I copied pos
to another variable, adding a sine to it.
So the previous line became:
vec2 water_pos = pos;
water_pos += 0.003 * cos(pos.y * 230.0);
col += 0.4 * scene(water_pos * vec2(1.0, -1.0));
Now, I want to create an animated gif, so I will need the water to move... To do this, I must declare a uniform time
at the top of the file, right after uniform float ratio;
:
I now have this at the top of my file:
precision highp float;
varying vec2 UV;
uniform float ratio;
uniform float time;
[...]
Now, I can take time
into account in my water_pos
. I replaced the original line with :
water_pos += 0.003 * cos(pos.y * 230.0 + time * 6.28);
(Note: I had to reload (CTRL
+R
) for shadergif to manage the new uniform and actually move my water)
Result (It's alive!):
Final artistic tweaks
I decided to change the bridge cable function to something more realistic:
cable_f += 0.09 * (1.0 - abs(cos(20.0 * p.x + 3.1416 / 2.0))) - 0.09;
Also, I reordered the code so I only draw once in bridge
(Note that I also changed the colors):
vec4 bridge(vec2 p){
vec4 col = vec4(0.0);;
float deck_f = 0.0 + 0.2 * (cos(2.0 * p.x));
float cable_f = 0.14 + 0.2 * cos(2.0 * p.x);
cable_f += 0.09 * (1.0 - abs(cos(20.0 * p.x + 3.1416 / 2.0))) - 0.09;
if(distance(p.y, deck_f) < 0.01){
col.rgb += vec3(0.1, 0.1, 0.2);
} else if ( p.y < deck_f + 0.14 &&
p.y > 0.0 &&
cos(p.x * 40.0) > 0.99 ){
col.rgb += vec3(0.2, 0.2, 0.5);
} else if (distance(p.y, cable_f) < 0.004){
col.rgb += vec3(0.2, 0.3, 0.4);
} else if(cos(p.x * 490.0) > 0.9 && p.y > deck_f && p.y < cable_f){
col.rgb += vec3(0.6);
}
return col;
}
I also added this line in the scene
to create a sun:
col.rg += 1.8 * pow((1.0 - distance(p, vec2(0.0))), 18.0);
The development process for this line was something like:
col.rg += distance(p, vec2(0.0))
Nice, I want the color to be the most intense near the center though:
col.rg += 1.0 - distance(p, vec2(0.0))
What if I take the power:
col.rg += pow(1.0 - distance(p, vec2(0.0)), 2)
Wow that's a nice glow, lets put a bigger exponent:
col.rg += pow(1.0 * distance(p, vec2(0.0)), 4)
[...]
As you see, I try stuff and see if I like it. You should do the same. Find the right combination of math and human input.
The code looks like this:
precision highp float;
varying vec2 UV;
uniform float ratio;
uniform float time;
vec4 bridge(vec2 p){
vec4 col = vec4(0.0);;
float deck_f = 0.0 + 0.2 * (cos(2.0 * p.x));
float cable_f = 0.14 + 0.2 * cos(2.0 * p.x);
cable_f += 0.09 * (1.0 - abs(cos(20.0 * p.x + 3.1416 / 2.0))) - 0.09;
if(distance(p.y, deck_f) < 0.01){
col.rgb += vec3(0.1, 0.1, 0.2);
} else if ( p.y < deck_f + 0.14 &&
p.y > 0.0 &&
cos(p.x * 40.0) > 0.99 ){
col.rgb += vec3(0.2, 0.2, 0.5);
} else if (distance(p.y, cable_f) < 0.004){
col.rgb += vec3(0.2, 0.3, 0.4);
} else if(cos(p.x * 490.0) > 0.9 && p.y > deck_f && p.y < cable_f){
col.rgb += vec3(0.6);
}
return col;
}
vec4 scene(vec2 p){
vec4 col = vec4(0.0);
col.r += 0.8 - 0.5 * p.y;
col.g += 0.2 - 0.3 * p.y;
col.b += 0.3;
col += bridge(p);
col.rg += 1.8 * pow((1.0 - distance(p, vec2(0.0))), 18.0);
if(p.y < 0.0){
col *= 0.0;
}
return col;
}
void main(void){
float x = UV.x * ratio;
float y = UV.y;
vec2 pos = vec2(x, y) - vec2(0.5);
vec4 col = vec4(0.0);
col += scene(pos);
vec2 water_pos = pos;
water_pos += 0.003 * cos(pos.y * 230.0 + time * 6.28);
col += 0.4 * scene(water_pos * vec2(1.0, -1.0));
if(pos.y < 0.0){
col.b += 0.2;
}
col.a = 1.0;
gl_FragColor = col;
}