Our next goal is to move model building process out of TheGame class.
We will need some kind of models text descriptor, XML format seems quite suitable. We will keep models descriptions outside of executable, in /dt folder. Then we'll need a class that will load, read and process these descriptors and build actual 3D models in the memory.
We already have FileLoader class. Based on it, we will create a class that can parse XML formatted txt files. Will call it XMLParser. It will be a part of engine.
1. Start VS, open C:\CPP\a997modeler\p_windows\p_windows.sln.
2. Under xEngine add new header file XMLParser.h
Location: C:\CPP\engine
Code:
#pragma once
#include "FileLoader.h"
class XMLparser : public FileLoader
{
public:
char* readFrom;
std::string currentTag = "";
int tagLength = 0;
std::string tagName = "";
bool closedTag = false; // > or />
public:
XMLparser(std::string filePath) : FileLoader(filePath) { removeComments(this); readFrom = pData; };
static int removeComments(XMLparser* pXP);
static int processSource(XMLparser* pXP);
int nextTag() { return nextTag(this); }; //returns 0 if no more tags, 1 - tag extractedb
static bool nextTag(XMLparser* pXP); //returns 0 if no more tags, 1 - tag extractedb
static int nameEndsAt(std::string varName, std::string tag);
virtual int processTag() { return 1; };
static std::string buildFullPath(XMLparser* pXP, std::string filePath);
static bool varExists(std::string varName, std::string tag);
static std::string getStringValue(std::string varName, std::string tag);
static int setCharsValue(char* pChars, int charsLength, std::string varName, std::string tag);
static int setIntValue(int* pInt, std::string varName, std::string tag);
static int setFloatValue(float* pFloat, std::string varName, std::string tag);
static int setIntArray(int* pInts, int arrayLength, std::string varName, std::string tag);
static int setFloatArray(float* pFloats, int arrayLength, std::string varName, std::string tag);
static int setUintColorValue(unsigned int* pInt, std::string varName, std::string tag);
static int setIntBoolValue(int* pInt, std::string varName, std::string tag);
};
Please note, it inherits FileLoader class.
3. Under xEngine add new C++ file XMLParser.cpp
Location: C:\CPP\engine
Code:
#include "XMLparser.h"
#include "platform.h"
#include "utils.h"
#include "MyColor.h"
#include <vector>
extern std::string filesRoot;
int XMLparser::removeComments(XMLparser* pXP) {
//find all occurances of "/*"
std::vector<char*> commentsStarts;
// /* comments */
char* scanFrom = pXP->pData;
while (1) {
char* commentStarts = strstr(scanFrom, "/*");
if (commentStarts == NULL)
break;
commentsStarts.push_back(commentStarts);
scanFrom = commentStarts + 2;
}
//here we have a list of /* comments */
while(commentsStarts.size() > 0){
//get last comment
char* commentStarts = commentsStarts.back();
commentsStarts.pop_back();
char* commentEnds = strstr(commentStarts, "*/");
int commentLength = (int)(commentEnds - commentStarts) + 2;
//shift text left
myStrcpy_s(commentStarts, pXP->dataSize - (int)(commentStarts - pXP->pData), commentStarts + commentLength);
pXP->dataSize -= commentLength;
//mylog("======\n%s----------\n", pXP->pData);
}
// //line comments
scanFrom = pXP->pData;
while (1) {
char* commentStarts = strstr(scanFrom, "//");
if (commentStarts == NULL)
break;
char* commentEnds = strstr(commentStarts, "\n");
int commentLength = (int)(commentEnds - commentStarts) + 1;
//shift text left
myStrcpy_s(commentStarts, pXP->dataSize - (int)(commentStarts - pXP->pData), commentStarts + commentLength);
pXP->dataSize -= commentLength;
scanFrom = commentStarts;
//mylog("======\n%s----------\n", pXP->pData);
}
// <!-- comments -->
scanFrom = pXP->pData;
while (1) {
char* commentStarts = strstr(scanFrom, "<!--");
if (commentStarts == NULL)
break;
commentsStarts.push_back(commentStarts);
scanFrom = commentStarts + 4;
}
//here we have a list of <!-- comments -->
while (commentsStarts.size() > 0) {
//get last comment
char* commentStarts = commentsStarts.back();
commentsStarts.pop_back();
char* commentEnds = strstr(commentStarts, "-->");
int commentLength = (int)(commentEnds - commentStarts) + 3;
//shift text left
myStrcpy_s(commentStarts, pXP->dataSize - (int)(commentStarts - pXP->pData), commentStarts + commentLength);
pXP->dataSize -= commentLength;
//mylog("======\n%s----------\n", pXP->pData);
}
return 1;
}
bool XMLparser::nextTag(XMLparser* pXP) {
//returns 0 if no more tags, 1 - tag extracted
char* tagStarts = strstr(pXP->readFrom, "<");
if (tagStarts == NULL)
return false;
pXP->readFrom++;
char* tagEnds = strstr(pXP->readFrom, ">");
if (tagEnds == NULL)
return false;
pXP->readFrom = tagEnds + 1;
pXP->tagLength = (int)(tagEnds - tagStarts) + 1;
pXP->currentTag.assign(tagStarts, pXP->tagLength);
return true;
}
int XMLparser::nameEndsAt(std::string varName, std::string tag) {
int scanFrom = 0;
int nameLength = varName.length();
std::string optsBefore = "< ";
std::string optsAfter = " =/>\n";
while (1) {
int varStartsAt = tag.find(varName, scanFrom);
if (varStartsAt == std::string::npos)
return -1;
scanFrom = varStartsAt + nameLength;
char charBefore = tag.at(varStartsAt - 1);
if (optsBefore.find(charBefore) == std::string::npos)
continue;
char charAfter = tag.at(scanFrom);
if (optsAfter.find(charAfter) == std::string::npos)
continue;
return scanFrom;
}
}
int XMLparser::processSource(XMLparser* pXP) {
while (pXP->nextTag()) {
//extract tagName
int nameStart = pXP->currentTag.find_first_not_of(" <");
int nameEnd = pXP->currentTag.find_first_of(" =/>\n", nameStart+1);
pXP->tagName = pXP->currentTag.substr(nameStart, (nameEnd - nameStart));
//open/closed tag
char lastChar = pXP->currentTag.at(pXP->tagLength - 2);
pXP->closedTag = (lastChar == '/');
//mylog("[%s] [%s] closed=%d nameStart=%d nameEnd=%d\n", pXP->currentTag.c_str(), pXP->tagName.c_str(), pXP->closedTag, nameStart, nameEnd);
pXP->processTag();
}
return 1;
}
std::string XMLparser::buildFullPath(XMLparser* pXP, std::string filePath) {
if (filePath.at(0) != '/')
filePath = pXP->inAppFolder + filePath;
return (filesRoot + filePath);
}
std::string XMLparser::getStringValue(std::string varName, std::string tag) {
//returns std::string
int valueStartsAt = nameEndsAt(varName, tag);
if (valueStartsAt < 0)
return ""; //var not found
valueStartsAt = tag.find_first_not_of(" ", valueStartsAt);
char c = tag.at(valueStartsAt);
if (c != '=')
return ""; //var exists, but value not set
valueStartsAt = tag.find_first_not_of(" ", valueStartsAt + 1);
c = tag.at(valueStartsAt);
std::string optsQuote = "\"'";
int valueEndsAt = 0;
if (optsQuote.find(c) != std::string::npos) {
//the value is in quotes
valueStartsAt++;
valueEndsAt = tag.find(c, valueStartsAt);
}
else { //value not quoted
valueEndsAt = tag.find_first_of(" />\n", valueStartsAt);
}
return tag.substr(valueStartsAt, valueEndsAt - valueStartsAt);
}
bool XMLparser::varExists(std::string varName, std::string tag) {
int valueStartsAt = nameEndsAt(varName, tag);
if (valueStartsAt < 0)
return false; //var not found
return true;
}
int XMLparser::setCharsValue(char* pChars, int charsLength, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string val = getStringValue(varName, tag);
myStrcpy_s(pChars, charsLength, (char*)val.c_str());
return 1;
}
int XMLparser::setIntValue(int* pInt, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string val = getStringValue(varName, tag);
*pInt = stoi(val);
return 1;
}
int XMLparser::setFloatValue(float* pFloat, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string val = getStringValue(varName, tag);
*pFloat = stof(val);
return 1;
}
int XMLparser::setFloatArray(float* pFloats, int arrayLength, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string valuesString = getStringValue(varName, tag);
std::vector<std::string> valuesVector = splitString(valuesString, ",");
int valsN = valuesVector.size();
if (valsN != arrayLength) {
mylog("ERROR in XMLparser::getFloatArray, %s, %s\n", varName.c_str(), tag.c_str());
return -1;
}
for (int i = 0; i < valsN; i++) {
if (valuesVector.at(i).compare("x") != 0)
pFloats[i] = stof(valuesVector.at(i));
}
return 1;
}int XMLparser::setIntArray(int* pInts, int arrayLength, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string valuesString = getStringValue(varName, tag);
std::vector<std::string> valuesVector = splitString(valuesString, ",");
int valsN = valuesVector.size();
if (valsN != arrayLength) {
mylog("ERROR in XMLparser::getIntArray, %s, %s\n", varName.c_str(), tag.c_str());
return -1;
}
for (int i = 0; i < valsN; i++) {
if(valuesVector.at(i).compare("x") != 0)
pInts[i] = stoi(valuesVector.at(i));
}
return 1;
}
int XMLparser::setUintColorValue(unsigned int* pInt, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
MyColor clr;
std::string val = getStringValue(varName, tag);
if (val.at(0) == '#') {
//the value is in HTML HEX format (like #ff0000)
int r = std::stoi(val.substr(1, 2), nullptr, 16);
int g = std::stoi(val.substr(3, 2), nullptr, 16);
int b = std::stoi(val.substr(5, 2), nullptr, 16);
int a = 255;
if (val.size() > 7)
a = std::stoi(val.substr(7, 2), nullptr, 16);
clr.setRGBA(r, g, b, a);
}
else if (val.find(",") > 0) {
//the value is an array of ints (?)
std::vector<std::string> valuesVector = splitString(val, ",");
int r = std::stoi(valuesVector[0]);
int g = std::stoi(valuesVector[1]);
int b = std::stoi(valuesVector[2]);
int a = 255;
if (valuesVector.size() > 3)
a = std::stoi(valuesVector[3]);
clr.setRGBA(r, g, b, a);
}
else {
mylog("ERROR in XMLparser::setUintColorValue: unhandled Color format %s\n",tag.c_str());
return -1;
}
*pInt = clr.getUint32();
return 1;
}
int XMLparser::setIntBoolValue(int* pInt, std::string varName, std::string tag) {
if (!varExists(varName, tag))
return 0; //var not found
std::string val = getStringValue(varName, tag);
if (val.compare("") == 0) *pInt = 1;
else if (val.compare("1") == 0) *pInt = 1;
else if (val.compare("0") == 0) *pInt = 0;
else if (val.compare("yes") == 0) *pInt = 1;
else if (val.compare("no") == 0) *pInt = 0;
else if (val.compare("true") == 0) *pInt = 1;
else if (val.compare("false") == 0) *pInt = 0;
else {
mylog("ERROR in XMLparser::setIntBoolValue, %s=%s in %s\n",varName.c_str(),val.c_str(),tag.c_str());
return -1;
}
return 1;
}
XMLParser class will load given xml/txt file, will remove comments, then will scan it tag by tag and will call virtual processTag() function for each tag for execution. This function is virtual because XMLParser by itself doesn't know what to do with these tags. Sibling class ModelLoader will know and will have it's own processTag() implementation.
Also XMLParser class has a set of functions for reading tag's variables/properties values.
Now, using XMLParser, we will create ModelLoader class, which will read given XML descriptor file and will execute it's instructions in ModelBuilder. This class will belong to the modeler.
4. Under modeler add new header file ModelLoader.h
Location: C:\CPP\engine\modeler
Code:
#pragma once
#include "XMLparser.h"
#include "ModelBuilder.h"
class ModelLoader : public XMLparser
{
public:
ModelBuilder* pModelBuilder = NULL;
bool ownModelBuilder = false;
std::vector<GameSubj*>* pSubjsVector = NULL;
public:
ModelLoader(std::vector<GameSubj*>* pSubjsVector0, int subjN, ModelBuilder* pMB, std::string filePath) : XMLparser(filePath) {
pSubjsVector = pSubjsVector0;
if (pMB != NULL) {
ownModelBuilder = false;
pModelBuilder = pMB;
}
else {
ownModelBuilder = true;
pModelBuilder = new ModelBuilder();
}
pModelBuilder->useSubjN(subjN);
};
virtual ~ModelLoader() {
if (!ownModelBuilder)
return;
pModelBuilder->buildDrawJobs(*pSubjsVector);
delete pModelBuilder;
};
static int fillProps_vs(VirtualShape* pVS, std::string tagStr); //virtual shape
static int processTag_a(ModelLoader* pML); //apply
static int setValueFromIntHashMap(int* pInt, std::map<std::string, int> intHashMap, std::string varName, std::string tagStr);
static int setTexture(ModelLoader* pML, int* pInt, std::string txName);
static int setMaterialTextures(ModelLoader* pML, Material* pMT);
static int fillProps_mt(Material* pMT, std::string tagStr, ModelLoader* pML); //Material
int processTag() { return processTag(this); };
static int processTag(ModelLoader* pML);
static int loadModel(std::vector<GameSubj*>* pSubjsVector0, std::string sourceFile, std::string subjClass);
};
Please note, it inherits XMLParser class.
5. Under modeler add new C++ file ModelLoader.cpp
Location: C:\CPP\engine\modeler
Code:
#include "ModelLoader.h"
#include "platform.h"
#include "TheGame.h"
#include "DrawJob.h"
#include "Texture.h"
#include "utils.h"
extern TheGame theGame;
int ModelLoader::loadModel(std::vector<GameSubj*>* pSubjsVector0, std::string sourceFile, std::string subjClass) {
//returns element's (Subj) number or -1
int subjN = pSubjsVector0->size();
GameSubj* pGS = theGame.newGameSubj(subjClass);
pSubjsVector0->push_back(pGS);
//pGS->djStartN = DrawJob::drawJobs.size();
ModelLoader* pML = new ModelLoader(pSubjsVector0, subjN, NULL, sourceFile);
processSource(pML);
delete pML;
//pGS->djTotalN = DrawJob::drawJobs.size() - pGS->djStartN;
return subjN;
}
int ModelLoader::processTag_a(ModelLoader* pML) {
//apply
ModelBuilder* pMB = pML->pModelBuilder;
std::string tagStr = pML->currentTag;
pMB->lockGroup(pMB);
std::vector<std::string> applyTosVector = splitString(pML->getStringValue("a", tagStr), ",");
Material* pMT = pMB->materialsList.at(pMB->usingMaterialN);
int texN = pMT->uTex1mask;
if (texN < 0)
texN = pMT->uTex0;
float xywh[4] = { 0,0,1,1 };
setFloatArray(xywh, 4, "xywh", tagStr);
std::string flipStr = getStringValue("flip", tagStr);
TexCoords tc;
tc.set(texN, xywh[0], xywh[1], xywh[2], xywh[3], flipStr);
setFloatArray(xywh, 4, "xywh2nm", tagStr);
flipStr = getStringValue("flip2nm", tagStr);
TexCoords tc2nm;
tc2nm.set(pMT->uTex2nm, xywh[0], xywh[1], xywh[2], xywh[3], flipStr);
for (int aN = 0; aN < (int)applyTosVector.size(); aN++) {
pMB->buildFace(pMB, applyTosVector.at(aN), pMB->pCurrentVShape, &tc, &tc2nm);
}
//mylog("vertsN=%d\n",pMB->vertices.size());
pMB->releaseGroup(pMB);
return 1;
}
int ModelLoader::setValueFromIntHashMap(int* pInt, std::map<std::string, int> intHashMap, std::string varName, std::string tagStr) {
if (!varExists(varName, tagStr))
return 0;
std::string str0 = getStringValue(varName, tagStr);
if (intHashMap.find(str0) == intHashMap.end()) {
mylog("ERROR in ModelLoader::setValueFromIntMap, %s not found, %s\n", varName.c_str(), tagStr.c_str());
return -1;
}
*pInt = intHashMap[getStringValue(varName, tagStr)];
return 1;
}
int ModelLoader::setTexture(ModelLoader* pML, int* pInt, std::string txName) {
ModelBuilder* pMB = pML->pModelBuilder;
std::string varName = txName + "_use";
if (setValueFromIntHashMap(pInt, pMB->texturesHashMap, varName, pML->currentTag) == 0) {
//the texture is not in hash table
varName = txName + "_src";
if (varExists(varName, pML->currentTag)) {
std::string txFile = getStringValue(varName, pML->currentTag);
varName = txName + "_ckey";
unsigned int intCkey = 0;
setUintColorValue(&intCkey, varName, pML->currentTag);
*pInt = Texture::loadTexture(buildFullPath(pML, txFile), intCkey);
}
}
return 1;
}
int ModelLoader::setMaterialTextures(ModelLoader* pML, Material* pMT) {
setTexture(pML, &pMT->uTex0, "uTex0");
setTexture(pML, &pMT->uTex1mask, "uTex1mask");
setTexture(pML, &pMT->uTex2nm, "uTex2nm");
setTexture(pML, &pMT->uTex3, "uTex3");
return 1;
}
int ModelLoader::fillProps_mt(Material* pMT, std::string tagStr, ModelLoader* pML) {
setCharsValue(pMT->shaderType, 20, "mt_type", tagStr);
setMaterialTextures(pML, pMT);
//color
if (varExists("uColor", tagStr)) {
unsigned int uintColor = 0;
setUintColorValue(&uintColor, "uColor", tagStr);
pMT->uColor.setUint32(uintColor);
}
//mylog("mt.uTex0=%d, mt.uTex1mask=%d\n", mt.uTex0, mt.uTex1mask);
if (varExists("primitiveType", tagStr)) {
std::string str0 = getStringValue("primitiveType", tagStr);
if (str0.compare("GL_POINTS") == 0) pMT->primitiveType = GL_POINTS;
else if (str0.compare("GL_LINES") == 0) pMT->primitiveType = GL_LINES;
else if (str0.compare("GL_LINE_STRIP") == 0) pMT->primitiveType = GL_LINE_STRIP;
else if (str0.compare("GL_LINE_LOOP") == 0) pMT->primitiveType = GL_LINE_LOOP;
else if (str0.compare("GL_TRIANGLE_STRIP") == 0) pMT->primitiveType = GL_TRIANGLE_STRIP;
else if (str0.compare("GL_TRIANGLE_FAN") == 0) pMT->primitiveType = GL_TRIANGLE_FAN;
else pMT->primitiveType = GL_TRIANGLES;
}
setIntValue(&pMT->uTex1alphaChannelN, "uTex1alphaChannelN", tagStr);
setIntValue(&pMT->uTex0translateChannelN, "uTex0translateChannelN", tagStr);
setIntBoolValue(&pMT->uAlphaBlending, "uAlphaBlending", tagStr);
setFloatValue(&pMT->uAlphaFactor, "uAlphaFactor", tagStr);
setFloatValue(&pMT->uAmbient, "uAmbient", tagStr);
setFloatValue(&pMT->uSpecularIntencity, "uSpecularIntencity", tagStr);
setFloatValue(&pMT->uSpecularMinDot, "uSpecularMinDot", tagStr);
setFloatValue(&pMT->uSpecularPowerOf, "uSpecularPowerOf", tagStr);
pML->pModelBuilder->useMaterial(pMT);
return 1;
}
int ModelLoader::processTag(ModelLoader* pML) {
ModelBuilder* pMB = pML->pModelBuilder;
if (pML->tagName.compare("texture_as") == 0) {
//saves texture N in texturesMap under given name
std::string keyName = getStringValue("texture_as", pML->currentTag);
if (pMB->texturesHashMap.find(keyName) != pMB->texturesHashMap.end())
return pMB->texturesHashMap[keyName];
else { //add new
std::string txFile = getStringValue("src", pML->currentTag);
unsigned int intCkey = 0;
setUintColorValue(&intCkey, "ckey", pML->currentTag);
int txN = Texture::loadTexture(buildFullPath(pML, txFile), intCkey);
pMB->texturesHashMap[keyName] = txN;
//mylog("%s=%d\n", keyName.c_str(), pMB->texturesMap[keyName]);
return txN;
}
}
if (pML->tagName.find("mt_") == 0) {
//sets current material
ModelBuilder* pMB = pML->pModelBuilder;
if (!pML->closedTag) {
//save previous material in stack
if (pMB->usingMaterialN >= 0)
pMB->materialsStack.push_back(pMB->usingMaterialN);
}
Material mt;
return fillProps_mt(&mt, pML->currentTag, pML);
}
if (pML->tagName.find("/mt_") == 0) {
//restore previous material
if (pMB->materialsStack.size() > 0) {
pMB->usingMaterialN = pMB->materialsStack.back();
pMB->materialsStack.pop_back();
}
return 1;
}
if (pML->tagName.compare("vs") == 0) {
//sets virtual shape
ModelBuilder* pMB = pML->pModelBuilder;
if (pML->closedTag) {
if (pMB->pCurrentVShape != NULL)
delete pMB->pCurrentVShape;
}
else { //open tag
//save previous vshape in stack
if (pMB->pCurrentVShape != NULL)
pMB->vShapesStack.push_back(pMB->pCurrentVShape);
}
pMB->pCurrentVShape = new VirtualShape();
fillProps_vs(pMB->pCurrentVShape, pML->currentTag);
return 1;
}
if (pML->tagName.compare("/vs") == 0) {
//restore previous virtual shape
if (pMB->vShapesStack.size() > 0) {
if (pMB->pCurrentVShape != NULL)
delete(pMB->pCurrentVShape);
pMB->pCurrentVShape = pMB->vShapesStack.back();
pMB->vShapesStack.pop_back();
}
return 1;
}
if (pML->tagName.compare("group") == 0) {
std::string notAllowed[] = { "pxyz","axyz","align","headTo" };
int notAllowedLn = sizeof(notAllowed) / sizeof(notAllowed[0]);
for (int i = 0; i < notAllowedLn; i++)
if (varExists(notAllowed[i], pML->currentTag)) {
mylog("ERROR in ModelLoader::processTag: use %s in </group>: %s\n", notAllowed[i].c_str(), pML->currentTag.c_str());
return -1;
}
pMB->lockGroup(pMB);
return 1;
}
//mylog("%s, %s /group?=%d\n",pML->currentTag.c_str(), pML->tagName.c_str(), (pML->tagName.compare("/group") == 0));
if (pML->tagName.compare("/group") == 0) {
pMB->releaseGroup(pMB);
return 1;
}
if (pML->tagName.compare("a") == 0)
return processTag_a(pML); //apply
mylog("ERROR in ModelLoader::processTag, unhandled tag %s, file %s\n", pML->currentTag.c_str(), pML->fullPath.c_str());
return -1;
}
int ModelLoader::fillProps_vs(VirtualShape* pVS, std::string tagStr) {
//sets virtual shape
setCharsValue(pVS->shapeType, 20, "vs", tagStr);
setFloatArray(pVS->whl, 3, "whl", tagStr);
//extensions
float ext;
if (varExists("ext", tagStr)) {
setFloatValue(&ext, "ext", tagStr);
pVS->setExt(ext);
}
if (varExists("extX", tagStr)) {
setFloatValue(&ext, "extX", tagStr);
pVS->setExtX(ext);
}
if (varExists("extY", tagStr)) {
setFloatValue(&ext, "extY", tagStr);
pVS->setExtY(ext);
}
if (varExists("extZ", tagStr)) {
setFloatValue(&ext, "extZ", tagStr);
pVS->setExtZ(ext);
}
setFloatValue(&pVS->extU, "extU", tagStr);
setFloatValue(&pVS->extD, "extD", tagStr);
setFloatValue(&pVS->extL, "extL", tagStr);
setFloatValue(&pVS->extR, "extR", tagStr);
setFloatValue(&pVS->extF, "extF", tagStr);
setFloatValue(&pVS->extB, "extB", tagStr);
//sections
setIntValue(&pVS->sectionsR, "sectR", tagStr);
setIntValue(&pVS->sections[0], "sectX", tagStr);
setIntValue(&pVS->sections[1], "sectY", tagStr);
setIntValue(&pVS->sections[2], "sectZ", tagStr);
//mylog("pVS->shapeType=%s whl=%fx%fx%f\n", pVS->shapeType, pVS->whl[0], pVS->whl[1], pVS->whl[2]);
return 1;
}
Also it will require some changes in ModelBuilder1base.
6. Open ModelBuilder1base.h and replace code by:
#pragma once
#include <string>
#include <vector>
#include "Vertex01.h"
#include "Triangle01.h"
#include "VirtualShape.h"
#include "Group01.h"
#include "Material.h"
#include "GameSubj.h"
#include <map>
class ModelBuilder1base
{
public:
std::vector<Vertex01*> vertices;
std::vector<Triangle01*> triangles;
std::vector<int> subjNumbersList;
int usingSubjN = -1;
std::vector<Group01*> groupsStack;
Group01* pCurrentGroup = NULL;
std::vector<VirtualShape*> vShapesStack;
VirtualShape* pCurrentVShape = NULL;
std::vector<Material*> materialsList;
int usingMaterialN = -1;
std::vector<int> materialsStack;
std::map<std::string, int> texturesHashMap;
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);
};
Important changes:
- texturesHashMap will allow ModelLoader address textures by given name.
- materialsStack will add an opportunity to switch between Materials.
7. Open ModelBuilder1base.cpp and replace code by:
#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;
}
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 0;
//if (pMT->uTex2nm >= 0)
// calculateTangentSpace(useVertices, useTriangles);
pMT->pickShaderNumber();
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] = (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;
}
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) {
if (pMB->groupsStack.size() > 0) {
pMB->pCurrentGroup = pMB->groupsStack.back();
pMB->groupsStack.pop_back();
}
else
pMB->pCurrentGroup = NULL;
}
Now - model.
Having such a nice set of shaders, we can think about some REAL model.
For example:
It is recognizable, simple, square and pretty enough.
Besides, it was under my hand.
I combined involved projections into a single 1024x512 PNG image:
- Bluish images on the right are Normal Maps. We will need them later.
8. The model will belong to the Project, not to engine. So, in Windows File Explorer under
C:\CPP\a997modeler\dt
add new subfolder
C:\CPP\a997modeler\dt\models\misc\marlboro01.
Download PNG file hereand save it to C:\CPP\a997modeler\dt\models\misc\marlboro01\
Now - model descriptor.
9. Copy this code to a Text Editor and save it to/as
C:\CPP\a997modeler\dt\models\misc\marlboro01\root01.txt
<texture_as="tx0" src="marlboro03small.png"/>
<mt_type="phong" uTex0_use="tx0" />
<vs="box_tank" whl="53,83,21" ext=1 sectR=1 />
<a="front v,back v" xywh="2,1,323,495"/>
<a="right all" xywh="327,1,128,495"/>
<a="left all" xywh="457,1,128,495"/>
<a="top" xywh="588,1,323,133"/>
<a="bottom" xywh="587,136,324,134"/>
Hope, it's understandable what it does. Just in case:
- Line 1: Loads the Texture. "tx0" - is just a name to be used in materials.
- Line 2: Creates Material using tx0: textured Phong. All following "a" commands will use this material.
- Line 3: Sets a Virtual Shape. The box is 55x85x23 mm. "whl" (width/height/length) has smaller numbers because we also have 1 mm extensions on all sides. "sectR=1" is a number of radial sections for extensions.
- The rest: Applies projections to all sides. "xywh" - texture fragment coordinates (x, y, width, height).
- ModelLoader's functionality is defined by these commands.
Currently in TheGame.cpp model creation takes 51 lines (lines from 22 to 72). Now it will be just 4.
10. 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"
#include "TexCoords.h"
#include "ModelLoader.h"
extern std::string filesRoot;
extern float degrees2radians;
std::vector<GameSubj*> TheGame::gameSubjs;
int TheGame::getReady() {
bExitGame = false;
Shader::loadShaders();
glEnable(GL_CULL_FACE);
int subjN = ModelLoader::loadModel(&gameSubjs, "/dt/models/misc/marlboro01/root01.txt", "");
GameSubj* pGS = gameSubjs.at(subjN);
pGS->name.assign("box1");
pGS->ownSpeed.setDegrees(0, 2, 0);
//pGS->ownCoords.setDegrees(0, -90, 0);
//===== set up camera
mainCamera.ownCoords.setDegrees(15, 180, 0); //set camera angles/orientation
mainCamera.viewRangeDg = 30;
mainCamera.stageSize[0] = 80;
mainCamera.stageSize[1] = 120;
memcpy(mainCamera.lookAtPoint, pGS->ownCoords.pos, sizeof(float) * 3);
mainCamera.onScreenResize();
//===== 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);
float nearClip = mainCamera.focusDistance - 50;
float farClip = mainCamera.focusDistance + 50;
if (nearClip < 0) nearClip = 0;
mat4x4_perspective(mProjection, mainCamera.viewRangeDg * degrees2radians, screenAspectRatio, nearClip, farClip);
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);
}
}
//synchronization
while (1) {
long long int currentMillis = getSystemMillis();
long long int millisSinceLastFrame = currentMillis - lastFrameMillis;
if (millisSinceLastFrame >= millisPerFrame) {
lastFrameMillis = currentMillis;
break;
}
}
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;
screenAspectRatio = (float)width / height;
glViewport(0, 0, width, height);
mainCamera.onScreenResize();
mylog(" screen size %d x %d\n", width, height);
return 1;
}
int TheGame::run() {
getReady();
while (!bExitGame) {
drawFrame();
}
cleanUp();
return 1;
}
GameSubj* TheGame::newGameSubj(std::string subjClass) {
return (new GameSubj());
}
Other changes:
- We have a new function newGameSubj(..). ModelLoader will use it to create new GameSubjs.
- stageSize now is 80x120
- nearClip and farClip are a bit different
- keystone effect handling (line 64)
11. In TheGame.h new declaration, newGameSubj(..) .
Open TheGame.h and replace code by:
#pragma once
#include <vector>
#include "GameSubj.h"
#include "Camera.h"
class TheGame
{
public:
int screenSize[2];
float screenAspectRatio = 1;
//synchronization
long long int lastFrameMillis = 0;
int targetFPS = 30;
int millisPerFrame = 1000 / targetFPS;
bool bExitGame;
Camera mainCamera;
float dirToMainLight[4] = { 1,1,1,0 };
//static arrays (vectors) of active GameSubjs
static std::vector<GameSubj*> gameSubjs;
public:
int run();
int getReady();
int drawFrame();
int cleanUp();
int onScreenResize(int width, int height);
static GameSubj* newGameSubj(std::string subjClass);
};
12. Build and run. Result:
Now
Android
13. Close and re-open VS. Open C:\CPP\a997modeler\p_android\p_android.sln.
14. Under xEngine add Existing Item
from C:\CPP\engine.
Files XMLparser.cpp and XMLparser.h
Add
15. Under modeler add Existing Item
from C:\CPP\engine\modeler
Files ModelLoader.cpp and ModelLoader.h
Add
16. Turn on, unlock, plug in, allow.
Build and run.
Good.
VS top menu -> Debug -> Stop Debugging.