RTS Game - The terrain approach

Started by
10 comments, last by xynapse 12 years, 6 months ago
Thanks JTippetts - i am right now looking into this,

meanwhile - a working class update again, this time i have added few methods for 'getting' the height and normal values out of the raw heightmap file.




Here are the results of rendering one cell.

Terrain size: 1024x1024

VBO vertices: 1048576


Terrain spitted into: 64x64 Cells

Cells created: 16x16 which is 256 cells in total


Each cell vertices: 64x64 = 4096

Indices per cell: 23814


Triangles per cell: 23814 / 3 = 7938








approach4.jpg




[media]
[/media]




Moving on to implement JTippetts method, will keep this updated - maybe somebody will find it useful.







Promised class updates.



namespace XYNAPSE
{

// --[ Struct ]---------------------------------------------------------------
//
// - Class :
// - Prototype : struct tTerrainCell
//
// - Purpose : This holds index buffer data per cell
//
// -----------------------------------------------------------------------------
struct tTerrainCell
{
GLuint m_uiIBO;
int m_iIndexesCount;
};

// --[ Class ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Purpose : This class is responsible for generating the VBO / Cells IBO's
// : From the heightmap file.
// : Also this class handles all updates and rendering of the terrain
// -----------------------------------------------------------------------------
class CTerrain
{

public:
CTerrain();

bool Create(const std::string& strFileName, int iSize, int iCellSize); // Creates the terrain from heightmap file
void Destroy(); // Proper way of destructing the Terrain object
void Render(const CMatrix4x4& m_mat4ViewMatrix,const CMatrix4x4& m_mat4ProjectionMatrix); // Renders the terrain cells we're in
CVector3 Update(const CVector3& vOriginPosition); // Call from the engine before rendering the frame

float HeightAt(float x, float z);
CVector3 NormalAt(float x, float z);
private:
int m_iHeightMapSize; // This is the map size m_iHeightMapSize x m_iHeightMapSize
int m_iCellSize; // Cell size
int m_iCurrentCell; // Current cell we're in - temporary for tests
GLuint m_uiVAO; // Not used
GLuint m_uiVBOVertices; // VBO for the whole heightmap
CMatrix4x4 m_mat4Model;
std::vector<tTerrainCell> m_vCells; // This holds our cells with index buffers
XYNAPSE::CShader *m_pShader; // Shader for rendering purposes
BYTE pHeightMap[1024*1024]; // This is where our heightmap is stored


unsigned int HeightIndexAt(int x, int z);
CVector3 NormalAtPixel(int x, int z);
float HeightAtPixel(int x, int z);
};

}








namespace XYNAPSE
{
CTerrain::CTerrain()
{

}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : void CTerrain::Destroy()
//
// - Purpose : Call this before deleting the terrain object
//
// -----------------------------------------------------------------------------
void CTerrain::Destroy()
{
// Delete the shader object
if(m_pShader!=NULL)
{
m_pShader->Delete();
SAFE_DELETE(m_pShader);
}

// Delete the VBO buffer
if(m_uiVBOVertices!=NULL)
{
glDeleteBuffers(1,&m_uiVBOVertices);
}

// Delete the Index Buffers from the Cells
for(int a=0;a<m_vCells.size();a++)
{
if(m_vCells[a].m_uiIBO)
{
glDeleteBuffers(1, &m_vCells[a].m_uiIBO);
}

}

}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : bool CTerrain::Create(const std::string& strFileName, int iWidth, int iHeight)
//
// - Purpose : This reads in the height data from a raw file and splits the map into defined cells
// The cell size is called: iTerrainSplitCellSize
//
// We generate ONE single VBO that holds vertexes for the whole map
// And separate per-cell IBOs
//
// So after loading we get:
// Index buffers = ( m_iHeight / iTerrainSplitCellSize ) * ( m_iWidth / iTerrainSplitCellSize )
// Vertex buffer = 1
//
// -----------------------------------------------------------------------------
bool CTerrain::Create(const std::string& strFileName, int iSize, int iCellSize)
{

// Load the heights to a byte array
// If fp failed, return false.
m_iHeightMapSize = iSize;
m_iCellSize = iCellSize;
int iCellsCount = m_iHeightMapSize/m_iCellSize;

XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Loading \"%s\" terrain file, size=%ix%i.",strFileName.data(),m_iHeightMapSize,m_iHeightMapSize);

FILE *fp=NULL;
fp = fopen(strFileName.data(), "rb");
if(!fp)
{
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"ERROR, Couldn't load \"%s\" terrain file, file not found.",strFileName.data());
return false;
}
// Sanity check
size_t result;
result = fread(pHeightMap, 1, m_iHeightMapSize * m_iHeightMapSize, fp);
fclose(fp);

if(result != (m_iHeightMapSize * m_iHeightMapSize) )
{
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"ERROR, failed to load %i x %i bytes.",m_iHeightMapSize*m_iHeightMapSize);
return false;
}

// Generate Vertex buffer
std::vector<XYN_vbo_vert> m_vVertices;
for(int x = 0; x < m_iHeightMapSize; x++)
{
for(int z = 0; z < m_iHeightMapSize; z++)
{

XYN_vbo_vert newVertexData;
newVertexData.vPosition=CVector3(x, HeightAt(x,z) ,z);
newVertexData.vNormals= NormalAt(x,z);
m_vVertices.push_back(newVertexData);
}
}
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Terrain debug: VBO %i entries.",m_vVertices.size());
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Terrain debug: Cells after splitting %i x %i",iCellsCount,iCellsCount);

// Generate separate IBO for each cell
for(int iCellX=0;iCellX<iCellsCount;iCellX++)
{
for(int iCellZ=0;iCellZ<iCellsCount;iCellZ++)
{

// We need <unsigned int> to store the indices correctly
// As the last vertex number would for example: 1024x1024 = 1048576
// Unsigned short won't handle that.
std::vector<unsigned int> vIndices;

int startX = iCellX * m_iCellSize;
int startZ = iCellZ * m_iCellSize;

int endX = startX + m_iCellSize;
int endZ = startZ + m_iCellSize;
vIndices.clear();
for (int x = startX; x < endX -1 ; ++x)
{
for (int z = startZ; z < endZ -1 ; ++z)
{

// CCW
//
// *2
// | \
// *1 _*3
////////////
vIndices.push_back( x * m_iHeightMapSize + z);
vIndices.push_back( ( x + 1 ) * m_iHeightMapSize + z);
vIndices.push_back( x * m_iHeightMapSize + z + 1);
// CCW
//
// *1-*2
// \ |
// *3
/////////////
vIndices.push_back((x+1)*m_iHeightMapSize + z);
vIndices.push_back((x+1)*m_iHeightMapSize + z + 1);
vIndices.push_back(x*m_iHeightMapSize + z + 1);

}

}


// Create new Cell objet
tTerrainCell newCell;

// Store the number of indexes for this cell
newCell.m_iIndexesCount=vIndices.size();

XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Terrain debug: Cell[%i][%i] Indices: %i\n",iCellX,iCellZ,vIndices.size());

// Generate the IBO
bool bGLError=false;
glGenBuffers( 1, &newCell.m_uiIBO);
if(glGetError()) bGLError=true;

glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, newCell.m_uiIBO );
if(glGetError()) bGLError=true;

glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * newCell.m_iIndexesCount, &vIndices[0], GL_STATIC_DRAW );
if(glGetError()) bGLError=true;

glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
if(glGetError()) bGLError=true;

// Error handling
if(bGLError==true)
{

XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"ERROR, glGenBuffers for Cell[%i][%i]\n",iCellX,iCellZ);
this->Destroy();
return false;
}


// Push back newly created cell to our list
m_vCells.push_back(newCell);


}
}




XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Terrain debug: Cells created: %i\n",m_vCells.size());

// Generate the VBO for the whole mesh
glGenBuffers(1, &m_uiVBOVertices);
glBindBuffer(GL_ARRAY_BUFFER, m_uiVBOVertices);
glBufferData(GL_ARRAY_BUFFER, m_vVertices.size() * sizeof(XYN_vbo_vert), &m_vVertices[0], GL_STATIC_DRAW_ARB);
glBindBuffer( GL_ARRAY_BUFFER, 0 );

// Set the Model matrix to 0,0,0
m_mat4Model.Clear();

// Create terrain shader for rendering tests purposes
m_pShader = new XYNAPSE::CShader;

if(m_pShader->loadShader("data/shaders/terrain.vert","data/shaders/terrain.frag")==false)
return false;

// All clear!
return true;
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : void CTerrain::Update(const CVector3& vOriginPosition);
//
// - Purpose : This generates a list of terrain cells to render according to
// The position in vOriginPosition vector.
//
// -----------------------------------------------------------------------------
CVector3 CTerrain::Update(const CVector3& vOriginPosition)
{
// Find out which cell array belongs to our current camera position
int iCellX = (vOriginPosition.x/m_iCellSize);
int iCellZ = (vOriginPosition.z/m_iCellSize);

// Do some minimals fix
if(iCellX<0) iCellX = 0;
if(iCellZ<0) iCellZ = 0;

// Do some maximals fix
if(iCellX>(m_iHeightMapSize/m_iCellSize)-1) iCellX = (m_iHeightMapSize/m_iCellSize)-1;
if(iCellZ>(m_iHeightMapSize/m_iCellSize)-1) iCellZ = (m_iHeightMapSize/m_iCellSize)-1;

// This is the index to our vector holding cells
// We render m_iCurrentCell index buffer to render
// the cell that belongs to position vOriginPosition,
m_iCurrentCell = iCellX * (m_iHeightMapSize/m_iCellSize) + iCellZ;

// For debug usage, so we know in which cell vOriginPosition is placed
return CVector3(iCellX,iCellZ,m_iCurrentCell);
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : void CTerrain::Render(const CMatrix4x4& m_mat4ViewMatrix,const CMatrix4x4& m_mat4ProjectionMatrix)
//
// - Purpose : Renders visible cells of the terrain that were previously
// calculated by Update method.
//
// -----------------------------------------------------------------------------
void CTerrain::Render(const CMatrix4x4& m_mat4ViewMatrix,const CMatrix4x4& m_mat4ProjectionMatrix)
{
// Bind the VBO
glBindBuffer(GL_ARRAY_BUFFER, m_uiVBOVertices);

//Setup the in_VertexPosition
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),(char*)0);

//Setup the in_VertexNormal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*5));

//Setup the in_VertexTexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*3));

//Setup the in_VertexTangent
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*8));

// Bind the selected Cell IBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_vCells[m_iCurrentCell].m_uiIBO);

// Turn the shader on
m_pShader->TurnOn();

// Inject matrices to the shader
m_pShader->SetMatrix4x4(m_pShader->getUniform("mat4_Model"), m_mat4Model);
m_pShader->SetMatrix4x4(m_pShader->getUniform("mat4_View"), m_mat4ViewMatrix);
m_pShader->SetMatrix4x4(m_pShader->getUniform("mat4_Projection"), m_mat4ProjectionMatrix);

// Draw indexed triangles
glDrawElements(GL_TRIANGLES,m_vCells[m_iCurrentCell].m_iIndexesCount,GL_UNSIGNED_INT,0);

// Turn the shader off
m_pShader->TurnOff();

// Boil out with the bindings
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);


}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : float CTerrain::HeightAt(float x, float z)
//
// - Purpose : Given a (x, z) position on the rendered height map this method
// calculates the exact height of the height map at that (x, z)
// position using bilinear interpolation.
//
// -----------------------------------------------------------------------------
float CTerrain::HeightAt(float x, float z)
{

int iGridSpacing = 1;
int iHeightScale = 1;
x /= static_cast<float>(iGridSpacing);
z /= static_cast<float>(iGridSpacing);

long ix = FloatToLong(x);
long iz = FloatToLong(z);
float topLeft = pHeightMap[HeightIndexAt(ix, iz)] * iHeightScale;
float topRight = pHeightMap[HeightIndexAt(ix + 1, iz)] * iHeightScale;
float bottomLeft = pHeightMap[HeightIndexAt(ix, iz + 1)] * iHeightScale;
float bottomRight = pHeightMap[HeightIndexAt(ix + 1, iz + 1)] * iHeightScale;
float percentX = x - static_cast<float>(ix);
float percentZ = z - static_cast<float>(iz);

return bilerp(topLeft, topRight, bottomLeft, bottomRight, percentX, percentZ);
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : float CTerrain::HeightAt(float x, float z)
//
// - Purpose : Given a 2D height map coordinate, this method returns the index
// into the height map. This method wraps around for coordinates larger
// than the height map size.
//
// -----------------------------------------------------------------------------
unsigned int CTerrain::HeightIndexAt(int x, int z)
{
return (((x + m_iHeightMapSize) % m_iHeightMapSize) + ((z + m_iHeightMapSize) % m_iHeightMapSize) * m_iHeightMapSize);
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : float CTerrain::HeightAtPixel(int x, int z)
//
// - Purpose : Returns a single float (raw) height value, without more complex calculations
//
// -----------------------------------------------------------------------------
float CTerrain::HeightAtPixel(int x, int z)
{
return pHeightMap[z * m_iHeightMapSize + x];
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : CVector3 CTerrain::NormalAt(float x, float z)
//
// - Purpose : Given a (x, z) position on the rendered height map this method
// calculates the exact normal of the height map at that (x, z) position
// using bilinear interpolation.
//
// -----------------------------------------------------------------------------
CVector3 CTerrain::NormalAt(float x, float z)
{

int iGridSpacing = 1;
int iHeightScale = 1;

x /= static_cast<float>(iGridSpacing);
z /= static_cast<float>(iGridSpacing);

long ix = FloatToLong(x);
long iz = FloatToLong(z);

float percentX = x - static_cast<float>(ix);
float percentZ = z - static_cast<float>(iz);

CVector3 topLeft;
CVector3 topRight;
CVector3 bottomLeft;
CVector3 bottomRight;
CVector3 normal;

topLeft=NormalAtPixel(ix, iz);
topRight=NormalAtPixel(ix + 1, iz);
bottomLeft=NormalAtPixel(ix, iz + 1);
bottomRight=NormalAtPixel(ix + 1, iz + 1);

CVector3 vNormal;
vNormal = bilerp(topLeft, topRight, bottomLeft, bottomRight, percentX, percentZ);
vNormal.Normalize();
return vNormal;
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : CVector3 CTerrain::NormalAtPixel(int x, int z)
//
// - Purpose : Returns the normal at the specified location on the height map.
// The normal is calculated using the properties of the height map.
// This approach is much quicker and more elegant than triangulating the
// height map and averaging triangle surface normals.
//
// -----------------------------------------------------------------------------
CVector3 CTerrain::NormalAtPixel(int x, int z)
{
int iGridSpacing = 1;
int iHeightScale = 1;

CVector3 vNormal;

if (x > 0 && x < m_iHeightMapSize - 1)
vNormal.x = HeightAtPixel(x - 1, z) - HeightAtPixel(x + 1, z);
else if (x > 0)
vNormal.x = 2.0f * (HeightAtPixel(x - 1, z) - HeightAtPixel(x, z));
else
vNormal.x = 2.0f * (HeightAtPixel(x, z) - HeightAtPixel(x + 1, z));

if (z > 0 && z < m_iHeightMapSize - 1)
vNormal.z = HeightAtPixel(x, z - 1) - HeightAtPixel(x, z + 1);
else if (z > 0)
vNormal.z = 2.0f * (HeightAtPixel(x, z - 1) - HeightAtPixel(x, z));
else
vNormal.z = 2.0f * (HeightAtPixel(x, z) - HeightAtPixel(x, z + 1));

vNormal.y = 2.0f * iGridSpacing;
vNormal.Normalize();
return vNormal;
}
}

perfection.is.the.key
Advertisement
Cut down on the memory use by only storing X and Y, then use VTF to get the height in a vertex shader. Also, texture coords can be generated with a vertex shader if you use standard splatting
Updating after the weekend with a new Util function that does the gluUnproject without calling OpenGL functions.

This operates on matrices and vectors only, so somebody can find it usefull.

I am going to use it to continue with JTippetts's approach - for finding the cells to render, so here's what will be needed next in order to run the terrain rendering.



// --[ Function ]---------------------------------------------------------------
//
// - Prototype : static CVector4 UnProject(const CVector2& vMousePosition, const CVector2& vScreenSize, const CMatrix4x4& mViewMatrix, const CMatrix4x4& mModelMatrix, const CMatrix4x4& mProjectionMatrix, float zValue)
//
// - Purpose : Converts Screen Space coordinates to World Space coordinates
//
// -----------------------------------------------------------------------------
CVector4 UnProject(const CVector2& vMousePosition, const CVector2& vScreenSize, const CMatrix4x4& mViewMatrix, const CMatrix4x4& mProjectionMatrix, float zValue)
{
CVector4 vPositionWorldSpace;

CMatrix4x4 matMVP = mViewMatrix * mProjectionMatrix;

CMatrix4x4 matInverse = matMVP.inverse();


float in[4];
float winZ = 1.0;

CVector4 vIn = CVector4(
(2.0f*((float)(vMousePosition.x-0)/(vScreenSize.x-0)))-1.0f,
1.0f-(2.0f*((float)(vMousePosition.y-0)/(vScreenSize.y-0))),
2.0* zValue -1.0,
1.0);



//Multiply the vector by inversed matrix
vPositionWorldSpace = vIn * matInverse;


vPositionWorldSpace.w = 1.0 / vPositionWorldSpace.w;

//Divide result vector by it's w component after matrix multiplication ( perspective division )
vPositionWorldSpace.x *= vPositionWorldSpace.w;
vPositionWorldSpace.y *= vPositionWorldSpace.w;
vPositionWorldSpace.z *= vPositionWorldSpace.w;

return vPositionWorldSpace;
}






Time to move on, will update when i get that frustum and cells rendering with rev projection done.

perfection.is.the.key

Cut down on the memory use by only storing X and Y, then use VTF to get the height in a vertex shader. Also, texture coords can be generated with a vertex shader if you use standard splatting




hupsilardee thanks for replying, what's VTF and how fast is it comparing to stored values within VBO ?

that looks promising..


perfection.is.the.key

This topic is closed to new replies.

Advertisement