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
[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;
}
}