Support me!
If you enjoy these webpages and you want to show your gratitude, feel free to support me in anyway!
Like Me On Facebook! Megabyte Softworks Facebook
Like Me On Facebook! Megabyte Softworks Patreon
Donate $1
Donate $2
Donate $5
Donate $10
Donate Custom Amount
21.) Multilayered Terrain
<< back to OpenGL 3 series

Hello guys! This is the 21-th tutorial from my series, and it will teach you how to render terrains using heightmaps loaded from regular grayscale images. Our terrain will have multiple (three) layers of textures on it, which transition smoothly between selves. Not only that, we will also use a special "path" texture for describing path, pavement (or call it how you want ) on that terrain.

This is the longest article I have ever written, as this topic is pretty wide and there are lots of things asking for explanations . But if you go through it, you will receive like 1,000,000 Exp in OpenGL . So let's go.

Heightmap

Heightmap is in most common sense a 2D grid of values, each value meaning height in that point. Our ultimate goal is to read these heights from somewhere, create some rendering data from them, and then render the heightmap in 3D. I briefly explained heightmaps in Tutorial 5 - Indexed Drawing, so you can also have a look there.

The question now is - where and how to store these values? And you probably already know the answer - the easiest way to store heights is to create a heightmap image, where intensity (shade of grey) will represent height, with white intensity (255) being the maximum possible height and black (0) being the minimum possible height. This way, we can easily store 255 different heights, which is (for now) more than enough. The heightmap I'm using in this tutorial looks like this:

(The heightmap was created by me, in my own world editor, where you can raise hills with a circle, so that's why you can see so many circles in that heightmap)

But the point is, that these white circles are highest places in the map, and black spots are the lowest. If we stretch this small image over a 3D plane and then raise the vertices according to their corresponding heights, we will get our result - 3D heightmap.

Loading The Heightmap

As always, I will create a wrapper class, that will contain heightmap loading, rendering, releasing etc. This class is called CMultilayeredHeightmap. Here it is:

class CMultiLayeredHeightmap
{
public:
	static bool LoadTerrainShaderProgram();
	static void ReleaseTerrainShaderProgram();

	bool LoadHeightMapFromImage(string sImagePath);
	void ReleaseHeightmap();

	void RenderHeightmap();

	void SetRenderSize(float fQuadSize, float fHeight);
	void SetRenderSize(float fRenderX, float fHeight, float fRenderZ);

	int GetNumHeightmapRows();
	int GetNumHeightmapCols();

	static CShaderProgram* GetShaderProgram();

	CMultiLayeredHeightmap();

private:
	UINT uiVAO;

	bool bLoaded;
	bool bShaderProgramLoaded;
	int iRows;
	int iCols;

	glm::vec3 vRenderScale;

	CVertexBufferObject vboHeightmapData;
	CVertexBufferObject vboHeightmapIndices;

	static CShaderProgram spTerrain;
	static CShader shTerrainShaders[NUMTERRAINSHADERS];
};

Let's analyze the function LoadHeightMapFromImage(). I will paste it here by parts, and we will analyze each part. So the function begins like this:

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	if(bLoaded)
	{
		bLoaded = false;
		ReleaseHeightmap();
	}
	FREE_IMAGE_FORMAT fif = FIF_UNKNOWN;
	FIBITMAP* dib(0);

	fif = FreeImage_GetFileType(sImagePath.c_str(), 0); // Check the file signature and deduce its format

	if(fif == FIF_UNKNOWN) // If still unknown, try to guess the file format from the file extension
		fif = FreeImage_GetFIFFromFilename(sImagePath.c_str());

	if(fif == FIF_UNKNOWN) // If still unknown, return failure
		return false;

	if(FreeImage_FIFSupportsReading(fif)) // Check if the plugin has reading capabilities and load the file
		dib = FreeImage_Load(fif, sImagePath.c_str());
	if(!dib)
		return false;

	BYTE* bDataPointer = FreeImage_GetBits(dib); // Retrieve the image data
	iRows = FreeImage_GetHeight(dib);
	iCols = FreeImage_GetWidth(dib);

	// We also require our image to be either 24-bit (classic RGB) or 8-bit (luminance)
	if(bDataPointer == NULL || iRows == 0 || iCols == 0 || (FreeImage_GetBPP(dib) != 24 && FreeImage_GetBPP(dib) != 8))
		return false;

	// How much to increase data pointer to get to next pixel data
	unsigned int ptr_inc = FreeImage_GetBPP(dib) == 24 ? 3 : 1;
	// Length of one row in data
	unsigned int row_step = ptr_inc*iCols;


	// ...
}

The first part is almost identical to part where we're loading textures. We just use FreeImage library to load image data, deduce its properties - whether it's 24-bit RGB image or 8-bit intensity image (exactly what we need). So there are few variables, that are interesting to us - bDataPointer is the pointer in memory to image data, in a row-major order (the image is stored as rows from top to bottom). iRows, iCols is the number of rows and columns of our heightmap, i.e. the size of image, i.e. width and height of the image. So it means, that our heightmap will have exactly iRows*iCols vertices in it.

Also notice last two rows with two variables:

  • ptr_inc - this means by how much we need to increase data pointer to move by one height value in data - point is, that when we have for example 24-bit image, for now we would only care for R value as the value with intensity, and we need to move 3 bytes from current pointer position to point at next value, but if we have 8-bit image (our case), we need to move by 1 byte only
  • row_step - the width of one row in memory, it's just number of columns multiplied by ptr_inc

Knowing the meaning of all important variables and data of image loaded in memory, we can move on to building OpenGL objects of it.

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	// ...

	vboHeightmapData.CreateVBO();
	// All vertex data are here (there are iRows*iCols vertices in this heightmap), we will get to normals later
	vector< vector< glm::vec3> > vVertexData(iRows, vector(iCols));
	vector< vector< glm::vec2> > vCoordsData(iRows, vector(iCols));

	float fTextureU = float(iCols)*0.1f;
	float fTextureV = float(iRows)*0.1f;

	FOR(i, iRows)
	{
		FOR(j, iCols)
		{
			float fScaleC = float(j)/float(iCols-1);
			float fScaleR = float(i)/float(iRows-1);
			float fVertexHeight = float(*(bDataPointer+row_step*i+j*ptr_inc))/255.0f;
			vVertexData[i][j] = glm::vec3(-0.5f+fScaleC, fVertexHeight, -0.5f+fScaleR);
			vCoordsData[i][j] = glm::vec2(fTextureU*fScaleC, fTextureV*fScaleR);
		}
	}

	// ...
}

This part ain't much difficult - what we do here is create a VBO in first line, adn then we want to precalculate vertex positions and its texture coordinates. What exactly we do is that we stretch heightmap points into range <-0.5, 0.5> on X and Z axis uniformly and <0.0, 1.0> on Y axis. The picture will surely help you to understand what I'm doing:

We wanna do it this way, because then we can easily scale whole heightmap to get desired render sizes. Evey single point in this grid is one height value, ranging from 0.0 to 1.0, which can be easily scaled to desired render height. And because it's in <-0.5, 0.5>, we will get the thing centered, for easier positioning of heightmap.

Another thing we are calculating here are texture coordinates. What we need to think through is how many times should we map a texture across heightmap. Well it depends on size of texture and things like that, but for this tutorial, I decided, that every 10 rows and every 10 columns we want to have one instance of texture in that dimension. That's why you can see two variables: fTextureU and fTextureV, which both represent how many times should we map the texture across respective dimension.

This was pretty easy part, only some linear calculations. The main concern in generating heightmap is calculating its normals.

Normals - Y U NO CALCULATE STRAIGHTFORWARD

Really, why?

OK, what's the problem with normals? First, let's think through what we have done so far. We have calculated only vertices with their texture coordinates. Let's imagine, we want to render heightmap now, without normals. How shall we do it? The answer is Indexed Rendering, as we did in Tutorial 5 - Indexed Drawing (why didn't I name it rendering instead? ). I will try to remind you this stuff here, but just briefly. Every row of quads in heightmap will be rendered using triangle strips. At the end of each row, we use primitive restart index to start a new bunch of triangle strips. This picture will help you to imagine it and understand - pay attention to the order of vertices, because this is the way I'm using:

As you can see, the rendering and wireframe model of heightmap is made up of triangles. And what we can do is to calculate the normal of each triangle. Yea, that's right. But does that help us? Yea, it does, but it isn't enough - every vertex of heightmap is surrounded by multiple triangles, so the normal of vertex should be calculated from normals of all surrounding triangles! In this tutorial, I just sum all normals of surrounding triangles, and then normalize final vector. Result looks more than fine.

In order to do so, I precalculate triangle normals first to have them all in one place. After this, I can proceed with calculating vertex normals. So we need to calculate normals for each quad in heightmap. There are (iRows-1)*(iCols-1) quads in heightmap. Every quad has two triangles. Therefore we need to calculate two normals per quad. Let's number them normal 0 and normal 1, as you can see here:

So what we gotta do is to precalculate these normals with consistent ordering of triangles. This code does exactly that:

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	// ...

	// Normals are here - the heightmap contains ( (iRows-1)*(iCols-1) quads, each one containing 2 triangles, therefore array of we have 3D array)
	vector< vector > vNormals[2];
	FOR(i, 2)vNormals[i] = vector< vector >(iRows-1, vector(iCols-1));

	FOR(i, iRows-1)
	{
		FOR(j, iCols-1)
		{
			glm::vec3 vTriangle0[] = 
			{
				vVertexData[i][j],
				vVertexData[i+1][j],
				vVertexData[i+1][j+1]
			};
			glm::vec3 vTriangle1[] = 
			{
				vVertexData[i+1][j+1],
				vVertexData[i][j+1],
				vVertexData[i][j]
			};

			glm::vec3 vTriangleNorm0 = glm::cross(vTriangle0[0]-vTriangle0[1], vTriangle0[1]-vTriangle0[2]);
			glm::vec3 vTriangleNorm1 = glm::cross(vTriangle1[0]-vTriangle1[1], vTriangle1[1]-vTriangle1[2]);

			vNormals[0][i][j] = glm::normalize(vTriangleNorm0);
			vNormals[1][i][j] = glm::normalize(vTriangleNorm1);
		}
	}

	// ...
}

When we're done with it, we can calculate final normals from this data. Let's take one vertex into consideration. Which triangles are adjacent to it? Image will help you to see it:

  • On upper-left side of our vertex, there are two adjacent triangles (if we aren't in top row of heightmap or leftmost column)
  • On the upper-right of our vertex is one adjacent triangle (if we aren't in top row of heightmap or in rightmost column)
  • On bottom-right side of our vertex, there are two adjacent triangles (if we aren't in bottom row of heightmap or rightmost column)
  • On the bottom-left of our vertex is one adjacent triangle (if we aren't in bottom row of heightmap or in leftmost column)

Now, we need to sum normals of these triangles, and then normalize final vector. Code below is doing this final normal calculation:

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	// ...

	vector< vector > vFinalNormals = vector< vector >(iRows, vector(iCols));

	FOR(i, iRows)
		FOR(j, iCols)
	{
		// Now we wanna calculate final normal for [i][j] vertex. We will have a look at all triangles this vertex is part of, and then we will make average vector
		// of all adjacent triangles' normals

		glm::vec3 vFinalNormal = glm::vec3(0.0f, 0.0f, 0.0f);

		// Look for upper-left triangles
		if(j != 0 && i != 0)
			FOR(k, 2)vFinalNormal += vNormals[k][i-1][j-1];
		// Look for upper-right triangles
		if(i != 0 && j != iCols-1)vFinalNormal += vNormals[0][i-1][j];
		// Look for bottom-right triangles
		if(i != iRows-1 && j != iCols-1)
			FOR(k, 2)vFinalNormal += vNormals[k][i][j];
		// Look for bottom-left triangles
		if(i != iRows-1 && j != 0)
			vFinalNormal += vNormals[1][i][j-1];
		vFinalNormal = glm::normalize(vFinalNormal);

		vFinalNormals[i][j] = vFinalNormal; // Store final normal of j-th vertex in i-th row
	}

	// ...
}
Putting data together for rendering

With all vertex data calculated, we can finally start adding them, vertex-by-vertex, to our first VBO:

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	// ...

	// First, create a VBO with only vertex data
	vboHeightmapData.CreateVBO(iRows*iCols*(2*sizeof(glm::vec3)+sizeof(glm::vec2))); // Preallocate memory
	FOR(i, iRows)
	{
		FOR(j, iCols)
		{
			vboHeightmapData.AddData(&vVertexData[i][j], sizeof(glm::vec3)); // Add vertex
			vboHeightmapData.AddData(&vCoordsData[i][j], sizeof(glm::vec2)); // Add tex. coord
			vboHeightmapData.AddData(&vFinalNormals[i][j], sizeof(glm::vec3)); // Add normal
		}
	}

	// ...
}

Then we create our second VBO, that stores indices. Note, that primitive restart index is index witha value of iRows*iCols, because it's the first free number, as vertices are indexed from 0 to iRows*iCols -1 , inclusive. If you don't know how to that indexed rendering, I will redirect you again to Tutorial 5 - Indexed Drawing for explanation how this works:

bool CMultiLayeredHeightmap::LoadHeightMapFromImage(string sImagePath)
{
	// ...


	vboHeightmapIndices.CreateVBO();
	int iPrimitiveRestartIndex = iRows*iCols;
	FOR(i, iRows-1)
	{
		FOR(j, iCols)
			FOR(k, 2)
		{
			int iRow = i+(1-k);
			int iIndex = iRow*iCols+j;
			vboHeightmapIndices.AddData(&iIndex, sizeof(int));
		}
		// Restart triangle strips
		vboHeightmapIndices.AddData(&iPrimitiveRestartIndex, sizeof(int));
	}

	glGenVertexArrays(1, &uiVAO);
	glBindVertexArray(uiVAO);
	// Attach vertex data to this VAO
	vboHeightmapData.BindVBO();
	vboHeightmapData.UploadDataToGPU(GL_STATIC_DRAW);

	// Vertex positions
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 2*sizeof(glm::vec3)+sizeof(glm::vec2), 0);
	// Texture coordinates
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2*sizeof(glm::vec3)+sizeof(glm::vec2), (void*)sizeof(glm::vec3));
	// Normal vectors
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 2*sizeof(glm::vec3)+sizeof(glm::vec2), (void*)(sizeof(glm::vec3)+sizeof(glm::vec2)));

	// And now attach index data to this VAO
	// Here don't forget to bind another type of VBO - the element array buffer, or simplier indices to vertices
	vboHeightmapIndices.BindVBO(GL_ELEMENT_ARRAY_BUFFER);
	vboHeightmapIndices.UploadDataToGPU(GL_STATIC_DRAW);

	bLoaded = true; // If get here, we succeeded with generating heightmap
	return true;
}

And that's all from loading! Now everything is prepared, rendering is just a matter of few calls:

void CMultiLayeredHeightmap::RenderHeightmap()
{
	spTerrain.UseProgram();

	spTerrain.SetUniform("fRenderHeight", vRenderScale.y);
	spTerrain.SetUniform("fMaxTextureU", float(iCols)*0.1f);
	spTerrain.SetUniform("fMaxTextureV", float(iRows)*0.1f);

	spTerrain.SetUniform("HeightmapScaleMatrix", glm::scale(glm::mat4(1.0), glm::vec3(vRenderScale)));

	// Now we're ready to render - we are drawing set of triangle strips using one call, but we g otta enable primitive restart
	glBindVertexArray(uiVAO);
	glEnable(GL_PRIMITIVE_RESTART);
	glPrimitiveRestartIndex(iRows*iCols);

	int iNumIndices = (iRows-1)*iCols*2 + iRows-1;
	glDrawElements(GL_TRIANGLE_STRIP, iNumIndices, GL_UNSIGNED_INT, 0);
}
The Terrain Shader Program

Yea, calling render function was pretty short. But if we really want our terrain to have multiple layers of texture, we need to write a shader program for it. And that's the second part of this tutorial, because it's now a whole another thing. But it's really not difficult.

What we gotta do here is to render heightmap with texture according to its height.

Vertex Shader

Because our heightmap data are constructed in unit size, so that heightmap can be easily resizeable, first thing we will need to send to shader somehow is rendering size of heightmap. To do that, we simply send a scale matrix to our vertex shader. This is what our vertex shader looks like:

#version 330

uniform struct Matrices
{
	mat4 projMatrix;
	mat4 modelMatrix;
	mat4 viewMatrix;                                                                           
	mat4 normalMatrix;
} matrices;

layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec2 inCoord;
layout (location = 2) in vec3 inNormal;

smooth out vec2 vTexCoord;
smooth out vec3 vNormal;
smooth out vec3 vWorldPos;
smooth out vec4 vEyeSpacePos;

uniform mat4 HeightmapScaleMatrix;

void main()
{
  vec4 inPositionScaled = HeightmapScaleMatrix*vec4(inPosition, 1.0);
  mat4 mMVP = matrices.projMatrix*matrices.viewMatrix*matrices.modelMatrix;
   
  gl_Position = mMVP*inPositionScaled;
  vEyeSpacePos = matrices.viewMatrix*matrices.modelMatrix*vec4(inPosition, 1.0);
  
  vTexCoord = inCoord;
	vNormal = inNormal;
   
  vec4 vWorldPosLocal = matrices.modelMatrix*inPositionScaled;
	vWorldPos = vWorldPosLocal.xyz;
  
}

The uniform mat4 HeightmapScaleMatrix; is our scale matrix, which is set before rendering heightmap. Before we proceed with classic transformation, we will multiply incoming vertex position with our scale matrix and store it in inPositionScaled. Then, we work with everything as usual.

Fragment Shader

Fragment shader is the last important thing, that requires attention. The purpose of it is to have multiple textures in multiple samplers, and then map them to our terrain according to its height. For this tutorial, I decided to go for 3 different textures when mapping terrain. These are ranges:

  • if height is in <0.00...0.15>, then we have a FUNGUS texture
  • if height is in (0.15...0.30>, then we have a transition between FUNGUS and SANDGRASS texture
  • if height is in (0.30...0.65>, then we have a SANDGRASS texture
  • if height is in (0.60...0.85>, then we have a transition between SANDGRASS and ROCKS texture
  • if height is in (0.85...1.00>, then we have a ROCKS texture

To visualize this, here is a helpful picture:

For the purpose of this tutorial, the range values are hardcoded in fragment shader. Of course, you may extend this all, and create some uniform array of ranges, to be able to change things at runtime. So finally, here is the first part of fragment shader, that calculates proper color from these three textures. These textures can be accessed with gSampler[0], gSampler[1] and gSampler[2]:

#version 330

smooth in vec2 vTexCoord;
smooth in vec3 vNormal;
smooth in vec3 vWorldPos;
smooth in vec4 vEyeSpacePos;

uniform sampler2D gSampler[5];
uniform sampler2D shadowMap;

uniform vec4 vColor;

#include "dirLight.frag"
uniform DirectionalLight sunLight;
uniform float fRenderHeight;
uniform float fMaxTextureU;
uniform float fMaxTextureV;

out vec4 outputColor;

void main()
{
	vec3 vNormalized = normalize(vNormal);
	
	vec4 vTexColor = vec4(0.0);
	
	float fScale = vWorldPos.y/fRenderHeight;

	const float fRange1 = 0.15f;
	const float fRange2 = 0.3f;
	const float fRange3 = 0.65f;
   const float fRange4 = 0.85f;

	if(fScale >= 0.0 && fScale <= fRange1)vTexColor = texture2D(gSampler[0], vTexCoord);
	else if(fScale <= fRange2)
	{
		fScale -= fRange1;
		fScale /= (fRange2-fRange1);
		
		float fScale2 = fScale;
		fScale = 1.0-fScale; 
		
		vTexColor += texture2D(gSampler[0], vTexCoord)*fScale;
		vTexColor += texture2D(gSampler[1], vTexCoord)*fScale2;
	}
	else if(fScale <= fRange3)vTexColor = texture2D(gSampler[1], vTexCoord);
	else if(fScale <= fRange4)
	{
		fScale -= fRange3;
		fScale /= (fRange4-fRange3);
		
		float fScale2 = fScale;
		fScale = 1.0-fScale; 
		
		vTexColor += texture2D(gSampler[1], vTexCoord)*fScale;
		vTexColor += texture2D(gSampler[2], vTexCoord)*fScale2;		
	}
	else vTexColor = texture2D(gSampler[2], vTexCoord);

   // ...
}                      

Notice, that everytime we are in transitioning part, we calculate scale (fScale variable). Then we add fScale much of one texture, and 1-fScale much of second texture. Have a look at these lines, and think about them until you understand them for 100% - they are the core part of this fragment shader.

Adding Path / Pavement

Everything is looking smooth and nice now. But what if I wanted to create a pavement going through arbitrary places? And yes, I do want to create one, so we will add additional functionality to the fragment shader, that will handle paths.

Our path needs two things - path texture, which will be mapped to places with path, and path map, which will tell fragment shader, where to put path. Our path map in this tutorial looks like this:

Our path texture, which is pure sand, is stored in gSampler[3], and path map is stored in gSampler[4]. What we need to do now is to map the path map texture over the whole heightmap. But the texture coordinates, that our fragment shader got from vertex shader are scaled according to number of columns and rows in the heightmap. So we need to rescale them first, and that's why I set uniform variables fMaxTextureU and fMaxTextureV, that tell us, what is the maximum texture coordinate value in each dimension. With this, we can easily find a path texture coordinate:

#version 330

smooth in vec2 vTexCoord;
smooth in vec3 vNormal;
smooth in vec3 vWorldPos;
smooth in vec4 vEyeSpacePos;

uniform sampler2D gSampler[5];
uniform sampler2D shadowMap;

uniform vec4 vColor;

#include "dirLight.frag"
uniform DirectionalLight sunLight;
uniform float fRenderHeight;
uniform float fMaxTextureU;
uniform float fMaxTextureV;

out vec4 outputColor;

void main()
{
	// ...
   
	vec2 vPathCoord = vec2(vTexCoord.x/fMaxTextureU, vTexCoord.y/fMaxTextureV);
	vec4 vPathIntensity = texture2D(gSampler[4], vPathCoord); // Black color means there is a path
	fScale = vPathIntensity.x;
  
	vec4 vPathColor = texture2D(gSampler[3], vTexCoord); 
	vec4 vFinalTexColor = fScale*vTexColor+(1-fScale)*vPathColor;

	vec4 vMixedColor = vFinalTexColor*vColor;
	vec4 vDirLightColor = GetDirectionalLightColor(sunLight, vNormal);


	outputColor = vMixedColor*(vDirLightColor);
  
}                      

Let's go through these lines:

  • vPathCoord is the texture coordinate of path in range <0...1>
  • vPathIntensity is the texture color we get from our path map
  • fScale is the scale used for mixing the terrain color and path color
  • vPathColor is the texture color from path texture, which is pure sand now
  • vFinalTexColor is the combined terrain and path texture according to scale

The rest of the fragment shader is just applying directional light and outputting color. Hope this simple explanation helped you to understand it all. But I really encourage you to think through the whole fragment shader, analyze it, and really understand it, because this will raise your skills to another level and you will be able to apply this knowledge in many places .

Now before rendering our heightmap (calling RenderHeightmap() function, we only need to bind textures to different texture units and samplers. Here is the chunk of code that does this in RenderScene():

void RenderScene(LPVOID lpParam)
{
	// ...

	// Now we're going to render terrain

	hmWorld.SetRenderSize(300.0f, 35.0f, 300.0f);
	CShaderProgram* spTerrain = CMultiLayeredHeightmap::GetShaderProgram();

	spTerrain->UseProgram();

	spTerrain->SetUniform("matrices.projMatrix", oglControl->GetProjectionMatrix());
	spTerrain->SetUniform("matrices.viewMatrix", cCamera.Look());

	// We bind all 5 textures - 3 of them are textures for layers, 1 texture is a "path" texture, and last one is
	// the places in heightmap where path should be and how intense should it be
	FOR(i, 5)
	{
		char sSamplerName[256];
		sprintf(sSamplerName, "gSampler[%d]", i);
		tTextures[i].BindTexture(i);
		spTerrain->SetUniform(sSamplerName, i);
	}

	// ... set some uniforms
	spTerrain->SetModelAndNormalMatrix("matrices.modelMatrix", "matrices.normalMatrix", glm::mat4(1.0));
	spTerrain->SetUniform("vColor", glm::vec4(1, 1, 1, 1));

	dlSun.SetUniformData(spTerrain, "sunLight");

	// ... and finally render heightmap
	hmWorld.RenderHeightmap();

	// ...
}
Result

This is how our terrain looks like:

Pretty neat isn't it? Of course, don't forget to load heightmap first in InitScene function, and also release it in ReleaseScene function. I'm pretty sure that graphics card drivers can deal with not releasing things, but why make their lives harder?

The heightmap name is consider_this_question.bmp. I forgot to change it before I uploaded tutorial, so I let it there for now . There's story behind this name. When I was coding these things, the Dream Theater (my favorite band) album came out, and Consider this question is the first lyrical part of Illumination Theory song . So that's why, in case you are wondering >.

I'm not sure what next tutorial is gonna be about, but probably one of these things:

  • Specular lights - so that I revisit / finalize lighting knowledge
  • Particle systems with transform feedback - because rendering fire is nice thing to know
  • Shadows - because shadows are gray

But I guess that the ordering I used here will be real ordering for next 3 tutorials. Farewell.

Download 27.17 MB (6983 downloads)