Windows
1. Start VS. Open C:\CPP\a998engine\p_windows\p_windows.sln
MyColor class
The reason to have it as a separate class, is that we will need to keep colors in THREE different formats:
- as an array of 4 single byte integers (from 0 to 255, as it is stored in files)
- as an array of 4 floats (from 0.0 to 1.0, as required by OpenGL)
- as a single 32-bit integer (4 bytes, 1 byte for each channel)
Any change to either of these formats must be reflected in the other two.
2. Under xEngine add new header (.h) file MyColor.h
Location - C:\CPP\engine
Code:
#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; };
};
- Please note: we are using here 2 custom defined variable types: myUint32 and myUint8.
- Can't use "normal" _int32 or _int8 because they are Windows-specific, Android has different definitions.
3. Open platform.h and replace code by following:
#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. Under xEngine add new C++ File (.cpp) file MyColor.cpp
Location - C:\CPP\engine
Code:
#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 class
"Material" in our case is an instruction HOW to render, which shader to use, which textures and colors, etc. For example, "Draw green surface with ucolor_flat shader" is one Material. "Draw red surface with same shader" is another one. WHAT to render - is a separate question. We don't have much to store in this class yet, but we will later.
5. Under xEngine add new header (.h) file Material.h
Location - C:\CPP\engine
Code:
#pragma once
#include "MyColor.h"
class Material
{
public:
int shaderN = -1;
int primitiveType = GL_TRIANGLES;
MyColor uColor;
int uTex0 = -1;
};
6. Under xEngine add new C++ (.cpp) file Material.cpp
Location - C:\CPP\engine
We can leave it empty so far.
Now we are ready for our engine's key class, which will represent WHAT to render. Also it will have Material info, so it will know WHAT to render and HOW. I will call it a DrawJob.
Each game subject can have 1 or a few DrawJobs. For example, let's say we want to draw a red cube with a picture on 1 side. So, the subject will have 2 DrawJobs:
- 5-sided mesh with color reference and ucolor_flat shader
- 1-sided mesh with texture reference and tex_flat shader
TheGame class will scan game subjects and their DrawJobs and will execute them.
Each DrawJob must have a reference to involved data sources, in our sample - a single vertex_buffer VBO (Vertex Buffer Object which was generated from vertices array). Each element of this VBO consists of 5 floats (alternatively it could be two arrays/VBOs: one keeping xyz info and another one - tUV).
Also, DrawJob must include instructions how to interpret these data: for each involved variable (such as aPos or aTuv) it must tell where it is located (in which VBO and a position/offset inside of this VBO). So, we'll need an extra structure, we'll call it AttribRef (for Attribute Reference).
VBOs themselves will reside separately (in GPU). In the DrawJob (in AttribRefs to be more accurate) we'll need only VBO ids.
DrawJob class
7. Under xEngine add new header (.h) file DrawJob.h
Location - C:\CPP\engine
Code:
#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. Under xEngine add new C++ (.cpp) file DrawJob.cpp
Location - C:\CPP\engine
Code:
#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;
}
Try to pay a bit closer attention to this class, especially to AttribRef structure and to it's usage. If it's too difficult now, don't worry, you can come back to this later.
9. 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 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. Build and run - good.
Now let's try on
Android
11. Re-start VS. Open C:\CPP\a998engine\p_android\p_android.sln.
12. Under xEndine add Existing Item
Go to C:\CPP\engine
Pick
- DrawJob.cpp
- DrawJob.h
- Material.cpp
- Material.h
- MyColor.cpp
- MyColor.h
Add.
Add Android's myUint32/8 implementations.
13. Open platform.h and replace code by:
#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. Switch on, unlock and plug in Android, allow debugging.
Build and run - perfect.