Глава 16. Modeler

Наверное, пора уже подумать, где же брать реальные 3D модели для Проекта. Варианты такие:

  • Web. Широкий выбор моделей разных форматов от профессиональных 3D художников и любителей. Многие из моделей – бесплатные.
  • Самим освоить Blender, Maya, 3Ds Max или еще какой-то 3D редактор. ОГРОМНАЯ задача сама-по-себе. Как-то пока не ощущаю себя готовым для этого.
  • Найти 3Д художника. Проблема со всеми тремя упомянутыми вариантами – а как читать/загружать эти модели в программу и как ими управлять? Неочевидная задача, между прочим. Мы непременно вернемся к этому вопросу попозже. А сейчас я бы предпочел вариант побыстрее.
  • А можно генерировать модели программно. Не факт что они смогут полноценно конкурировать с профессиональными 3D работами, но в любом случае, иметь такую опцию определенно не помешает.

Вобщем, давайте начнем с последнего варианта – генерировать модели программно. Это будет набор инструментов и классов, часть нашего графического движка. Создадим для него каталог.

1. В Windows File Explorer-е в каталоге C:\CPP\engine создадим новый под-каталог modeler.


2. Пройдем в каталог C:\CPP . Сделаем копию каталога a998engine и переименуем ее в a997modeler.


3. Запускаем VS. Открываем C:\CPP\a997modeler\p_windows\p_windows.sln.

Под xEngine (right-click на xEngine ) добавим New Filter, назовем его modeler.


4. Добавим ссылку на новый каталог:

Откроем p_windows project Properties. All Configurations / Active(Win32),

C/C++ -> General -> Additional Include Directories -> Edit, добавим новую строку,

идем в C:\CPP\engine\modeler,

Select Folder, Ok, Apply, Ok.


Идея “modeler-а” – построить массивы вертексов и индексов, и затем перевести их в VBO, EBO, VAO, и в итоге – в DrawJobs.

5. Создадим первый класс, Vertex01. Как обычно, по-файлово:

Под modeler добавим новый header файл Vertex01.h

Location – C:\CPP\engine\modeler

Код:

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

Не обращайте внимание на “лишние” переменные, объясню их попозже, по мере использования. Файл cpp пока не нужен.


Поскольку мы планируем использовать индексы (EBO) для рисования массивов треугольников, то понадобится свой класс и для них.

6. Под modeler добавим новый header файл Triangle01.h

Location – C:\CPP\engine\modeler

Код:

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

То же самое, не обращайте внимание на “лишние” переменные, обэясню их попозже, по мере использования. Файл cpp тоже пока не нужен.


Еще понадобится понятие групп вершин/треугольников для разных манипуляций.

7, Под modeler добавим новый header файл Group01.h

Location – C:\CPP\engine\modeler

Код:

#pragma once

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

В основном нам надо будет знать где текущая группа начинается, поэтому переменных “to” тут нет.


Еще понадобится какой-то описатель (descriptor), что (какую форму/shape) мы рисуем/строим. Назовем его VirtualShape. “Virtual” потому что сам-по-себе вершин он строить не будет.

8. Под modeler добавим новый header файл VirtualShape.h

Location – C:\CPP\engine\modeler

Код:

#pragma once
#include <string>

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

Главная операция/команда modeler-а будет buildFace() – построить проекцию. Например, скажем, “на левой стороне текущей VirtualShape“. Эта команда создаст 4 уже реальных вертекса на левой стороне виртуальной формы (в этом примере – кубика с размерами whl) и 2 соответствующих треугольника (6 индексов).


Вынесем некоторые сопутствующие вычисления в набор utils.

9. Откроем utils.h и заменим код на:

#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. Откроем utils.cpp и заменим код на:

#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);
}

Эта новая функция mat4x4_mul_vec4plus() – умножение вектора на матрицу. Отличие от linmath варианта в том, что избегает запись результатв в исходный вектор, плюс переустанавливает 4-ый элемент в исходном векторе. Если в ноль, то результирующий вектор будет просто развернут, без смещения (используется для расчета нормалей например).


В DrawJob добавлен новый функционал.

11. Откроем DrawJob.h и заменим код на:

#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);
};

Новая функция setDesirableOffsets(..) заполняет референсы атрибутов в DrawJob для заданного шейдера (AttribRef пока только для aPos и aTuv, позже атрибутов будет больше).


12. Откроем DrawJob.cpp и заменим код на:

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

Продолжение следует…


Leave a Reply

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