Chapter 16. Modeler

Now it's time to think where to get actual 3D models for our Project. Our options are:

  • Web. There is a wide choice of pre-built models in different formats from professional and amateur 3D artists. Many of models are free.
  • Learn Blender, Maya, 3Ds Max or some other 3D editor. A HUGE task by itself. Don't feel ready for it yet.
  • Find an artist. May not be cheap plus implies communication problems: artists think and work in different terms and concepts than programmers... The challenge with all 3 mentioned options - is how to read/load such models to the app and how to control them. It can be a quite untrivial task. We'll definitely address this issue later. But now I'd prefer a faster result.
  • We can generate models programmatically. Doubtful that it will compete with professional 3D art work, but anyway, having such option definitely won't hurt.

So, let's start with the last option - generate models programmatically. It will be a set of tools and classes, a considerable part of our engine. Let's create a placeholder for it.

1. In Windows File Explorer under C:\CPP\engine folder create a new sub-folder modeler.


2. Go to C:\CPP folder. Make a copy of a998engine and name it a997modeler.


3. Start VS. Open C:\CPP\a997modeler\p_windows\p_windows.sln.

Under xEngine (right-click on xEngine ) add New Filter, call it modeler.


4. Add a reference to new folder:

Open p_windows project Properties. All Configurations / Active(Win32),

C/C++ -> General -> Additional Include Directories -> Edit, add a new line,

browse to C:\CPP\engine\modeler,

Select Folder, Ok, Apply, Ok.


The idea of the "modeler" is to build an array of vertices and indices and then convert them to VBOs, EBOs, VAOs and DrawJobs.

5. Add the first class, Vertex01. As usual, file-by-file:

Under modeler add new header file Vertex01.h

Location - C:\CPP\engine\modeler

Code:

#pragma once

class Vertex01
{
public:
	int subjN = -1; //game subject number
	int materialN = -1; //material number
	int flag = 0;
	int endOfSequence = 0; //for sequentional (unindexed) primitives (like GL_LINE_STRIP for example)
	int altN = -1; //vertex' position in alternative array
	//atributes
	float aPos[4] = { 0,0,0,0 }; //position x,y,z + 4-th float for matrix operations
	float aNormal[4] = { 0,0,0,0 }; //normal (surface reflection vector) x,y,z + 4-th float for matrix operations
	float aTuv[2] = { 0,0 }; //2D texture coordinates
	float aTuv2[2] = { 0,0 }; //for normal maps
	//tangent space (for normal maps)
	float aTangent[3] = { 0,0,0 };
	float aBinormal[3] = { 0,0,0 };
};

Don't worry about extra variables, will explain them later, as we use them. Don't need cpp file yet.


Since we are planning to use indexes (EBO) to draw triangles arrays, we'll need a placeholder for them.

6. Under modeler add new header file Triangle01.h

Location - C:\CPP\engine\modeler

Code:

#pragma once

class Triangle01
{
public:
	int subjN = -1; //game subject number
	int materialN = -1; //material number
	int flag = 0;
	int idx[3] = { 0,0,0 }; //3 vertex indices
};

Same here, don't worry about extra variables, will explain them later, as we use them.


Also we'll need vertices/triangles groups for various manipulations.

7, Under modeler add new header file Group01.h

Location - C:\CPP\engine\modeler

Code:

#pragma once

class Group01
{
public:
	int fromVertexN = 0;
	int fromTriangleN = 0;
};

We mostly need to know where/when the current group started, that's why there is no "to" variables.


Besides, we'll need some kind of descriptor of which shape we are drawing/building. I'll call it VirtualShape. "Virtual" because it won't create any actual vertices by itself.

8. Under modeler add new header file VirtualShape.h

Location - C:\CPP\engine\modeler

Code:

#pragma once
#include <string>

class VirtualShape
{
public:
	std::string type = "box";
	float whl[3] = { 0 }; //width/height/length (x,y,z sizes/dimensions)
};

The key operation/command of the modeler will be buildFace(). let's say "on the left side of current VirtualShape". This command will create 4 actual vertices on the left side of the shape (in this sample - of the box with whl dimensions) and 2 corresponding triangle indices (6 indices total).


Some calculations we'll move to utils set.

9. Open utils.h and replace code by:

#pragma once
#include <string>
#include "linmath.h"

int checkGLerrors(std::string ref);
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3);


10. Open utils.cpp and replace code by:

#include "utils.h"
#include "platform.h"

int checkGLerrors(std::string ref) {
    //can be used after any GL call
    int res = glGetError();
    if (res == 0)
        return 0;
    std::string errCode;
    switch (res) {
        //case GL_NO_ERROR: errCode = "GL_NO_ERROR"; break;
        case GL_INVALID_ENUM: errCode = "GL_INVALID_ENUM"; break;
        case GL_INVALID_VALUE: errCode = "GL_INVALID_VALUE"; break;
        case GL_INVALID_OPERATION: errCode = "GL_INVALID_OPERATION"; break;
        case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = "GL_INVALID_FRAMEBUFFER_OPERATION"; break;
        case GL_OUT_OF_MEMORY: errCode = "GL_OUT_OF_MEMORY"; break;
        default: errCode = "??"; break;
    }
    mylog("GL ERROR %d-%s in %s\n", res, errCode.c_str(), ref.c_str());
    return -1;
}
void mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3) {
    vec4 v2;
    if (vOut == vIn) {
        memcpy(&v2, vIn, sizeof(vec4));
        vIn = v2;
    }
    vIn[3] = (float)v3;
    mat4x4_mul_vec4(vOut, M, vIn);
}

This new mat4x4_mul_vec4plus() function is a vector/matrix multiplication. The difference from linmath function is that it avoids writing result to the argument vector, plus it overwrites the last position of the argument vector. If it's 0, the resulting vector will ignore position part of the transform matrix, so it will be just a direction (used for rotating normals for example).


Also need to extend DrawJob

11. 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);

	virtual int setDesirableOffsets(int* pStride, int shaderN, int VBOid) { return setDesirableOffsetsForSingleVBO(this, pStride, shaderN, VBOid); };
	static int setDesirableOffsetsForSingleVBO(DrawJob* pDJ, int* pStride, int shaderN, int VBOid);

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

This new setDesirableOffsets..() function fills out DrawJob's attribute references (AttribRef aPos and aTuv, later we will have more attributes).


12. 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;
}
int DrawJob::setDesirableOffsetsForSingleVBO(DrawJob* pDJ, int* pStride, int shaderN, int VBOid) {
	//sets desirable offsets and stride according to given shader needs
	//assuming that we have 1 single VBO
	Shader* pSh = Shader::shaders.at(shaderN);
	int stride = 0;
	pDJ->aPos.offset = 0; //attribute o_aPos, always 0
	stride += sizeof(float) * 3; //aPos size - 3 floats (x,y,z)
	if (pSh->l_aTuv >= 0) { //attribute TUV (texture coordinates)
		pDJ->aTuv.offset = stride; //attribute TUV (texture coordinates)
		stride += sizeof(float) * 2;
	}
	*pStride = stride;
	//add stride and VBOid to all attributes
	AttribRef* pAR = NULL;
	pAR = &pDJ->aPos; pAR->glVBOid = VBOid; pAR->stride = stride;
	pAR = &pDJ->aTuv; pAR->glVBOid = VBOid; pAR->stride = stride;

	return 1;
}

To be continued...


Leave a Reply

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