Introduction

The pixel-art, 2D guy try to make a 3D environment.

Scary Shader

There were a days when I was scared of shaders. I tried them once and decided that this is not something for me. And don't get me wrong, It's still not something I like it's just another tool for realization of my vision.

Speaking of vision. That was my biggest problem. I was trying to learn shaders the academic way - lesson by lesson, all the theory behind it. And even when skipped few lessons to the good part it turns out a colour triangle. Or a box.

"Don't learn to code, code to learn." - internet.

This time it was different. Around April I started to look at the Godot 3.0 and it's new and awesome 3D engine. When I realized my laptop is too weak for that kind of stuff I bough a PC. Low spec but still superior to the any notebook (less than 14"). I wanted to have an easy way to make a nice looking terrain for all future games. Or at last look what Godot offers. That way I ended up making a first shader. More like copy-pasting but still I get familiar with the code. Now it was not so scary and after lots of stupid mistakes I was able to move code and get an overall understanding of it. The first milestone was done.

Making a simple terrain based on height-map turns out very easy. So I needed a new challenge. So I added the support for 4 textures and mountains, rivers, etc. But it looked more like a desert.

At that stage I was ready to get to the fun part. The grass shader. I did not know anything besides how it should look. And that main goal is still not fulfilled in hundred percent. But the grass looks so good already that I wanted to show it to everybody.

How do you make a grass?

I do some research and found this awesome tutorial about using particles for making huge amount of grass. Thanks to Bastiaan Olij (GitHub, twitter) I learned how to make it. He made a great video about it. You should watch it also. There's a lot of information how to setup everything and the base of the shader. My shader is based on this and uses the same technique.

My first attempt was to recreate the grass from his video.

But the grass in the end looked kinda artificial. It's a tutorial, I know it but I'm just making a point here.

Still from Bastiaan Olij's tutorial on grass shader

And mine version wasn't much better. This was a great start in terms of performance. I was able to render a large field of grass just like that. It just needs a little bit of artistic touch.

Some iterations later. It started to looked a little bit more realistic. Fog and overall mood helps also. Good enough from a distance.

I started to add more variety and effects. Found out about PBR and the world started to glow! I tested so many settings and effects.

And with full PBR materials the scene looked more realistic but in the end I did not like it. I wasn't able to make it the way I wanted. I just tested random settings (different parameters for generating PBR materials) to get something promising. And creating own PBR textures are hard and time consuming. Software that helps doing so don't help much. But the rocks looks kinda interesting.

I take one step back. And balanced realism, performance and aesthetic of wild jungle. In the end I use one texture per material.

Let's look a the final (at the moment) appearance of the shader.

The Grass Shader

Mythic Treasure Hunt
Mythic Treasure Hunt
Mythic Treasure Hunt
Mythic Treasure Hunt
Mythic Treasure Hunt

More or less this one shader is used to generate grass, bushes, palms and butterfly.

But first a little bit of background and idea behind all of this. Then the exact shader will be easy to understand. Or easier at last.

Global overview

Each map is created from the 2048x2048 height-map (gray scale). Textures and particles use also features-map (RGB).

On the height-map white colour represents peek point (top of the mountain) and the black represents deepest point (rivers, deep ocean). Everything in between is left for the artist imagination. I looked a the real maps of terrain for clues. That's how I made mountains.

Features Map (left) vs Height Map (right)

Right image. Look at the pyramid shape. And the road with lots of dots around. Then there is a mountain (this white thing).

Left image. The space where pyramid is located is full blue surrounded by red. Road, dots and mountain are blue.

Now look how engine renders it.

Pyramid structure
Road with stones and mountains in the background

The path/road is made as a grow like in the vinyls. It simulates corrosion. And it gets the rock material. The rocks around road are made as little bumps covered with same material. Different brush size and you gave a lot of easy to make rocks.

Zone

Terrain mesh is textured based on features-map file. This file is created using four colours. Each one for different texture and biome.

I called it zones.

  • Black (default) for sand
  • Red for forest (dark grass)
  • Green for grass (flowers)
  • Blue for rock

Grass

Each zone is made of different grass. Or the other way. Each grass type has preference: red, green, blue; All set do false by default. I choose to have flowers on green zone. Palms are always on red. For simplicity at the moment.

Different types of grass + palm for scale
Top view of the grass

Particles Everywhere!

But how that grass works? It's basically a particle. Each particle is a mesh. One of those five types. As a mesh it has it's own shaders for texturing and movement. But that's nothing special. It's a simple, low-poly mesh (few sprites to be exact). But as a particle that one mesh is instanced hundred of times in one spot. Particle generator needs to have explosiveness to 1.0 and enable the local coordinates (TODO: on or off?). And then the magic comes - the particle shader. It is run for each of the particle mesh. It has only a vector shader. And we move only one vector - the point where our particle mesh will be rendered.

That way we can manipulate each of the particle. Making it perfect for flooding massive terrain with small mashes. 16384 for main grass, 16384 for "dead" grass and  1024 for each of other types. That's a lot of grass and still it runs surprisingly fast. Quality of the grass dictates the frame-rate more than the number of particles itself.

One more thing is needed to take the full power of particles. A small gdscript for moving the AABB cage with active camera. Thanks to this we render only the grass that is near to the camera. If the camera never moves or when the particle cage is big enough to cover whole map at once this additional script is not needed.

The shader

After this long intro we can look at THE SHADER.

Sections

This shader can be split to five sections. And few sub-sections. Even looking at this table of content you can get a glimpse of what is happening.

  • settings
    • terrain settings
    • vegetation settings
    • map file settings
  • functions
    • calculating height
    • random generator
      • magic float nambers
      • vector2
    • matrix transformation
      • position
      • angle
      • scale
  • vertex
    • get position
    • randomize position
    • flat land
    • zones
    • scaling
    • set final transformation

Or around 128 lines of code. So let's me comment on each section. But first the code in full print.

Printed shader code

The Code

Line by line lets examine this shader. If you like to look a the big file also here is the code.

Remember to change the type to particle.

shader_type particles;

Next are all kind of settings. Most of them are self-explanatory so I will not cover them.

Terrain settings

Should be exactly the same as in the terrain shader. This way the grass is generated over the same terrain.

// SET RANDOM FLOAT FOR EACH VEGETATION TYPE
uniform float RANDOM_SEED = 0.1234;

// TERRAIN SETTINGS
uniform float TERRAIN_SURFACE_SCALE = 4.0;
uniform float TERRAIN_HEIGHT_SCALE = 64.0;
uniform float TERRAIN_WATER_LEVEL = 4.0;
uniform float TERRAIN_MOINTAINS_LEVEL = 0.65;
uniform float TERRAIN_MOINTAINS_SCALE = 3.0;

SURFACE_SCALE is a simple scale (x,y,z) to make the map bigger. HEIGHT_SCALE is scaling in Y direction - height. The image file have only 255 levels and making it 255 pixels height is too small. This scales this up to make terrain much bigger. Downside is losing precision that can make terrain to look like in a Minecraft.

MOUNTAINS_LEVEL adds even more scaling in Y direction. When the height-map value is greater than mountains level it gets scaled by MOUNTAINS_SCALE factor.

Vegetation settings

Those manipulates our grass.

// VEGETATION SETTINGS
uniform float GRASS_ROWS = 64;
uniform float GRASS_SPACING = 12.0;
uniform float GRASS_TWIST_SCALE_MIN = 0.5;
uniform float GRASS_TWIST_SCALE_MAX = 2.0;
uniform bool ZONE_RED = false;
uniform bool ZONE_GREEN = false;
uniform bool ZONE_BLUE = false;

Particles are placed in rows making a square. GRASS_ROWS tells the shader how big the square should be. Number of particles to generate should be set to GRASS_ROWS*2. In this example it will be 4096.

GRASS_SPACING defines the space between each particle and TWIST_SCALE defines minimum and maximum ranges of random scaling for each particle.

ZONE_* defines if this particle should be rendered over this terrain type. Default is false for all but it should be set at last one for true.

Maps

Last the height-map and features-map. For calculations also size of those maps is needed.

// MAPS
uniform sampler2D HEIGHT_MAP;
uniform sampler2D FEATURES_MAP;
uniform vec2 MAP_SIZE = vec2(2048.0, 2048.0);

Helper Functions

To make life easier shaders can use custom functions. Then those can be used in vertex shader with different parameters. Best example is random generator that is used in many places.

Height

Calculating height is super simple. The whole map is one to one representation of a height-map texture. Adding all kind of scaling and in the end it's reading pixel colour from particle position.

float get_height(vec2 pos) {
	// center position to mach the terrain algorithm
	pos -= 0.5 * MAP_SIZE;
	pos /= MAP_SIZE; 

	// read height from texture
	float h = texture(HEIGHT_MAP, pos).r; 

	// check if we hit the mountain level
	if (h>TERRAIN_MOINTAINS_LEVEL) { 
		// scale up for mountain effect
		h += (h-TERRAIN_MOINTAINS_LEVEL)*TERRAIN_MOINTAINS_SCALE; 
	}

	// adjust for the overall terrain scale
	return TERRAIN_HEIGHT_SCALE * h; 
}

Random Numbers

This is a magic code. I don't understand it fully and you don't need also. It generates random numbers that are random enough. Just find a good seed value.

float fake_random(vec2 p){
	return fract(sin(dot(p.xy, vec2(12.9898,78.233))) * 43758.5453);
}
vec2 faker(vec2 p){
	return vec2(fake_random(p),fake_random(p*RANDOM_SEED));
}

The Matrix Has You

This peace of code have a story behind. But first of all what it is doing? Nothing special, transforming one position to another but scaled, moved and rotated. Very useful for making different looking models from same source.

// THREE DIMENSIONAL MATRIX MANIPULATION
mat4 enterTheMatrix(vec3 pos, vec3 axis, float angle, float scale){
    axis = normalize(axis);
    float s = sin(angle);
    float c = cos(angle);
    float oc = 1.0 - c;

	// converts matrix for position, angle (for each axix) and scale
    return mat4(vec4((oc * axis.x * axis.x + c)* scale,		oc * axis.x * axis.y - axis.z * s,	oc * axis.z * axis.x + axis.y * s,	0.0),
                vec4(oc * axis.x * axis.y + axis.z * s,		(oc * axis.y * axis.y + c) * scale,	oc * axis.y * axis.z - axis.x * s,	0.0),
                vec4(oc * axis.z * axis.x - axis.y * s,		oc * axis.y * axis.z + axis.x * s,	(oc * axis.z * axis.z + c) * scale,	0.0),
                vec4(pos.x,									pos.y,								pos.z,								1.0));
}

What is the story? The original tutorial covers only transforming particles in three dimensions to put all particles on the ground. But they looked like some artificial plantation. To make it look natural I needed to move each of the particle in random position (little change from main grid). But they still looked like exact clones. Next thing was to scale them to make little grass and few bigger ones. Then rotate in 0-360 radius over the Y axis. Sounds easy as just another parameters. In Godot  world I was familiar with rotate() and scale(). But in shader world I need to write it my self using matrix transformations. Things i don't have any knowledge at all.

So I started to search for other tutorials, code examples and theory.

I know how to translate already. It was the last value in each of the TRANSFORM arrays. Looking at this I figure out that scaling is just those exact three number in diagonal. It worked. But then rotation and combining this all together was a different kind of thing.

Fortunately I found an example in GLSL.

mat4 rotationMatrix(vec3 axis, float angle)
{
    axis = normalize(axis);
    float s = sin(angle);
    float c = cos(angle);
    float oc = 1.0 - c;
    
    return mat4(oc * axis.x * axis.x + c,           oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,  0.0,
                oc * axis.x * axis.y + axis.z * s,  oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,  0.0,
                oc * axis.z * axis.x - axis.y * s,  oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c,           0.0,
                0.0,                                0.0,                                0.0,                                1.0);
}

Looked good. So I ported it by just blindly changing the code so the error handling will not find anything. It turns out there is mat4 equivalent but I just need to put vec4 inside.  And after I get rid of all errors it worked in the first try!

Vertex

Now the code that runs all of this.

Position

Put particle on row (X) and column (Z).

vec3 pos = vec3(0.0, 0.0, 0.0);
pos.z = float(INDEX);
pos.x = mod(pos.z, GRASS_ROWS);
pos.z = (pos.z - pos.x) / GRASS_ROWS;

Then canter and apply spacing.

pos.x -= GRASS_ROWS * 0.5;
pos.z -= GRASS_ROWS * 0.5;
pos *= GRASS_SPACING;

pos.x += (EMISSION_TRANSFORM[3][0] - mod(EMISSION_TRANSFORM[3][0], GRASS_SPACING*TERRAIN_SURFACE_SCALE))/TERRAIN_SURFACE_SCALE;
	pos.z += (EMISSION_TRANSFORM[3][2] - mod(EMISSION_TRANSFORM[3][2], GRASS_SPACING*TERRAIN_SURFACE_SCALE))/TERRAIN_SURFACE_SCALE;

Randomize position

At this stage the grass is placed perfectly on the terrain. We need to make it more random. Here are the first use of random function I mentioned before.

vec2 noise = faker(pos.xz);
pos.x += (noise.x * 4.0 ) * GRASS_SPACING;
pos.z += (noise.y * 4.0 ) * GRASS_SPACING;

Flat Land

And finally apply height using another function I created.

pos.y = get_height(pos.xz);

To check if mesh is on flat land or cliff I calculate difference in height to the near points.

float y2 = get_height(pos.xz + vec2(1.0, 0.0));
float y3 = get_height(pos.xz + vec2(0.0, 1.0));

Then later in code it is checked and the particle is moved far away so it will never be rendered.

if (abs(y2 - pos.y) > 0.5) {
	pos.y = -10000.0;
} else if (abs(y3 - pos.y) > 0.5) {
	pos.y = -10000.0;
}

Zones

Terrain mask is made for each zone.

float terrain_mask = 0.0;
if (ZONE_RED) {
	terrain_mask += texture(FEATURES_MAP, feat_pos).r;
}
if (ZONE_GREEN) {
	terrain_mask += texture(FEATURES_MAP, feat_pos).g;
}
if (ZONE_BLUE) {
	terrain_mask += texture(FEATURES_MAP, feat_pos).b;
}

This way at the end I can decide if this particle should be visible on this zone or not.

if (terrain_mask < 0.65) {
	pos.y = -10000.0;
}

There is also check for underwater terrain. I can manipulate this parameter to allow grass to be under water if I want. Or make the other way so e.g. bigger bushes will not be rendered in lower grounds near water. That's why parameters are important. The give designers more options to play with.

 if (pos.y < TERRAIN_WATER_LEVEL) {
	pos.y = -10000.0;
}

Scaling

Scaling is guarded by two parameters. Each mesh have it's own values to make it look normal. Like trees needs to be scaled down mostly but grass is already small so only a little bit of scaling up is needed.

float scale_mod = clamp(noise.x * GRASS_TWIST_SCALE_MAX, GRASS_TWIST_SCALE_MIN, GRASS_TWIST_SCALE_MAX);

Final Transformation

All this calculations can now be applied to the particle. Here is where the matrix manipulations kicks in.

TRANSFORM = enterTheMatrix(PARAMETERS)

First parameter sets position (with overall terrain scale applied)

vec3(pos.x * TERRAIN_SURFACE_SCALE, pos.y * TERRAIN_SURFACE_SCALE, pos.z * TERRAIN_SURFACE_SCALE)

Second is a lock axis parameter. If I want to rotate mesh only over Y axis I need to zero the other once - (0.0, 1.0, 0.0).  For example (1.0,1.0,1.0) will rotate in all directions at once.

vec3(0.0, 1.0, 0.0)

Third one is the exact rotation in 0-360 degree. I just make a random value and camp it to 0-360. Clamp means that anything below 0 or above 360 will be converted to 0, 360 respectively.

clamp(noise.y * 320.0, 0.0, 320.0)		

Last there is a scale factor. Just a variable I calculated already.

scale_mod

Putting it all together.

TRANSFORM = enterTheMatrix(
    vec3(pos.x * TERRAIN_SURFACE_SCALE, pos.y * TERRAIN_SURFACE_SCALE, pos.z * TERRAIN_SURFACE_SCALE),
    vec3(0.0, 1.0, 0.0),
    clamp(noise.y * 320.0, 0.0, 320.0),
    scale_mod
);

The End

Look for future iterations at https://github.com/w84death/mystic-treasure-hunt.

I hope that at this moment you'll be able to make and awesome grass shader!

Download (updated version)