You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
22 KiB
381 lines
22 KiB
/*
|
|
OneLoneCoder.com - 3D Graphics Part #2 - Normals, Culling, Lighting & Object Files
|
|
"Tredimensjonal Grafikk" - @Javidx9
|
|
|
|
License
|
|
~~~~~~~
|
|
One Lone Coder Console Game Engine Copyright (C) 2018 Javidx9
|
|
This program comes with ABSOLUTELY NO WARRANTY.
|
|
This is free software, and you are welcome to redistribute it
|
|
under certain conditions; See license for details.
|
|
Original works located at:
|
|
https://www.github.com/onelonecoder
|
|
https://www.onelonecoder.com
|
|
https://www.youtube.com/javidx9
|
|
GNU GPLv3
|
|
https://github.com/OneLoneCoder/videos/blob/master/LICENSE
|
|
|
|
From Javidx9 :)
|
|
~~~~~~~~~~~~~~~
|
|
Hello! Ultimately I don't care what you use this for. It's intended to be
|
|
educational, and perhaps to the oddly minded - a little bit of fun.
|
|
Please hack this, change it and use it in any way you see fit. You acknowledge
|
|
that I am not responsible for anything bad that happens as a result of
|
|
your actions. However this code is protected by GNU GPLv3, see the license in the
|
|
github repo. This means you must attribute me if you use it. You can view this
|
|
license here: https://github.com/OneLoneCoder/videos/blob/master/LICENSE
|
|
Cheers!
|
|
|
|
Background
|
|
~~~~~~~~~~
|
|
3D Graphics is an interesting, visually pleasing suite of algorithms. This is the
|
|
first video in a series that will demonstrate the fundamentals required to
|
|
build your own software based 3D graphics systems.
|
|
|
|
Video
|
|
~~~~~
|
|
https://youtu.be/ih20l3pJoeU
|
|
https://youtu.be/XgMWc6LumG4
|
|
|
|
Author
|
|
~~~~~~
|
|
Twitter: @javidx9
|
|
Blog: http://www.onelonecoder.com
|
|
Discord: https://discord.gg/WhwHUMV
|
|
|
|
|
|
Last Updated: 29/07/2018
|
|
*/
|
|
|
|
|
|
#include "olcConsoleGameEngine.h"
|
|
#include <fstream>
|
|
#include <strstream>
|
|
#include <algorithm>
|
|
using namespace std;
|
|
|
|
|
|
struct vec3d
|
|
{
|
|
float x, y, z;
|
|
};
|
|
|
|
struct triangle
|
|
{
|
|
vec3d p[3];
|
|
|
|
wchar_t sym;
|
|
short col;
|
|
};
|
|
|
|
struct mesh
|
|
{
|
|
vector<triangle> tris;
|
|
|
|
bool LoadFromObjectFile(string sFilename)
|
|
{
|
|
ifstream f(sFilename);
|
|
if (!f.is_open())
|
|
return false;
|
|
|
|
// Local cache of verts
|
|
vector<vec3d> verts;
|
|
|
|
while (!f.eof())
|
|
{
|
|
char line[128];
|
|
f.getline(line, 128);
|
|
|
|
strstream s;
|
|
s << line;
|
|
|
|
char junk;
|
|
|
|
if (line[0] == 'v')
|
|
{
|
|
vec3d v;
|
|
s >> junk >> v.x >> v.y >> v.z;
|
|
verts.push_back(v);
|
|
}
|
|
|
|
if (line[0] == 'f')
|
|
{
|
|
int f[3];
|
|
s >> junk >> f[0] >> f[1] >> f[2];
|
|
tris.push_back({ verts[f[0] - 1], verts[f[1] - 1], verts[f[2] - 1] });
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
};
|
|
|
|
struct mat4x4
|
|
{
|
|
float m[4][4] = { 0 };
|
|
};
|
|
|
|
class olcEngine3D : public olcConsoleGameEngine
|
|
{
|
|
public:
|
|
olcEngine3D()
|
|
{
|
|
m_sAppName = L"3D Demo";
|
|
}
|
|
|
|
|
|
private:
|
|
mesh meshCube;
|
|
mat4x4 matProj;
|
|
|
|
vec3d vCamera;
|
|
|
|
|
|
float fTheta;
|
|
|
|
void MultiplyMatrixVector(vec3d &i, vec3d &o, mat4x4 &m)
|
|
{
|
|
o.x = i.x * m.m[0][0] + i.y * m.m[1][0] + i.z * m.m[2][0] + m.m[3][0];
|
|
o.y = i.x * m.m[0][1] + i.y * m.m[1][1] + i.z * m.m[2][1] + m.m[3][1];
|
|
o.z = i.x * m.m[0][2] + i.y * m.m[1][2] + i.z * m.m[2][2] + m.m[3][2];
|
|
float w = i.x * m.m[0][3] + i.y * m.m[1][3] + i.z * m.m[2][3] + m.m[3][3];
|
|
|
|
if (w != 0.0f)
|
|
{
|
|
o.x /= w; o.y /= w; o.z /= w;
|
|
}
|
|
}
|
|
|
|
// Taken From Command Line Webcam Video
|
|
CHAR_INFO GetColour(float lum)
|
|
{
|
|
short bg_col, fg_col;
|
|
wchar_t sym;
|
|
int pixel_bw = (int)(13.0f*lum);
|
|
switch (pixel_bw)
|
|
{
|
|
case 0: bg_col = BG_BLACK; fg_col = FG_BLACK; sym = PIXEL_SOLID; break;
|
|
|
|
case 1: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_QUARTER; break;
|
|
case 2: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_HALF; break;
|
|
case 3: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_THREEQUARTERS; break;
|
|
case 4: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_SOLID; break;
|
|
|
|
case 5: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_QUARTER; break;
|
|
case 6: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_HALF; break;
|
|
case 7: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_THREEQUARTERS; break;
|
|
case 8: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_SOLID; break;
|
|
|
|
case 9: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_QUARTER; break;
|
|
case 10: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_HALF; break;
|
|
case 11: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_THREEQUARTERS; break;
|
|
case 12: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_SOLID; break;
|
|
default:
|
|
bg_col = BG_BLACK; fg_col = FG_BLACK; sym = PIXEL_SOLID;
|
|
}
|
|
|
|
CHAR_INFO c;
|
|
c.Attributes = bg_col | fg_col;
|
|
c.Char.UnicodeChar = sym;
|
|
return c;
|
|
}
|
|
|
|
public:
|
|
bool OnUserCreate() override
|
|
{
|
|
//meshCube.tris = {
|
|
|
|
//// SOUTH
|
|
//{ 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f },
|
|
//{ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f },
|
|
|
|
//// EAST
|
|
//{ 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f },
|
|
//{ 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f },
|
|
|
|
//// NORTH
|
|
//{ 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f },
|
|
//{ 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f },
|
|
|
|
//// WEST
|
|
//{ 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f },
|
|
//{ 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
|
|
|
|
//// TOP
|
|
//{ 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f },
|
|
//{ 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f },
|
|
|
|
//// BOTTOM
|
|
//{ 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f },
|
|
//{ 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f },
|
|
|
|
//};
|
|
|
|
meshCube.LoadFromObjectFile("VideoShip.obj");
|
|
|
|
// Projection Matrix
|
|
float fNear = 0.1f;
|
|
float fFar = 1000.0f;
|
|
float fFov = 90.0f;
|
|
float fAspectRatio = (float)ScreenHeight() / (float)ScreenWidth();
|
|
float fFovRad = 1.0f / tanf(fFov * 0.5f / 180.0f * 3.14159f);
|
|
|
|
matProj.m[0][0] = fAspectRatio * fFovRad;
|
|
matProj.m[1][1] = fFovRad;
|
|
matProj.m[2][2] = fFar / (fFar - fNear);
|
|
matProj.m[3][2] = (-fFar * fNear) / (fFar - fNear);
|
|
matProj.m[2][3] = 1.0f;
|
|
matProj.m[3][3] = 0.0f;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OnUserUpdate(float fElapsedTime) override
|
|
{
|
|
// Clear Screen
|
|
Fill(0, 0, ScreenWidth(), ScreenHeight(), PIXEL_SOLID, FG_BLACK);
|
|
|
|
// Set up rotation matrices
|
|
mat4x4 matRotZ, matRotX;
|
|
fTheta += 1.0f * fElapsedTime;
|
|
|
|
// Rotation Z
|
|
matRotZ.m[0][0] = cosf(fTheta);
|
|
matRotZ.m[0][1] = sinf(fTheta);
|
|
matRotZ.m[1][0] = -sinf(fTheta);
|
|
matRotZ.m[1][1] = cosf(fTheta);
|
|
matRotZ.m[2][2] = 1;
|
|
matRotZ.m[3][3] = 1;
|
|
|
|
// Rotation X
|
|
matRotX.m[0][0] = 1;
|
|
matRotX.m[1][1] = cosf(fTheta * 0.5f);
|
|
matRotX.m[1][2] = sinf(fTheta * 0.5f);
|
|
matRotX.m[2][1] = -sinf(fTheta * 0.5f);
|
|
matRotX.m[2][2] = cosf(fTheta * 0.5f);
|
|
matRotX.m[3][3] = 1;
|
|
|
|
// Store triagles for rastering later
|
|
vector<triangle> vecTrianglesToRaster;
|
|
|
|
// Draw Triangles
|
|
for (auto tri : meshCube.tris)
|
|
{
|
|
triangle triProjected, triTranslated, triRotatedZ, triRotatedZX;
|
|
|
|
// Rotate in Z-Axis
|
|
MultiplyMatrixVector(tri.p[0], triRotatedZ.p[0], matRotZ);
|
|
MultiplyMatrixVector(tri.p[1], triRotatedZ.p[1], matRotZ);
|
|
MultiplyMatrixVector(tri.p[2], triRotatedZ.p[2], matRotZ);
|
|
|
|
// Rotate in X-Axis
|
|
MultiplyMatrixVector(triRotatedZ.p[0], triRotatedZX.p[0], matRotX);
|
|
MultiplyMatrixVector(triRotatedZ.p[1], triRotatedZX.p[1], matRotX);
|
|
MultiplyMatrixVector(triRotatedZ.p[2], triRotatedZX.p[2], matRotX);
|
|
|
|
// Offset into the screen
|
|
triTranslated = triRotatedZX;
|
|
triTranslated.p[0].z = triRotatedZX.p[0].z + 8.0f;
|
|
triTranslated.p[1].z = triRotatedZX.p[1].z + 8.0f;
|
|
triTranslated.p[2].z = triRotatedZX.p[2].z + 8.0f;
|
|
|
|
// Use Cross-Product to get surface normal
|
|
vec3d normal, line1, line2;
|
|
line1.x = triTranslated.p[1].x - triTranslated.p[0].x;
|
|
line1.y = triTranslated.p[1].y - triTranslated.p[0].y;
|
|
line1.z = triTranslated.p[1].z - triTranslated.p[0].z;
|
|
|
|
line2.x = triTranslated.p[2].x - triTranslated.p[0].x;
|
|
line2.y = triTranslated.p[2].y - triTranslated.p[0].y;
|
|
line2.z = triTranslated.p[2].z - triTranslated.p[0].z;
|
|
|
|
normal.x = line1.y * line2.z - line1.z * line2.y;
|
|
normal.y = line1.z * line2.x - line1.x * line2.z;
|
|
normal.z = line1.x * line2.y - line1.y * line2.x;
|
|
|
|
// It's normally normal to normalise the normal
|
|
float l = sqrtf(normal.x*normal.x + normal.y*normal.y + normal.z*normal.z);
|
|
normal.x /= l; normal.y /= l; normal.z /= l;
|
|
|
|
//if (normal.z < 0)
|
|
if(normal.x * (triTranslated.p[0].x - vCamera.x) +
|
|
normal.y * (triTranslated.p[0].y - vCamera.y) +
|
|
normal.z * (triTranslated.p[0].z - vCamera.z) < 0.0f)
|
|
{
|
|
// Illumination
|
|
vec3d light_direction = { 0.0f, 0.0f, -1.0f };
|
|
float l = sqrtf(light_direction.x*light_direction.x + light_direction.y*light_direction.y + light_direction.z*light_direction.z);
|
|
light_direction.x /= l; light_direction.y /= l; light_direction.z /= l;
|
|
|
|
// How similar is normal to light direction
|
|
float dp = normal.x * light_direction.x + normal.y * light_direction.y + normal.z * light_direction.z;
|
|
|
|
// Choose console colours as required (much easier with RGB)
|
|
CHAR_INFO c = GetColour(dp);
|
|
triTranslated.col = c.Attributes;
|
|
triTranslated.sym = c.Char.UnicodeChar;
|
|
|
|
// Project triangles from 3D --> 2D
|
|
MultiplyMatrixVector(triTranslated.p[0], triProjected.p[0], matProj);
|
|
MultiplyMatrixVector(triTranslated.p[1], triProjected.p[1], matProj);
|
|
MultiplyMatrixVector(triTranslated.p[2], triProjected.p[2], matProj);
|
|
triProjected.col = triTranslated.col;
|
|
triProjected.sym = triTranslated.sym;
|
|
|
|
// Scale into view
|
|
triProjected.p[0].x += 1.0f; triProjected.p[0].y += 1.0f;
|
|
triProjected.p[1].x += 1.0f; triProjected.p[1].y += 1.0f;
|
|
triProjected.p[2].x += 1.0f; triProjected.p[2].y += 1.0f;
|
|
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();
|
|
|
|
// Store triangle for sorting
|
|
vecTrianglesToRaster.push_back(triProjected);
|
|
}
|
|
|
|
}
|
|
|
|
// Sort triangles from back to front
|
|
sort(vecTrianglesToRaster.begin(), vecTrianglesToRaster.end(), [](triangle &t1, triangle &t2)
|
|
{
|
|
float z1 = (t1.p[0].z + t1.p[1].z + t1.p[2].z) / 3.0f;
|
|
float z2 = (t2.p[0].z + t2.p[1].z + t2.p[2].z) / 3.0f;
|
|
return z1 > z2;
|
|
});
|
|
|
|
for (auto &triProjected : vecTrianglesToRaster)
|
|
{
|
|
// Rasterize triangle
|
|
FillTriangle(triProjected.p[0].x, triProjected.p[0].y,
|
|
triProjected.p[1].x, triProjected.p[1].y,
|
|
triProjected.p[2].x, triProjected.p[2].y,
|
|
triProjected.sym, triProjected.col);
|
|
|
|
/*DrawTriangle(triProjected.p[0].x, triProjected.p[0].y,
|
|
triProjected.p[1].x, triProjected.p[1].y,
|
|
triProjected.p[2].x, triProjected.p[2].y,
|
|
PIXEL_SOLID, FG_BLACK);*/
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int main()
|
|
{
|
|
olcEngine3D demo;
|
|
if (demo.ConstructConsole(256, 240, 4, 4))
|
|
demo.Start();
|
|
return 0;
|
|
}
|
|
|
|
|