Глава 13. Базовые классы 2

Windows

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


MyColor класс

Причина держать цвет в отдельном классе в том, что он будет нужен в ТРЕХ разных форматах:

  • как массив 4-х byte integers (от 0 до 255, как это хранится в файлах)
  • как массив 4-х floats (от 0.0 до 1.0, как требуется OpenGL-ом)
  • и как 32-bit integer (4 байта, по одному на каждый канал)

Любое изменение в любом из форматов должно отражаться в двух остальных.

2. Под xEngine добавим новый header (.h) файл MyColor.h

Location – C:\CPP\engine

Код:

#pragma once
#include "platform.h"

class MyColor
{
private:
    float rgba[4] = { 0,0,0,0 };
    union {
        myUint32 uint32value = 0;
        myUint8  channel[4]; //4 channels
    } RGBA;
public:
    float* forGL() { return rgba; };
    static void cloneIntToFloat(MyColor* pCl);
    static void cloneFloatToInt(MyColor* pCl);
    void setRGBA(float* rgba) { return setRGBA(this, rgba); };
    static void setRGBA(MyColor* pCl, float* rgba);
    void setRGBA(float r, float g, float b, float a) { return setRGBA(this, r, g, b, a); };
    static void setRGBA(MyColor* pCl, float r, float g, float b, float a);
    void setRGBA(int R, int G, int B, int A) { return setRGBA(this, R, G, B, A); };
    static void setRGBA(MyColor* pCl, int R, int G, int B, int A);
    void setRGBA(unsigned char* RGBA) { return setRGBA(this, RGBA); };
    static void setRGBA(MyColor* pCl, unsigned char* RGBA);
    void setUint32(unsigned int RGBA) { return setUint32(this, RGBA); };
    static void setUint32(MyColor* pCl, unsigned int RGBA);
    bool isZero() { return (RGBA.uint32value == 0); };
    bool isSet() { return (RGBA.uint32value != 0); };
    void clear() { setUint32(0); };
    unsigned int getUint32() { return (unsigned int)RGBA.uint32value; };
};

  • Заметим: у нас тут 2 собственных типа переменных: myUint32 and myUint8.
  • Мы не можем использовать “нормальные” _int32 или _int8 потому что они Windows-specific, в Андроиде они представлены по-другому.

3. Откроем platform.h и заменим код на:

#pragma once
#include <glad/glad.h>
#include <stdio.h>

typedef unsigned _int64 myUint64;
typedef unsigned _int32 myUint32;
typedef unsigned _int16 myUint16;
typedef unsigned _int8  myUint8;

void mylog(const char* _Format, ...);
void mySwapBuffers();
void myPollEvents();
int myFopen_s(FILE** pFile, const char* filePath, const char* mode);


4. Под xEngine добавим новый C++ File (.cpp) MyColor.cpp

Location – C:\CPP\engine

Код:

#include "MyColor.h"

float chanIntToFloat = 1.0f / 255;

void MyColor::cloneIntToFloat(MyColor* pCl) {
	for (int i = 0; i < 4; i++)
		pCl->rgba[i] = chanIntToFloat * pCl->RGBA.channel[i];
}
void MyColor::cloneFloatToInt(MyColor* pCl) {
	for (int i = 0; i < 4; i++)
		pCl->RGBA.channel[i] = (int)(pCl->rgba[i] * 255);
}
void MyColor::setRGBA(MyColor* pCl, float* rgba) {
	for (int i = 0; i < 4; i++)
		pCl->rgba[i] = rgba[i];
	cloneFloatToInt(pCl);
}
void MyColor::setRGBA(MyColor* pCl, float r, float g, float b, float a) {
	pCl->rgba[0] = r;
	pCl->rgba[1] = g;
	pCl->rgba[2] = b;
	pCl->rgba[3] = a;
	cloneFloatToInt(pCl);
}
void MyColor::setRGBA(MyColor* pCl, int R, int G, int B, int A) {
	pCl->RGBA.channel[0] = R;
	pCl->RGBA.channel[1] = G;
	pCl->RGBA.channel[2] = B;
	pCl->RGBA.channel[3] = A;
	cloneIntToFloat(pCl);
}
void MyColor::setRGBA(MyColor* pCl, unsigned char* RGBA) {
	for (int i = 0; i < 4; i++)
		pCl->RGBA.channel[i] = RGBA[i];
	cloneIntToFloat(pCl);
}
void MyColor::setUint32(MyColor* pCl, unsigned int RGBA) {
	pCl->RGBA.uint32value = RGBA;
	cloneIntToFloat(pCl);
}


Material класс

“Material” в нашем случае – это инструкция КАК рендрить, каким шейдером, какую текстуру или цвет, и т.д. Например, “Рисовать зеленую поверхность ucolor_flat шейдером” – это один Material. “Рисовать красную поверхность тем же шейдером” – уже другой. ЧТО рендрить – это отдельный вопрос. Пока у нас не так много что держать в этом классе, но потом будет больше.

5. Под xEngine добавим новый header (.h) файл Material.h

Location – C:\CPP\engine

Код:

#pragma once
#include "MyColor.h"

class Material
{
public:
	int shaderN = -1;
	int primitiveType = GL_TRIANGLES;
	MyColor uColor;
	int uTex0 = -1;
};


6. Под xEngine добавим новый C++ (.cpp) файл Material.cpp

Location – C:\CPP\engine

Пока оставим его пустым.


И теперь мы готовы к ключевому классу нашего движка, который будет представлять ЧТО рендрить. Также в нем будет и Material, так что он будет знать ЧТО рендрить и КАК. Назовем его DrawJob.

Каждый game subject будет иметь 1 или несколько DrawJobs. Например, хотим мы нарисовать синий кубик с картинкой на одной стороне. Значит, у subject-а будет 2 DrawJobs:

  • 5-сторонний меш со ссылкой на цвет и с шейдером ucolor_flat
  • 1-сторонний меш со ссылкой на текстуру и с шейдером tex_flat

Класс TheGame будет сканировать массив game subject-ов и их DrawJobs и будет их исполнять.

Каждый DrawJob должен иметь ссылки на задействованные массивы данных, в нашем примере – на один vertex_buffer VBO (Vertex Buffer Object, который мы сгенерировали из массива вертексов). Каждый элемент этого VBO состоит из 5 floats (а могло быть и 2 VBO: в одном xyz координаты, а в другом текстурные – tUV).

Также DrawJob должен иметь информацию как эти данные интерпретировать и использовать: для каждой задействованной переменной (таких как aPos или aTuv) он должен указать где ее искать (в каком VBO и ее позицию/offset/смещение внутри этого VBO). Для этого понадобится новая структура, назовем ее AttribRef (for Attribute Reference).

Сами VBO будут храниться отдельно (в GPU). В DrawJob же AttribRefs, если быть точнее) нам будет нужен только номер – VBO id.


DrawJob class

7. Под xEngine добавим новый header (.h) файл DrawJob.h

Location – C:\CPP\engine

Код:

#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

	//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) { return setDesirableOffsetsBasic(this, pStride, shaderN); };
	static int setDesirableOffsetsBasic(DrawJob* pDJ, int* pStride, int shaderN);

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


8. Под xEngine добавим новый C++ (.cpp) файл DrawJob.cpp

Location – C:\CPP\engine

Код:

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

int DrawJob::setDesirableOffsetsBasic(DrawJob* pDJ, int* pStride, int shaderN) {
	//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;
	return 1;
}
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);
	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());
	}
	glDrawArrays(pMt->primitiveType, 0, pDJ->pointsN);
	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;
}

Постарайтесь присмотреться к этому классу повнимательнее, особенно к структуре AttribRef и к ее использованию. Если пока трудновато, не волнуйтесь, всегда будет можно к этому вернуться.


9. Откроем TheGame.cpp и заменим код на:

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

extern std::string filesRoot;

static const struct
{
    float x, y, z, tu, tv;
} vertices[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
};

DrawJob* pDJ;
float angle_z = 0;

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

    pDJ = new DrawJob();
    //define material
    pDJ->mt.shaderN = Shader::spN_flat_tex; 
    pDJ->mt.primitiveType = GL_TRIANGLE_STRIP;
    pDJ->mt.uTex0 = Texture::loadTexture(filesRoot + "/dt/sample_img.png");
    //pDJ->mt.color0.setRGBA(1.f, 0.f, 1.f, 1.f);
    //pDJ->mt.color0.setRGBA(255, 0, 255,255);

    //build and attach data source(s)
    pDJ->pointsN = 4; //number of vertices
    unsigned int glVBOid = pDJ->newBufferId();
    glBindBuffer(GL_ARRAY_BUFFER, glVBOid);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //describe data source
    int stride = sizeof(float) * 5;
    //attributes references
    AttribRef* pAR;
    pAR = &pDJ->aPos; pAR->offset = 0;                 pAR->glVBOid = glVBOid; pAR->stride = stride;
    pAR = &pDJ->aTuv; pAR->offset = sizeof(float) * 3; pAR->glVBOid = glVBOid; pAR->stride = stride;

    //create and fill vertex attributes array (VAO)
    pDJ->buildVAO();
    //end of data stream setting, now it is stored in VAO
    return 1;
}
int TheGame::drawFrame() {
    myPollEvents();

    mat4x4 m, p, mvp;
    //glClearColor(0.0, 0.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    angle_z += 0.01f;
    mat4x4_identity(m);
    mat4x4_rotate_Z(m, m, angle_z);
    mat4x4_scale_aniso(m, m, 2.0, 1.0, 1.0);
    mat4x4_ortho(p, -screenRatio, screenRatio, -1.f, 1.f, 1.f, -1.f);
    mat4x4_mul(mvp, p, m);

    pDJ->execute((float*)mvp, 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;
}


10. Компиляция и запуск – отлично.


Теперь попробуем на

Андроиде

11. Пере-запускаем VS. Открываем C:\CPP\a998engine\p_android\p_android.sln.


12. Под xEndine добавим Existing Item

Идем в C:\CPP\engine

Выбираем

  • DrawJob.cpp
  • DrawJob.h
  • Material.cpp
  • Material.h
  • MyColor.cpp
  • MyColor.h

Add.


Теперь надо добавить Android’s myUint32/8 implementations.

13. Откроем platform.h и заменим код на:

#pragma once

typedef uint32_t myUint64;
typedef uint32_t myUint32;
typedef uint8_t  myUint16;
typedef uint8_t  myUint8;

void mylog(const char* _Format, ...);
void mySwapBuffers();
void myPollEvents();
int myFopen_s(FILE** pFile, const char* filePath, const char* mode);


14. Включаем, разблокируем и подключаем Android, разрешаем debugging.

Компиляция и запуск – отлично.


Leave a Reply

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