#define OLC_PGE_APPLICATION #include "pixelGameEngine.h" #include #include using namespace olc; struct vec2d { float u=0; float v=0; float w=1; }; struct vec3d { float x=0; float y=0; float z=0; float w=1; }; struct triangle { vec3d p[3]; vec2d uv[3]; Pixel col; }; struct mesh { std::vector tris; Decal*texture; void Parse(std::string str,int&v,int&uv) { std::cout<>v; str.erase(0,str.find("/")+1); std::stringstream s2(str.substr(0,str.find("/")+1)); s2>>uv; //std::cout<<" "< verts; std::vector uvs; std::string data; while (f.good()) { f>>data; if (data=="v") { float x,y,z; f>>x>>y>>z; verts.push_back({x,y,z}); //std::cout<>u>>v; uvs.push_back({u,v}); //std::cout<>t1>>t2>>t3; int v1,v2,v3,uv1,uv2,uv3; Parse(t1,v1,uv1); Parse(t2,v2,uv2); Parse(t3,v3,uv3); tris.push_back({verts[v1-1],verts[v2-1],verts[v3-1], uvs[uv1-1],uvs[uv2-1],uvs[uv3-1]}); } } return true; } }; struct mat4x4 { float m[4][4] = { 0 }; }; class olcEngine3D : public PixelGameEngine { public: Decal*texture; olcEngine3D() { sAppName = "3D Demo"; } private: mesh meshCube; mat4x4 matProj; vec3d vCamera={0,0,0}; vec3d vLookDir; float zOffset=2; float fTheta=0; float fYaw=0; float pitch=-M_PI/6; vec3d Matrix_MultiplyVector(mat4x4 &m, vec3d &i) { vec3d v; v.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + i.w * m.m[3][0]; v.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + i.w * m.m[3][1]; v.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + i.w * m.m[3][2]; v.w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + i.w * m.m[3][3]; return v; } mat4x4 Matrix_MakeIdentity() { mat4x4 matrix; matrix.m[0][0] = 1.0f; matrix.m[1][1] = 1.0f; matrix.m[2][2] = 1.0f; matrix.m[3][3] = 1.0f; return matrix; } mat4x4 Matrix_MakeRotationX(float fAngleRad) { mat4x4 matrix; matrix.m[0][0] = 1.0f; matrix.m[1][1] = cosf(fAngleRad); matrix.m[1][2] = sinf(fAngleRad); matrix.m[2][1] = -sinf(fAngleRad); matrix.m[2][2] = cosf(fAngleRad); matrix.m[3][3] = 1.0f; return matrix; } mat4x4 Matrix_MakeRotationY(float fAngleRad) { mat4x4 matrix; matrix.m[0][0] = cosf(fAngleRad); matrix.m[0][2] = sinf(fAngleRad); matrix.m[2][0] = -sinf(fAngleRad); matrix.m[1][1] = 1.0f; matrix.m[2][2] = cosf(fAngleRad); matrix.m[3][3] = 1.0f; return matrix; } mat4x4 Matrix_MakeRotationZ(float fAngleRad) { mat4x4 matrix; matrix.m[0][0] = cosf(fAngleRad); matrix.m[0][1] = sinf(fAngleRad); matrix.m[1][0] = -sinf(fAngleRad); matrix.m[1][1] = cosf(fAngleRad); matrix.m[2][2] = 1.0f; matrix.m[3][3] = 1.0f; return matrix; } mat4x4 Matrix_MakeTranslation(float x, float y, float z) { mat4x4 matrix; matrix.m[0][0] = 1.0f; matrix.m[1][1] = 1.0f; matrix.m[2][2] = 1.0f; matrix.m[3][3] = 1.0f; matrix.m[3][0] = x; matrix.m[3][1] = y; matrix.m[3][2] = z; return matrix; } mat4x4 Matrix_MakeProjection(float fFovDegrees, float fAspectRatio, float fNear, float fFar) { float fFovRad = 1.0f / tanf(fFovDegrees * 0.5f / 180.0f * 3.14159f); mat4x4 matrix; matrix.m[0][0] = fAspectRatio * fFovRad; matrix.m[1][1] = fFovRad; matrix.m[2][2] = fFar / (fFar - fNear); matrix.m[3][2] = (-fFar * fNear) / (fFar - fNear); matrix.m[2][3] = 1.0f; matrix.m[3][3] = 0.0f; return matrix; } mat4x4 Matrix_MultiplyMatrix(mat4x4 &m1, mat4x4 &m2) { mat4x4 matrix; for (int c = 0; c < 4; c++) for (int r = 0; r < 4; r++) matrix.m[r][c] = m1.m[r][0] * m2.m[0][c] + m1.m[r][1] * m2.m[1][c] + m1.m[r][2] * m2.m[2][c] + m1.m[r][3] * m2.m[3][c]; return matrix; } mat4x4 Matrix_PointAt(vec3d &pos, vec3d &target, vec3d &up) { // Calculate new forward direction vec3d newForward = Vector_Sub(target, pos); newForward = Vector_Normalise(newForward); // Calculate new Up direction vec3d a = Vector_Mul(newForward, Vector_DotProduct(up, newForward)); vec3d newUp = Vector_Sub(up, a); newUp = Vector_Normalise(newUp); // New Right direction is easy, its just cross product vec3d newRight = Vector_CrossProduct(newUp, newForward); // Construct Dimensioning and Translation Matrix mat4x4 matrix; matrix.m[0][0] = newRight.x; matrix.m[0][1] = newRight.y; matrix.m[0][2] = newRight.z; matrix.m[0][3] = 0.0f; matrix.m[1][0] = newUp.x; matrix.m[1][1] = newUp.y; matrix.m[1][2] = newUp.z; matrix.m[1][3] = 0.0f; matrix.m[2][0] = newForward.x; matrix.m[2][1] = newForward.y; matrix.m[2][2] = newForward.z; matrix.m[2][3] = 0.0f; matrix.m[3][0] = pos.x; matrix.m[3][1] = pos.y; matrix.m[3][2] = pos.z; matrix.m[3][3] = 1.0f; return matrix; } mat4x4 Matrix_QuickInverse(mat4x4 &m) // Only for Rotation/Translation Matrices { mat4x4 matrix; matrix.m[0][0] = m.m[0][0]; matrix.m[0][1] = m.m[1][0]; matrix.m[0][2] = m.m[2][0]; matrix.m[0][3] = 0.0f; matrix.m[1][0] = m.m[0][1]; matrix.m[1][1] = m.m[1][1]; matrix.m[1][2] = m.m[2][1]; matrix.m[1][3] = 0.0f; matrix.m[2][0] = m.m[0][2]; matrix.m[2][1] = m.m[1][2]; matrix.m[2][2] = m.m[2][2]; matrix.m[2][3] = 0.0f; matrix.m[3][0] = -(m.m[3][0] * matrix.m[0][0] + m.m[3][1] * matrix.m[1][0] + m.m[3][2] * matrix.m[2][0]); matrix.m[3][1] = -(m.m[3][0] * matrix.m[0][1] + m.m[3][1] * matrix.m[1][1] + m.m[3][2] * matrix.m[2][1]); matrix.m[3][2] = -(m.m[3][0] * matrix.m[0][2] + m.m[3][1] * matrix.m[1][2] + m.m[3][2] * matrix.m[2][2]); matrix.m[3][3] = 1.0f; return matrix; } vec3d Vector_Add(vec3d &v1, vec3d &v2) { return { v1.x + v2.x, v1.y + v2.y, v1.z + v2.z }; } vec3d Vector_Sub(vec3d &v1, vec3d &v2) { return { v1.x - v2.x, v1.y - v2.y, v1.z - v2.z }; } vec3d Vector_Mul(vec3d &v1, float k) { return { v1.x * k, v1.y * k, v1.z * k }; } vec3d Vector_Div(vec3d &v1, float k) { return { v1.x / k, v1.y / k, v1.z / k }; } float Vector_DotProduct(vec3d &v1, vec3d &v2) { return v1.x*v2.x + v1.y*v2.y + v1.z * v2.z; } float Vector_Length(vec3d &v) { return sqrtf(Vector_DotProduct(v, v)); } vec3d Vector_Normalise(vec3d &v) { float l = Vector_Length(v); return { v.x / l, v.y / l, v.z / l }; } vec3d Vector_CrossProduct(vec3d &v1, vec3d &v2) { vec3d v; v.x = v1.y * v2.z - v1.z * v2.y; v.y = v1.z * v2.x - v1.x * v2.z; v.z = v1.x * v2.y - v1.y * v2.x; return v; } vec3d Vector_IntersectPlane(vec3d &plane_p, vec3d &plane_n, vec3d &lineStart, vec3d &lineEnd, float &t) { plane_n = Vector_Normalise(plane_n); float plane_d = -Vector_DotProduct(plane_n, plane_p); float ad = Vector_DotProduct(lineStart, plane_n); float bd = Vector_DotProduct(lineEnd, plane_n); t = (-plane_d - ad) / (bd - ad); vec3d lineStartToEnd = Vector_Sub(lineEnd, lineStart); vec3d lineToIntersect = Vector_Mul(lineStartToEnd, t); return Vector_Add(lineStart, lineToIntersect); } int Triangle_ClipAgainstPlane(vec3d plane_p, vec3d plane_n, triangle &in_tri, triangle &out_tri1, triangle &out_tri2) { // Make sure plane normal is indeed normal plane_n = Vector_Normalise(plane_n); // Return signed shortest distance from point to plane, plane normal must be normalised auto dist = [&](vec3d &p) { vec3d n = Vector_Normalise(p); return (plane_n.x * p.x + plane_n.y * p.y + plane_n.z * p.z - Vector_DotProduct(plane_n, plane_p)); }; // Create two temporary storage arrays to classify points either side of plane // If distance sign is positive, point lies on "inside" of plane vec3d* inside_points[3]; int nInsidePointCount = 0; vec3d* outside_points[3]; int nOutsidePointCount = 0; vec2d* inside_tex[3]; int nInsideTexCount = 0; vec2d* outside_tex[3]; int nOutsideTexCount = 0; // Get signed distance of each point in triangle to plane float d0 = dist(in_tri.p[0]); float d1 = dist(in_tri.p[1]); float d2 = dist(in_tri.p[2]); if (d0 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[0]; inside_tex[nInsideTexCount++] = &in_tri.uv[0]; } else { outside_points[nOutsidePointCount++] = &in_tri.p[0]; outside_tex[nOutsideTexCount++] = &in_tri.uv[0]; } if (d1 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[1]; inside_tex[nInsideTexCount++] = &in_tri.uv[1]; } else { outside_points[nOutsidePointCount++] = &in_tri.p[1]; outside_tex[nOutsideTexCount++] = &in_tri.uv[1]; } if (d2 >= 0) { inside_points[nInsidePointCount++] = &in_tri.p[2]; inside_tex[nInsideTexCount++] = &in_tri.uv[2]; } else { outside_points[nOutsidePointCount++] = &in_tri.p[2]; outside_tex[nOutsideTexCount++] = &in_tri.uv[2]; } // Now classify triangle points, and break the input triangle into // smaller output triangles if required. There are four possible // outcomes... if (nInsidePointCount == 0) { // All points lie on the outside of plane, so clip whole triangle // It ceases to exist return 0; // No returned triangles are valid } if (nInsidePointCount == 3) { // All points lie on the inside of plane, so do nothing // and allow the triangle to simply pass through out_tri1 = in_tri; return 1; // Just the one returned original triangle is valid } if (nInsidePointCount == 1 && nOutsidePointCount == 2) { // Triangle should be clipped. As two points lie outside // the plane, the triangle simply becomes a smaller triangle // Copy appearance info to new triangle out_tri1.col = in_tri.col; // The inside point is valid, so keep that... out_tri1.p[0] = *inside_points[0]; out_tri1.uv[0] = *inside_tex[0]; // but the two new points are at the locations where the // original sides of the triangle (lines) intersect with the plane float t; out_tri1.p[1] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); out_tri1.uv[1].u = t * (outside_tex[0]->u - inside_tex[0]->u) + inside_tex[0]->u; out_tri1.uv[1].v = t * (outside_tex[0]->v - inside_tex[0]->v) + inside_tex[0]->v; out_tri1.uv[1].w = t * (outside_tex[0]->w - inside_tex[0]->w) + inside_tex[0]->w; out_tri1.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[1], t); out_tri1.uv[2].u = t * (outside_tex[1]->u - inside_tex[0]->u) + inside_tex[0]->u; out_tri1.uv[2].v = t * (outside_tex[1]->v - inside_tex[0]->v) + inside_tex[0]->v; out_tri1.uv[2].w = t * (outside_tex[1]->w - inside_tex[0]->w) + inside_tex[0]->w; return 1; // Return the newly formed single triangle } if (nInsidePointCount == 2 && nOutsidePointCount == 1) { // Triangle should be clipped. As two points lie inside the plane, // the clipped triangle becomes a "quad". Fortunately, we can // represent a quad with two new triangles // Copy appearance info to new triangles out_tri1.col = in_tri.col; out_tri2.col = in_tri.col; // The first triangle consists of the two inside points and a new // point determined by the location where one side of the triangle // intersects with the plane out_tri1.p[0] = *inside_points[0]; out_tri1.p[1] = *inside_points[1]; out_tri1.uv[0] = *inside_tex[0]; out_tri1.uv[1] = *inside_tex[1]; float t; out_tri1.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[0], *outside_points[0], t); out_tri1.uv[2].u = t * (outside_tex[0]->u - inside_tex[0]->u) + inside_tex[0]->u; out_tri1.uv[2].v = t * (outside_tex[0]->v - inside_tex[0]->v) + inside_tex[0]->v; out_tri1.uv[2].w = t * (outside_tex[0]->w - inside_tex[0]->w) + inside_tex[0]->w; // The second triangle is composed of one of he inside points, a // new point determined by the intersection of the other side of the // triangle and the plane, and the newly created point above out_tri2.p[0] = *inside_points[1]; out_tri2.uv[0] = *inside_tex[1]; out_tri2.p[1] = out_tri1.p[2]; out_tri2.uv[1] = out_tri1.uv[2]; out_tri2.p[2] = Vector_IntersectPlane(plane_p, plane_n, *inside_points[1], *outside_points[0], t); out_tri2.uv[2].u = t * (outside_tex[0]->u - inside_tex[1]->u) + inside_tex[1]->u; out_tri2.uv[2].v = t * (outside_tex[0]->v - inside_tex[1]->v) + inside_tex[1]->v; out_tri2.uv[2].w = t * (outside_tex[0]->w - inside_tex[1]->w) + inside_tex[1]->w; return 2; // Return two newly formed triangles which form a quad } } public: bool OnUserCreate() override { texture = new Decal(new Sprite("High.png")); meshCube.LoadFromObjectFile("Artisans Hub.obj"); matProj=Matrix_MakeProjection(90.0f,(float)ScreenHeight() / (float)ScreenWidth(),0.1f,1000.0f); return true; } bool OnUserUpdate(float fElapsedTime) override { if (GetKey(olc::DOWN).bHeld) { pitch-=1*fElapsedTime; } if (GetKey(olc::UP).bHeld) { pitch+=1*fElapsedTime; } vec3d vForward=Vector_Mul(vLookDir,20*fElapsedTime); if (GetKey(olc::W).bHeld) { vCamera=Vector_Add(vCamera,vForward); } if (GetKey(olc::S).bHeld) { vCamera=Vector_Sub(vCamera,vForward); } if (GetKey(olc::A).bHeld) { fYaw-=2*fElapsedTime; } if (GetKey(olc::D).bHeld) { fYaw+=2*fElapsedTime; } // Set up rotation matrices mat4x4 matRotZ, matRotX, matTrans, matWorld; matRotZ=Matrix_MakeRotationZ(fTheta*0.5f); matRotX=Matrix_MakeRotationX(fTheta); matTrans=Matrix_MakeTranslation(0.0f,0.0f,5.0f); matWorld=Matrix_MakeIdentity(); matWorld=Matrix_MultiplyMatrix(matRotZ,matRotX); matWorld=Matrix_MultiplyMatrix(matWorld,matTrans); vec3d vUp={0,1,0}; vec3d vTarget={0,sinf(pitch),cosf(pitch)}; mat4x4 matCameraRot=Matrix_MakeRotationY(fYaw); vLookDir=Matrix_MultiplyVector(matCameraRot,vTarget); vTarget=Vector_Add(vCamera,vLookDir); mat4x4 matCamera = Matrix_PointAt(vCamera,vTarget,vUp); mat4x4 matView=Matrix_QuickInverse(matCamera); std::vectorvecTrianglesToRaster; // Draw Triangles for (auto&tri : meshCube.tris) { triangle triProjected, triTransformed,triViewed; triTransformed.p[0]=Matrix_MultiplyVector(matWorld,tri.p[0]); triTransformed.p[1]=Matrix_MultiplyVector(matWorld,tri.p[1]); triTransformed.p[2]=Matrix_MultiplyVector(matWorld,tri.p[2]); triTransformed.uv[0]=tri.uv[0]; triTransformed.uv[1]=tri.uv[1]; triTransformed.uv[2]=tri.uv[2]; vec3d normal,line1,line2; line1=Vector_Sub(triTransformed.p[1],triTransformed.p[0]); line2=Vector_Sub(triTransformed.p[2],triTransformed.p[0]); normal=Vector_CrossProduct(line1,line2); normal=Vector_Normalise(normal); vec3d vCameraRay=Vector_Sub(triTransformed.p[0],vCamera); if (Vector_DotProduct(normal,vCameraRay)<0) { vec3d light_dir=Vector_Mul(vLookDir,-1); light_dir=Vector_Normalise(light_dir); float dp = std::max(0.7f,Vector_DotProduct(light_dir,normal)); triViewed.p[0]=Matrix_MultiplyVector(matView,triTransformed.p[0]); triViewed.p[1]=Matrix_MultiplyVector(matView,triTransformed.p[1]); triViewed.p[2]=Matrix_MultiplyVector(matView,triTransformed.p[2]); triViewed.uv[0]=triTransformed.uv[0]; triViewed.uv[1]=triTransformed.uv[1]; triViewed.uv[2]=triTransformed.uv[2]; triViewed.col=Pixel(255*dp*dp,255*dp*dp,255*dp*dp); int nClippedTriangles = 0; triangle clipped[2]; nClippedTriangles = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.1f }, { 0.0f, 0.0f, 1.0f }, triViewed, clipped[0], clipped[1]); for (int n=0;n 2D triProjected.p[0]=Matrix_MultiplyVector(matProj,clipped[n].p[0]); triProjected.p[1]=Matrix_MultiplyVector(matProj,clipped[n].p[1]); triProjected.p[2]=Matrix_MultiplyVector(matProj,clipped[n].p[2]); triProjected.col=clipped[n].col; triProjected.uv[0]=clipped[n].uv[0]; triProjected.uv[1]=clipped[n].uv[1]; triProjected.uv[2]=clipped[n].uv[2]; triProjected.uv[0].u = triProjected.uv[0].u / triProjected.p[0].w; triProjected.uv[1].u = triProjected.uv[1].u / triProjected.p[1].w; triProjected.uv[2].u = triProjected.uv[2].u / triProjected.p[2].w; triProjected.uv[0].v = triProjected.uv[0].v / triProjected.p[0].w; triProjected.uv[1].v = triProjected.uv[1].v / triProjected.p[1].w; triProjected.uv[2].v = triProjected.uv[2].v / triProjected.p[2].w; triProjected.uv[0].w = 1.0f / triProjected.p[0].w; triProjected.uv[1].w = 1.0f / triProjected.p[1].w; triProjected.uv[2].w = 1.0f / triProjected.p[2].w; triProjected.p[0]=Vector_Div(triProjected.p[0],triProjected.p[0].w); triProjected.p[1]=Vector_Div(triProjected.p[1],triProjected.p[1].w); triProjected.p[2]=Vector_Div(triProjected.p[2],triProjected.p[2].w); triProjected.p[0].x*=-1.0f; triProjected.p[1].x*=-1.0f; triProjected.p[2].x*=-1.0f; triProjected.p[0].y*=-1.0f; triProjected.p[1].y*=-1.0f; triProjected.p[2].y*=-1.0f; // Scale into view vec3d vOffsetView={1,1,0}; triProjected.p[0] = Vector_Add(triProjected.p[0],vOffsetView); triProjected.p[1] = Vector_Add(triProjected.p[1],vOffsetView); triProjected.p[2] = Vector_Add(triProjected.p[2],vOffsetView); triProjected.p[0].x *= 0.5f * (float)ScreenWidth(); triProjected.p[0].y *= 0.5f * (float)ScreenHeight(); triProjected.p[1].x *= 0.5f * (float)ScreenWidth(); triProjected.p[1].y *= 0.5f * (float)ScreenHeight(); triProjected.p[2].x *= 0.5f * (float)ScreenWidth(); triProjected.p[2].y *= 0.5f * (float)ScreenHeight(); vecTrianglesToRaster.push_back(triProjected); } } } //std::sort(vecTrianglesToRaster.begin(),vecTrianglesToRaster.end(),[](triangle&t1,triangle&t2){return (t1.p[0].z+t1.p[1].z+t1.p[2].z)/3.0f>(t2.p[0].z+t2.p[1].z+t2.p[2].z)/3.0f;}); ClearBuffer(BLACK,true); int triRenderCount=0; for (auto&triToRaster:vecTrianglesToRaster) { triangle clipped[2]; std::listlistTriangles; listTriangles.push_back(triToRaster); int nNewTriangles=1; for (int p = 0; p < 4; p++) { int nTrisToAdd = 0; while (nNewTriangles > 0) { // Take triangle from front of queue triangle test = listTriangles.front(); listTriangles.pop_front(); nNewTriangles--; // Clip it against a plane. We only need to test each // subsequent plane, against subsequent new triangles // as all triangles after a plane clip are guaranteed // to lie on the inside of the plane. I like how this // comment is almost completely and utterly justified switch (p) { case 0: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, test, clipped[0], clipped[1]); break; case 1: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, (float)ScreenHeight() - 1, 0.0f }, { 0.0f, -1.0f, 0.0f }, test, clipped[0], clipped[1]); break; case 2: nTrisToAdd = Triangle_ClipAgainstPlane({ 0.0f, 0.0f, 0.0f }, { 1.0f, 0.0f, 0.0f }, test, clipped[0], clipped[1]); break; case 3: nTrisToAdd = Triangle_ClipAgainstPlane({ (float)ScreenWidth() - 1, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, test, clipped[0], clipped[1]); break; } // Clipping may yield a variable number of triangles, so // add these new ones to the back of the queue for subsequent // clipping against next planes for (int w = 0; w < nTrisToAdd; w++) listTriangles.push_back(clipped[w]); } nNewTriangles = listTriangles.size(); } for (auto&t:listTriangles) { // Rasterize triangle SetDecalStructure(DecalStructure::LIST); SetDecalMode(DecalMode::NORMAL); DrawPolygonDecal(texture,{ {t.p[0].x, t.p[0].y}, {t.p[1].x, t.p[1].y}, {t.p[2].x, t.p[2].y} },{ {t.uv[0].u,t.uv[0].v}, {t.uv[1].u,t.uv[1].v}, {t.uv[2].u,t.uv[2].v}, },{t.uv[0].w,t.uv[1].w,t.uv[2].w},{t.p[0].z,t.p[1].z,t.p[2].z},{t.col,t.col,t.col}); /*SetDecalMode(DecalMode::WIREFRAME); DrawPolygonDecal(nullptr,{ {t.p[0].x, t.p[0].y}, {t.p[1].x, t.p[1].y}, {t.p[2].x, t.p[2].y} },{ {0,0}, {0,0}, {0,0}, },WHITE);*/ SetDecalStructure(DecalStructure::FAN); triRenderCount++; } } SetDecalMode(DecalMode::NORMAL); DrawStringDecal({0,0},"Triangles: "+std::to_string(triRenderCount),WHITE,{2,2}); return true; } }; int main() { olcEngine3D demo; if (demo.Construct(1280, 720, 1, 1)) demo.Start(); return 0; }