Chapter 15. Indexes and EBO - Element Buffer Object

In our latest samples we used GL_TRIANGLE_STRIP primitive (to draw a rectangle as a sequential array of vertices). But the most useful and most common primitive is GL_TRIANGLES. We used it earlier to draw a triangle (assuming that vertices go in correct sequential order). For more complicated surfaces normal approach is to keep an array of vertices in VBO regardless of their order. And to keep the draw order separately as an array of indexes (vertices numbers) in so called EBO (Element Buffer Object) or IBO (index Buffer Object, which is the same, just alternative name).

Windows

1. Start VS, open C:\CPP\a998engine\p_windows\p_windows.sln.


Need to modify DrawJob class a bit.

2. Open DrawJob.h and replace code by:

#pragma once
#include "Material.h"
#include <vector>

struct AttribRef //attribute reference/description
{
	unsigned int glVBOid = 0; //buffer object id
	int offset = 0; //variable's offset inside of VBO's element
	int stride = 0; //Buffer's element size in bytes
};

class DrawJob
{
public:
	Material mt;
	int pointsN = 0; //N of points to draw
	unsigned int glVAOid = 0; //will hold data stream attributes mapping/positions
	unsigned int glEBOid = 0; //Element Buffer Object (vertex indices)

	//common attributes
	AttribRef aPos;
	AttribRef aTuv;

	//static arrays (vectors) of all loaded DrawJobs, VBO ids
	static std::vector<DrawJob*> drawJobs;
	static std::vector<unsigned int> buffersIds;
public:
	DrawJob();
	virtual ~DrawJob(); //destructor
	static int cleanUp();
	static int newBufferId();
	int buildVAO() { return buildVAOforShader(this, mt.shaderN); };
	static int buildVAOforShader(DrawJob* pDJ, int shaderN);
	static int attachAttribute(int varLocationInShader, int attributeSizeInFloats, AttribRef* pAttribRef);

	int execute(float* uMVP, Material* pMt) { return executeDrawJob(this, uMVP, pMt); };
	static int executeDrawJob(DrawJob* pDJ, float* uMVP, Material* pMt);
};

Now we have glEBOid property there.


In DrawJob::executeDrawJob(), in case EBO is provided, we will use glDrawElements command instead of glDrawArrays as we always did before. We'll keep using glDrawArrays for sequential (unindexed) vertices arrays (as for GL_TRIANGLE_STRIP for example).

In DrawJob::buildVAOforShader(), when EBO presented, it will be attached to VAO.

3. Open DrawJob.cpp and replace code by:

#include "DrawJob.h"
#include "platform.h"
#include "utils.h"
#include "Shader.h"
#include "Texture.h"

//static arrays (vectors) of all loaded DrawJobs, VBO ids
std::vector<DrawJob*> DrawJob::drawJobs;
std::vector<unsigned int> DrawJob::buffersIds;

DrawJob::DrawJob() {
	drawJobs.push_back(this);
}
DrawJob::~DrawJob() {
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	if (glVAOid > 0)
		glDeleteVertexArrays(1, &glVAOid);
}
int DrawJob::newBufferId() {
	unsigned int bufferId;
	glGenBuffers(1, &bufferId);
	buffersIds.push_back(bufferId);
	return (int)bufferId;
}
unsigned int activeVBOid;
int DrawJob::buildVAOforShader(DrawJob* pDJ, int shaderN) {
	//delete VAO if exists already
	if (pDJ->glVAOid > 0) {
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glDeleteVertexArrays(1, &(pDJ->glVAOid));
	}
	glGenVertexArrays(1, &pDJ->glVAOid);
	glBindVertexArray(pDJ->glVAOid);

	//open shader descriptor to access variables locations
	Shader* pShader = Shader::shaders.at(pDJ->mt.shaderN);

	activeVBOid = 0;
	attachAttribute(pShader->l_aPos, 3, &pDJ->aPos);
	attachAttribute(pShader->l_aTuv, 2, &pDJ->aTuv);

	if (pDJ->glEBOid > 0)
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pDJ->glEBOid);

	glBindVertexArray(0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	return 1;
}

int DrawJob::attachAttribute(int varLocationInShader, int attributeSizeInFloats, AttribRef* pAR) {
	if (varLocationInShader < 0)
		return 0; //not used in this shader
	if (pAR->glVBOid == 0) {
		mylog("ERROR in DrawJob::attachAttribute, nk such attribute/VBO\n");
		return -1;
	}
	glEnableVertexAttribArray(varLocationInShader);
	if (activeVBOid != pAR->glVBOid) {
		activeVBOid = pAR->glVBOid;
		//attach input stream data
		glBindBuffer(GL_ARRAY_BUFFER, activeVBOid);
	}
	glVertexAttribPointer(varLocationInShader, attributeSizeInFloats, GL_FLOAT, GL_FALSE, pAR->stride, (void*)(long)pAR->offset);
	return 1;
}
int DrawJob::executeDrawJob(DrawJob* pDJ, float* uMVP, Material* pMt) {
	if (pMt == NULL)
		pMt = &(pDJ->mt);
	glBindVertexArray(pDJ->glVAOid);
	Shader* pShader = Shader::shaders.at(pMt->shaderN);
	glUseProgram(pShader->GLid);
	glUniformMatrix4fv(pShader->l_uMVP, 1, GL_FALSE, (const GLfloat*)uMVP);

	//attach textures
	if (pShader->l_uTex0 >= 0) {
		int textureId = Texture::getGLid(pMt->uTex0);
		//pass textureId to shader program
		glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
		glBindTexture(GL_TEXTURE_2D, textureId);
		// Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.    
		glUniform1i(pShader->l_uTex0, 0);
	}
	//other uniforms
	if (pShader->l_uColor >= 0) {
		//float uColor[4] = { 1.f, 0.f, 1.f, 1.f }; //R,G,B, alpha
		glUniform4fv(pShader->l_uColor, 1, pMt->uColor.forGL());
	}
	if (pDJ->glEBOid == 0) {
		glDrawArrays(pMt->primitiveType, 0, pDJ->pointsN);
	}
	else { //use EBO
		glDrawElements(pMt->primitiveType, pDJ->pointsN, GL_UNSIGNED_SHORT, 0);
	}
	glBindVertexArray(0);
	return 1;
}
int DrawJob::cleanUp() {
	int itemsN = drawJobs.size();
	//delete all drawJobs
	for (int i = 0; i < itemsN; i++) {
		DrawJob* pDJ = drawJobs.at(i);
		delete pDJ;
	}
	drawJobs.clear();
	//delete Buffers
	itemsN = buffersIds.size();
	//delete all buffers
	for (int i = 0; i < itemsN; i++) {
		unsigned int id = buffersIds.at(i);
		glDeleteBuffers(1, &id);
	}
	buffersIds.clear();

	return 1;
}


In TheGame.cpp we'll add frontIndices array: 6 indices for 2 triangles: North-West->South-West->South-East and NW->SE->NE.

In TheGame::getReady(), when creating front DrawJob (with picture), we'll generate EBO and will change pDJ->mt.primitiveType from GL_TRIANGLE_STRIP to GL_TRIANGLES.

Also we'll need to change pDJ->pointsN from 4 (4 sequential vertices) to 6 (2 indexed triangles).

4. Open TheGame.cpp and replace code by:

#include "TheGame.h"
#include "platform.h"
#include "linmath.h"
#include "Texture.h"
#include "Shader.h"
#include "DrawJob.h"

extern std::string filesRoot;

//static array (vector) for loaded gameSubjs
std::vector<GameSubj*> TheGame::gameSubjs;

static const struct
{
    float x, y, z, tu, tv;
} frontVertices[4] =
{
    { -0.5f,  0.5f, 0.f, 0.f, 0.f }, //top-left
    { -0.5f, -0.5f, 0.f, 0.f, 1.f }, //bottom-left
    {  0.5f,  0.5f, 0.f, 1.f, 0.f }, //top-right
    {  0.5f, -0.5f, 0.f, 1.f, 1.f }  //bottom-right
};
GLushort frontIndices[6] = { 0,1,3, 0,3,2 };

int TheGame::getReady() {
    bExitGame = false;
    Shader::loadShaders();
    glEnable(GL_CULL_FACE);

    GameSubj* pGS = new GameSubj();
    gameSubjs.push_back(pGS);
    pGS->djStartN = DrawJob::drawJobs.size();

    pGS->name.assign("img1");
    //pGS->ownCoords.setPosition(-50, 50, 0);
    pGS->ownSpeed.setDegrees(1, 2, 3);
    pGS->scale[0] = 400;
    pGS->scale[1] = pGS->scale[0] / 2;

    //face DrawJob
    //build VBO
    unsigned int VBOid = DrawJob::newBufferId();
    glBindBuffer(GL_ARRAY_BUFFER, VBOid);
    glBufferData(GL_ARRAY_BUFFER, sizeof(frontVertices), frontVertices, GL_STATIC_DRAW);
    int stride = sizeof(float) * 5;
    //add DrawJob
    DrawJob* pDJ = new DrawJob();
    pDJ->pointsN = 6; //number of points (vertices or indices in case of EBO)
    //define material
    pDJ->mt.shaderN = Shader::spN_flat_tex;
    pDJ->mt.primitiveType = GL_TRIANGLES; //GL_TRIANGLE_STRIP;
    pDJ->mt.uTex0 = Texture::loadTexture(filesRoot + "/dt/sample_img.png");
    //attributes references
    AttribRef* pAR;
    pAR = &pDJ->aPos; pAR->offset = 0;                 pAR->glVBOid = VBOid; pAR->stride = stride;
    pAR = &pDJ->aTuv; pAR->offset = sizeof(float) * 3; pAR->glVBOid = VBOid; pAR->stride = stride;

    //build and attrach EBO
    pDJ->glEBOid = DrawJob::newBufferId();
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pDJ->glEBOid);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frontIndices), frontIndices, GL_STATIC_DRAW);

    //create and fill vertex attributes array (VAO)
    pDJ->buildVAO();
    pGS->djTotalN = DrawJob::drawJobs.size() - pGS->djStartN;

    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    mat4x4 mProjection, mMVP;
    mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 200.f, -200.f);

    //scan subjects
    int subjsN = gameSubjs.size();
    for (int subjN = 0; subjN < subjsN; subjN++) {
        GameSubj* pGS = gameSubjs.at(subjN);
        //behavior - apply rotation speed
        pGS->moveSubj();
        //prepare subject for rendering
        pGS->buildModelMatrix(pGS);
        //build MVP matrix for given subject
        mat4x4_mul(mMVP, mProjection, pGS->ownModelMatrix);
        //render subject
        for (int i = 0; i < pGS->djTotalN; i++) {
            DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
            pDJ->execute((float*)mMVP, NULL);
        }
    }
    mySwapBuffers();
    return 1;
}
int TheGame::cleanUp() {
    Texture::cleanUp();
    Shader::cleanUp();
    DrawJob::cleanUp();
    return 1;
}
int TheGame::onScreenResize(int width, int height) {
    if (screenSize[0] == width && screenSize[1] == height)
        return 0;
    screenSize[0] = width;
    screenSize[1] = height;
    screenRatio = (float)width / (float)height;
    glViewport(0, 0, width, height);
    mylog(" screen size %d x %d\n", width, height);
    return 1;
}
int TheGame::run() {
    getReady();
    while (!bExitGame) {
        drawFrame();
    }
    cleanUp();
    return 1;
}


5. Build an run (green arrow), same picture, which means the transition from non-indexed GL_TRIANGLE_STRIP to indexed GL_TRIANGLES was successful.


Leave a Reply

Your email address will not be published. Required fields are marked *