Well Met! This is the 29th tutorial from my series, which will upgrade previous terrain tutorial by adding seamless grass without any significant impact on the performance. So let's get started right away .
First of all, let's discuss some possibilities of how to render grass. Basically, there are two main approaches on how to achieve desired result. First would be to actually try to render every grass blade separately. That would require us to write some shader (geometry shader), which would generate some grass blades on random positions and would randomize the shape of grass blades somehow. In the past, it would be impossible to render grass the way I just described, but today this approach isn't actually that bad - on modern GPUs, this is completely achievable without significant FPS drops and maybe I will implement this approach in some later tutorial. But not today! . Today, I will stick to another, simpler approach, results of which are actually really good and the performance is even better, not too much work for GPUs. We will render grass by mapping a simple grass texture with alpha value onto the quads and we will create several grass patches along the terrain. One grass patch will actually consist of 3 overlapping quads arranged in a way, that you won't be able to recognize, that there are any quads involved. The whole process of filling the terrain with grass looks like this:
What is alpha test, you might ask? This is the key-part in this tutorial, so it deserves an separate paragraph in this article .
Alpha test is a simple, yet very powerful technique for rendering images, where transparent pixels should be entirely skipped, not even writing anything to depth or color buffer. Let's have a look at our grass texture (grassPack.dds), which is a texture with an extra alpha channel for transparency:
As you can see, this texture consists of 4 different grass clusters, each is 256px wide (that makes the texture width 1024px). What you can see there is an alpha channel (the black-white image). The usual purpose of alpha channel is the image transparency. The whiter the alpha (closer to 1), the more opaque the object is. In other words, you can see that alpha channel copies the contours of grass blades, which means, that whenever we render quad with this grass texture, we will completely filter out any fragments, that aren't under the grass' alpha mask. To be more specific, when the pixel in the texture, that's about to be rendered and colored in fragment shader, has alpha value zero (or close to it, or below certain threshold), we will simply DISCARD that particular fragment, not doing anything to the framebuffer or Z-buffer. It's that simple!
Before making any rendering, we need to calculate the positions of the grass patches along the heightmap. All of this is done at the end of LoadHeightMapFromImage function and is shown below:
bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
// ...
vboGrassData.CreateVBO();
float fGrassPatchOffsetMin = 1.5f;
float fGrassPatchOffsetMax = 2.5f;
float fGrassPatchHeight = 5.0f;
glm::vec3 vCurPatchPos(-vRenderScale.x*0.5f + fGrassPatchOffsetMin, 0.0f, vRenderScale.z*0.5f - fGrassPatchOffsetMin);
iNumGrassTriangles = 0;
while(vCurPatchPos.x < vRenderScale.x*0.5f)
{
vCurPatchPos.z = vRenderScale.z*0.5f - fGrassPatchOffsetMin;
while(vCurPatchPos.z > -vRenderScale.z*0.5f)
{
vCurPatchPos.y = GetHeightFromRealVector(vCurPatchPos)-0.3f;
vboGrassData.AddData(&vCurPatchPos, sizeof(glm::vec3));
iNumGrassTriangles += 1;
vCurPatchPos.z -= fGrassPatchOffsetMin+(fGrassPatchOffsetMax-fGrassPatchOffsetMin)*float(rand()%1000)*0.001f;
}
vCurPatchPos.x += fGrassPatchOffsetMin+(fGrassPatchOffsetMax-fGrassPatchOffsetMin)*float(rand()%1000)*0.001f;
}
glGenVertexArrays(1, &uiGrassVAO);
glBindVertexArray(uiGrassVAO);
// Attach vertex data to this VAO
vboGrassData.BindVBO();
vboGrassData.UploadDataToGPU(GL_STATIC_DRAW);
// Vertex positions
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), 0);
bLoaded = true; // If get here, we succeeded with generating heightmap
return true;
}
This code has 2 main control parameters - fGrassPatchOffsetMin and fGrassPatchOffsetMax. What we're doing here is, that we start at the left-most and front-most point of the heightmap and from there we go through the heightmap using two while cycles, while making sure that we're still inside the heightmap's boundaries. Every pass of these two cycles adds a position of the grass clusters to the grass-dedicated VBO called vboGrassData and then moves itself by a random value between fGrassPatchOffsetMin and fGrassPatchOffsetMax. It also keeps track of the total number of grass patches generated in the variable iNumGrassPatches. The VBO is then uploaded to GPU and we're done with generation of grass patches POSITIONS, not the patches itself. To actually produce some geometry with grass, we will use a separate shader program, which consists of vertex, geometry and fragment shader. The geometry is thus produced in geometry shader. Let's analyze the grass shader program now, as most important parts of this tutorial are hidden there .
The code of the grass vertex shader is really simple and not much to explain here - it just takes the incoming grass patches positions and passes them further to the geometry shader:
#version 330
layout (location = 0) in vec3 inPosition;
void main()
{
gl_Position = vec4(inPosition, 1.0);
}
Now pay attention! Most important stuff is in this part of whole shader program. This shader receives positions of grass patches on the map and then outputs three quads with a grass texture applied, rotated in a way, that the grass would appear seamless no matter what angle we look at it from. This way is shown on the picture below:
So what we're supposed to do is to receive the grass patch position and generate 3 of these quads with texture coordinates. Because we have 4 kinds of grass in our texture, we will randomly choose one. But how can we possibly choose random patch and ensure, that every time this shader runs, the same grass patches generate same random texture (because if it wouldn't, the grass would flicker by changing the texture every frame )? The answer is actually pretty simple - because GLSL doesn't provide any kind of random function (and the reason is probably something like this, that you might want to get same random output between two consecutive shader runs), we will copy & paste random function from particle system tutorial, where we provided seed for random number generator depending on time passed. Now, the seed won't be changing as the time changes, but the seed will actually be SET according to grass patch position. This way, we ensure, that the RNG seed doesn't change between the shader calls and that it generates random stuff for every grass patch, but still same random values .
Another thing to mention here is that I also programmed a simple grass waving using sine function. As you all know, sine function returns values between -1 and 1, which is really nice for waving. However, to make waving look a little bit more realistic, we will have different inputs for sine function for different grass patches and also depending on the output of sine function, the waving effect will be more or less present. I will explain this more in-depth a little later, let's get back to generating these grass patches now . The goal is to create 3 quads of grass just as depicted on the picture above:
To generate them, we will use 3 basis vectors, among which we generate the quads. Because there is no quad primitive anymore, the best way to create quad is to output triangle strips. These vectors are generated in this part of geometry shader:
void main()
{
// ...
float PIover180 = 3.1415/180.0;
vec3 vBaseDir[] =
{
vec3(1.0, 0.0, 0.0),
vec3(float(cos(45.0*PIover180)), 0.0f, float(sin(45.0*PIover180))),
vec3(float(cos(-45.0*PIover180)), 0.0f, float(sin(-45.0*PIover180)))
};
// ...
}
Now we will simply go through them using for cycle, where each cycle generates one properly rotated grass quad. Quad consists of 4 vertices, so we will output them in this order: top-left, bottom-left, top-right and bottom-right vertex. All top-vertices will also be animated a little. The height of grass patches is also random, and is calculated and saved in fGrassPatchHeight variable in the grass shader. The first effect that's being added is waving - this is really simple. The shader takes as uniform time passed and using that time, we calculate sine depending on a time and rotate the basis vertex before-hand using rotationMatrix function:
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);
}
This function takes axis and angle and returns a matrix, which actually performs rotation around axis. So we rotate our vertices around axis (0, 1, 0), which is vertical, and we offset these vertices a little . And how exactly? If you look closely at the call:
vec3 vBaseDirRotated = (rotationMatrix(vec3(0, 1, 0), sin(fTimePassed*0.7f)*0.1f)*vec4(vBaseDir[i], 1.0)).xyz;
I actually take the sine of the time passed multiplied by some constants (0.7 and 0.1), which basically generates a small angle (in radians, this is important to point out), which we use to rotate the vectors that define grass.
Another factor that influences these top vertices is wind. Wind is also calculated using sine function, but this time, the input to this sine function isn't only time passed, but time passed multiplied altered by grass position, so that the whole grass field doesn't wave uniformly (that would appear too artificial). So current call does this:
float fWindPower = 0.5f+sin(vGrassFieldPos.x/30+vGrassFieldPos.z/30+fTimePassed*(1.2f+fWindStrength/20.0f));
if(fWindPower < 0.0f)
fWindPower = fWindPower*0.2f;
else fWindPower = fWindPower*0.3f;
So basically the first line (fWindPower) gives me constant ranging from -0.5 to 0.5. When the number is below zero, I multiply it by 0.2, otherwise by 0.3, so that the waving doesn't look that artificial (try to remove that if else and you will see). Another thing to notice is fWindStrength constant, which is defined in shader (yes, it could have been uniform, so that you can control wind strength ) and it defines, how quickly the grass waves, or how strong the wind is. It is very simple approximation, that doesn't look that bad even with higher numbers. You can play around with this, or you can even come up with any creative solution using mathematics .
When all is said and done, the final grass quad calculation looks like this:
for(int i = 0; i < 3; i++)
{
// Grass patch top left vertex
vec3 vBaseDirRotated = (rotationMatrix(vec3(0, 1, 0), sin(fTimePassed*0.7f)*0.1f)*vec4(vBaseDir[i], 1.0)).xyz;
vLocalSeed = vGrassFieldPos*float(i);
int iGrassPatch = randomInt(0, 3);
float fGrassPatchHeight = 3.5+randZeroOne()*2.0;
float fTCStartX = float(iGrassPatch)*0.25f;
float fTCEndX = fTCStartX+0.25f;
float fWindPower = 0.5f+sin(vGrassFieldPos.x/30+vGrassFieldPos.z/30+fTimePassed*(1.2f+fWindStrength/20.0f));
if(fWindPower < 0.0f)
fWindPower = fWindPower*0.2f;
else fWindPower = fWindPower*0.3f;
fWindPower *= fWindStrength;
vec3 vTL = vGrassFieldPos - vBaseDirRotated*fGrassPatchSize*0.5f + vWindDirection*fWindPower;
vTL.y += fGrassPatchHeight;
gl_Position = mMVP*vec4(vTL, 1.0);
vTexCoord = vec2(fTCStartX, 1.0);
vWorldPos = vTL;
vEyeSpacePos = mMV*vec4(vTL, 1.0);
EmitVertex();
// Grass patch bottom left vertex
vec3 vBL = vGrassFieldPos - vBaseDir[i]*fGrassPatchSize*0.5f;
gl_Position = mMVP*vec4(vBL, 1.0);
vTexCoord = vec2(fTCStartX, 0.0);
vWorldPos = vBL;
vEyeSpacePos = mMV*vec4(vBL, 1.0);
EmitVertex();
// Grass patch top right vertex
vec3 vTR = vGrassFieldPos + vBaseDirRotated*fGrassPatchSize*0.5f + vWindDirection*fWindPower;
vTR.y += fGrassPatchHeight;
gl_Position = mMVP*vec4(vTR, 1.0);
vTexCoord = vec2(fTCEndX, 1.0);
vWorldPos = vTR;
vEyeSpacePos = mMV*vec4(vTR, 1.0);
EmitVertex();
// Grass patch bottom right vertex
vec3 vBR = vGrassFieldPos + vBaseDir[i]*fGrassPatchSize*0.5f;
gl_Position = mMVP*vec4(vBR, 1.0);
vTexCoord = vec2(fTCEndX, 0.0);
vWorldPos = vBR;
vEyeSpacePos = mMV*vec4(vBR, 1.0);
EmitVertex();
EndPrimitive();
}
You can see, that top grass vertices are influenced by wind, but bottom vertices are stable, they don't change (not even waving), so they are like roots of grass. One thing I need to mention is, that by the start of for cycle, I set the local seed for random number generation to grass patch position multiplied by i. What does this mean? This means, that every grass patch has random values (like random texture from these 4 in the texture), but these values are always the same, because the seed is same! If you won't change the seed, you would get different random values with different runs, which would result in constantly changing grass and that would be horrible . This is a really nice and easy solution and works fine.
Fragment shader of this grass program does standard texturing, but here is where the grass magic and alpha testing happens. Let's have a look at the shader:
#version 330
smooth in vec2 vTexCoord;
smooth in vec3 vWorldPos;
smooth in vec4 vEyeSpacePos;
out vec4 outputColor;
uniform sampler2D gSampler;
uniform vec4 vColor;
uniform vec3 vEyePosition;
uniform float fAlphaTest;
uniform float fAlphaMultiplier;
void main()
{
vec4 vTexColor = texture2D(gSampler, vTexCoord);
float fNewAlpha = vTexColor.a*fAlphaMultiplier;
if(fNewAlpha < fAlphaTest)
discard;
if(fNewAlpha > 1.0f)
fNewAlpha = 1.0f;
vec4 vMixedColor = vTexColor*vColor;
outputColor = vec4(vMixedColor.zyx, fNewAlpha);
}
The incoming alpha value (that comes from the grass texture) is first multiplied by fAlphaMultiplier, which is like alpha booster constant (this is not necessary, but gives a nicer results, when you choose correct alpha booster), and then is tested against fAlphaTest, which is an uniform constant that's set from the application. If the calculated alpha is below the fAlphaTest, the fragment is discarded and nothing is output. This means, that depth buffer values won't be altered, nor will be the color buffer. You can think of discard like a very strong return command .
If you made it through the whole article, the probability, that you can program something like this:
has just raised a lot ! The grass makes our terrain feel a lot better, as it's not so static as it was. The grass I have programmed looks almost identical to that in Crysis 3... just kidding ! But it's not that bad I think . I hope that you have enjoyed my tutorial once more .
Download 5.56 MB (4024 downloads)