I'm trying to implement terrain collision for my height map terrain, and I'm following this. The tutorial is for java but I'm using C++, though the principles are the same so it shouldn't be a problem.
To start off we need a function to get the height of the terrain based on the camera's position. WorldX and WorldZ is the camera's position (x, z) and heights is an 2D-array containing all the heights of the vertices.
float HeightMap::getHeightOfTerrain(float worldX, float worldZ, float heights[][256])
{
//Image is (256 x 256)
float gridLength = 256;
float terrainLength = 256;
float terrainX = worldX;
float terrainZ = worldZ;
float gridSquareLength = terrainLength / ((float)gridLength - 1);
int gridX = (int)std::floor(terrainX / gridSquareLength);
int gridZ = (int)std::floor(terrainZ / gridSquareLength);
//Check if position is on the terrain
if (gridX >= gridLength - 1 || gridZ >= gridLength - 1 || gridX < 0 || gridZ < 0)
{
return 0;
}
//Find out where the player is on the grid square
float xCoord = std::fmod(terrainX, gridSquareLength) / gridSquareLength;
float zCoord = std::fmod(terrainZ, gridSquareLength) / gridSquareLength;
float answer = 0.0;
//Top triangle of a square else the bottom
if (xCoord <= (1 - zCoord))
{
answer = barryCentric(glm::vec3(0, heights[gridX][gridZ], 0),
glm::vec3(1, heights[gridX + 1][gridZ], 0), glm::vec3(0, heights[gridX][gridZ + 1], 1),
glm::vec2(xCoord, zCoord));
}
else
{
answer = barryCentric(glm::vec3(1, heights[gridX + 1][gridZ], 0),
glm::vec3(1, heights[gridX + 1][gridZ + 1], 1), glm::vec3(0, heights[gridX][gridZ + 1], 1),
glm::vec2(xCoord, zCoord));
}
return answer;
}
To find the height of the triangle the camera is currently standing on we use the baryCentric interpolation function.
float HeightMap::barryCentric(glm::vec3 p1, glm::vec3 p2, glm::vec3 p3, glm::vec2 pos)
{
float det = (p2.z - p3.z) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.z - p3.z);
float l1 = ((p2.z - p3.z) * (pos.x - p3.x) + (p3.x - p2.x) * (pos.y - p3.z)) / det;
float l2 = ((p3.z - p1.z) * (pos.x - p3.x) + (p1.x - p3.x) * (pos.y - p3.z)) / det;
float l3 = 1.0f - l1 - l2;
return l1 * p1.y + l2 * p2.y + l3 * p3.y;
}
Then we just have to use the height we have calculated to check for collision during the game
float terrainHeight = heightMap.getHeightOfTerrain(camera.Position.x, camera.Position.z, heights);
if (camera.Position.y < terrainHeight)
{
camera.Position.y = terrainHeight;
};
Now according to the tutorial this should work perfectly fine, but the height is rather off and at some places it doesn't even work. I figured it might have something to do with the translation and scaling part of the terrain
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -0.3f, -15.0f));
model = glm::scale(model, glm::vec3(0.1f, 0.1f, 0.1f));
and that I should multiply the values of the heights array by 0.1, as the scaling does that part for the terrain on the GPU side, but that didn't do the trick.
Note
In the tutorial the first lines in the getHeightOfTerrain function says
float terrainX = worldX - x;
float terrainZ = worldZ - z;
where x and z is the world position of the terrain. This is done to get the player position relative to the terrain's position. I tried with the values from the translation part, but it doensn't work either. I changed these lines because it doesn't seem necessary.
Additional
This is the vector where I store all the vertices in the grid with their respective x,y and z-values.
std::vector<glm::vec3> heightVertices(NumVertices);
To get the 2D-array for the getHeightOfTerrain function, I simply do
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++)
{
heights[i][j] = heightVertices[i].y;
}
}
And to get all the vertices for the terrain I use a BMP-file and carry on like below. If there would a problem in the code I assume it has to do with the vertices of the terrain, so I'll just list that part
FILE *filePtr;
BITMAPFILEHEADER bitmapFileHeader; // For file
BITMAPINFOHEADER bitmapInfoHeader; // For image
int imageSize, index;
unsigned char height; // Color values
filePtr = fopen(filename, "rb");
if (filePtr == NULL)
{
printf("Cannot open file! \n");
getchar();
return false;
}
fread(&bitmapFileHeader, sizeof(BITMAPFILEHEADER), 1, filePtr);
fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, filePtr);
terrainWidth = bitmapInfoHeader.biWidth;
terrainHeight = bitmapInfoHeader.biHeight;
imageSize = terrainWidth * terrainHeight * 3;
// Initialize the array which stores the image data
unsigned char* bitmapImage = new unsigned char[imageSize];
fseek(filePtr, bitmapFileHeader.bfOffBits, SEEK_SET);
// Store image data in bitmapImage
fread(bitmapImage, 1, imageSize, filePtr);
fclose(filePtr);
// Size of the image length and height (vertices of the terrain).
heightMap = new glm::vec3[terrainWidth * terrainHeight];
int k = 0;
float heightFactor = 10.0f;
// Read the image data into our heightMap array
for (int j = 0; j < terrainHeight; j++)
{
for (int i = 0; i < terrainWidth; i++)
{
height = bitmapImage[k];
index = (terrainHeight * j) + i;
heightMap[index].x = (float)i;
heightMap[index].y = (float)height / heightFactor;
heightMap[index].z = (float)j;
k += 3;
}
}
delete[] bitmapImage;
bitmapImage = 0;
// Create vertices of the grid
int cols = terrainWidth;
int rows = terrainHeight;
// Create the grid
NumVertices = rows * cols;
NumFaces = (rows - 1)*(cols - 1) * 2;
std::vector<glm::vec3> heightVertices(NumVertices);
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
heightVertices[i*cols + j] = heightMap[i*cols + j];
}
}