I learn a lot doing my shader experiments in Islands of Shaders, then Mystic Treasure Hunt and now in Valley of Shaders. Over this course I made lot of awesome game worlds. But I lost the track and they became slow and bloated. That's why this last project emerged. It has one purpose: for making simple and (very) fast shader for easy creating terrains for any future games.

Like this prototype of pirate game.

And I think that I succeed enough to share. The project will be continued. But it is a good starting point for you to learn, look and perhaps made own much better version. Nevertheless let's dive in to the code (open this file in any editor for overview reference).

Single Terrain Shader

Why that name? Because it use just one big plain mesh with one shader file. Then it generates animated water, shore, textured grass and sharp mountains. All procedural. No other files needed.

Flat plane


Those are all the boring parameters.

uniform vec2 heightmap_size = vec2(1024.0, 1024.0);
uniform float height_factor = 64.0;
uniform float mountains_factor = 24.;
uniform float UV_FACTOR = 4.;
varying float color_height;
uniform sampler2D heightmap;
uniform sampler2D noisemap;
uniform float white_line = 0.9;
uniform float green_line = 0.35;
uniform float ground_line = 0.33;
uniform float blue_line = 0.32;

Heightmap is a NoiseTexture. It is shader for all nodes that need positioning in the game. Noisemap is also a NoiseTexture but is it used just for randomization.

float get_height(vec2 pos) {
	pos -= .5 * heightmap_size;
	pos /= heightmap_size;
	return texture(heightmap, pos).r;

Getting height from heightmap is very simple. Just remember to center the position first.

Vertex Shader

Now the actual height algorithm. The vertex shader.

void vertex() {

Save height to h. This will be used for vertex and fragment shader. Thats why global color_height is needed.

float h = get_height(VERTEX.xz);
color_height = h;

Now calculate each "layer". That is water, ground and spiky mountains.

float shore_line = step(blue_line, color_height);
float mountains_line = smoothstep(green_line, white_line, color_height);

Instead of branching (if-s) I'm using step and mix. Shore line will be 1 if less than blue_line (height of water) else 0

Mountains line will be used for generating more spikes at the top and less as we gets down to the green line. Smooth curve is preferred.

float ran = texture(noisemap, VERTEX.xz * 8.).x * mountains_factor;

Random value for mountains with parameter.

Noised terrain
h = mix(blue_line, h, shore_line);

Now the h is flattened at water level or unattached at shore level.

Flat water
	float anim = mix(sin(TIME * 2. + VERTEX.z * ran) * cos(TIME * 2. + VERTEX.x * ran), 0., shore_line);

Animation or sinus by sinus. It is added to values less than shore line. Heigher values will always get 0. So effect will not be affecting them.

h = h * height_factor + anim;

Height is calculated. To have nice visual effect height factor is applied. Basically everything is scaled in Y-axis. Mountains grows. Animation is added. If it's not zero.

float fh = mix(h, h + ran, mountains_line);

Final height is a mix of height and those random spikes saturated at top. This makes mountains to look like mountains and not like piles of ground.

Mountain peaks
VERTEX.y = fh;

Vertex height is set.

Fragment Shader

void fragment() {

I use two noise map textures. First is for small, detailed changes like colour between grass and mountain peeks. Second is much bigger (32x) and is used for grass texture to make it less uniform.

float ran = texture(noisemap, UV * RANDOM_UV_FACTOR).x;
float ran2 = texture(noisemap, UV * GRASS_UV_FACTOR * 32.).x;
Uniform colour

Terrain needs basic colour. For this I'm using height (via parameter color_height set in vertex shader).

vec3 alb = vec3(color_height);
Colour based on height

Now it looks more interesting. But it needs textures. Procedural textures. Each layer will be calculated separately for different effects.

Exactly as in vector shader I'm using step() and mix(). First the border is defined using step() and a little bit of randomness. Next mix() for each colour. Think of it as: at y_line height, first parameter is a colour for pixels above and second for pixels beneath it. Grass have lots of randomness while sand uses ran2 and less random values. This makes one noisy and other smooth look.

// sand (yellow) vs grass (green)
float y_line = step(ground_line + ran * .15, color_height);
alb.r = mix(.2 + ran *.3, (.3 - ran * .1) * ran2, 	y_line);
alb.g = mix(.3 + ran *.2, (.9 - ran * .1) * ran2, 	y_line);
alb.b = mix(.2 + ran *.2, (0.1) * ran2, 		y_line);
Grass procedural texture

Mountains should have white peaks. This is easy. I'm just adding full white above green_line and pass unchanged colours for the rest.

// rest vs white top
float g_line = step(green_line + ran * .3, color_height);
alb.r = mix(alb.r, 1., g_line);
alb.g = mix(alb.g, 1., g_line);
alb.b = mix(alb.b, 1., g_line);
White top of the mountains peaks

Water is exactly the same but in reverse. Also I added a little bit of randomness.

// water (blue) vs rest
float b_line = step(blue_line, color_height);
alb.r = mix(.0 + ran * .05, 	alb.r, b_line);
alb.g = mix(.2 + ran * .05, 	alb.g, b_line);
alb.b = mix(.7, alb.b, b_line);
Blue water

Lastly emission and transmission to bring some life in. Grass transmit light green, water blueish colour and mountain peaks transmit and emit white (to simulate snow).

EMISSION = mix(vec3(0.), vec3(.1, .2, 1.), g_line);
TRANSMISSION = mix(vec3(0.), vec3(.3, .3, 1.), g_line);
TRANSMISSION += mix(vec3(color_height * ran * 8.), vec3(0.), b_line);
TRANSMISSION += mix(vec3(.5, 0.8, .2), TRANSMISSION, g_line);
Emission and transmission

For PBR effects (if enabled) few additional settings. Here are my latest values:

SPECULAR = mix(1., .4, b_line);
ROUGHNESS = mix(.6, 1., b_line);
METALLIC = mix(0.5, 0., b_line);
Spectacular, toughness and metallic

And finally add the colours.

ALBEDO = alb;

Final Image

This shader renders in 60fps on most netbooks that are less than 2 years old.

Netbook ready graphics settings

And on better hardware (GeForce 1050 2GB) I can bump the effects a little bit and still have 60fps.

Screenshot from game prototype that uses this shader

Mission accomplished!

If You use this shader in your project send me some screenshots ;)