Dear GameDev's,
i am up to create a new engine from scratch for a typical strategy game like "Settlers V" - where camera can go 'around' a unit, zoom in, zoom out, but never sees the horizon.
My terrain (heightmap) shall be around 5km x 5km - which is 5120x5120 RAW heightmap that is to be loaded when engine is initializing and we are talking about PC platform here.
This is exactly what i need to achieve in terms of Camera view, no more of the terrain ahead will be visible.
Camera is limited to see 50 meters around player when max zoomed out - and that's the limit, no more can be seen.
I need to render only this part that is visible to the camera frustum as fast as possible, so i can have more power for frag shaders / eventual post processing effect to add (which is obvious).
There is only one small question at the start i would like to ask before i go further:
a) What kind of approach should i take to store such big terrain when loading to the engine ( should i cut this terrain into pieces - how do i handle them, separate vbos, how big shall be the one 'piece' )
b) After successful load, how do i know which part of the terrain should i render ? ( how is this connected to the terrain VBO / VBOs )
Is there anything that covers point a/b in a single well known solution?
Somebody told me that i should use QuadTree Frustum Culling for this type of game, can i ask you for an advise please?
Thank you for your help.
RTS Game - The terrain approach
Personally, I wouldn't use a quadtree for this. Given the constraints on the camera, you can achieve faster results using a simpler grid partition, and just calculating the intersection of the frustum against the grid.
Consider that you split your terrain into a grid of chunks. The terrain has a minimum Y and a maximum Y bound; that is, there are values of Y for which the terrain will lie between, forming two upper and lower bounding planes on the world. You can reverse project the top-left and top-right corners of the screen against the Lower Y plane, and reverse project the bottom-left and bottom-right corners of the screen against the Upper Y plane, to calculate four points of a box-shape that outline the visible area. Using the X/Z values of the reverse-projected area, you can determine which grid squares the points lie within, then use some sort of polygon rasterization algorithm to iterate and draw just the grid units covered by the frustum box. Remember to extend the corners of the box outward from the center of the box by some arbitrary amount (determined by testing, as it depends upon the size of your grid units relative to the screen) in order to provide sufficient padding at the edges of the screen so that no blank areas are shown, where the view box crosses just the corner of a grid unit, leaving that grid unit undrawn.
Calculating the visible grids directly in this fashion eliminates the recursion of the quadtree, which can result in a significant number of redundant tests.
The grid unit chunks could be separate VBOs, or you could just have one large VBO and use separate index buffers for the grid units.
Consider that you split your terrain into a grid of chunks. The terrain has a minimum Y and a maximum Y bound; that is, there are values of Y for which the terrain will lie between, forming two upper and lower bounding planes on the world. You can reverse project the top-left and top-right corners of the screen against the Lower Y plane, and reverse project the bottom-left and bottom-right corners of the screen against the Upper Y plane, to calculate four points of a box-shape that outline the visible area. Using the X/Z values of the reverse-projected area, you can determine which grid squares the points lie within, then use some sort of polygon rasterization algorithm to iterate and draw just the grid units covered by the frustum box. Remember to extend the corners of the box outward from the center of the box by some arbitrary amount (determined by testing, as it depends upon the size of your grid units relative to the screen) in order to provide sufficient padding at the edges of the screen so that no blank areas are shown, where the view box crosses just the corner of a grid unit, leaving that grid unit undrawn.
Calculating the visible grids directly in this fashion eliminates the recursion of the quadtree, which can result in a significant number of redundant tests.
The grid unit chunks could be separate VBOs, or you could just have one large VBO and use separate index buffers for the grid units.
JTippetts, thank you again for your help - i will go without quad-tree rendering for this engine.
As soon as i manage to load and divide the terrain into cells, and get this thing going on a single VBO with separate index buffers
I will come back here to ask again about the procedure to compute which cells to render in regards to current view.
Time to get to work.
As soon as i manage to load and divide the terrain into cells, and get this thing going on a single VBO with separate index buffers
I will come back here to ask again about the procedure to compute which cells to render in regards to current view.
Time to get to work.
(LOUD THINKING)
OK have my terrain loaded, time to setup the Indexes as my VBO is constructed at this moment.
I use a 1024x1024 raw height field for start.
I split this 1024x1024 terrain into let's say 32x32 chunks which i call Cells.
That means :
OK have my terrain loaded, time to setup the Indexes as my VBO is constructed at this moment.
I use a 1024x1024 raw height field for start.
I split this 1024x1024 terrain into let's say 32x32 chunks which i call Cells.
That means :
1024 / 32 = 32 cells in X - width
1024 / 32 = 32 cells int Z - height
[/quote]
which is 1024 cells in total ( Index buffers ).
If there's something I'm doing wrong at this step, please let me know.
After i load the vertexes into a VBO buffer i need to create those per-cell Index Buffers and store them in a vector for later enumerator usage.
- First thing is to calculate index value for each vertex within a Cell.
- iterate through the starting X and Z till ending X and Z values for each cell
(0,0 -> 32,32)
(32,32 -> 64,64)
(64,64 -> 96,96)
(.............................)
- Calculate the index values for this cell push'ing_back each index value to a:
std::vector<unsigned short> m_vIndices;
- After we calculate them, generate the Index buffer for each cell and get rid of the m_vIndices to free the memory.
This is exactly where i am at right now and don't want to make any mistake.
Having VBO ready i need to find a good mechanism to handle those Index Buffers for later usage.
0) Do we see a potential failure at the start with such a large number of index buffers ?
1) Is it ok to hold Cells Index Buffers in a std::vector<GLuint> ? - how do i access them later on while looking for a cell by having the X,Z values - possible?
2) How do i generate indexes per Cell to render them efficiently - i believe strips should do the thing, any good ready - to - go pseudo code so i could have a look please?
Hopefully i managed to write it clear.
Should i go with with:
glDrawElements(GL_TRIANGLE_STRIP
or
glDrawElements(GL_TRIANGLES
For the terrain rendering?
Quick notes:
glDrawElements(GL_TRIANGLE_STRIP
or
glDrawElements(GL_TRIANGLES
For the terrain rendering?
Quick notes:
<::CTerrain::Create> [2011/10/5 19:14:11] Loading "terrain.raw" terrain file, width=1024 height=1024.
<::CTerrain::Create> [2011/10/5 19:14:11] Terrain debug: Cells after splitting: 64x64
<::CTerrain::Create> [2011/10/5 19:14:12] Terrain debug: Indexes : 2097150
<::CTerrain::Create> [2011/10/5 19:14:12] Terrain debug: Vertexes: 1048576
[/quote]
As promised i'm back with the news and a source code listing - so someone might find it useful.
First of all i'll drop the terrain class here so we all know what i am talking about.
Terrain.h
Terrain.cpp ( creation / render methods )
This works as it is intended to, the index buffers consist of a :
First of all i'll drop the terrain class here so we all know what i am talking about.
Terrain.h
namespace XYNAPSE
{
// --[ Struct ]---------------------------------------------------------------
//
// - Class :
// - Prototype : struct tTerrainCell
//
// - Purpose : This holds index buffer data per cell
//
// -----------------------------------------------------------------------------
struct tTerrainCell
{
GLuint m_uiIBO;
int m_iIndexesCount;
// more to come.
};
class CTerrain
{
public:
CTerrain();
bool Create(const std::string& strFileName, int iWidth, int iHeight);
void Destroy();
void Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix);
void Update(const CVector3& vOriginPosition);
private:
int m_iHeight;
int m_iWidth;
int m_iVertexCount;
int hLOD;
std::vector<XYN_vbo_vert> m_vVertices;
std::vector<tTerrainCell> m_vCells;
GLuint m_uiVAO;
GLuint m_uiVBOVertices;
GLuint m_usIndices;
CMatrix4x4 m_matModel;
XYNAPSE::CShader *m_pShader;
BYTE hHeightField[1024][1024];
};
}
Terrain.cpp ( creation / render methods )
// --[ 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 iWidth, int iHeight)
{
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Loading \"%s\" terrain file, width=%i height=%i.",strFileName.data(),iWidth,iHeight);
// Load the heights to a byte array
// If fp failed, return false.
m_iHeight = iHeight;
m_iWidth = iWidth;
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(hHeightField, 1, m_iWidth * m_iHeight, fp);
fclose(fp);
if(result != (m_iWidth * m_iHeight) )
{
return false;
}
// Ok time to split the terrain into iTerrainSplitCellSize cells
// Here we define the Cell Size
int iTerrainSplitCellSize = 16;
int iCellsAfterSplittingHeight = m_iHeight/iTerrainSplitCellSize;
int iCellsAfterSplittingWidth = m_iWidth/iTerrainSplitCellSize;
// Generate Vertex buffer
for(int x=0;x<m_iWidth;x++)
{
for(int z=0;z<m_iHeight;z++)
{
XYN_vbo_vert newVertexData;
newVertexData.x=x;
newVertexData.y=0;
newVertexData.z=z;
m_vVertices.push_back(newVertexData);
}
}
// Generate separate IBO's for each cell
for(int iCellX=0;iCellX<iCellsAfterSplittingWidth;iCellX++)
{
for(int iCellZ=0;iCellZ<iCellsAfterSplittingHeight;iCellZ++)
{
int iCellXEnd=(iCellX*iTerrainSplitCellSize)+iTerrainSplitCellSize;
int iCellZEnd=(iCellZ*iTerrainSplitCellSize)+iTerrainSplitCellSize;
std::vector<unsigned short> vIndices;
for(int x=iCellX*iTerrainSplitCellSize; x < ( iCellXEnd ); x++)
{
for(int z=iCellZ*iTerrainSplitCellSize; z < ( iCellZEnd ); z++)
{
vIndices.push_back( x * m_iWidth + z );
vIndices.push_back( (x + 1) * m_iWidth + z);
}
}
// Create new Cell objet
tTerrainCell newCell;
// Store the number of indexes for this cell
newCell.m_iIndexesCount=vIndices.size();
// Generate the IBO
glGenBuffers( 1, &newCell.m_uiIBO);
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, newCell.m_uiIBO );
glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned short) * vIndices.size(), &vIndices[0], GL_STATIC_DRAW );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
// Push back newly created cell to our list
m_vCells.push_back(newCell);
// Pop some debug note
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Terrain debug: Cell[%i][%i] Indexes: %i\n",iCellX,iCellZ,vIndices.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_matModel.Translate(0,0,0);
// 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::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
//
// - Purpose : Renders visible cells of the terrain that were previously
// calculated by Update method.
//
// -----------------------------------------------------------------------------
void CTerrain::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
{
// Turn the shader on
m_pShader->TurnOn();
// Inject matrices to the shader
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Model"), m_matModel);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_View"), mViewMatrix);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Projection"), mProjectionMatrix);
// Bind the VBO
glBindBuffer(GL_ARRAY_BUFFER, m_uiVBOVertices);
//Setup the VertexPosition
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),(char*)0);
//Setup the VertexNormal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*5));
//Setup the VertexTexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*3));
//Setup the 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[0].m_uiIBO);
// Draw Triangle strip
glDrawElements(GL_TRIANGLE_STRIP,m_vCells[0].m_iIndexesCount,GL_UNSIGNED_SHORT,0);
// Boil out with the bindings
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);
// Turn the shader off
m_pShader->TurnOff();
}
This works as it is intended to, the index buffers consist of a :
<XYNAPSE::CTerrain::Create> [2011/10/6 10:43:31] Terrain debug: Cell[0][0] Indexes: 512
[/quote]
512 entries for 16x16 Cell.
Rendering with strips looks promising too
The next thing i want to ask is the data-holding approach.
Having in mind a map like 256x256 would create :
<XYNAPSE::CTerrain::Create> [2011/10/6 11:12:15] Terrain debug: Cells created: 256[/quote]
256 Index buffers.
- Now is this something that i should be worried about in future ( for example when speaking of a map like 2048 x 2048 ) ?
Ok i have switched from rendering triangle strips to indexed triangles as strips were causing 'known' problems when a connection between few cells was made.
Now it's working fine and it is time now to find out which Cells to render by known the player position.
Knowing that my cells are stored in a
and my player is located let's say :
x: 120, z: 46
I need to calculate the number that will point to the cell he is currently standing in.
Anyone has any good idea ?
Ps. as promissed, updated terrain class below.
Now it's working fine and it is time now to find out which Cells to render by known the player position.
Knowing that my cells are stored in a
struct tTerrainCell
{
GLuint m_uiIBO;
int m_iIndexesCount;
};
std::vector<tTerrainCell> m_vCells;
and my player is located let's say :
x: 120, z: 46
I need to calculate the number that will point to the cell he is currently standing in.
Anyone has any good idea ?
Ps. as promissed, updated terrain class below.
namespace XYNAPSE
{
// --[ Struct ]---------------------------------------------------------------
//
// - Class :
// - Prototype : struct tTerrainCell
//
// - Purpose : This holds index buffer data per cell
//
// -----------------------------------------------------------------------------
struct tTerrainCell
{
GLuint m_uiIBO;
int m_iIndexesCount;
};
class CTerrain
{
public:
CTerrain();
bool Create(const std::string& strFileName, int iSize);
void Destroy();
void Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix);
void Update(const CVector3& vOriginPosition);
private:
int m_iSize;
int m_iVertexCount;
unsigned int HeightIndexAt(int x, int z);
float HeightAtPixel(float x, float z);
CVector3 NormalAtPixel(int x, int z);
std::vector<tTerrainCell> m_vCells;
GLuint m_uiVAO;
GLuint m_uiVBOVertices;
GLuint m_usIndices;
CMatrix4x4 m_matModel;
XYNAPSE::CShader *m_pShader;
BYTE pHeightMap[1024*1024];
};
}
namespace XYNAPSE
{
CTerrain::CTerrain()
{
}
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)
{
// Load the heights to a byte array
// If fp failed, return false.
m_iSize = iSize;
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Loading \"%s\" terrain file, size=%ix%i.",strFileName.data(),m_iSize,m_iSize);
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_iSize * m_iSize, fp);
fclose(fp);
if(result != (m_iSize * m_iSize) )
{
return false;
}
// Ok time to split the terrain into iTerrainSplitCellSize cells
// Here we define the Cell Size
int iTerrainSplitCellSize = 16;
int iCellsAfterSplittingHeight = m_iSize/iTerrainSplitCellSize;
int iCellsAfterSplittingWidth = m_iSize/iTerrainSplitCellSize;
// Generate Vertex buffer
std::vector<XYN_vbo_vert> m_vVertices;
for(int x=0;x<m_iSize;x++)
{
for(int z=0;z<m_iSize;z++)
{
XYN_vbo_vert newVertexData;
newVertexData.vPosition=CVector3(x,0,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",iCellsAfterSplittingHeight,iCellsAfterSplittingWidth);
// Generate separate IBO's for each cell
for(int iCellX=0;iCellX<iCellsAfterSplittingWidth;iCellX++)
{
for(int iCellZ=0;iCellZ<iCellsAfterSplittingHeight;iCellZ++)
{
std::vector<unsigned short> vIndices;
int startX=iCellX*iTerrainSplitCellSize;
int startZ=iCellZ*iTerrainSplitCellSize;
int endX=(startX)+iTerrainSplitCellSize;
int endZ=(startZ)+iTerrainSplitCellSize;
for (int x = startX; x < endX ; ++x)
{
for (int z = startZ; z < endZ ; ++z)
{
// CCW
//
// *2
// | \
// *1 _*3
////////////
vIndices.push_back(x*m_iSize + z);
vIndices.push_back((x+1)*m_iSize + z);
vIndices.push_back(x*m_iSize + z + 1);
//
// *1-*2
// |
// *3
/////////////
vIndices.push_back((x+1)*m_iSize + z);
vIndices.push_back((x+1)*m_iSize + z + 1);
vIndices.push_back(x*m_iSize + z + 1);
}
}
// Create new Cell objet
tTerrainCell newCell;
// Store the number of indexes for this cell
newCell.m_iIndexesCount=vIndices.size();
// Generate the IBO
glGenBuffers( 1, &newCell.m_uiIBO);
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, newCell.m_uiIBO );
glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned short) * vIndices.size(), &vIndices[0], GL_STATIC_DRAW );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
// 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_matModel.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.
//
// -----------------------------------------------------------------------------
void CTerrain::Update(const CVector3& vOriginPosition)
{
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : void CTerrain::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
//
// - Purpose : Renders visible cells of the terrain that were previously
// calculated by Update method.
//
// -----------------------------------------------------------------------------
void CTerrain::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
{
// Turn the shader on
m_pShader->TurnOn();
// Inject matrices to the shader
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Model"), m_matModel);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_View"), mViewMatrix);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Projection"), mProjectionMatrix);
// Bind the VBO
glBindBuffer(GL_ARRAY_BUFFER, m_uiVBOVertices);
//Setup the VertexPosition
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),(char*)0);
//Setup the VertexNormal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*5));
//Setup the VertexTexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*3));
//Setup the 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[0].m_uiIBO);
// Draw Triangle strip
glDrawElements(GL_TRIANGLES,m_vCells[0].m_iIndexesCount,GL_UNSIGNED_SHORT,0);
// Boil out with the bindings
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);
// Turn the shader off
m_pShader->TurnOff();
}
}
Ok, coming back with the results once more.
I convert given X,Z position of some point to find a Cell by calling Update method before i render the terrain.
The given X,Z values are scaled to a number of cells created.
This is how it is happening and works as expected.
[media]
Next step.
Render cells that are visible within the viewport.
And here comes the question in regards to:
I convert given X,Z position of some point to find a Cell by calling Update method before i render the terrain.
The given X,Z values are scaled to a number of cells created.
This is how it is happening and works as expected.
// Find out which cell array belongs to our current camera position
int iCellX = (vOriginPosition.x/m_iTerrainSplitCellSize);
int iCellZ = (vOriginPosition.z/m_iTerrainSplitCellSize);
// Do some minimals fix
if(iCellX<0) iCellX = 0;
if(iCellZ<0) iCellZ = 0;
// Do some maximals fix
if(iCellX>m_iSize/m_iTerrainSplitCellSize) iCellX = m_iSize/m_iTerrainSplitCellSize;
if(iCellZ>m_iSize/m_iTerrainSplitCellSize) iCellZ = m_iSize/m_iTerrainSplitCellSize;
// 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_iSize/m_iTerrainSplitCellSize) + iCellZ;
[media]
[/media]
Next step.
Render cells that are visible within the viewport.
And here comes the question in regards to:
You can reverse project the top-left and top-right corners of the screen against the Lower Y plane, and reverse project the bottom-left and bottom-right corners of the screen against the Upper Y plane, to calculate four points of a box-shape that outline the visible area. Using the X/Z values of the reverse-projected area, you can determine which grid squares the points lie within, then use some sort of polygon rasterization algorithm to iterate and draw just the grid units covered by the frustum box.
[/quote]
JTippetts - can i ask you to shed a bit of light on this - maybe some example ?
As always, dropping here the updated Terrain Class for anyone who is interested.
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); // Creates the terrain from heightmap file
void Destroy(); // Proper way of destructing the Terrain object
void Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix); // Renders the terrain cells we're in
CVector2 Update(const CVector3& vOriginPosition); // Call from the engine before rendering the frame
private:
int m_iSize; // This is the map size m_iSize x m_iSize
int m_iTerrainSplitCellSize; // Cell size
int m_iCurrentCell; // Current cell we're in - temporary for tests
std::vector<tTerrainCell> m_vCells; // This holds our cells with index buffers
GLuint m_uiVAO; // Not used
GLuint m_uiVBOVertices; // VBO for the whole heightmap
CMatrix4x4 m_matModel; // Model matrix
XYNAPSE::CShader *m_pShader; // Shader for rendering purposes
BYTE pHeightMap[1024*1024]; // This is where our heightmap is stored
};
}
namespace XYNAPSE
{
CTerrain::CTerrain()
{
}
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)
{
// Load the heights to a byte array
// If fp failed, return false.
m_iSize = iSize;
XYNAPSE::CLogger::Instance()->Write(XLOGEVENT_LOCATION,"Loading \"%s\" terrain file, size=%ix%i.",strFileName.data(),m_iSize,m_iSize);
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_iSize * m_iSize, fp);
fclose(fp);
if(result != (m_iSize * m_iSize) )
{
return false;
}
// Ok time to split the terrain into iTerrainSplitCellSize cells
// Here we define the Cell Size
m_iTerrainSplitCellSize = 16;
int iCellsAfterSplittingHeight = m_iSize/m_iTerrainSplitCellSize;
int iCellsAfterSplittingWidth = m_iSize/m_iTerrainSplitCellSize;
// Generate Vertex buffer
std::vector<XYN_vbo_vert> m_vVertices;
for(int x=0;x<m_iSize;x++)
{
for(int z=0;z<m_iSize;z++)
{
XYN_vbo_vert newVertexData;
newVertexData.vPosition=CVector3(x,0,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",iCellsAfterSplittingHeight,iCellsAfterSplittingWidth);
// Generate separate IBO's for each cell
for(int iCellX=0;iCellX<iCellsAfterSplittingWidth;iCellX++)
{
for(int iCellZ=0;iCellZ<iCellsAfterSplittingHeight;iCellZ++)
{
std::vector<unsigned short> vIndices;
int startX=iCellX*m_iTerrainSplitCellSize;
int startZ=iCellZ*m_iTerrainSplitCellSize;
int endX=(startX)+m_iTerrainSplitCellSize;
int endZ=(startZ)+m_iTerrainSplitCellSize;
for (int x = startX; x < endX ; ++x)
{
for (int z = startZ; z < endZ ; ++z)
{
// CCW
//
// *2
// | \
// *1 _*3
////////////
vIndices.push_back(x*m_iSize + z);
vIndices.push_back((x+1)*m_iSize + z);
vIndices.push_back(x*m_iSize + z + 1);
//
// *1-*2
// |
// *3
/////////////
vIndices.push_back((x+1)*m_iSize + z);
vIndices.push_back((x+1)*m_iSize + z + 1);
vIndices.push_back(x*m_iSize + z + 1);
}
}
// Create new Cell objet
tTerrainCell newCell;
// Store the number of indexes for this cell
newCell.m_iIndexesCount=vIndices.size();
// Generate the IBO
glGenBuffers( 1, &newCell.m_uiIBO);
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, newCell.m_uiIBO );
glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned short) * vIndices.size(), &vIndices[0], GL_STATIC_DRAW );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, 0 );
// 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_matModel.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.
//
// -----------------------------------------------------------------------------
CVector2 CTerrain::Update(const CVector3& vOriginPosition)
{
// Find out which cell array belongs to our current camera position
int iCellX = (vOriginPosition.x/m_iTerrainSplitCellSize);
int iCellZ = (vOriginPosition.z/m_iTerrainSplitCellSize);
// Do some minimals fix
if(iCellX<0) iCellX = 0;
if(iCellZ<0) iCellZ = 0;
// Do some maximals fix
if(iCellX>m_iSize/m_iTerrainSplitCellSize) iCellX = m_iSize/m_iTerrainSplitCellSize;
if(iCellZ>m_iSize/m_iTerrainSplitCellSize) iCellZ = m_iSize/m_iTerrainSplitCellSize;
// 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_iSize/m_iTerrainSplitCellSize) + iCellZ;
// For debug usage, so we know in which cell vOriginPosition is placed
return CVector2(iCellX,iCellZ);
}
// --[ Method ]---------------------------------------------------------------
//
// - Class : CTerrain
// - Prototype : void CTerrain::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
//
// - Purpose : Renders visible cells of the terrain that were previously
// calculated by Update method.
//
// -----------------------------------------------------------------------------
void CTerrain::Render(const CMatrix4x4& mViewMatrix,const CMatrix4x4& mProjectionMatrix)
{
// Turn the shader on
m_pShader->TurnOn();
// Inject matrices to the shader
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Model"), m_matModel);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_View"), mViewMatrix);
m_pShader->SetMatrix4x4(m_pShader->getUniform("m_Projection"), mProjectionMatrix);
// Bind the VBO
glBindBuffer(GL_ARRAY_BUFFER, m_uiVBOVertices);
//Setup the VertexPosition
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),(char*)0);
//Setup the VertexNormal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*5));
//Setup the VertexTexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE,sizeof(XYN_vbo_vert),BUFFER_OFFSET(sizeof(GLfloat)*3));
//Setup the 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);
// Draw Triangle strip
glDrawElements(GL_TRIANGLES,m_vCells[m_iCurrentCell].m_iIndexesCount,GL_UNSIGNED_SHORT,0);
// Boil out with the bindings
glBindBuffer( GL_ARRAY_BUFFER, 0 );
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(3);
// Turn the shader off
m_pShader->TurnOff();
}
}
The four points that you obtain by reverse-projecting the screen against the planes bounding the terrain on Y will form a quadrilateral shape. You can calculate which cell each of the points is in by dividing the coordinate by the cell-size, and taking the floor to get the integral CellX, CellZ coordinates. Once you have these cells, you can use a rasterization technique to draw the cells. If you consider that your grid of cells is very much like a pixelated surface, where each cell is a pixel, then you can see how a basic software rasterization algorithm could be handy for drawing all of the visible cells. A real quick Google pulls up some articles on software rasterization that could come in handy as references. The basics of rasterization, though, are that you iterate the rows, or scan lines, of a triangle primitive. The visible quad is split into triangles, and the triangles are rendered by calling each rasterized cell's draw routine.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement