Grass Geometry Shader

Introduction

Geometry shaders are powerful shaders that allow you to create primitives like points, lines, and triangles directly on the GPU. The purpose of this article will not be to explain how to set one up, but to cover the different details of the implementation of this particular grass shader (control of the length of the strand, randomization, wind, etc…).

Strand features

For my shader, I decided to describe my strand of grass with three properties : length, width, and gravity. The tricky part will be to bend the strand with the gravity, by keeping the length of the strand the same. As for the width, it is the one at the bottom of the strand, which we will interpolate to zero when getting closer to the top. To construct the strand, we will also need the get a normal and a position as input of our geometry shader.

// Base properties
float _Length;
float _Width;
float _Gravity;

// This struct is the input data from the geometry shader.
// Simply convert the data from the vertex shader to world position
struct geomInput 
{
	float4 pos : POSITION;
	float4 nor : NORMAL;
};

Then, let’s move on to our first iteration of the geometry shader. The first thing I tend to do is to retrieve the input data of the geometry shader in clean variables :

[maxvertexcount(80)] // Don't mind this value for now
void geom( triangle geomInput i[3], inout TriangleStream stream )
{
	// Because access the input data directly tend to make the code a mess, I usually repack everything in clean variables

	float4 P1 = i[0].pos;
	float4 P2 = i[1].pos;
	float4 P3 = i[2].pos;

	float4 N1 = i[0].nor;
	float4 N2 = i[1].nor;
	float4 N3 = i[2].nor;
}

Because we retrieve triangles, we have to decide of a position and a direction of growth for our strand of grass. I simply average the positions and the normals, and decide on a tangent as a lateral direction for our strand.

// I compute the point at the center of the face, its normal, and choose the lateral direction of the strand of grass.

float4 P = (P1+P2+P3)/3.0f;
float4 N = (N1+N2+N3)/3.0f;
float4 T = float4(normalize((P2-P1).xyz), 0.0f);

And here, we have all the necessary data to create the strand. To create the geometry, we simply have to create horizontal slices of our strand. The more slices, the better the strand will look, but the more geometry their will be. And, of course, we will have to do it twice (for the front, and for the back). You can either define the number of steps as a property, or hardcode it in the shader. I decided to hardcode it, because you have to change the maxvertexcount based on it, so no matter what you have to go and edit the shader.

for( int i = 0; i < _Steps; i++ ) 
{
	// Retrieve the normalized time along the strand.
	// It will be used to interpolate all the data from the bottom to the top of the strand.
	// t0 is the current step of the strand we are drawing.
	// t1 is the next step to be drawn. It will be t0 at the next iteration.
	// You could store its value for optimization.

	float t0 = (float)i / _Steps;
	float t1 = (float)(i+1) / _Steps;

	// Make our normal bend down with gravity.
	// The further we are on the strand, and the longer it is, the more it bends.
	// We then normalize this new direction, and scale it by the length at the current iteration of the loop.

	float4 p0 = normalize(N - (float4(0, _Length * t0, 0, 0) * _Gravity * t0)) * (_Length * t0);
	float4 p1 = normalize(N - (float4(0, _Length * t1, 0, 0) * _Gravity * t1)) * (_Length * t1);

	// Interpolate the width, and scale the lateral direction vector with it

	float4 w0 = T * lerp(_Width, 0, t0);
	float4 w1 = T * lerp(_Width, 0, t1);

And here, you have all the necessary data to construct a first sketch of the strand. p0 – w0, p0 + w0, p1 – w1 and p1 + w1 are the four points of the face you are drawing in this iteration of the loop. Simply fill the TriangleStream with the necessary data to send to the fragment shader. Computing uvs and a normal for a quick shading should be pretty easy too. Right now, you should have something similar to this :

Grass_Step1

Randomization

There aren’t many things to randomize, but adding chaos to the length and the direction of the strands of grass is a necessary step to make it a believable grass. We will need a noise texture, and two variables to control the intensity of the effect and the direction and the length of the strands.

// Randomization properties
sampler2D _Noise;
sampler2D _Noise_ST;
float _DirectionIntensity;
float _LengthIntensity;

Then it is really a simple matter of using the noise. However, what uv coordinates should we use ? I decided to use the world position, and to modulate it with the texture scale and offset. We could use the texture coordinate of our object, but this way, no matter how we place our objects, it will stay coherent. This will be even more important when we start adding wind.

// Sample textures

float3 noise = tex2Dlod(_Noise, float4(P.xz * _Noise_ST.xy + _Noise_ST.zw, 0, 0)).xyz;

// Modulate strand length

float l = _Length + noise.r * _LengthIntensity;

// Modulate grow direction

float3 noiseNormal = (noise * 2 - 1) * _DirectionIntensity; // Convert the noise sample to a vector and scale it with the intensity.
N = normalize(float4((N + noiseNormal).xyz, 0)); // Add the noise normal and normalize. Make sure the fourth component is null to avoid issues.

It’s short, nothing overly complicated here, but it does the job pretty well. Right now, you should have something similar to this :

Grass_Step2

Wind

The wind is one of the hardest thing to get right. Having it life-like took me some time, but I am really happy with how it turned out. The first thing you might want to do is using a sinus function over time, but it is really boring to look at. You will realize it is looping really quickly. To avoid this, I combine it with a “wind” texture. To control all this, we will need these parameters :

// Wind parameters
sampler2D _Wind;
float4 _Wind_ST;
float _WindFrequency;
float _WindMin;
float _WindMax;

The way it’s going to work is like this : our wind texture will give us the direction of the wind. We will use the offset provided in the inspector as a speed to pan the uvs over time and make the wind move over our grass. Here is what my wind texture looks like :

Grass_Flow

Then, we will make this direction oscillate between the min and max values provided at the given frequency. This way, the wind won’t feel like a constant direction applied to the geometry, but feel less predictable. Of course, you you stare at the grass long enough, you will be able to spot the pattern, but it’s not as obvious as a simple sin. Here is what we need to add to the code to make it work :

// Remap given value s from the first range [a1,a2] to the second range [b1,b2]
float remap(float s, float a1, float a2, float b1, float b2)
{
	return b1 + (s - a1)*(b2 - b1) / (a2 - a1);
}
// Sample the texture
float3 wind = normalize(tex2Dlod(_Wind, float4(P.xz * _Wind_ST.xy + _Wind_ST.zw * _Time.w, 0, 0)).xyz); // We normalize because it needs to be only a direction. No information about intensity should be provided here.

// And here it is : we convert our wind to a vector in the [-1,1] range.
// As for the sinus, we use the time value modulated by the frequency.
// And finally, we remap it between our minimum and maximum values and use it to scale our wind normal.
float3 windNormal = (wind * 2 - 1) * remap(sin(_Time.w*_WindFrequency), -1, 1, _WindMin, _WindMax);
				
// Of course, we need to add it to our provided normal.
N = normalize(float4((N + noiseNormal + windNormal).xyz, 0));

And we are done ! You should have something similar to the video above. Of course, it takes some time to find values for all this parameters. And as we use a triangle to create a strand, you will need to have a mesh with enough geometry. Or use tesselation to control the quantity of grass.

Conslusion

This conclude this quick “tutorial” about this grass geometry shader. If you have any question or want to point something out, don’t hesitate to contact me here in the comments, or by using the contact form. Thanks a lot for taking the time to read me, and see you soon !

2 Comments:

  1. Nice! Thought the tutorial was well written. Think I will try my own implementation tomorrow. How do you shade the grass in Unity? I haven’t use Unity in a while, but at that time you couldn’t use a geometry shader with the renderer.

    • Yeah, you still can’t feed the standard shader model when using a geometry shader (or at least, it’s not possible without calling all the functions yourself). Right now, I compute the normal of each point as a cross product of the tangent vector “T” and the growth vector “p0” or “p1” of the current point, and simply use a lambert shading in the fragment shader because it does the trick. A more advanced shading method is possible (adding specular, or moving to PBR), but I didn’t think it would be worth it as grass is already detailed enough as it is. Using at least the data of the lightmaps to shade the grass when lighting is baked would be interesting though.

Leave a Reply

Your email address will not be published. Required fields are marked *