Our current rectangular model does not allow us to appreciate all the beauty of the Phong shader, in particular - the glares. For that we'll need something more round-shaped. Let's extend our Modeler accordingly.
Windows
1. Start VS. Open C:\CPP\a997modeler\p_windows\p_windows.sln.
First - we'll need to extend VirtualShape.h.
2. Open VirtualShape.h and replace code by:
#pragma once
#include "platform.h"
class VirtualShape
{
public:
char shapeType[20] = "box";
float whl[3] = { 0 }; //width/height/length (x,y,z sizes/dimensions)
int sections[3] = { 1,1,1 }; //number of sections for each axis
int sectionsR = 1; //number of radial sections
//side extensions
float extU = 0; //up
float extD = 0; //down
float extL = 0; //left
float extR = 0; //right
float extF = 0; //front
float extB = 0; //back
public:
void setShapeType(std::string needType) { setShapeType(this, needType); };
static void setShapeType(VirtualShape* pVS, std::string needType) { myStrcpy_s(pVS->shapeType, 20, (char*)needType.c_str()); };
void setExt(float v) { setExt(this, v); };
void setExtX(float v) { setExtX(this, v); };
void setExtY(float v) { setExtY(this, v); };
void setExtZ(float v) { setExtZ(this, v); };
static void setExt(VirtualShape* pVS, float v) { pVS->extU = v; pVS->extD = v; pVS->extL = v; pVS->extR = v; pVS->extF = v; pVS->extB = v; };
static void setExtX(VirtualShape* pVS, float v) { pVS->extL = v; pVS->extR = v; };
static void setExtY(VirtualShape* pVS, float v) { pVS->extU = v; pVS->extD = v; };
static void setExtZ(VirtualShape* pVS, float v) { pVS->extF = v; pVS->extB = v; };
};
New concept here is - "extensions". They are needed for shapes with rounded corners and edges:
Another change here - char* shapeType instead of std::string.
Have new platform-specific function here - myStrcpy_s(), char array copy. Same situation as with myFopen_s(). Another platform-specific function we will need soon is myMkDir(), making a directory.
3. Open platform.h and replace code by
#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);
int myMkDir(const char* outPath);
void myStrcpy_s(char* dst, int maxSize, const char* src);
4. Open platform.cpp and replace code by
#include <stdarg.h>
#include <stdio.h>
#include <GLFW/glfw3.h>
#include "platform.h"
#include "TheGame.h"
#include <direct.h> //myMkDir: mkdir
#include <sys/stat.h> //myMkDir: if file esists
extern GLFWwindow* myMainWindow;
extern TheGame theGame;
void mylog(const char* _Format, ...) {
#ifdef _DEBUG
va_list _ArgList;
va_start(_ArgList, _Format);
vprintf(_Format, _ArgList);
va_end(_ArgList);
#endif
};
void mySwapBuffers() {
glfwSwapBuffers(myMainWindow);
}
void myPollEvents() {
glfwPollEvents();
//check if closing the window
theGame.bExitGame = glfwWindowShouldClose(myMainWindow);
//check screen size
int width, height;
glfwGetFramebufferSize(myMainWindow, &width, &height);
theGame.onScreenResize(width, height);
}
int myFopen_s(FILE** pFile, const char* filePath, const char* mode) {
return fopen_s(pFile, filePath, mode);
}
int myMkDir(const char* outPath) {
struct stat info;
if (stat(outPath, &info) == 0)
return 0; //exists already
int status = _mkdir(outPath);
if (status == 0)
return 1; //Successfully created
mylog("ERROR creating, status=%d\n", status);
return -1;
}
void myStrcpy_s(char* dst, int maxSize, const char* src) {
strcpy_s(dst, maxSize, src);
//fill tail by zeros
int strLen = strlen(dst);
if (strLen < maxSize)
for (int i = strLen; i < maxSize; i++)
dst[i] = 0;
}
ModelBuilder class becomes dangerously big. We will split it in 2 layers. 1-st layer - basic functionality, we'll call it ModelBuilder1base.
Only 1 new function - moveGroupDg(…) - shifts and rotates vertex group (added to place extensions to their places)
5. Under modeler add new Header File (.h) ModelBuilder1base.h
Location: C:\CPP\engine\modeler\
Code:
#pragma once
#include <string>
#include <vector>
#include "Vertex01.h"
#include "Triangle01.h"
#include "VirtualShape.h"
#include "Group01.h"
#include "Material.h"
#include "GameSubj.h"
class ModelBuilder1base
{
public:
std::vector<Vertex01*> vertices;
std::vector<Triangle01*> triangles;
std::vector<VirtualShape*> vShapesStack;
std::vector<Group01*> groupsStack;
std::vector<Material*> materialsList;
std::vector<int> subjNumbersList;
int usingSubjN = -1;
int usingMaterialN = -1;
Group01* pCurrentGroup = NULL;
public:
virtual ~ModelBuilder1base();
int useSubjN(int subjN) { return useSubjN(this, subjN); };
static int useSubjN(ModelBuilder1base* pMB, int subjN);
int useMaterial(Material* pMT) { return useMaterial(this, pMT); };
static int useMaterial(ModelBuilder1base* pMB, Material* pMT);
static void lockGroup(ModelBuilder1base* pMB);
static void releaseGroup(ModelBuilder1base* pMB);
static int addVertex(ModelBuilder1base* pMB, float kx, float ky, float kz, float nx, float ny, float nz);
static int add2triangles(ModelBuilder1base* pMB, int nNW, int nNE, int nSW, int nSE, int n);
static int addTriangle(ModelBuilder1base* pMB, int n0, int n1, int n2);
int buildDrawJobs(std::vector<GameSubj*> gameSubjs) { return buildDrawJobs(this, gameSubjs); };
static int buildDrawJobs(ModelBuilder1base* pMB, std::vector<GameSubj*> gameSubjs);
static int rearrangeArraysForDrawJob(ModelBuilder1base* pMB, std::vector<Vertex01*> allVertices, std::vector<Vertex01*> useVertices, std::vector<Triangle01*> useTriangles);
static int buildSingleDrawJob(Material* pMT, std::vector<Vertex01*> useVertices, std::vector<Triangle01*> useTriangles);
static int moveGroupDg(ModelBuilder1base* pMB, float aX, float aY, float aZ, float kX, float kY, float kZ);
};
6. Under modeler add new C++ File (.cpp) ModelBuilder1base.cpp
Location: C:\CPP\engine\modeler\
Code:
#include "ModelBuilder1base.h"
#include "platform.h"
#include "utils.h"
#include "DrawJob.h"
#include "Shader.h"
extern float degrees2radians;
ModelBuilder1base::~ModelBuilder1base() {
//clear all vectors
int itemsN = vertices.size();
for (int i = 0; i < itemsN; i++)
delete vertices.at(i);
vertices.clear();
itemsN = triangles.size();
for (int i = 0; i < itemsN; i++)
delete triangles.at(i);
triangles.clear();
itemsN = vShapesStack.size();
for (int i = 0; i < itemsN; i++)
delete vShapesStack.at(i);
vShapesStack.clear();
itemsN = groupsStack.size();
for (int i = 0; i < itemsN; i++)
delete groupsStack.at(i);
groupsStack.clear();
itemsN = materialsList.size();
for (int i = 0; i < itemsN; i++)
delete materialsList.at(i);
materialsList.clear();
subjNumbersList.clear();
}
int ModelBuilder1base::useSubjN(ModelBuilder1base* pMB, int subjN) {
pMB->usingSubjN = subjN;
int itemsN = pMB->subjNumbersList.size();
bool newN = true;
if (itemsN > 0)
for (int i = 0; i < itemsN; i++)
if (pMB->subjNumbersList.at(i) == subjN) {
newN = false;
break;
}
if (newN)
pMB->subjNumbersList.push_back(subjN);
return subjN;
}
int ModelBuilder1base::useMaterial(ModelBuilder1base* pMB, Material* pMT) {
int itemsN = pMB->materialsList.size();
if (itemsN > 0)
for (int i = 0; i < itemsN; i++)
if (memcmp(pMB->materialsList.at(i), pMT, sizeof(Material)) == 0) {
pMB->usingMaterialN = i;
return i;
}
//if here - add new material to the list
pMB->usingMaterialN = itemsN;
//create a copy of new Material and add to the list
Material* pMTnew = new Material(*pMT);
pMB->materialsList.push_back(pMTnew);
return itemsN;
}
int ModelBuilder1base::add2triangles(ModelBuilder1base* pMB, int nNW, int nNE, int nSW, int nSE, int n) {
//indexes: NorthWest, NorthEast, SouthWest,SouthEast
if (n % 2 == 0) { //even number
addTriangle(pMB, nNW, nSW, nNE);
addTriangle(pMB, nNE, nSW, nSE);
}
else { //odd number
addTriangle(pMB, nNW, nSE, nNE);
addTriangle(pMB, nNW, nSW, nSE);
}
return pMB->triangles.size() - 1;
}
int ModelBuilder1base::addTriangle(ModelBuilder1base* pMB, int i0, int i1, int i2) {
Triangle01* pTR = new Triangle01();
pMB->triangles.push_back(pTR);
pTR->idx[0] = i0;
pTR->idx[1] = i1;
pTR->idx[2] = i2;
pTR->subjN = pMB->usingSubjN;
pTR->materialN = pMB->usingMaterialN;
return pMB->triangles.size() - 1;
}
void ModelBuilder1base::lockGroup(ModelBuilder1base* pMB) {
if (pMB->pCurrentGroup != NULL)
pMB->groupsStack.push_back(pMB->pCurrentGroup);
pMB->pCurrentGroup = new Group01();
pMB->pCurrentGroup->fromVertexN = pMB->vertices.size();
pMB->pCurrentGroup->fromTriangleN = pMB->triangles.size();
}
void ModelBuilder1base::releaseGroup(ModelBuilder1base* pMB) {
delete pMB->pCurrentGroup;
if (pMB->groupsStack.size() > 0) {
pMB->pCurrentGroup = pMB->groupsStack.back();
pMB->groupsStack.pop_back();
}
else
pMB->pCurrentGroup = NULL;
}
int ModelBuilder1base::addVertex(ModelBuilder1base* pMB, float kx, float ky, float kz, float nx, float ny, float nz) {
Vertex01* pVX = new Vertex01();
pMB->vertices.push_back(pVX);
pVX->aPos[0] = kx;
pVX->aPos[1] = ky;
pVX->aPos[2] = kz;
//normal
pVX->aNormal[0] = nx;
pVX->aNormal[1] = ny;
pVX->aNormal[2] = nz;
pVX->subjN = pMB->usingSubjN;
pVX->materialN = pMB->usingMaterialN;
return pMB->vertices.size() - 1;
}
int ModelBuilder1base::buildDrawJobs(ModelBuilder1base* pMB, std::vector<GameSubj*> gameSubjs) {
int totalSubjsN = pMB->subjNumbersList.size();
if (totalSubjsN < 1) {
pMB->subjNumbersList.push_back(-1);
totalSubjsN = 1;
}
int totalMaterialsN = pMB->materialsList.size();
if (totalSubjsN < 2 && totalMaterialsN < 2) {
//simple single DrawJob
Material* pMT = pMB->materialsList.at(0);
GameSubj* pGS = NULL;
int gsN = pMB->subjNumbersList.at(0);
if (gsN >= 0)
pGS = gameSubjs.at(gsN);
if (pGS != NULL)
pGS->djStartN = DrawJob::drawJobs.size();
buildSingleDrawJob(pMT, pMB->vertices, pMB->triangles);
if (pGS != NULL)
pGS->djTotalN = DrawJob::drawJobs.size() - pGS->djStartN;
return 1;
}
int totalVertsN = pMB->vertices.size();
int totalTrianglesN = pMB->triangles.size();
//clear flags
for (int vN = 0; vN < totalVertsN; vN++) {
Vertex01* pVX = pMB->vertices.at(vN);
pVX->flag = 0;
}
for (int tN = 0; tN < totalTrianglesN; tN++) {
Triangle01* pTR = pMB->triangles.at(tN);
pTR->flag = 0;
}
int addedDJs = 0;
for (int sN = 0; sN < totalSubjsN; sN++) {
GameSubj* pGS = NULL;
int gsN = pMB->subjNumbersList.at(sN);
if (gsN >= 0)
pGS = gameSubjs.at(gsN);
if (pGS != NULL)
pGS->djStartN = DrawJob::drawJobs.size();
for (int mtN = 0; mtN < totalMaterialsN; mtN++) {
Material* pMT = pMB->materialsList.at(mtN);
std::vector<Vertex01*> useVertices;
std::vector<Triangle01*> useTriangles;
for (int vN = 0; vN < totalVertsN; vN++) {
Vertex01* pVX = pMB->vertices.at(vN);
if (pVX->flag != 0)
continue;
if (pVX->subjN != gsN)
continue;
if (pVX->materialN != mtN)
continue;
//if here - make a copy
Vertex01* pVX2 = new Vertex01(*pVX);
useVertices.push_back(pVX2);
pVX2->altN = vN;
pVX->flag = 1;
if (pVX->endOfSequence > 0) {
rearrangeArraysForDrawJob(pMB, pMB->vertices, useVertices, useTriangles);
buildSingleDrawJob(pMT, useVertices, useTriangles);
addedDJs++;
//clear and proceed to next sequence
int useVerticesN = useVertices.size();
for (int i = 0; i < useVerticesN; i++)
delete useVertices.at(i);
useVertices.clear();
}
}
int useVerticesN = useVertices.size();
if (useVerticesN < 1)
continue; //to next material
//pick triangles
for (int tN = 0; tN < totalTrianglesN; tN++) {
Triangle01* pTR = pMB->triangles.at(tN);
if (pTR->flag != 0)
continue;
if (pTR->subjN != gsN)
continue;
if (pTR->materialN != mtN)
continue;
//if here - make a copy
Triangle01* pTR2 = new Triangle01(*pTR);
useTriangles.push_back(pTR2);
pTR->flag = 1;
}
rearrangeArraysForDrawJob(pMB, pMB->vertices, useVertices, useTriangles);
buildSingleDrawJob(pMT, useVertices, useTriangles);
addedDJs++;
//clear all for next material
for (int i = 0; i < useVerticesN; i++)
delete useVertices.at(i);
useVertices.clear();
int useTrianglesN = useTriangles.size();
for (int i = 0; i < useTrianglesN; i++)
delete useTriangles.at(i);
useTriangles.clear();
}
if (pGS != NULL)
pGS->djTotalN = DrawJob::drawJobs.size() - pGS->djStartN;
}
return addedDJs;
}
int ModelBuilder1base::buildSingleDrawJob(Material* pMT, std::vector<Vertex01*> useVertices, std::vector<Triangle01*> useTriangles) {
int totalVertsN = useVertices.size();
if (totalVertsN < 1)
return 1;
DrawJob* pDJ = new DrawJob();
//copy material to DJ
memcpy(&pDJ->mt, pMT, sizeof(Material));
//calculate VBO element size (stride) and variables offsets in VBO
int VBOid = DrawJob::newBufferId();
int stride = 0;
pDJ->setDesirableOffsets(&stride, pDJ->mt.shaderN, VBOid);
//create an array for VBO
int bufferSize = totalVertsN * stride;
float* vertsBuffer = new float[bufferSize];
//fill vertsBuffer
Shader* pSh = Shader::shaders.at(pDJ->mt.shaderN);
int floatSize = sizeof(float);
for (int vN = 0; vN < totalVertsN; vN++) {
Vertex01* pVX = useVertices.at(vN);
int idx = vN * stride / floatSize;
//pick data from vertex and move to the buffer
memcpy(&vertsBuffer[idx + pDJ->aPos.offset / floatSize], pVX->aPos, 3 * floatSize);
if (pSh->l_aNormal >= 0) //normal
memcpy(&vertsBuffer[idx + pDJ->aNormal.offset / floatSize], pVX->aNormal, 3 * floatSize);
if (pSh->l_aTuv >= 0) //attribute TUV (texture coordinates)
memcpy(&vertsBuffer[idx + pDJ->aTuv.offset / floatSize], pVX->aTuv, 2 * floatSize);
if (pSh->l_aTuv2 >= 0) //attribute TUV2 (normal maps)
memcpy(&vertsBuffer[idx + pDJ->aTuv2.offset / floatSize], pVX->aTuv2, 2 * floatSize);
if (pSh->l_aTangent >= 0)
memcpy(&vertsBuffer[idx + pDJ->aTangent.offset / floatSize], pVX->aTangent, 3 * floatSize);
if (pSh->l_aBinormal >= 0)
memcpy(&vertsBuffer[idx + pDJ->aBinormal.offset / floatSize], pVX->aBinormal, 3 * floatSize);
}
//buffer is ready, create VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOid);
glBufferData(GL_ARRAY_BUFFER, bufferSize * floatSize, vertsBuffer, GL_STATIC_DRAW);
delete[] vertsBuffer;
pDJ->pointsN = totalVertsN;
int totalTrianglesN = useTriangles.size();
if (totalTrianglesN > 0) {
//create EBO
int totalIndexesN = totalTrianglesN * 3;
//create buffer
GLushort* indexBuffer = new GLushort[totalIndexesN];
for (int tN = 0; tN < totalTrianglesN; tN++) {
Triangle01* pTR = useTriangles[tN];
int idx = tN * 3;
indexBuffer[idx + 0] = (GLushort)pTR->idx[0];
indexBuffer[idx + 1] = (GLushort)pTR->idx[1];
indexBuffer[idx + 2] = (GLushort)pTR->idx[2];
}
//buffer is ready, create IBO
pDJ->glEBOid = DrawJob::newBufferId();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pDJ->glEBOid);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, totalIndexesN * sizeof(GLushort), indexBuffer, GL_STATIC_DRAW);
delete[] indexBuffer;
pDJ->pointsN = totalIndexesN;
}
//create and fill vertex attributes array (VAO)
pDJ->buildVAO();
return 1;
}
int ModelBuilder1base::rearrangeArraysForDrawJob(ModelBuilder1base* pMB, std::vector<Vertex01*> allVertices, std::vector<Vertex01*> useVertices, std::vector<Triangle01*> useTriangles) {
int totalTrianglesN = useTriangles.size();
if (totalTrianglesN < 1)
return 0;
int totalVerticesN = useVertices.size();
//save new vertices order in original vertices array
//since triangles indices refer to original vertices order
for (int i = 0; i < totalVerticesN; i++) {
Vertex01* pVX1 = useVertices.at(i);
Vertex01* pVX0 = allVertices.at(pVX1->altN);
pVX0->altN = i;
}
//replace triangle original indices by new numbers saved in original vertices altN
for (int tN = 0; tN < totalTrianglesN; tN++) {
Triangle01* pTR = useTriangles.at(tN);
for (int i = 0; i < 3; i++) {
Vertex01* pVX0 = allVertices.at(pTR->idx[i]);
pTR->idx[i] = pVX0->altN;
}
}
return 1;
}
int ModelBuilder1base::moveGroupDg(ModelBuilder1base* pMB, float aX, float aY, float aZ, float kX, float kY, float kZ) {
//moves and rotates vertex group
//rotation angles are set in degrees
mat4x4 transformMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
mat4x4_translate(transformMatrix, kX, kY, kZ);
//rotation order: Z-X-Y
if (aY != 0) mat4x4_rotate_Y(transformMatrix, transformMatrix, degrees2radians * aY);
if (aX != 0) mat4x4_rotate_X(transformMatrix, transformMatrix, degrees2radians * aX);
if (aZ != 0) mat4x4_rotate_Z(transformMatrix, transformMatrix, degrees2radians * aZ);
int vertsN = pMB->vertices.size();
for (int i = pMB->pCurrentGroup->fromVertexN; i < vertsN; i++) {
Vertex01* pVX = pMB->vertices.at(i);
mat4x4_mul_vec4plus(pVX->aPos, transformMatrix, pVX->aPos, 1);
mat4x4_mul_vec4plus(pVX->aNormal, transformMatrix, pVX->aNormal, 0);
}
return 1;
}
Changes in ModelBuilder class:
a) Now it inherits ModelBuilder1base.
b) New ModelBuilder's functions:
- buildFace(…) - dispatches call depending on VirtualShape type
- buildBoxFace(…) - extended to handle "extensions"
- boxFacePlain(…) - draws simple rectangular box face
- boxFaceTank(…) - draws box face with rounded edges and corners
- cylinderWrap(…) - draws cylinder sector (for box edges extensions)
- capWrap(…) - draws "cap" (hemi-sphere) sector (for box corners extensions)
7. Open ModelBuilder.h and replace code by:
#pragma once
#include "ModelBuilder1base.h"
class ModelBuilder : public ModelBuilder1base
{
public:
virtual ~ModelBuilder();
static int buildFace(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS);
static int buildBoxFace(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS);
static int buildBoxFacePlain(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS);
static int buildBoxFaceTank(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS);
static int cylinderWrap(ModelBuilder* pMB, VirtualShape* pVS, float angleFrom, float angleTo);
static int capWrap(ModelBuilder* pMB, VirtualShape* pVS, float angleFrom, float angleTo);
};
8. Open ModelBuilder.cpp and replace code by:
#include "ModelBuilder.h"
#include "platform.h"
#include "utils.h"
#include "DrawJob.h"
#include "Shader.h"
extern float degrees2radians;
ModelBuilder::~ModelBuilder() {
}
int ModelBuilder::buildFace(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS) {
if (strstr(pVS->shapeType, "box") == pVS->shapeType)
return buildBoxFace(pMB, applyTo, pVS);
return -1;
}
int ModelBuilder::buildBoxFace(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS) {
//this code is for simple box
VirtualShape vs; //face VS,
mat4x4 transformMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
vs.sectionsR = pVS->sectionsR;
//rotate desirable side to face us.
if (applyTo.find("front") == 0) {
//Side <front> is facing us as is.
vs.whl[0] = pVS->whl[0];
vs.whl[1] = pVS->whl[1];
vs.sections[0] = pVS->sections[0];
vs.sections[1] = pVS->sections[1];
//extensions
vs.extF = pVS->extF;
vs.extL = pVS->extL;
vs.extR = pVS->extR;
vs.extU = pVS->extU;
vs.extD = pVS->extD;
//define how to move/place generated face back to the VirtualShape
//just shift closer to us by length/2
mat4x4_translate(transformMatrix, 0, 0, pVS->whl[2] / 2);
}
else if (applyTo.find("back") == 0) {
vs.whl[0] = pVS->whl[0];
vs.whl[1] = pVS->whl[1];
vs.sections[0] = pVS->sections[0];
vs.sections[1] = pVS->sections[1];
//extensions
vs.extF = pVS->extB;
vs.extL = pVS->extR;
vs.extR = pVS->extL;
vs.extU = pVS->extU;
vs.extD = pVS->extD;
//rotate 180 degrees around Y and shift farther from us by half-length
mat4x4_translate(transformMatrix, 0, 0, -pVS->whl[2] / 2);
mat4x4_rotate_Y(transformMatrix, transformMatrix, degrees2radians * 180);
}
else if (applyTo.find("left") == 0) {
vs.whl[0] = pVS->whl[2]; //width = original length
vs.whl[1] = pVS->whl[1];
vs.sections[0] = pVS->sections[2];
vs.sections[1] = pVS->sections[1];
//extensions
vs.extF = pVS->extL;
vs.extL = pVS->extB;
vs.extR = pVS->extF;
vs.extU = pVS->extU;
vs.extD = pVS->extD;
//rotate -90 degrees around Y (CW) and shift half-width to the left
mat4x4_translate(transformMatrix, -pVS->whl[0] / 2, 0, 0);
mat4x4_rotate_Y(transformMatrix, transformMatrix, -degrees2radians * 90);
}
else if (applyTo.find("right") == 0) {
vs.whl[0] = pVS->whl[2]; //width = original length
vs.whl[1] = pVS->whl[1];
vs.sections[0] = pVS->sections[2];
vs.sections[1] = pVS->sections[1];
//extensions
vs.extF = pVS->extR;
vs.extL = pVS->extF;
vs.extR = pVS->extB;
vs.extU = pVS->extU;
vs.extD = pVS->extD;
//rotate +90 degrees around Y (CCW) and shift half-width to the right
mat4x4_translate(transformMatrix, pVS->whl[0] / 2, 0, 0);
mat4x4_rotate_Y(transformMatrix, transformMatrix, degrees2radians * 90);
}
else if (applyTo.find("top") == 0) {
vs.whl[0] = pVS->whl[0];
vs.whl[1] = pVS->whl[2]; //height = original length
vs.sections[0] = pVS->sections[0];
vs.sections[1] = pVS->sections[2];
//extensions
vs.extF = pVS->extU;
vs.extL = pVS->extR;
vs.extR = pVS->extL;
vs.extU = pVS->extF;
vs.extD = pVS->extB;
//rotate -90 degrees around X (CW) and 180 around Y, and shift half-height up
mat4x4_translate(transformMatrix, 0, pVS->whl[1] / 2, 0);
mat4x4_rotate_Y(transformMatrix, transformMatrix, -degrees2radians * 180);
mat4x4_rotate_X(transformMatrix, transformMatrix, -degrees2radians * 90);
}
else if (applyTo.find("bottom") == 0) {
vs.whl[0] = pVS->whl[0];
vs.whl[1] = pVS->whl[2]; //height = original length
vs.sections[0] = pVS->sections[0];
vs.sections[1] = pVS->sections[2];
//extensions
vs.extF = pVS->extD;
vs.extL = pVS->extL;
vs.extR = pVS->extR;
vs.extU = pVS->extF;
vs.extD = pVS->extB;
//rotate 90 around X (CCW) and shift half-height down
mat4x4_translate(transformMatrix, 0, -pVS->whl[1] / 2, 0);
mat4x4_rotate_X(transformMatrix, transformMatrix, degrees2radians * 90);
}
lockGroup(pMB);
//create vertices
if (strstr(pVS->shapeType, "tank") != nullptr)
buildBoxFaceTank(pMB, applyTo, &vs);
else
buildBoxFacePlain(pMB, applyTo, &vs);
//move face to it's place (apply transform matrix)
int vertsN = pMB->vertices.size();
for (int i = pMB->pCurrentGroup->fromVertexN; i < vertsN; i++) {
Vertex01* pVX = pMB->vertices.at(i);
mat4x4_mul_vec4plus(pVX->aPos, transformMatrix, pVX->aPos, 1);
mat4x4_mul_vec4plus(pVX->aNormal, transformMatrix, pVX->aNormal, 0);
}
releaseGroup(pMB);
return 1;
}
int ModelBuilder::buildBoxFacePlain(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS) {
if (pVS->whl[0] == 0 || pVS->whl[1] == 0)
return 0;
//create vertices
int sectionsX = pVS->sections[0];
int sectionsY = pVS->sections[1];
int pointsX = sectionsX + 1;
int pointsY = sectionsY + 1;
float stepX = pVS->whl[0] / sectionsX;
float stepY = pVS->whl[1] / sectionsY;
float kY = pVS->whl[1] / 2;
for (int iy = 0; iy < pointsY; iy++) {
float kX = -pVS->whl[0] / 2;
for (int ix = 0; ix < pointsX; ix++) {
int nSE = addVertex(pMB, kX, kY, pVS->extF, 0, 0, 1); //vertex number on south-east
if (iy > 0 && ix > 0) {
//add 2 triangles
int nSW = nSE - 1; //vertex number south-west
int nNE = nSE - pointsX; //north-east
int nNW = nSW - pointsX; //north-west
add2triangles(pMB, nNW, nNE, nSW, nSE, iy + ix);
}
kX += stepX;
}
kY -= stepY;
}
return 1;
}
int ModelBuilder::buildBoxFaceTank(ModelBuilder* pMB, std::string applyTo, VirtualShape* pVS) {
//for diamond effect - sectionsRad=1, don't merge normals
bool drawMiddle = true;
//edges
bool drawTop = false;
bool drawBottom = false;
bool drawLeft = false;
bool drawRight = false;
//corners
bool drawTopLeft = false;
bool drawTopRight = false;
bool drawBottomLeft = false;
bool drawBottomRight = false;
if (pVS->extF == 0 || applyTo.find(" all") != std::string::npos) {
drawTop = true;
drawBottom = true;
drawLeft = true;
drawRight = true;
drawTopLeft = true;
drawTopRight = true;
drawBottomLeft = true;
drawBottomRight = true;
}
else if (applyTo.find(" h") != std::string::npos) {
drawLeft = true;
drawRight = true;
}
else if (applyTo.find(" v") != std::string::npos) {
drawTop = true;
drawBottom = true;
}
if (applyTo.find(" no") != std::string::npos) {
if (applyTo.find(" noM") != std::string::npos) {
//middle
if (applyTo.find(" noMrow") != std::string::npos) {
drawMiddle = false;
drawLeft = false;
drawRight = false;
}
if (applyTo.find(" noMcol") != std::string::npos) {
drawMiddle = false;
drawTop = false;
drawBottom = false;
}
if (applyTo.find(" noMid") != std::string::npos)
drawMiddle = false;
}
if (applyTo.find(" noN") != std::string::npos) {
//north
if (applyTo.find(" noNrow") != std::string::npos) {
drawTop = false;
drawTopLeft = false;
drawTopRight = false;
}
if (applyTo.find(" noNedge") != std::string::npos)
drawTop = false;
if (applyTo.find(" noNW") != std::string::npos)
drawTopLeft = false;
if (applyTo.find(" noNE") != std::string::npos)
drawTopRight = false;
}
if (applyTo.find(" noS") != std::string::npos) {
//south
if (applyTo.find(" noSrow") != std::string::npos) {
drawBottom = false;
drawBottomLeft = false;
drawBottomRight = false;
}
if (applyTo.find(" noSedge") != std::string::npos)
drawBottom = false;
if (applyTo.find(" noSW") != std::string::npos)
drawBottomLeft = false;
if (applyTo.find(" noSE") != std::string::npos)
drawBottomRight = false;
}
if (applyTo.find(" noW") != std::string::npos) {
//west
if (applyTo.find(" noWcol") != std::string::npos) {
drawLeft = false;
drawTopLeft = false;
drawBottomLeft = false;
}
if (applyTo.find(" noWedge") != std::string::npos)
drawLeft = false;
}
if (applyTo.find(" noE") != std::string::npos) {
//east
if (applyTo.find(" noEcol") != std::string::npos) {
drawRight = false;
drawTopRight = false;
drawBottomRight = false;
}
if (applyTo.find(" noEedge") != std::string::npos)
drawRight = false;
}
}
lockGroup(pMB);
//middle
if (pVS->whl[0] > 0 && pVS->whl[1] > 0 && drawMiddle) {
buildBoxFacePlain(pMB, applyTo, pVS);
}
VirtualShape vs;
//edges
//vs.type.assign("cylinder");
vs.sectionsR = pVS->sectionsR;
if (pVS->whl[0] > 0) {
vs.sections[2] = pVS->sections[0]; //cylinder Z sections n
vs.whl[2] = pVS->whl[0]; //cylinder length Z
vs.whl[0] = pVS->extF * 2; //cylinder diameter X
if (pVS->extU > 0 && drawTop) {
vs.whl[1] = pVS->extU * 2; //cylinder diameter Y
lockGroup(pMB);
cylinderWrap(pMB, &vs, 0, 90);
//rotate -90 degrees around Y and shift up
moveGroupDg(pMB, 0, -90, 0, 0, pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
if (pVS->extD > 0 && drawBottom) {
vs.whl[1] = pVS->extD * 2; //cylinder diameter Y
lockGroup(pMB);
cylinderWrap(pMB, &vs, -90, 0);
//rotate -90 degrees around Y and shift down
moveGroupDg(pMB, 0, -90, 0, 0, -pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
}
if (pVS->whl[1] > 0) {
vs.sections[2] = pVS->sections[1]; //cylinder Z sections n
vs.whl[2] = pVS->whl[1]; //cylinder length Z
vs.whl[1] = pVS->extF * 2; //cylinder diameter Y
if (pVS->extL > 0 && drawLeft) {
vs.whl[0] = pVS->extL * 2; //cylinder diameter X
lockGroup(pMB);
cylinderWrap(pMB, &vs, 90, 180);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 90, 0, 0, -pVS->whl[0] * 0.5f, 0, 0);
releaseGroup(pMB);
}
if (pVS->extR > 0 && drawRight) {
vs.whl[0] = pVS->extR * 2; //cylinder diameter X
lockGroup(pMB);
cylinderWrap(pMB, &vs, 0, 90);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 90, 0, 0, pVS->whl[0] * 0.5f, 0, 0);
releaseGroup(pMB);
}
}
//corners
//vs.type.assign("cap");
vs.sectionsR = pVS->sectionsR;
vs.sections[2] = pVS->sectionsR;
vs.whl[2] = pVS->extF;
if (pVS->extU > 0) {
//top corners
vs.whl[1] = pVS->extU * 2;
if (pVS->extL > 0 && drawTopLeft) {
vs.whl[0] = pVS->extL * 2;
lockGroup(pMB);
capWrap(pMB, &vs, 90, 180);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 0, 0, 0, -pVS->whl[0] * 0.5f, pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
if (pVS->extR > 0 && drawTopRight) {
vs.whl[0] = pVS->extR * 2;
lockGroup(pMB);
capWrap(pMB, &vs, 0, 90);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 0, 0, 0, pVS->whl[0] * 0.5f, pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
}
if (pVS->extD > 0) {
//bottom corners
vs.whl[1] = pVS->extD * 2;
if (pVS->extL > 0 && drawBottomLeft) {
vs.whl[0] = pVS->extL * 2;
lockGroup(pMB);
capWrap(pMB, &vs, -180, -90);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 0, 0, 0, -pVS->whl[0] * 0.5f, -pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
if (pVS->extR > 0 && drawBottomRight) {
vs.whl[0] = pVS->extR * 2;
lockGroup(pMB);
capWrap(pMB, &vs, -90, 0);
//rotate 90 degrees around Y and shift left
moveGroupDg(pMB, 0, 0, 0, pVS->whl[0] * 0.5f, -pVS->whl[1] * 0.5f, 0);
releaseGroup(pMB);
}
}
if (pVS->extF == 0) {
int vertsN = pMB->vertices.size();
for (int i = pMB->pCurrentGroup->fromVertexN; i < vertsN; i++) {
Vertex01* pVX = pMB->vertices.at(i);
//normal
v3set(pVX->aNormal, 0, 0, 1);
}
}
releaseGroup(pMB);
return 1;
}
int ModelBuilder::cylinderWrap(ModelBuilder* pMB, VirtualShape* pVS, float angleFrom, float angleTo) {
// angleFrom/To - in degrees
lockGroup(pMB);
float stepZ = pVS->whl[2] / pVS->sections[2];
float stepDg = (angleTo - angleFrom) / pVS->sectionsR; //in degrees
for (int nz = 0; nz <= pVS->sections[2]; nz++) {
float kz = stepZ * nz - pVS->whl[2] * 0.5f;
for (int rpn = 0; rpn <= pVS->sectionsR; rpn++) {
// rpn - radial point number
float angleRd = (angleFrom + stepDg * rpn) * degrees2radians;
float kx = cosf(angleRd);
float ky = sinf(angleRd);
int nSE = addVertex(pMB, kx, ky, kz, kx, ky, 0);
if (nz > 0 && rpn > 0) {
int nSW = nSE - 1;
int nNW = nSW - pVS->sectionsR - 1;
int nNE = nSE - pVS->sectionsR - 1;
add2triangles(pMB, nNE, nNW, nSE, nSW, nz + rpn);
}
}
}
//scale to desirable diameters
mat4x4 transformMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
mat4x4_scale_aniso(transformMatrix, transformMatrix, pVS->whl[0] * 0.5f, pVS->whl[1] * 0.5f, 1);
int vertsN = pMB->vertices.size();
for (int i = pMB->pCurrentGroup->fromVertexN; i < vertsN; i++) {
Vertex01* pVX = pMB->vertices.at(i);
mat4x4_mul_vec4plus(pVX->aPos, transformMatrix, pVX->aPos, 1);
}
releaseGroup(pMB);
return 1;
}
int ModelBuilder::capWrap(ModelBuilder* pMB, VirtualShape* pVS, float angleFrom, float angleTo) {
// angleFrom/To - in degrees
lockGroup(pMB);
//center point
int n0 = addVertex(pMB, 0, 0, 1, 0, 0, 1);
float stepZdg = 90.0f / pVS->sections[2]; //in degrees
float stepRdg = (angleTo - angleFrom) / pVS->sectionsR; //in degrees
for (int nz = 1; nz <= pVS->sections[2]; nz++) {
float angleZrd = stepZdg * nz * degrees2radians;
float kz = cosf(angleZrd);
float R = sinf(angleZrd);
for (int rpn = 0; rpn <= pVS->sectionsR; rpn++) {
// rpn - radial point number
float angleRd = (angleFrom + stepRdg * rpn) * degrees2radians;
float kx = cosf(angleRd) * R;
float ky = sinf(angleRd) * R;
int nSE = addVertex(pMB, kx, ky, kz, kx, ky, kz);
if (rpn > 0) {
if (nz == 1) {
int nSW = nSE - 1;
addTriangle(pMB, n0, nSW, nSE);
}
else {
int nSW = nSE - 1;
int nNW = nSW - pVS->sectionsR - 1;
int nNE = nSE - pVS->sectionsR - 1;
add2triangles(pMB, nNW, nNE, nSW, nSE, nz + rpn);
}
}
}
}
//scale to desirable diameters
mat4x4 transformMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
mat4x4_scale_aniso(transformMatrix, transformMatrix, pVS->whl[0] * 0.5f, pVS->whl[1] * 0.5f, pVS->whl[2]);
int vertsN = pMB->vertices.size();
for (int i = pMB->pCurrentGroup->fromVertexN; i < vertsN; i++) {
Vertex01* pVX = pMB->vertices.at(i);
mat4x4_mul_vec4plus(pVX->aPos, transformMatrix, pVX->aPos, 1);
}
releaseGroup(pMB);
return 1;
}
9. Open TheGame.cpp and replace code by:
#include "TheGame.h"
#include "platform.h"
#include "utils.h"
#include "linmath.h"
#include "Texture.h"
#include "Shader.h"
#include "DrawJob.h"
#include "ModelBuilder.h"
extern std::string filesRoot;
std::vector<GameSubj*> TheGame::gameSubjs;
int TheGame::getReady() {
bExitGame = false;
Shader::loadShaders();
glEnable(GL_CULL_FACE);
//=== create box ========================
GameSubj* pGS = new GameSubj();
gameSubjs.push_back(pGS);
pGS->name.assign("box1");
pGS->ownCoords.setPosition(0, 0, 0);
pGS->ownCoords.setDegrees(0, 0, 0);
pGS->ownSpeed.setDegrees(0,1,0);
ModelBuilder* pMB = new ModelBuilder();
pMB->useSubjN(gameSubjs.size() - 1);
//define VirtualShape
VirtualShape vs;
vs.setShapeType("box-tank");
vs.whl[0] = 60;
vs.whl[1] = 160;
vs.whl[2] = 390;
vs.setExt(20);
vs.extD = 0;
vs.extF = 0; //to make front face "flat"
vs.sectionsR = 2;
Material mt;
//define material - flat red
mt.shaderN = Shader::spN_phong_ucolor;
mt.primitiveType = GL_TRIANGLES;
mt.uColor.setRGBA(255, 0, 0,255); //red
pMB->useMaterial(&mt);
pMB->buildBoxFace(pMB,"front v", &vs);
pMB->buildBoxFace(pMB, "back v", &vs);
pMB->buildBoxFace(pMB, "top", &vs);
pMB->buildBoxFace(pMB, "bottom", &vs);
pMB->buildBoxFace(pMB, "left all", &vs);
mt.uColor.setRGBA(0, 0, 255,255); pMB->useMaterial(&mt); //blue
pMB->buildBoxFace(pMB, "right all", &vs);
pMB->buildDrawJobs(gameSubjs);
delete pMB;
//===== set up camera
v3set(mainCamera.ownCoords.pos, 0, 200, 1000); //set position
float cameraDir[3];
v3set(cameraDir, 0, -200, -1000); //set direction vector
float cameraYawDg = v3yawDg(cameraDir);
float cameraPitchDg = v3pitchDg(cameraDir);
//mylog("cameraYaw=%f, cameraPitch=%f\n", cameraYawDg, cameraPitchDg);
mainCamera.ownCoords.setDegrees(cameraPitchDg, cameraYawDg, 0);
float cameraUp[4] = { 0,1,0,0 }; //y - up
mat4x4_mul_vec4plus(cameraUp, *mainCamera.ownCoords.getRotationMatrix(), cameraUp, 0);
mat4x4_look_at(mainCamera.lookAtMatrix, mainCamera.ownCoords.pos, pGS->ownCoords.pos, cameraUp);
//===== set up light
v3set(dirToMainLight, -1, 1, 1);
vec3_norm(dirToMainLight, dirToMainLight);
return 1;
}
int TheGame::drawFrame() {
myPollEvents();
//glClearColor(0.0, 0.0, 0.5, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//calculate halfVector
float dirToCamera[4] = { 0,0,-1,0 }; //-z
mat4x4_mul_vec4plus(dirToCamera, *mainCamera.ownCoords.getRotationMatrix(), dirToCamera, 0);
float uHalfVector[4] = { 0,0,0,0 };
for (int i = 0; i < 3; i++)
uHalfVector[i] = (dirToCamera[i] + dirToMainLight[i]) / 2;
vec3_norm(uHalfVector, uHalfVector);
mat4x4 mProjection, mViewProjection, mMVP, mMV4x4;
//mat4x4_ortho(mProjection, -(float)screenSize[0] / 2, (float)screenSize[0] / 2, -(float)screenSize[1] / 2, (float)screenSize[1] / 2, 100.f, 500.f);
mat4x4_perspective(mProjection, 3.14f / 6.0f, (float)screenSize[0] / screenSize[1], 700.f, 1300.f);
mat4x4_mul(mViewProjection, mProjection, mainCamera.lookAtMatrix);
//mViewProjection[1][3] = 0; //keystone effect
//scan subjects
int subjsN = gameSubjs.size();
for (int subjN = 0; subjN < subjsN; subjN++) {
GameSubj* pGS = gameSubjs.at(subjN);
//behavior - apply rotation speed
pGS->moveSubj();
//prepare subject for rendering
pGS->buildModelMatrix(pGS);
//build MVP matrix for given subject
mat4x4_mul(mMVP, mViewProjection, pGS->ownModelMatrix);
//build Model-View (rotation) matrix for normals
mat4x4_mul(mMV4x4, mainCamera.lookAtMatrix, (vec4*)pGS->ownCoords.getRotationMatrix());
//convert to 3x3 matrix
float mMV3x3[3][3];
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
mMV3x3[y][x] = mMV4x4[y][x];
//render subject
for (int i = 0; i < pGS->djTotalN; i++) {
DrawJob* pDJ = DrawJob::drawJobs.at(pGS->djStartN + i);
pDJ->execute((float*)mMVP, *mMV3x3, dirToMainLight, uHalfVector, NULL);
}
}
mySwapBuffers();
return 1;
}
int TheGame::cleanUp() {
int itemsN = gameSubjs.size();
//delete all UISubjs
for (int i = 0; i < itemsN; i++) {
GameSubj* pGS = gameSubjs.at(i);
delete pGS;
}
gameSubjs.clear();
//clear all other classes
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;
}
The difference with previous version is:
TheGame::getReady() now builds not just a "box", but "box-tank" (box with rounded edges and corners), lines from 33 to 55.
Please note, that now instead of plain "front", "right", etc. we're also using "front v", "right all", etc. This is about "extensions":
10. Build and run:
Before:
After:
Now - with glares.
Android
Here we will need to add Android's myMkDir() and myStrcpy_s() implementations.
11. Re-start VS. Open C:\CPP\a997modeler\p_android\p_android.sln.
12. 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);
int myMkDir(const char* outPath);
void myStrcpy_s(char* dst, int maxSize, const char* src);
13. Open platform.cpp and replace code by
#include <android/log.h>
#include "stdio.h"
#include "TheGame.h"
#include <sys/stat.h> //mkdir for Android
extern struct android_app* androidApp;
extern const ASensor* accelerometerSensor;
extern ASensorEventQueue* sensorEventQueue;
extern EGLDisplay androidDisplay;
extern EGLSurface androidSurface;
extern TheGame theGame;
void mylog(const char* _Format, ...) {
#ifdef _DEBUG
char outStr[1024];
va_list _ArgList;
va_start(_ArgList, _Format);
vsprintf(outStr, _Format, _ArgList);
__android_log_print(ANDROID_LOG_INFO, "mylog", outStr, NULL);
va_end(_ArgList);
#endif
};
void mySwapBuffers() {
eglSwapBuffers(androidDisplay, androidSurface);
}
void myPollEvents() {
// Read all pending events.
int ident;
int events;
struct android_poll_source* source;
// If not animating, we will block forever waiting for events.
// If animating, we loop until all events are read, then continue
// to draw the next frame of animation.
while ((ident = ALooper_pollAll(0, NULL, &events,
(void**)&source)) >= 0) {
// Process this event.
if (source != NULL) {
source->process(androidApp, source);
}
// If a sensor has data, process it now.
if (ident == LOOPER_ID_USER) {
if (accelerometerSensor != NULL) {
ASensorEvent event;
while (ASensorEventQueue_getEvents(sensorEventQueue,
&event, 1) > 0) {
//LOGI("accelerometer: x=%f y=%f z=%f",
// event.acceleration.x, event.acceleration.y,
// event.acceleration.z);
}
}
}
// Check if we are exiting.
if (androidApp->destroyRequested != 0) {
theGame.bExitGame = true;
break;
}
}
}
int myFopen_s(FILE** pFile, const char* filePath, const char* mode) {
*pFile = fopen(filePath, mode);
if (*pFile == NULL) {
mylog("ERROR: can't open file %s\n", filePath);
return -1;
}
return 1;
}
int myMkDir(const char* outPath) {
struct stat info;
if (stat(outPath, &info) == 0)
return 0; //exists already
int status = mkdir(outPath, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
if (status == 0)
return 1; //Successfully created
mylog("ERROR creating, status=%d, errno: %s.\n", status, std::strerror(errno));
return -1;
}
void myStrcpy_s(char* dst, int maxSize, const char* src) {
strcpy(dst, src);
//fill tail by zeros
int strLen = strlen(dst);
if (strLen < maxSize)
for (int i = strLen; i < maxSize; i++)
dst[i] = 0;
}
14. Under modeler add Existing Item
from C:\CPP\engine\modeler
- ModelBuilder1base.cpp
- ModelBuilder1base.h
Add.
15. Switch on, unlock, plug in, allow.
Rebuild solution. Run.
Good.