Наверное, пора уже подумать, где же брать реальные 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;
}
Продолжение следует…