Chapter 33. Polygons Intersection

Just a friendly reminder: we are still trying to "draw" the joint (slit) between the pack and the lid, as a small normal map applied to existing textured mesh's fragment. Like this:

In previous chapter we implemented a concept of selecting and manipulating "marked vertices/triangles groups". So, now we can select one (in this sample - "box_right"), duplicate it, rotate and move at our discretion. Particularly - facing us, like this:

The next task is to cut this white rectangle out of the mesh.

We will scan mesh's triangles and will build intersections between these triangles and white rectangle. Both triangle and rectangle are polygons. So, we are talking about Polygons Intersection.

  • The intersection of red triangle and white rectangle is 4-points polygon (2 triangles).
  • The intersection of left blue - is a 5-points polygon (3 triangles).

New concepts here are: Polygons and Polygons Intersection. The polygon is basically an array of Polygon Ribs.

Now - implementation:

Windows

1. Start VS, open C:\CPP\a997modeler\p_windows\p_windows.sln.


2. Under modeler add new header file PolygonRib.h

Location: C:\CPP\engine\modeler

Code:

#pragma once
#include "Vertex01.h"
#include <vector>

class PolygonRib
{
public:
	int i0;
	int i1;
	float* p0; //rib p0
	float* p1; //rib p1
	//line equation
	float a_slope = 0; //a
	float b_intercept = 0; //b
	bool isVertical = false;
	float x_vertical = 0;
	bool isHorizontal = false;
	//direction to "inner" side
	float xDirIn = 0;
	float yDirIn = 0;
public:
	PolygonRib(std::vector<Vertex01*>* pVxSrc, int idx0);
	static bool matchingLines(PolygonRib* pRibFrame, PolygonRib* pRibSrc);
	static bool parallelLines(PolygonRib* pRibFrame, PolygonRib* pRibSrc);
};


3. Under modeler add new C++ file PolygonRib.cpp

Location: C:\CPP\engine\modeler

Code:

#include "PolygonRib.h"

PolygonRib::PolygonRib(std::vector<Vertex01*>* pVxSrc, int idx0) {
	//2 points
	i0 = idx0;
	p0 = pVxSrc->at(idx0)->aPos;
	int ribsN = pVxSrc->size();
	int idx1 = (idx0 + 1) % ribsN;
	i1 = idx1;
	p1 = pVxSrc->at(idx1)->aPos;
	//3-rd "inner" ref point
	float* p2;
	int idx2 = (idx0 + 2) % ribsN;
	p2 = pVxSrc->at(idx2)->aPos;
	//find line equation
	if (p0[0] == p1[0]) {
		isVertical = true;
		x_vertical = p0[0];
		//"inner" side
		if (p2[0] < x_vertical)
			xDirIn = -1;
		else
			xDirIn = 1;
	}
	else if (p0[1] == p1[1]) {
		isHorizontal = true;
		a_slope = 0;
		b_intercept = p0[1];
		//"inner" side
		if (p2[1] < b_intercept)
			yDirIn = -1;
		else
			yDirIn = 1;
	}
	else {
		a_slope = (p1[1]-p0[1]) / (p1[0] - p0[0]);
		b_intercept = p0[1] - a_slope * p0[0];
		//"inner" side
		float y = a_slope * p2[0] + b_intercept;
		if(p2[1] < y)
			yDirIn = -1;
		else
			yDirIn = 1;
		float x = (p2[1] - b_intercept) / a_slope;
		if (p2[0] < x)
			xDirIn = -1;
		else
			xDirIn = 1;
	}
}
bool PolygonRib::matchingLines(PolygonRib* pRibFrame, PolygonRib* pRibSrc) {
	if (!parallelLines(pRibFrame, pRibSrc))
		return false;
	if (pRibFrame->b_intercept != pRibSrc->b_intercept)
		return false;
	if (pRibFrame->x_vertical != pRibSrc->x_vertical)
		return false;
	return true;
}
bool PolygonRib::parallelLines(PolygonRib* pRibFrame, PolygonRib* pRibSrc) {
	if (pRibFrame->isVertical != pRibSrc->isVertical)
		return false;
	if (pRibFrame->a_slope != pRibSrc->a_slope)
		return false;
	return true;
}


4. Under modeler add new header file Polygon.h

Location: C:\CPP\engine\modeler

Code:

#pragma once
#include "Vertex01.h"
#include "Triangle01.h"
#include "PolygonRib.h"
#include <vector>

class Polygon
{
public:
	std::vector<Vertex01*> vertices;
	std::vector<PolygonRib*> ribs;
	std::vector<Triangle01*> triangles;
	float normal[3] = {0,0,0};
	int ribsN = 0;
	//bounding box
	float bbMin[3] = { 0,0,0 };
	float bbMax[3] = { 0,0,0 };
public:
	virtual ~Polygon() { clearAll(this); };
	static void clearAll(Polygon* pPL);
	static int addVertex(Polygon* pPL, Vertex01* pV);
	static int addVertex(Polygon* pPL, float x, float y, float z);
	static int finalizePolygon(Polygon* pPL);
	static int setTriangle(Polygon* pPL, Triangle01* pT, std::vector<Vertex01*>* pVxSrc);
	static int setRectangle(Polygon* pPL, float w, float h);
	static int xyIntersection(Polygon* pDst, Polygon* pFrame, Polygon* pSrc);
	static int addLinesIntersection(Polygon* pDst, Polygon* pFrame, int ribNframe, Polygon* pSrc, int ribNsrc);

	static bool dotFits(float* p0, PolygonRib* pPR);
	static bool dotFits(float* p0, Polygon* pPL);
	static bool correctSide(float* p0, PolygonRib* pPR);
	static int addVert(Polygon* pDst, float* p0, Vertex01* pV0, Vertex01* pV1);
	static int addVert(Polygon * pDst, float* p0, Polygon * pSrc);
	static int buildTriangles(Polygon* pPL);
};


5. Under modeler add new C++ file Polygon.cpp

Location: C:\CPP\engine\modeler

Code:

#include "Polygon.h"
#include "linmath.h"
#include "utils.h"
#include "platform.h"
#include <algorithm>

extern float radians2degrees;

void Polygon::clearAll(Polygon* pPL) {
	for (int i = pPL->vertices.size() - 1; i >= 0; i--)
		delete pPL->vertices.at(i);
	pPL->vertices.clear();

	for (int i = pPL->ribs.size() - 1; i >= 0; i--)
		delete pPL->ribs.at(i);
	pPL->ribs.clear();

	for (int i = pPL->triangles.size() - 1; i >= 0; i--)
		delete pPL->triangles.at(i);
	pPL->triangles.clear();
}
int Polygon::addVertex(Polygon* pPL, float x, float y, float z) {
	Vertex01* pV = new Vertex01();
	pV->aPos[0] = x;
	pV->aPos[1] = y;
	pV->aPos[2] = z;
	pPL->vertices.push_back(pV);
	return 1;
}
int Polygon::addVertex(Polygon* pPL, Vertex01* pV0) {
	Vertex01* pV = new Vertex01(*pV0);
	pPL->vertices.push_back(pV);
//mylog("====Adding vertexs %f x %f\n",pV->aPos[0], pV->aPos[1]);
	return 1;
}
int Polygon::setTriangle(Polygon* pPL, Triangle01* pT, std::vector<Vertex01*>* pVxSrc) {
	clearAll(pPL);
	for (int i = 0; i < 3; i++) {
		int vN = pT->idx[i];
		Vertex01* pV = pVxSrc->at(vN);
		addVertex(pPL, pV);
	}
	finalizePolygon(pPL);
	return 1;
}
int Polygon::setRectangle(Polygon* pPL, float w, float h) {
	clearAll(pPL);
	w /= 2;
	h /= 2;
	//CCW
	addVertex(pPL, -w,  h, 0); //NW
	addVertex(pPL, -w, -h, 0); //SW
	addVertex(pPL,  w, -h, 0); //SE
	addVertex(pPL,  w,  h, 0); //NE
	finalizePolygon(pPL);
	return 1;
}
int Polygon::finalizePolygon(Polygon* pPL) {
	pPL->ribsN = pPL->vertices.size();
	for (int i = 0; i < pPL->ribsN; i++) {
		PolygonRib* pPR = new PolygonRib(&pPL->vertices,i);
		pPL->ribs.push_back(pPR);
	}
	//calculate polygon's normal
	float v0[3];
	float v2[3];
	for (int i = 0; i < 3; i++) {
		v0[i] = pPL->vertices.at(1)->aPos[i] - pPL->vertices.at(0)->aPos[i];
		v2[i] = pPL->vertices.at(2)->aPos[i] - pPL->vertices.at(0)->aPos[i];
	}
	vec3_mul_cross(pPL->normal, v0, v2);
	vec3_norm(pPL->normal, pPL->normal);
	//bounding box
	Vertex01* pV = pPL->vertices.at(0);
	v3copy(pPL->bbMin, pV->aPos);
	v3copy(pPL->bbMax, pV->aPos);
	for (int vN = pPL->vertices.size() - 1; vN >= 1; vN--) {
		pV = pPL->vertices.at(vN);
		for (int i = 0; i < 3; i++) {
			if (pPL->bbMin[i] > pV->aPos[i])
				pPL->bbMin[i] = pV->aPos[i];
			if (pPL->bbMax[i] < pV->aPos[i])
				pPL->bbMax[i] = pV->aPos[i];
		}
	}
	return 1;
}
int Polygon::addLinesIntersection(Polygon* pDst, Polygon* pFrame, int ribNframe, Polygon* pSrc, int ribNsrc) {
	PolygonRib* pRibFrame = pFrame->ribs.at(ribNframe);
	PolygonRib* pRibSrc = pSrc->ribs.at(ribNsrc);
	/*
mylog("==addLinesIntersection\n");
mylog("  fr %f x %f to %f x %f v=%d h=%d\n", pRibFrame->p0[0], pRibFrame->p0[1], pRibFrame->p1[0], pRibFrame->p1[1], pRibFrame->isVertical, pRibFrame->isHorizontal);
mylog("  tr %f x %f to %f x %f v=%d h=%d\n", pRibSrc->p0[0], pRibSrc->p0[1], pRibSrc->p1[0], pRibSrc->p1[1], pRibSrc->isVertical, pRibSrc->isHorizontal);
*/
	if (PolygonRib::matchingLines(pRibFrame, pRibSrc)) {
		Vertex01* pV0 = pSrc->vertices.at(pRibSrc->i0);
		Vertex01* pV1 = pSrc->vertices.at(pRibSrc->i1);
		int dstVertsN0 = pDst->vertices.size();
		if (dotFits(pRibFrame->p0, pRibSrc))
			addVert(pDst, pRibFrame->p0, pV0,pV1);
		if (dotFits(pRibFrame->p1, pRibSrc))
			addVert(pDst, pRibFrame->p1, pV0, pV1);
		if (dotFits(pRibSrc->p0, pRibFrame))
			addVertex(pDst, pV0);
		if (dotFits(pRibSrc->p1, pRibFrame))
			addVertex(pDst, pV1);
//mylog("  lines are identical\n");
		return pDst->vertices.size()- dstVertsN0;
	}
	if (PolygonRib::parallelLines(pRibFrame, pRibSrc)) {
//mylog("  lines are parallel\n");
		return 0;
	}
	//find lines intersection, assuming lines are not parallel
	float x,y;
	if (pRibFrame->isVertical) {
		x = pRibFrame->p0[0];
		y = pRibSrc->a_slope * x + pRibSrc->b_intercept;
	}
	else { //pRibFrame not vertical
		if (pRibSrc->isVertical) {
			x = pRibSrc->p0[0];
			y = pRibFrame->a_slope * x + pRibFrame->b_intercept;
		}
		else { //both lines are "normal"
			x = (pRibSrc->b_intercept - pRibFrame->b_intercept) / (pRibFrame->a_slope - pRibSrc->a_slope);
			y = pRibFrame->a_slope * x + pRibFrame->b_intercept;
		}
	}
	//check if belongs to both PolygonRibs
	float xy[2];
	xy[0] = x;
	xy[1] = y;
	if (!dotFits(xy, pRibFrame))
		return 0;
	if (!dotFits(xy, pRibSrc))
		return 0;
	addVert(pDst, xy, pSrc->vertices.at(pRibSrc->i0), pSrc->vertices.at(pRibSrc->i1));
	return 1;
}
bool Polygon::correctSide(float* p0, PolygonRib* pPR) {
	if (pPR->isVertical)
		if ((p0[0] - pPR->x_vertical) * pPR->xDirIn < 0)
			return false;
	if (pPR->isHorizontal)
		if ((p0[1] - pPR->b_intercept) * pPR->yDirIn < 0)
			return false;
	float y = pPR->a_slope * p0[0] + pPR->b_intercept;
	if ((p0[1] - y) * pPR->yDirIn < 0)
		return false;
	return true;
}
int Polygon::addVert(Polygon* pDst, float* p0, Vertex01* pV0, Vertex01* pV1) {
	float d[2];
	for (int i = 0; i < 2; i++)
		d[i] = pV0->aPos[i] - p0[i];
	float dist2v0 = v3lengthXY(d);
	if (dist2v0 == 0)
		return addVertex(pDst, pV0);
	for (int i = 0; i < 2; i++)
		d[i] = pV1->aPos[i] - p0[i];
	float dist2v1 = v3lengthXY(d);
	if (dist2v1 == 0)
		return addVertex(pDst, pV1);
	//if here - find mid values
	float k0 = dist2v1 / (dist2v0 + dist2v1);
	float k1 = dist2v0 / (dist2v0 + dist2v1);
	Vertex01* pVx = new Vertex01(*pV0);
	pVx->aPos[0] = p0[0];
	pVx->aPos[1] = p0[1];
	pVx->aPos[2] = k0 * pV0->aPos[2] + k1 * pV1->aPos[2];
	for (int i = 0; i < 3; i++)
		pVx->aNormal[i] = k0 * pV0->aNormal[i] + k1 * pV1->aNormal[i];
	for (int i = 0; i < 2; i++)
		pVx->aTuv[i] = k0 * pV0->aTuv[i] + k1 * pV1->aTuv[i];
	for (int i = 0; i < 2; i++)
		pVx->aTuv2[i] = k0 * pV0->aTuv2[i] + k1 * pV1->aTuv2[i];
	addVertex(pDst, pVx);
	delete pVx;
	return 0;
}
int Polygon::xyIntersection(Polygon* pDst, Polygon* pFrame, Polygon* pSrc) {
	//check bounding boxes, XY only
	for (int i = 0; i < 2; i++) {
		if (pFrame->bbMin[i] > pSrc->bbMax[i])
			return 0;
		if (pFrame->bbMax[i] < pSrc->bbMin[i])
			return 0;
	}
	//compare normals
	if (v3dotProduct(pFrame->normal, pSrc->normal) <= 0)
		return 0;
/*
mylog(">>>pFrame %fx%f to %fx%f to %fx%f \n",
	pFrame->vertices.at(0)->aPos[0], pFrame->vertices.at(0)->aPos[1],
	pFrame->vertices.at(1)->aPos[0], pFrame->vertices.at(1)->aPos[1],
	pFrame->vertices.at(2)->aPos[0], pFrame->vertices.at(2)->aPos[1]
);
mylog("   pSrc   %fx%f to %fx%f to %fx%f \n",
	pSrc->vertices.at(0)->aPos[0], pSrc->vertices.at(0)->aPos[1],
	pSrc->vertices.at(1)->aPos[0], pSrc->vertices.at(1)->aPos[1],
	pSrc->vertices.at(2)->aPos[0], pSrc->vertices.at(2)->aPos[1]
);
mylog("---SrcVerts\n");
*/
	//if here - have overlap
	int addedSrcVerts = 0;
	for (int vN = 0; vN < pSrc->ribsN; vN++) {
		Vertex01* pV = pSrc->vertices.at(vN);
		if (dotFits(pV->aPos, pFrame))
			addedSrcVerts += addVertex(pDst, pV);
	}
	if (addedSrcVerts == pSrc->ribsN)
		return addedSrcVerts;

//mylog("---FrameVerts\n");
	int addedFrameVerts = 0;
	for (int vN = 0; vN < pFrame->ribsN; vN++) {
		Vertex01* pV = pFrame->vertices.at(vN);
		if (dotFits(pV->aPos, pSrc)) {
			int frameVerts = addVert(pDst, pV->aPos, pSrc);
			addedFrameVerts += frameVerts;
		}
	}
	if (addedFrameVerts == pFrame->ribsN)
		return addedFrameVerts;

//mylog("---CrossVerts\n");
	//check ribs intersections
	int addedCrossVerts = 0;
	for (int ribNframe = 0; ribNframe < pFrame->ribsN; ribNframe++) {
		for (int ribNsrc = 0; ribNsrc < pSrc->ribsN; ribNsrc++) {
			int crossVerts = addLinesIntersection(pDst, pFrame, ribNframe, pSrc, ribNsrc);
			addedCrossVerts += crossVerts;
		}
	}
	return (addedSrcVerts + addedFrameVerts + addedCrossVerts);
}
bool Polygon::dotFits(float* p0, PolygonRib* pPR) {
//mylog("dotFits Rib %f x %f vs %f x %f to %f x %f\n", p0[0], p0[1], pPR->p0[0], pPR->p0[1], pPR->p1[0], pPR->p1[1]);
	//assuming that p0 is on the line
	int dir0;
	int dir1;
	if (pPR->isVertical) {
		if (pPR->p0[1] == p0[1])
			return true;
		else if (pPR->p0[1] < p0[1])
			dir0 = -1;
		else
			dir0 = 1;

		if (pPR->p1[1] == p0[1])
			return true;
		else if (pPR->p1[1] < p0[1])
			dir1 = -1;
		else
			dir1 = 1;
	}
	else{ //"normal" line
		if (pPR->p0[0] == p0[0])
			return true;
		else if (pPR->p0[0] < p0[0])
			dir0 = -1;
		else
			dir0 = 1;

		if (pPR->p1[0] == p0[0])
			return true;
		else if (pPR->p1[0] < p0[0])
			dir1 = -1;
		else
			dir1 = 1;
	}
//mylog("  fits?=%d\n", !(dir0 == dir1));
	if (dir0 == dir1)
		return false;
	return true;
}
bool Polygon::dotFits(float* p0, Polygon* pPL) {
//mylog("dotFits Polygon %f x %f\n",p0[0],p0[1]);

	for (int i = 0; i < pPL->ribsN; i++)
		if (!correctSide(p0, pPL->ribs.at(i))) {
//mylog("  don't Fit side %f x %f to %f x %f\n", pPL->ribs.at(i)->p0[0], pPL->ribs.at(i)->p0[1], pPL->ribs.at(i)->p1[0], pPL->ribs.at(i)->p1[1]);
			return false;
		}
//mylog("  dotFits!\n");
	return true;
}
int Polygon::buildTriangles(Polygon* pPL) {
	int vertsN = pPL->vertices.size();
	//mid point coords
	float p0[2] = { 0,0 };
	for (int vN = 0; vN < vertsN; vN++) {
		float* p1 = pPL->vertices.at(vN)->aPos;
		p0[0] += p1[0];
		p0[1] += p1[1];
	}
	p0[0] /= vertsN;
	p0[1] /= vertsN;
	for (int vN = 0; vN < vertsN; vN++) {
		float* p1 = pPL->vertices.at(vN)->aPos;
		float v1[3] ={0,0,0};
		v1[0] = p1[0] - p0[0];
		v1[1] = p1[1] - p0[1];
		float az = -atan2f(v1[0], v1[1]) * radians2degrees;
		//aTangent is not used at this point, ok to use it to store az
		pPL->vertices.at(vN)->aTangent[0] = az;
	}
	//sort vertices
	std::sort(pPL->vertices.begin(), pPL->vertices.end(), 
		[](Vertex01* pV0, Vertex01* pV1) {
		return pV0->aTangent[0] > pV1->aTangent[0]; });
	//check for redundancy
	for (int vN = pPL->vertices.size() - 1; vN > 0; vN--) {
		Vertex01* pV = pPL->vertices.at(vN);
		Vertex01* pVprev = pPL->vertices.at(vN-1);
		if (pV->aTangent[0] == pVprev->aTangent[0]) {
			delete pV;
			pPL->vertices.erase(pPL->vertices.begin() + vN);
		}
	}
	pPL->ribsN = pPL->vertices.size();
	//build triangles
	Vertex01* pV = pPL->vertices.at(0);
	for (int vN = 2; vN < pPL->ribsN; vN++) {
		Triangle01* pTR = new Triangle01();
		pPL->triangles.push_back(pTR);
		pTR->idx[0] = 0;
		pTR->idx[1] = vN;
		pTR->idx[2] = vN - 1;
		pTR->subjN = pV->subjN;
		pTR->materialN = pV->materialN;
		//mark
		myStrcpy_s(pTR->marks, 124, pV->marks);
	}
	return 1;
}
int Polygon::addVert(Polygon* pDst, float* p0, Polygon* pSrc) {
	//check where horizontal line drawn through p0 crosses polygon's ribs
	Vertex01 vx0;
	Vertex01 vx1;
	int vxN = 0;
	for (int ribN = 0; ribN < pSrc->ribsN; ribN++) {
		PolygonRib* pPR = pSrc->ribs.at(ribN);
		if (pPR->isHorizontal)
			continue;
		float p1[2];
		p1[1] = p0[1];
		if (pPR->isVertical)
			p1[0] = pPR->x_vertical;
		else
			p1[0] = (p1[1] - pPR->b_intercept) / pPR->a_slope;
		if (!dotFits(p1, pPR))
			continue;
		//if here - 1 intersection found
		Vertex01* pVdst = &vx0;
		if(vxN > 0)
			pVdst = &vx1;
		Vertex01* pV0src = pSrc->vertices.at(pPR->i0);
		Vertex01* pV1src = pSrc->vertices.at(pPR->i1);
		memcpy(pVdst, pV0src, sizeof(Vertex01));
		float d[2];
		for (int i = 0; i < 2; i++)
			d[i] = pV0src->aPos[i] - p1[i];
		float dist2v0 = v3lengthXY(d);
		if (dist2v0 == 0)
			memcpy(pVdst, pV0src, sizeof(Vertex01));
		else {
			for (int i = 0; i < 2; i++)
				d[i] = pV1src->aPos[i] - p1[i];
			float dist2v1 = v3lengthXY(d);
			if (dist2v1 == 0)
				memcpy(pVdst, pV1src, sizeof(Vertex01));
			else {
				//if here - find mid values
				float k0 = dist2v1 / (dist2v0 + dist2v1);
				float k1 = dist2v0 / (dist2v0 + dist2v1);
				pVdst->aPos[0] = p1[0];
				pVdst->aPos[1] = p1[1];
				pVdst->aPos[2] = k0 * pV0src->aPos[2] + k1 * pV1src->aPos[2];
				for (int i = 0; i < 3; i++)
					pVdst->aNormal[i] = k0 * pV0src->aNormal[i] + k1 * pV1src->aNormal[i];
				for (int i = 0; i < 2; i++)
					pVdst->aTuv[i] = k0 * pV0src->aTuv[i] + k1 * pV1src->aTuv[i];
				for (int i = 0; i < 2; i++)
					pVdst->aTuv2[i] = k0 * pV0src->aTuv2[i] + k1 * pV1src->aTuv2[i];
			}
		}
		vxN++;
		if (vxN > 1)
			break;
	}
	addVert(pDst, p0, &vx0, &vx1);
	return 1;
}


Now - model description.

6. Copy following code to a Text Editor and save (overwrite) it to/as

C:\CPP\a997modeler\dt\models\misc\marlboro01\root01.txt

<texture_as="tx0" src="marlboro03small.png" ckey="#00ff00"/>
<mt_type="phong" uTex0_use="tx0" />
<vs="box_tank" whl="53,83,21" ext=1 sectR=1 />
<a="front v" xywh="2,1,323,495" mark="box_front"/>
<a="back v"  xywh="2,1,323,495" mark="box_back"/>
<a="right all" xywh="327,1,128,495" mark="box_right"/>
<a="left all" xywh="457,1,128,495" mark="box_left"/>
<a="top" xywh="588,1,323,133"/>
<a="bottom" xywh="587,136,324,134"/>
//golden prints
<vs="box" whl="55.1,85.1,23.1" />
<texture_as="whitenoise" src="/dt/common/img/whitenoise/wn64_blur3.bmp"/>
<texture_as="gold" src="/dt/common/img/materials/gold02roman.bmp" />
<mt_type="mirror" uAlphaBlending uTex1mask_use="tx0" uTex1alphaChannelN=1 uTex0_use="whitenoise" uTex0translateChannelN=0 uTex3_use="gold" />
//side golden prints
<a="right" xywh="342,12,101,10" whl="x,1.8,18.1" pxyz="x,39.8, -0.3" /> //Please do not litter
<a="right" xywh="339,144,105,89" whl="x,15.35,18.9" pxyz="x,10.3,-0.12" /> //For special offers...
<a="left" xywh="475,15,95,48" whl="x,8.4,17" pxyz="x,36, 0.3" /> //Underage sale...
//front prints
<group>
	//bottom golden print "20 class a..."
	<a="front" xywh="20,498,289,13" whl="47.5,2,x" pxyz="1,-36,x" />
	//blazon/emblem
	<mt_type="mirror" uAlphaBlending uTex2nm_use="tx0" uTex0_use="whitenoise" uTex0translateChannelN=0 uTex3_use="gold" />
	<a="front" xywh2nm="589,415,128,94" whl="20.7,16,x" pxyz="0.3,6.1,x" /> //emblem
	//"Marlboro
	<mt_type="phong" uAlphaBlending uTex2nm_use="tx0" uColor="#1E211E" />
	<a="front" xywh2nm="590,275,301,136" whl="49.2,23.3,x" pxyz="0.21,-18,x" /> //marlboro
</group> 
<clone ay=180 />
//joint (slit) between the pack and the lid
<group> 
	<a2mesh wh="50,2" all markedAs="box_right" onThe="right" py=24.6 az=31 />
	<a2mesh wh="50,2" all markedAs="box_left" onThe="left" py=24.6 az=-31 />
	<a2mesh wh="53,2" all markedAs="box_front" py=17.8 />
	<a2mesh wh="6 ,2" all markedAs="box_back" onThe="back" py=31.5 px=23.5 />
	<a2mesh wh="6 ,2" all markedAs="box_back" onThe="back" py=31.5 px=-23.5 />
</group sizeD="0.1,0,0.1"> 

  • New code starts at line 32.
  • We have here new tags "a2mesh". It's like "a" (apply), but "apply to mesh".

To read and to execute it we need to add new functionality to ModelLoader.

7. Open ModelLoader.h and replace code by:

#pragma once
#include "XMLparser.h"
#include "ModelBuilder.h"
#include "GroupTransform.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->lockGroup(pModelBuilder);
		}
		pModelBuilder->useSubjN(subjN);
	};
	virtual ~ModelLoader() {
		if (!ownModelBuilder)
			return;
		pModelBuilder->buildDrawJobs(*pSubjsVector);
		delete pModelBuilder;
	};
	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_vs(VirtualShape* pVS, std::string tagStr); //virtual shape
	static int fillProps_mt(Material* pMT, std::string tagStr, ModelLoader* pML); //Material
	static int fillProps_gt(GroupTransform* pGS, ModelBuilder* pMB, std::string tagStr);
	int processTag() { return processTag(this); };
	static int processTag(ModelLoader* pML);
	static int loadModel(std::vector<GameSubj*>* pSubjsVector0, std::string sourceFile, std::string subjClass);
	static int processTag_clone(ModelLoader* pML);
	static int addMark(char* marks, std::string newMark);
	static int processTag_do(ModelLoader* pML);
	static int processTag_a2mesh(ModelLoader* pML);
};

  • New function here is processTag_a2mesh(..)

Implementation:

8. Open ModelLoader.cpp and replace code by:

#include "ModelLoader.h"
#include "platform.h"
#include "TheGame.h"
#include "DrawJob.h"
#include "Texture.h"
#include "utils.h"
#include "Polygon.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::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);
		//mark
		if (varExists("mark", pML->currentTag))
			addMark(pMB->pCurrentGroup->marks, getStringValue("mark", pML->currentTag));
		return 1;
	}
	if (pML->tagName.compare("/group") == 0) {
		GroupTransform gt;
		fillProps_gt(&gt, pMB, pML->currentTag);
		gt.executeGroupTransform(pMB);

		pMB->releaseGroup(pMB);
		return 1;
	}
	if (pML->tagName.compare("a") == 0)
		return processTag_a(pML); //apply 
	if (pML->tagName.compare("clone") == 0)
		return processTag_clone(pML);
	if (pML->tagName.compare("/clone") == 0)
		return processTag_clone(pML);
	if (pML->tagName.compare("do") == 0)
		return processTag_do(pML);
	if (pML->tagName.compare("a2mesh") == 0)
		return processTag_a2mesh(pML);

	//mylog("%s, %s /group?=%d\n",pML->currentTag.c_str(), pML->tagName.c_str(), (pML->tagName.compare("/group") == 0));
	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;
}
int ModelLoader::processTag_a(ModelLoader* pML) {
	//apply
	ModelBuilder* pMB = pML->pModelBuilder;
	std::string tagStr = pML->currentTag;
	pMB->lockGroup(pMB);
	//mark
	if (varExists("mark", tagStr))
		addMark(pMB->pCurrentGroup->marks, getStringValue("mark", tagStr));

	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);

	//adjusted VirtualShape
	VirtualShape* pVS_a = new VirtualShape(*pMB->pCurrentVShape);
	fillProps_vs(pVS_a, tagStr);

	for (int aN = 0; aN < (int)applyTosVector.size(); aN++) {
		//pMB->buildFace(pMB, applyTosVector.at(aN), pMB->pCurrentVShape, &tc, &tc2nm);
		pMB->buildFace(pMB, applyTosVector.at(aN), pVS_a, &tc, &tc2nm);
	}
	delete pVS_a;
	//mylog("vertsN=%d\n",pMB->vertices.size());

	GroupTransform GT_a;
	fillProps_gt(&GT_a, pMB, tagStr);
	GT_a.executeGroupTransform(pMB);

	pMB->releaseGroup(pMB);
	return 1;
}
int ModelLoader::processTag_clone(ModelLoader* pML) {
	ModelBuilder* pMB = pML->pModelBuilder;
	if (pML->tagName.compare("clone") == 0) {
		//mark what to clone
		GroupTransform gt;
		gt.pGroup = pMB->pLastClosedGroup;
		gt.flagSelection(&gt, &pMB->vertices, &pMB->triangles);

		//cloning
		pMB->lockGroup(pMB);
		gt.cloneFlagged(pMB, &pMB->vertices, &pMB->triangles, &pMB->vertices, &pMB->triangles);
	}
	GroupTransform gt;
	fillProps_gt(&gt, pMB, pML->currentTag);
	gt.executeGroupTransform(pMB);

	if (pML->tagName.compare("/clone") == 0 || pML->closedTag) {
		pMB->releaseGroup(pMB);
	}
	return 1;
}
int ModelLoader::addMark(char* marks, std::string newMark) {
	if (newMark.empty())
		return 0;
	std::string allMarks;
	allMarks.assign(marks);
	allMarks.append("<"+ newMark+">");
	myStrcpy_s(marks,124, allMarks.c_str());
	return 1;
}

int ModelLoader::fillProps_gt(GroupTransform* pGT, ModelBuilder* pMB, std::string tagStr) {
	pGT->pGroup = pMB->pCurrentGroup;
	//position
	setFloatArray(pGT->shift, 3, "pxyz", tagStr);
	setFloatValue(&pGT->shift[0], "px", tagStr);
	setFloatValue(&pGT->shift[1], "py", tagStr);
	setFloatValue(&pGT->shift[2], "pz", tagStr);
	//angles
	setFloatArray(pGT->spin, 3, "axyz", tagStr);
	setFloatValue(&pGT->spin[0], "ax", tagStr);
	setFloatValue(&pGT->spin[1], "ay", tagStr);
	setFloatValue(&pGT->spin[2], "az", tagStr);
	//scale
	setFloatArray(pGT->scale, 3, "scale", tagStr);

	pGT->onThe = getStringValue("onThe", tagStr);
	pGT->allign = getStringValue("allign", tagStr);
	pGT->headZto = getStringValue("headZto", tagStr);
	//limit to
	if(varExists("all",tagStr))
		pGT->pGroup = NULL;
	if (varExists("lastClosedGroup", tagStr))
		pGT->pGroup = pMB->pLastClosedGroup;
	if (varExists("markedAs", tagStr))
		pGT->limit2mark(pGT, getStringValue("markedAs", tagStr));
	setFloatArray(pGT->pMin, 3, "xyzMin", tagStr);
	setFloatArray(pGT->pMax, 3, "xyzMax", tagStr);

	if (varExists("sizeD", tagStr)) { //re-size
		float sizeD[3];
		setFloatArray(sizeD, 3, "sizeD", tagStr);
		//bounding box
		pGT->flagSelection(pGT, &pMB->vertices, NULL);
		float bbMin[3];
		float bbMax[3];
		pGT->buildBoundingBoxFlagged(bbMin, bbMax, &pMB->vertices);
		for (int i = 0; i < 3; i++) {
			float size = bbMax[i] - bbMin[i];
			pGT->scale[i] = (size + sizeD[i]) / size;
		}
	}

	return 1;
}
int ModelLoader::processTag_do(ModelLoader* pML) {
	ModelBuilder* pMB = pML->pModelBuilder;
	GroupTransform gt;
	fillProps_gt(&gt, pMB, pML->currentTag);
	gt.flagSelection(&gt, &pMB->vertices, &pMB->triangles);
	gt.transformFlagged(&gt, &pMB->vertices);
	return 1;
}
int ModelLoader::processTag_a2mesh(ModelLoader* pML) {
	ModelBuilder* pMB = pML->pModelBuilder;
	std::string tagStr = pML->currentTag;
	GroupTransform gt;
	fillProps_gt(&gt, pMB, pML->currentTag);
	gt.flagSelection(&gt, &pMB->vertices, &pMB->triangles);
	//clone a copy
	std::vector<Vertex01*> vx1;
	std::vector<Triangle01*> tr1;
	gt.cloneFlagged(NULL, &vx1, &tr1, &pMB->vertices, &pMB->triangles);
	// build transform and inverted martrices
	mat4x4 transformMatrix;
	gt.buildTransformMatrix(&gt, &transformMatrix);
	mat4x4 transformMatrixInverted;
	mat4x4_invert(transformMatrixInverted, transformMatrix);
	//move/rotate cloned
	gt.flagAll(&vx1, &tr1);
	//gt.transformFlagged(&pMB->vertices, &transformMatrixInverted);
	gt.transformFlaggedMx(&vx1, &transformMatrixInverted);

//gt.cloneFlagged(pMB, &pMB->vertices, &pMB->triangles, &vx1, &tr1);

	float wh[2];
	setFloatArray(wh, 2, "wh", tagStr);
	Polygon frame;
	frame.setRectangle(&frame, wh[0],wh[1]);
	//destination arrays
	std::vector<Vertex01*> vx2;
	std::vector<Triangle01*> tr2;
	Polygon triangle;
	for (int i = tr1.size() - 1; i >= 0; i--) {
		triangle.setTriangle(&triangle, tr1.at(i), &vx1);
		Polygon intersection;
		int pointsN = Polygon::xyIntersection(&intersection, &frame, &triangle);
		if (pointsN > 2) {
			Polygon::buildTriangles(&intersection);
			GroupTransform::flagAll (&intersection.vertices, &intersection.triangles);
			GroupTransform::cloneFlagged(NULL, &vx2, &tr2, &intersection.vertices, &intersection.triangles);
		}
	}
	gt.flagAll(&vx2, &tr2);
	//------------
	//replace material
	Material mt;
	mt.setShaderType("phong");
	mt.uColor.setRGBA(0,255,0,255);
	int mtN = pMB->useMaterial(pMB, &mt);
	for (int i = vx2.size() - 1; i >= 0; i--)
		vx2.at(i)->materialN = mtN;
	for (int i = tr2.size() - 1; i >= 0; i--)
		tr2.at(i)->materialN = mtN;
	//------------
 	//move/rotate
	gt.transformFlaggedMx(&vx2, &transformMatrix);
	//clone back to modelBuilder arrays
	gt.cloneFlagged(pMB, &pMB->vertices, &pMB->triangles, &vx2, &tr2);

	//clear memory
	for (int i = vx1.size() - 1; i >= 0; i--)
		delete vx1.at(i);
	vx1.clear();
	for (int i = tr1.size() - 1; i >= 0; i--)
		delete tr1.at(i);
	tr1.clear();
	for (int i = vx2.size() - 1; i >= 0; i--)
		delete vx2.at(i);
	vx2.clear();
	for (int i = tr2.size() - 1; i >= 0; i--)
		delete tr2.at(i);
	tr2.clear();

	return 1;
}

How it works:

Everything happens in processTag_a2mesh(..) (line 351).

Line 355. Reading position where we are applying it to.

Line 360. Cloning selected mesh.

Line 369. Placing cloned mesh facing us:

Line 374. Reading sizes what we want to cut.

Lines 381-390. Scanning mesh's triangles and building intersections (line 384). Resulting intersection is stored in Polygon intersection (line 383).

If intersection exists, we are building triangles (line 486) and saving result into vx2 (vertices) and tr2 (triangles) arrays (vectors) (line 388).

After all triangles processed, in vx2 / tr2 we have our desired fragment cutted out of initial mesh's copy:

To make it visible after moving it back to it's place, we are changing it's material (lines 394-401).

We'll remove changing material code a bit later.

Line 404. Positioning cutted fragment to it's final place.

Line 406. Cloning fragment back to ModelBuilder. However, it has the same coordinates as original mesh, so they are overlapping each other:

To shift it a bit out of original mesh, we have an extra command in root01.txt:

</group sizeD="0.1,0,0.1">

It's the last one, line 38.

ModelLoader will translate sizeD to scale, it's lines 327-339 in ModelLoader.cpp, function fillProps_gt(..).


9. Build and run. Result:

The goal is achieved.


Android

10. Close and re-open VS. Open C:\CPP\a997modeler\p_android\p_android.sln.


11. Under modeler add Existing Item from C:\CPP\engine\modeler

  • Polygon.cpp
  • Polygon.h
  • PolygonRib.cpp
  • PolygonRib. h

Add


12. Turn on, unlock, plug in, allow.

Build and run.

Ok.

VS top menu -> Debug -> Stop Debugging.


Our next task is to apply normal map to cutted fragments, preserving their original materials and textures.


Leave a Reply

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