From 98f3fc50cd4d68606218b1c7e935c641787d411b Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Mon, 2 Dec 2024 03:47:25 +0100 Subject: [PATCH] Last week of rasterizer :p silly cats FTW --- project/CMakeLists.txt | 2 - project/resources/cat.png | Bin 0 -> 69878 bytes project/src/Camera.h | 66 +++-- project/src/ColorRGB.h | 1 + project/src/DataTypes.h | 35 +-- project/src/HitTest.cpp | 84 ++++++- project/src/HitTest.h | 4 + project/src/Matrix.cpp | 507 +++++++++++++++++++------------------- project/src/Renderer.cpp | 210 +++++++++++----- project/src/Renderer.h | 140 +++++------ project/src/Utils.h | 4 +- project/src/Vector4.h | 12 +- project/src/main.cpp | 46 ++-- 13 files changed, 636 insertions(+), 475 deletions(-) create mode 100644 project/resources/cat.png diff --git a/project/CMakeLists.txt b/project/CMakeLists.txt index 1a4bbbb..f75d05a 100644 --- a/project/CMakeLists.txt +++ b/project/CMakeLists.txt @@ -21,8 +21,6 @@ set(IMGUI_SOURCES "imgui/backends/imgui_impl_sdlrenderer2.cpp" ) - - # Create the executable add_executable(${PROJECT_NAME} ${SOURCES}) #add_executable(${PROJECT_NAME} ${SOURCES} ${IMGUI_SOURCES}) diff --git a/project/resources/cat.png b/project/resources/cat.png new file mode 100644 index 0000000000000000000000000000000000000000..9878788d6b2a5db1777135f2e0a564ad9bb1a9a4 GIT binary patch literal 69878 zcmV)tK$pLXP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/project/src/Camera.h b/project/src/Camera.h index bd2e1e7..ae259fb 100644 --- a/project/src/Camera.h +++ b/project/src/Camera.h @@ -40,9 +40,10 @@ namespace dae { float zNear{.1f}; float zFar{100.f}; - void Initialize(float _fovAngle = 90.f, Vector3 _origin = {0.f, 0.f, 0.f}) { + void Initialize(float _fovAngle = 90.f, Vector3 _origin = {0.f, 0.f, 0.f}, float _aspect = 1.f) { fovAngle = _fovAngle; fov = tanf((fovAngle * TO_RADIANS) / 2.f); + aspect = _aspect; origin = _origin; } @@ -54,15 +55,14 @@ namespace dae { void CalculateViewMatrix() { - viewMatrix = Matrix::CreateLookAtLH(origin, forward, up); - invViewMatrix = Matrix::Inverse(viewMatrix); + viewMatrix = Matrix::CreateLookAtLH(origin, origin + forward, Vector3::UnitY); + Matrix temp = viewMatrix; - //TODO W1 - //ONB => invViewMatrix - //Inverse(ONB) => ViewMatrix + invViewMatrix = temp.Inverse(); - //ViewMatrix => Matrix::CreateLookAtLH(...) [not implemented yet] - //DirectX Implementation => https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixlookatlh + right = invViewMatrix.GetAxisX(); + up = invViewMatrix.GetAxisY(); + forward = invViewMatrix.GetAxisZ(); } void CalculateProjectionMatrix() { @@ -77,43 +77,50 @@ namespace dae { const float deltaTime = pTimer->GetElapsed(); //Keyboard Input - const uint8_t *pKeyboardState = SDL_GetKeyboardState(nullptr); - Vector3 zDirection{0.f, 0.f, 0.f}; - const Vector3 xDirection{0.f, 0.f, 0.f}; - const Vector3 yDirection{0.f, 0.f, 0.f}; + const uint8_t* pKeyboardState = SDL_GetKeyboardState(nullptr); + Vector3 zDirection{ 0.f,0.f,0.f }; + const Vector3 xDirection{ 0.f,0.f,0.f }; + const Vector3 yDirection{ 0.f,0.f,0.f }; - float MovementSpeed{10.f}; + float MovementSpeed{ 10.f }; - if (pKeyboardState[SDL_SCANCODE_LSHIFT]) { + if (pKeyboardState[SDL_SCANCODE_LSHIFT]) + { MovementSpeed *= 2; } - if (pKeyboardState[SDL_SCANCODE_W]) { + if (pKeyboardState[SDL_SCANCODE_W]) + { origin += forward * deltaTime * MovementSpeed; } - if (pKeyboardState[SDL_SCANCODE_A]) { + if (pKeyboardState[SDL_SCANCODE_A]) + { origin -= right * deltaTime * MovementSpeed; } - if (pKeyboardState[SDL_SCANCODE_S]) { + if (pKeyboardState[SDL_SCANCODE_S]) + { origin -= forward * deltaTime * MovementSpeed; } - if (pKeyboardState[SDL_SCANCODE_D]) { + if (pKeyboardState[SDL_SCANCODE_D]) + { origin += right * deltaTime * MovementSpeed; } - if (pKeyboardState[SDL_SCANCODE_E]) { + if (pKeyboardState[SDL_SCANCODE_E]) + { origin += up * deltaTime * MovementSpeed; } - if (pKeyboardState[SDL_SCANCODE_Q]) { + if (pKeyboardState[SDL_SCANCODE_Q]) + { origin -= up * deltaTime * MovementSpeed; } //Mouse Input - bool mousePosChange{false}; + bool mousePosChange{ false }; int mouseX{}, mouseY{}; const uint32_t mouseState = SDL_GetRelativeMouseState(&mouseX, &mouseY); mouseY *= -1; @@ -125,13 +132,16 @@ namespace dae { totalPitch += (static_cast(mouseY) * mouseSens) * TO_RADIANS; mousePosChange = true; - } else if (mouseState == SDL_BUTTON_LEFT) { + } + else if (mouseState == SDL_BUTTON_LEFT) + { zDirection = forward.Normalized() * static_cast(-mouseY); totalYaw += static_cast(mouseX) * mouseSens * TO_RADIANS; mousePosChange = true; - } else if (mouseState == SDL_BUTTON_X2) //lmb + rmb + } + else if (mouseState == SDL_BUTTON_X2) //lmb + rmb { origin.y += static_cast(mouseY) / 2; @@ -140,11 +150,12 @@ namespace dae { origin += ((zDirection + xDirection + yDirection) * cameraSpeed * deltaTime); - if (mousePosChange) { - const Matrix yawMatrix{Matrix::CreateRotationY(totalYaw)}; - const Matrix pitchMatrix{Matrix::CreateRotationX(totalPitch)}; + if (mousePosChange) + { + const Matrix yawMatrix{ Matrix::CreateRotationY(totalYaw) }; + const Matrix pitchMatrix{ Matrix::CreateRotationX(totalPitch) }; - const Matrix finalRotation{pitchMatrix * yawMatrix}; + const Matrix finalRotation{ pitchMatrix * yawMatrix }; forward = finalRotation.TransformVector(Vector3::UnitZ); forward.Normalize(); } @@ -152,6 +163,7 @@ namespace dae { //Update Matrices CalculateViewMatrix(); CalculateProjectionMatrix(); //Try to optimize this - should only be called once or when fov/aspectRatio changes + } }; } diff --git a/project/src/ColorRGB.h b/project/src/ColorRGB.h index be4a15c..f71f395 100644 --- a/project/src/ColorRGB.h +++ b/project/src/ColorRGB.h @@ -1,5 +1,6 @@ #pragma once #include "MathHelpers.h" +#include namespace dae { diff --git a/project/src/DataTypes.h b/project/src/DataTypes.h index be6d5a2..00ca474 100644 --- a/project/src/DataTypes.h +++ b/project/src/DataTypes.h @@ -8,23 +8,24 @@ namespace dae { struct Vertex { - Vector3 position{}; - ColorRGB color{colors::White}; - Vector2 uv{}; //W2 - //Vector3 normal{}; //W4 - //Vector3 tangent{}; //W4 - //Vector3 viewDirection{}; //W4 - }; + Vector4 position{}; + ColorRGB color{colors::White}; + Vector2 uv{}; + bool valid{ true }; + Vector3 normal{}; + Vector3 tangent{}; + Vector3 viewDir{}; + }; - struct Vertex_Out + struct Sample { - Vector4 position{}; - ColorRGB color{ colors::White }; - Vector2 uv{}; - //Vector3 normal{}; - //Vector3 tangent{}; - //Vector3 viewDirection{}; - }; + Vector2 uv{}; + Vector3 normal{}; + Vector3 tangent{}; + Vector3 viewDirection{}; + float depth{}; + Vector3 weight{}; + }; enum class PrimitiveTopology { @@ -36,9 +37,9 @@ namespace dae { std::vector vertices{}; std::vector indices{}; - PrimitiveTopology primitiveTopology{ PrimitiveTopology::TriangleStrip }; + PrimitiveTopology primitiveTopology{ PrimitiveTopology::TriangleList }; - std::vector vertices_out{}; + std::vector vertices_out{}; Matrix worldMatrix{}; }; } diff --git a/project/src/HitTest.cpp b/project/src/HitTest.cpp index ea42f92..48e190b 100644 --- a/project/src/HitTest.cpp +++ b/project/src/HitTest.cpp @@ -15,11 +15,84 @@ float max(float a, float b, float c) { float CrossZ(const Vector3& p0, const Vector3& p1, const Vector3& point) { - return (point.x - p0.x) * (p1.y - p0.y) - - (point.y - p0.y) * (p1.x - p0.x); + return (p1.x - p0.x) * (point.y - p0.y) + - (p1.y - p0.y) * (point.x - p0.x); } +template +Type interpolate(const Type& val0, const Type& val1, const Type& val2, float depth, float weight) +{ + return { + val0 * depth * weight + + val1 * depth * weight + + val2 * depth * weight + }; +} + +std::optional HitTest::TriangleHitTest(const Vector3& fragPos, const Vertex& v0, const Vertex& v1, const Vertex& v2) +{ + Vector3 weights; + + weights.x = CrossZ(v2.position, v1.position, fragPos); + if ( weights.x > 0 ) + return std::nullopt; + + weights.y = CrossZ(v0.position, v2.position, fragPos); + if ( weights.y > 0 ) + return std::nullopt; + + weights.z = CrossZ(v1.position, v0.position, fragPos); + if ( weights.z > 0 ) + return std::nullopt; + + const float totalWeight{ weights.x + weights.y + weights.z }; + + const float invTotalWeight{ 1.0f / totalWeight }; + const Vector3 normWeights{ + weights.x * invTotalWeight, + weights.y * invTotalWeight, + weights.z * invTotalWeight + }; + + const float depth = + 1 / (normWeights.x / v0.position.w + + normWeights.y / v1.position.w + + normWeights.z / v2.position.w); + + const Vector2 uv = + v0.uv * depth * normWeights.x / v0.position.w + + v1.uv * depth * normWeights.y / v1.position.w + + v2.uv * depth * normWeights.z / v2.position.w; + + const float interpolatedDepth = 1 / (normWeights.x / v0.position.w + normWeights.y / v1.position.w + normWeights.z / v2.position.w); + + auto interpolate = + [&](const Type& val0, const Type& val1, const Type& val2) -> Type + { + return (val0 / v0.position.w * normWeights.x + + val1 / v1.position.w * normWeights.y + + val2 / v2.position.w * normWeights.z) * interpolatedDepth; + + }; + + //Invert the normal + const Vector3 normal = interpolate(v0.normal, v1.normal, v2.normal).Normalized(); + const Vector3 tangent = interpolate(v0.tangent, v1.tangent, v2.tangent).Normalized(); + const Vector3 viewDir = interpolate(v0.viewDir, v1.viewDir, v2.viewDir).Normalized(); + + return Sample{ + .uv = uv, + .normal = normal, + .tangent = tangent, + .viewDirection = viewDir, + .depth = depth, + .weight = normWeights + }; +} + + + HitTest::HitTestSample HitTest::Triangle(const Vector3 &pos, const std::vector &vertices) { float minX = min(vertices[0].position.x, vertices[1].position.x, vertices[2].position.x); float maxX = max(vertices[0].position.x, vertices[1].position.x, vertices[2].position.x); @@ -46,9 +119,10 @@ HitTest::HitTestSample HitTest::Triangle(const Vector3 &pos, const std::vector(1.0f / totalWeight) }; + w0 *= invTotalWeight; + w1 *= invTotalWeight; + w2 *= invTotalWeight; assert(std::abs(w0 + w1 + w2 - 1) < 0.0001); diff --git a/project/src/HitTest.h b/project/src/HitTest.h index a1681f7..4fe4940 100644 --- a/project/src/HitTest.h +++ b/project/src/HitTest.h @@ -5,6 +5,7 @@ #ifndef GP1_RASTERIZER_HITTEST_H #define GP1_RASTERIZER_HITTEST_H +#include #include "DataTypes.h" #include "Maths.h" @@ -17,6 +18,9 @@ namespace HitTest { }; HitTestSample Triangle(const Vector3& pos, const std::vector& vertices); + + + std::optional TriangleHitTest(const Vector3& fragPos, const Vertex& v0, const Vertex& v1, const Vertex& v2); } #endif //GP1_RASTERIZER_HITTEST_H diff --git a/project/src/Matrix.cpp b/project/src/Matrix.cpp index 63720be..0eb4ccd 100644 --- a/project/src/Matrix.cpp +++ b/project/src/Matrix.cpp @@ -6,294 +6,281 @@ #include namespace dae { - Matrix::Matrix(const Vector3& xAxis, const Vector3& yAxis, const Vector3& zAxis, const Vector3& t) : - Matrix({ xAxis, 0 }, { yAxis, 0 }, { zAxis, 0 }, { t, 1 }) - { - } + Matrix::Matrix(const Vector3 &xAxis, const Vector3 &yAxis, const Vector3 &zAxis, const Vector3 &t) : + Matrix({xAxis, 0}, {yAxis, 0}, {zAxis, 0}, {t, 1}) { + } - Matrix::Matrix(const Vector4& xAxis, const Vector4& yAxis, const Vector4& zAxis, const Vector4& t) - { - data[0] = xAxis; - data[1] = yAxis; - data[2] = zAxis; - data[3] = t; - } + Matrix::Matrix(const Vector4 &xAxis, const Vector4 &yAxis, const Vector4 &zAxis, const Vector4 &t) { + data[0] = xAxis; + data[1] = yAxis; + data[2] = zAxis; + data[3] = t; + } - Matrix::Matrix(const Matrix& m) - { - data[0] = m[0]; - data[1] = m[1]; - data[2] = m[2]; - data[3] = m[3]; - } + Matrix::Matrix(const Matrix &m) { + data[0] = m[0]; + data[1] = m[1]; + data[2] = m[2]; + data[3] = m[3]; + } - Vector3 Matrix::TransformVector(const Vector3& v) const - { - return TransformVector(v.x, v.y, v.z); - } + Vector3 Matrix::TransformVector(const Vector3 &v) const { + return TransformVector(v.x, v.y, v.z); + } - Vector3 Matrix::TransformVector(float x, float y, float z) const - { - return Vector3{ - data[0].x * x + data[1].x * y + data[2].x * z, - data[0].y * x + data[1].y * y + data[2].y * z, - data[0].z * x + data[1].z * y + data[2].z * z - }; - } - - Vector3 Matrix::TransformPoint(const Vector3& p) const - { - return TransformPoint(p.x, p.y, p.z); - } - - Vector3 Matrix::TransformPoint(float x, float y, float z) const - { - return Vector3{ - data[0].x * x + data[1].x * y + data[2].x * z + data[3].x, - data[0].y * x + data[1].y * y + data[2].y * z + data[3].y, - data[0].z * x + data[1].z * y + data[2].z * z + data[3].z, - }; - } - - Vector4 Matrix::TransformPoint(const Vector4& p) const - { - return TransformPoint(p.x, p.y, p.z, p.w); - } - - Vector4 Matrix::TransformPoint(float x, float y, float z, float w) const - { - return Vector4{ - data[0].x * x + data[1].x * y + data[2].x * z + data[3].x, - data[0].y * x + data[1].y * y + data[2].y * z + data[3].y, - data[0].z * x + data[1].z * y + data[2].z * z + data[3].z, - data[0].w * x + data[1].w * y + data[2].w * z + data[3].w - }; - } - - const Matrix& Matrix::Transpose() - { - Matrix result{}; - for (int r{ 0 }; r < 4; ++r) - { - for (int c{ 0 }; c < 4; ++c) - { - result[r][c] = data[c][r]; - } - } - - data[0] = result[0]; - data[1] = result[1]; - data[2] = result[2]; - data[3] = result[3]; - - return *this; - } - - const Matrix& Matrix::Inverse() - { - //Optimized Inverse as explained in FGED1 - used widely in other libraries too. - const Vector3& a = data[0]; - const Vector3& b = data[1]; - const Vector3& c = data[2]; - const Vector3& d = data[3]; - - const float x = data[0][3]; - const float y = data[1][3]; - const float z = data[2][3]; - const float w = data[3][3]; - - Vector3 s = Vector3::Cross(a, b); - Vector3 t = Vector3::Cross(c, d); - Vector3 u = a * y - b * x; - Vector3 v = c * w - d * z; - - float det = Vector3::Dot(s, v) + Vector3::Dot(t, u); - assert((!AreEqual(det, 0.f)) && "ERROR: determinant is 0, there is no INVERSE!"); - float invDet = 1.f / det; - - s *= invDet; t *= invDet; u *= invDet; v *= invDet; - - Vector3 r0 = Vector3::Cross(b, v) + t * y; - Vector3 r1 = Vector3::Cross(v, a) - t * x; - Vector3 r2 = Vector3::Cross(d, u) + s * w; - Vector3 r3 = Vector3::Cross(u, c) - s * z; - - data[0] = Vector4{ r0.x, r1.x, r2.x, 0.f }; - data[1] = Vector4{ r0.y, r1.y, r2.y, 0.f }; - data[2] = Vector4{ r0.z, r1.z, r2.z, 0.f }; - data[3] = { { -Vector3::Dot(b, t)},{Vector3::Dot(a, t)},{-Vector3::Dot(d, s)},{Vector3::Dot(c, s)} }; - - return *this; - } - - Matrix Matrix::Transpose(const Matrix& m) - { - Matrix out{ m }; - out.Transpose(); - - return out; - } - - Matrix Matrix::Inverse(const Matrix& m) - { - Matrix out{ m }; - out.Inverse(); - - return out; - } - - Matrix Matrix::CreateLookAtLH(const Vector3& origin, const Vector3& forward, const Vector3& up) - { - Vector3 zAxis = forward.Normalized(); - Vector3 xAxis = Vector3::Cross(up, zAxis).Normalized(); - Vector3 yAxis = Vector3::Cross(zAxis, xAxis); - - return { xAxis, yAxis, zAxis, origin }; - } - - Matrix Matrix::CreatePerspectiveFovLH(float fov, float aspect, float zn, float zf) { - const float yScale = 1.f / tanf(fov / 2.f); - const float xScale = yScale / aspect; - - return { - { xScale, 0, 0, 0 }, - { 0, yScale, 0, 0 }, - { 0, 0, zf / (zf - zn), 1 }, - { 0, 0, -zn * zf / (zf - zn), 0 } + Vector3 Matrix::TransformVector(float x, float y, float z) const { + return Vector3{ + data[0].x * x + data[1].x * y + data[2].x * z, + data[0].y * x + data[1].y * y + data[2].y * z, + data[0].z * x + data[1].z * y + data[2].z * z }; } - Vector3 Matrix::GetAxisX() const - { - return data[0]; - } - Vector3 Matrix::GetAxisY() const - { - return data[1]; - } + Vector3 Matrix::TransformPoint(const Vector3 &p) const { + return TransformPoint(p.x, p.y, p.z); + } - Vector3 Matrix::GetAxisZ() const - { - return data[2]; - } + Vector3 Matrix::TransformPoint(float x, float y, float z) const { + return Vector3{ + data[0].x * x + data[1].x * y + data[2].x * z + data[3].x, + data[0].y * x + data[1].y * y + data[2].y * z + data[3].y, + data[0].z * x + data[1].z * y + data[2].z * z + data[3].z, + }; + } - Vector3 Matrix::GetTranslation() const - { - return data[3]; - } + Vector4 Matrix::TransformPoint(const Vector4 &p) const { + return TransformPoint(p.x, p.y, p.z, p.w); + } - Matrix Matrix::CreateTranslation(float x, float y, float z) - { - return CreateTranslation({ x, y, z }); - } + Vector4 Matrix::TransformPoint(float x, float y, float z, float w) const { + return Vector4{ + data[0].x * x + data[1].x * y + data[2].x * z + data[3].x, + data[0].y * x + data[1].y * y + data[2].y * z + data[3].y, + data[0].z * x + data[1].z * y + data[2].z * z + data[3].z, + data[0].w * x + data[1].w * y + data[2].w * z + data[3].w + }; + } - Matrix Matrix::CreateTranslation(const Vector3& t) - { - return { Vector3::UnitX, Vector3::UnitY, Vector3::UnitZ, t }; - } + const Matrix &Matrix::Transpose() { + Matrix result{}; + for (int r{0}; r < 4; ++r) { + for (int c{0}; c < 4; ++c) { + result[r][c] = data[c][r]; + } + } - Matrix Matrix::CreateRotationX(float pitch) - { - return { - {1, 0, 0, 0}, - {0, cos(pitch), -sin(pitch), 0}, - {0, sin(pitch), cos(pitch), 0}, - {0, 0, 0, 1} - }; - } + data[0] = result[0]; + data[1] = result[1]; + data[2] = result[2]; + data[3] = result[3]; - Matrix Matrix::CreateRotationY(float yaw) - { - return { - {cos(yaw), 0, -sin(yaw), 0}, - {0, 1, 0, 0}, - {sin(yaw), 0, cos(yaw), 0}, - {0, 0, 0, 1} - }; - } + return *this; + } - Matrix Matrix::CreateRotationZ(float roll) - { - return { - {cos(roll), sin(roll), 0, 0}, - {-sin(roll), cos(roll), 0, 0}, - {0, 0, 1, 0}, - {0, 0, 0, 1} - }; - } + const Matrix &Matrix::Inverse() { + //Optimized Inverse as explained in FGED1 - used widely in other libraries too. + const Vector3 &a = data[0]; + const Vector3 &b = data[1]; + const Vector3 &c = data[2]; + const Vector3 &d = data[3]; - Matrix Matrix::CreateRotation(float pitch, float yaw, float roll) - { - return CreateRotation({ pitch, yaw, roll }); - } + const float x = data[0][3]; + const float y = data[1][3]; + const float z = data[2][3]; + const float w = data[3][3]; - Matrix Matrix::CreateRotation(const Vector3& r) - { - return CreateRotationX(r[0]) * CreateRotationY(r[1]) * CreateRotationZ(r[2]); - } + Vector3 s = Vector3::Cross(a, b); + Vector3 t = Vector3::Cross(c, d); + Vector3 u = a * y - b * x; + Vector3 v = c * w - d * z; - Matrix Matrix::CreateScale(float sx, float sy, float sz) - { - return { {sx, 0, 0}, {0, sy, 0}, {0, 0, sz}, Vector3::Zero }; - } + float det = Vector3::Dot(s, v) + Vector3::Dot(t, u); + assert((!AreEqual(det, 0.f)) && "ERROR: determinant is 0, there is no INVERSE!"); + float invDet = 1.f / det; - Matrix Matrix::CreateScale(const Vector3& s) - { - return CreateScale(s[0], s[1], s[2]); - } + s *= invDet; + t *= invDet; + u *= invDet; + v *= invDet; + + Vector3 r0 = Vector3::Cross(b, v) + t * y; + Vector3 r1 = Vector3::Cross(v, a) - t * x; + Vector3 r2 = Vector3::Cross(d, u) + s * w; + Vector3 r3 = Vector3::Cross(u, c) - s * z; + + data[0] = Vector4{r0.x, r1.x, r2.x, 0.f}; + data[1] = Vector4{r0.y, r1.y, r2.y, 0.f}; + data[2] = Vector4{r0.z, r1.z, r2.z, 0.f}; + data[3] = {{-Vector3::Dot(b, t)}, + {Vector3::Dot(a, t)}, + {-Vector3::Dot(d, s)}, + {Vector3::Dot(c, s)}}; + + return *this; + } + + Matrix Matrix::Transpose(const Matrix &m) { + Matrix out{m}; + out.Transpose(); + + return out; + } + + Matrix Matrix::Inverse(const Matrix &m) { + Matrix out{m}; + out.Inverse(); + + return out; + } + + Matrix Matrix::CreateLookAtLH(const Vector3 &origin, const Vector3 &forward, const Vector3 &up) { + Vector3 zAxis = (forward - origin).Normalized(); + Vector3 xAxis = Vector3::Cross(up, zAxis).Normalized(); + Vector3 yAxis = Vector3::Cross(zAxis, xAxis).Normalized(); + Vector3 trans = + { + -Vector3::Dot(xAxis, origin), + -Vector3::Dot(yAxis, origin), + -Vector3::Dot(zAxis, origin) + }; + return { + {xAxis.x, yAxis.x, zAxis.x}, + {xAxis.y, yAxis.y, zAxis.y}, + {xAxis.z, yAxis.z, zAxis.z}, + {trans.x, trans.y, trans.z} + }; + } + + Matrix Matrix::CreatePerspectiveFovLH(float fovy, float aspect, float zn, float zf) { +// const float yScale = 1.f / tanf(fovy / 2.f); +// const float xScale = yScale / aspect; +// +// return { +// {xScale, 0, 0, 0}, +// {0, yScale, 0, 0}, +// {0, 0, zf / (zf - zn), 1}, +// {0, 0, -zn * zf / (zf - zn), 0} +// }; + + return Matrix( + { 1.f / (aspect * fovy), 0, 0, 0 }, + { 0, 1.f / fovy, 0, 0 }, + { 0, 0, (zf) / (zf - zn), 1 }, + { 0, 0, -(zf * zn) / (zf - zn), 0 } + ); + } + + Vector3 Matrix::GetAxisX() const { + return data[0]; + } + + Vector3 Matrix::GetAxisY() const { + return data[1]; + } + + Vector3 Matrix::GetAxisZ() const { + return data[2]; + } + + Vector3 Matrix::GetTranslation() const { + return data[3]; + } + + Matrix Matrix::CreateTranslation(float x, float y, float z) { + return CreateTranslation({x, y, z}); + } + + Matrix Matrix::CreateTranslation(const Vector3 &t) { + return {Vector3::UnitX, Vector3::UnitY, Vector3::UnitZ, t}; + } + + Matrix Matrix::CreateRotationX(float pitch) { + return { + {1, 0, 0, 0}, + {0, cos(pitch), -sin(pitch), 0}, + {0, sin(pitch), cos(pitch), 0}, + {0, 0, 0, 1} + }; + } + + Matrix Matrix::CreateRotationY(float yaw) { + return { + {cos(yaw), 0, -sin(yaw), 0}, + {0, 1, 0, 0}, + {sin(yaw), 0, cos(yaw), 0}, + {0, 0, 0, 1} + }; + } + + Matrix Matrix::CreateRotationZ(float roll) { + return { + {cos(roll), sin(roll), 0, 0}, + {-sin(roll), cos(roll), 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1} + }; + } + + Matrix Matrix::CreateRotation(float pitch, float yaw, float roll) { + return CreateRotation({pitch, yaw, roll}); + } + + Matrix Matrix::CreateRotation(const Vector3 &r) { + return CreateRotationX(r[0]) * CreateRotationY(r[1]) * CreateRotationZ(r[2]); + } + + Matrix Matrix::CreateScale(float sx, float sy, float sz) { + return {{sx, 0, 0}, {0, sy, 0}, {0, 0, sz}, Vector3::Zero}; + } + + Matrix Matrix::CreateScale(const Vector3 &s) { + return CreateScale(s[0], s[1], s[2]); + } #pragma region Operator Overloads - Vector4& Matrix::operator[](int index) - { - assert(index <= 3 && index >= 0); - return data[index]; - } - Vector4 Matrix::operator[](int index) const - { - assert(index <= 3 && index >= 0); - return data[index]; - } + Vector4 &Matrix::operator[](int index) { + assert(index <= 3 && index >= 0); + return data[index]; + } - Matrix Matrix::operator*(const Matrix& m) const - { - Matrix result{}; - Matrix m_transposed = Transpose(m); + Vector4 Matrix::operator[](int index) const { + assert(index <= 3 && index >= 0); + return data[index]; + } - for (int r{ 0 }; r < 4; ++r) - { - for (int c{ 0 }; c < 4; ++c) - { - result[r][c] = Vector4::Dot(data[r], m_transposed[c]); - } - } + Matrix Matrix::operator*(const Matrix &m) const { + Matrix result{}; + Matrix m_transposed = Transpose(m); - return result; - } + for (int r{0}; r < 4; ++r) { + for (int c{0}; c < 4; ++c) { + result[r][c] = Vector4::Dot(data[r], m_transposed[c]); + } + } - const Matrix& Matrix::operator*=(const Matrix& m) - { - Matrix copy{ *this }; - Matrix m_transposed = Transpose(m); + return result; + } - for (int r{ 0 }; r < 4; ++r) - { - for (int c{ 0 }; c < 4; ++c) - { - data[r][c] = Vector4::Dot(copy[r], m_transposed[c]); - } - } + const Matrix &Matrix::operator*=(const Matrix &m) { + Matrix copy{*this}; + Matrix m_transposed = Transpose(m); - return *this; - } + for (int r{0}; r < 4; ++r) { + for (int c{0}; c < 4; ++c) { + data[r][c] = Vector4::Dot(copy[r], m_transposed[c]); + } + } - bool Matrix::operator==(const Matrix& m) const - { - return data[0] == m.data[0] - && data[1] == m.data[1] - && data[2] == m.data[2] - && data[3] == m.data[3]; - } + return *this; + } + + bool Matrix::operator==(const Matrix &m) const { + return data[0] == m.data[0] + && data[1] == m.data[1] + && data[2] == m.data[2] + && data[3] == m.data[3]; + } #pragma endregion } \ No newline at end of file diff --git a/project/src/Renderer.cpp b/project/src/Renderer.cpp index 94d37e1..2ebbabd 100644 --- a/project/src/Renderer.cpp +++ b/project/src/Renderer.cpp @@ -1,11 +1,14 @@ //External includes #include +#include +#include #include "SDL_surface.h" //Project includes #include "Renderer.h" #include "Maths.h" #include "HitTest.h" +#include "utils.h" using namespace dae; @@ -21,13 +24,16 @@ Renderer::Renderer(SDL_Window *pWindow) : m_pWindow(pWindow) { m_pDepthBufferPixels = new float[m_Width * m_Height]; //Initialize Camera - m_Camera.Initialize(60.f, {.0f, .0f, -10.f}); - - //Initialize Camera - m_Camera.Initialize(60.f, {.0f, .0f, -10.f}); m_aspectRatio = static_cast(m_Width) / static_cast(m_Height); + m_Camera.Initialize(45.f, {.0f, 5.0f, -64.f}, m_aspectRatio); - m_pTexture = Texture::LoadFromFile("./Resources/uv_grid_2.png"); + m_currentDiffuse = Texture::LoadFromFile("./Resources/vehicle_diffuse.png"); + m_currentGloss = Texture::LoadFromFile("./Resources/vehicle_gloss.png"); + m_currentNormal = Texture::LoadFromFile("./Resources/vehicle_normal.png"); + m_currentSpecular = Texture::LoadFromFile("./Resources/vehicle_specular.png"); + + Utils::ParseOBJ("./Resources/vehicle.obj", m_mesh.vertices, m_mesh.indices, false); + m_worldMeshes.push_back(m_mesh); } Renderer::~Renderer() { @@ -36,6 +42,11 @@ Renderer::~Renderer() { void Renderer::Update(Timer *pTimer) { m_Camera.Update(pTimer); + + //Rotate the mesh + if(m_isRotating){ + m_worldMeshes[0].worldMatrix = Matrix::CreateRotationY(SDL_GetTicks() / 1000.f); + } } void Renderer::Render() { @@ -54,10 +65,11 @@ void Renderer::Render() { constexpr int numVerticies = 3; std::vector verticiesScreenSpace{}; -// VertexTransformationFunction(m_verticiesWorld, verticiesScreenSpace); - for (const Mesh ¤tMesh: m_meshesWorldStrip) { - VertexTransformationFunction(currentMesh.vertices, verticiesScreenSpace); + for (const Mesh ¤tMesh: m_worldMeshes) { + + const Matrix worldViewProjectionMatrix{ currentMesh.worldMatrix * m_Camera.viewMatrix * m_Camera.ProjectionMatrix }; + VertexTransformationFunction(worldViewProjectionMatrix, currentMesh, currentMesh.vertices, verticiesScreenSpace); int numTriangles{}; @@ -97,6 +109,10 @@ void Renderer::Render() { } break; } + if (!vertex0.valid and !vertex1.valid and !vertex2.valid) { + continue; + } + const float minX{std::min(vertex0.position.x, std::min(vertex1.position.x, vertex2.position.x))}; const float minY{std::min(vertex0.position.y, std::min(vertex1.position.y, vertex2.position.y))}; @@ -127,44 +143,31 @@ void Renderer::Render() { Vector3 P{static_cast(px) + 0.5f, static_cast(py) + 0.5f, 1.f}; - const Vector3 cross0{Vector3::Cross(vertex2.position - vertex1.position, P - vertex1.position)}; - if (cross0.z < 0) { - continue; - } - const Vector3 cross1{Vector3::Cross(vertex0.position - vertex2.position, P - vertex2.position)}; - if (cross1.z < 0) { - continue; - } - const Vector3 cross2{Vector3::Cross(vertex1.position - vertex0.position, P - vertex0.position)}; - if (cross2.z < 0) { + auto sample{ HitTest::TriangleHitTest(P, vertex0, vertex1, vertex2) }; + + if (!sample.has_value()) { continue; } - const int depthBufferIndex{px + (py * m_Width)}; + const Vector3 fragPos{ static_cast(px) + 0.5f, static_cast(py) + 0.5f, 1.f }; - float totalWeight{cross0.z + cross1.z + cross2.z}; - Vector3 weights{ - cross0.z / totalWeight, - cross1.z / totalWeight, - cross2.z / totalWeight, - }; + int depthBufferIndex{ px + (py * m_Width) }; - const float currentDepth = - 1 / (weights.x / vertex0.position.z + - weights.y / vertex1.position.z + - weights.z / vertex2.position.z); + float min{.985f}; + float max{1.f}; + float depthBuffer{(sample.value().depth - min) * (max - min)}; + float currentDepth = sample.value().depth; - const Vector2 UvCoords = - vertex0.uv * currentDepth * weights.x / vertex0.position.z + - vertex1.uv * currentDepth * weights.y / vertex1.position.z + - vertex2.uv * currentDepth * weights.z / vertex2.position.z; - - - if (m_pDepthBufferPixels[depthBufferIndex] >= currentDepth) { + if (m_pDepthBufferPixels[depthBufferIndex] > currentDepth) { m_pDepthBufferPixels[depthBufferIndex] = currentDepth; - //Update Color in Buffer - finalColor = m_pTexture->Sample(UvCoords); + if (m_isDepthBuffer) { + finalColor = ColorRGB{depthBuffer, depthBuffer, depthBuffer}; + } else { +// finalColor = m_pTexture->Sample(UvCoords); + finalColor = shadePixel(sample.value()); + } + finalColor.MaxToOne(); m_pBackBufferPixels[px + (py * m_Width)] = SDL_MapRGB(m_pBackBuffer->format, @@ -174,28 +177,14 @@ void Renderer::Render() { } + + } } } } - int x, y{0}; - //For loop over all the pixels - for (int px{}; px < 100; ++px) { - for (int py{}; py < 100; ++py) { - //Get the pixel position - x = px; - y = py; - ColorRGB test = m_pTexture->Sample(Vector2{static_cast(px) / 100, static_cast(py) / 100}); - m_pBackBufferPixels[x + (y * m_Width)] = SDL_MapRGB(m_pBackBuffer->format, - static_cast(test.r * 255), - static_cast(test.g * 255), - static_cast(test.b * 255)); - - } - - } //RENDER LOGIC @@ -205,20 +194,117 @@ void Renderer::Render() { SDL_UpdateWindowSurface(m_pWindow); } -void Renderer::VertexTransformationFunction(const std::vector &vertices_in, +void Renderer::VertexTransformationFunction(const Matrix& WorldViewProjectionMatrix, const Mesh& mesh, const std::vector &vertices_in, std::vector &vertices_out) const { - for (const Vertex &vert: vertices_in) { - Vector3 vertPos{m_Camera.invViewMatrix.TransformPoint(vert.position)}; +// vertices_out.clear(); + for (const Vertex& vert : vertices_in) + { + Vertex vertex_out{}; - vertPos.x = (vertPos.x / vertPos.z) / (m_aspectRatio * m_Camera.fov); - vertPos.y = (vertPos.y / vertPos.z) / m_Camera.fov; + Vector4 vertPos{ WorldViewProjectionMatrix.TransformPoint({vert.position.GetXYZ(), 1}) }; - vertPos.x = (vertPos.x + 1) / 2 * static_cast(m_Width); - vertPos.y = (1 - vertPos.y) / 2 * static_cast(m_Height); - vertices_out.push_back(Vertex{vertPos, vert.color, vert.uv}); + const Vector3 normal{ mesh.worldMatrix.TransformVector(vert.normal) }; + const Vector3 tangent{ mesh.worldMatrix.TransformVector(vert.tangent) }; + + + + vertPos.x /= vertPos.w; + vertPos.y /= vertPos.w; + vertPos.z /= vertPos.w; + + bool isValid{ true }; + + //Check if the vertex is inside the screen + if (vertPos.x < -1.f || vertPos.x > 1.f || + vertPos.y < -1.f || vertPos.y > 1.f || + vertPos.z < 0.f || vertPos.z > 1.f) + isValid = false; + + vertPos.x = ((vertPos.x + 1.f) / 2.f) * static_cast(m_Width); + vertPos.y = ((1.f - vertPos.y) / 2.f) * static_cast(m_Height); + + vertex_out.position = vertPos; + vertex_out.color = vert.color; + vertex_out.uv = vert.uv; + vertex_out.normal = normal; + vertex_out.tangent = tangent; + vertex_out.viewDir = WorldViewProjectionMatrix.TransformVector(vert.viewDir); + vertex_out.valid = isValid; + + vertices_out.push_back(vertex_out); } + } bool Renderer::SaveBufferToImage() const { return SDL_SaveBMP(m_pBackBuffer, "Rasterizer_ColorBuffer.bmp"); } + +ColorRGB Renderer::shadePixel(const Sample &sample) { + Vector3 lightDirection = { .577f, -.577f, .577f}; + Vector3 normal = sample.normal.Normalized(); + constexpr float lightIntensity{ 7.f }; + + ColorRGB color{ 1, 1, 1 }; + constexpr ColorRGB ambient{ .03f, .03f, .03f}; + + if(m_useNormals){ + const ColorRGB normalSample{ m_currentNormal->Sample(sample.uv) }; + const Vector4 normalMapSample{ + 2.f * normalSample.r - 1.f, + 2.f * normalSample.g - 1.f, + 2.f * normalSample.b - 1.f, + 0.f + }; + + const Vector3 biNormal{ Vector3::Cross(normal, sample.tangent) }; + const Matrix tangentToWorld{ + Vector4{ sample.tangent, 0.f }, + Vector4{ biNormal, 0.f }, + Vector4{ normal, 0.f }, + Vector4{ 0.f, 0.f, 0.f, 1.f } + }; + normal = tangentToWorld.TransformVector(normalMapSample).Normalized(); + } + + + const ColorRGB diffuseSample{ m_currentDiffuse->Sample(sample.uv) }; + double invPi = 1.0 / PI; + const ColorRGB lambert{ diffuseSample * lightIntensity * invPi }; + + //TODO: ask why deviding by PI causses Segmentation fault +// const ColorRGB lambert{ diffuseSample * lightIntensity / PI }; + + + float specularReflectance{ 1.f }; + float shininess{ 25.f }; + + specularReflectance *= m_currentGloss->Sample(sample.uv).r; + shininess *= m_currentSpecular->Sample(sample.uv).r; + + const float cosAngle = Vector3::Dot(normal, -lightDirection); + + const ColorRGB specular = specularReflectance * powf(cosAngle, shininess) * colors::White; + + if (cosAngle < 0) { + return ambient; + } + + switch(m_ShadeMode){ + case ShadeMode::ObservedArea: + break; + case ShadeMode::Diffuse: + color = lambert; + break; + case ShadeMode::Specular: + color = specular; + break; + case ShadeMode::Combined: + color = lambert + specular + ambient; + break; + } + + color *= ColorRGB{ cosAngle, cosAngle, cosAngle }; + + return color; +} diff --git a/project/src/Renderer.h b/project/src/Renderer.h index 77b1d41..f81117b 100644 --- a/project/src/Renderer.h +++ b/project/src/Renderer.h @@ -11,8 +11,16 @@ struct SDL_Window; struct SDL_Surface; + namespace dae { + enum class ShadeMode{ + ObservedArea, + Diffuse, + Specular, + Combined + }; + struct Mesh; class Timer; @@ -40,7 +48,48 @@ namespace dae { bool SaveBufferToImage() const; void - VertexTransformationFunction(const std::vector &vertices_in, std::vector &vertices_out) const; + VertexTransformationFunction(const Matrix& WorldViewProjectionMatrix, const Mesh& mesh, const std::vector &vertices_in, std::vector &vertices_out) const; + + ColorRGB shadePixel(const Sample& sample); + + void SwitchDepthBuffer(){ + m_isDepthBuffer = !m_isDepthBuffer; + } + + void SetShadeMode(ShadeMode mode) { + m_ShadeMode = mode; + } + + void ToggleRotation(){ + m_isRotating = !m_isRotating; + std::cout << "Rotation: " << m_isRotating << std::endl; + } + + void ToggleNormals(){ + m_useNormals = !m_useNormals; + std::cout << "Use Normals: " << m_useNormals << std::endl; + } + + void CycleRenderingMode(){ + switch (m_ShadeMode) { + case ShadeMode::ObservedArea: + m_ShadeMode = ShadeMode::Diffuse; + std::cout << "Diffuse" << std::endl; + break; + case ShadeMode::Diffuse: + m_ShadeMode = ShadeMode::Specular; + std::cout << "Specular" << std::endl; + break; + case ShadeMode::Specular: + m_ShadeMode = ShadeMode::Combined; + std::cout << "Combined" << std::endl; + break; + case ShadeMode::Combined: + m_ShadeMode = ShadeMode::ObservedArea; + std::cout << "Observed Area" << std::endl; + break; + } + } private: SDL_Window *m_pWindow{}; @@ -56,83 +105,22 @@ namespace dae { float m_aspectRatio{}; - Texture* m_pTexture{ nullptr }; -// -// std::vector m_verticiesWorld{ -// //Triangle 0 -// {{0.f,2.f,0.f}, {1,0,0}}, -// {{1.5f,-1.f,0.f}, {1,0,0}}, -// {{-1.5f,-1.f,0.f}, {1,0,0}}, -// -// //Triangle 1 -// {{0.f,4.f,2.f}, {1,0,0}}, -// {{3.f,-2.f,2.f}, {0,1,0}}, -// {{-3.f,-2.f,2.f}, {0,0,1}} -// }; + Texture* m_currentDiffuse{ nullptr }; + Texture* m_currentGloss{ nullptr }; + Texture* m_currentNormal{ nullptr }; + Texture* m_currentSpecular{ nullptr }; - std::vector m_meshesWorldList - { - Mesh - { - { - Vertex{ {-3, 3, -2 }, { 1, 1, 1 }, { 0.0f, 0.0f } }, - Vertex{ { 0, 3, -2 }, { 1, 1, 1 }, { 0.5f, 0.0f } }, - Vertex{ { 3, 3, -2 }, { 1, 1, 1 }, { 1.0f, 0.0f } }, - Vertex{ {-3, 0, -2 }, { 1, 1, 1 }, { 0.0f, 0.5f } }, - Vertex{ { 0, 0, -2 }, { 1, 1, 1 }, { 0.5f, 0.5f } }, - Vertex{ { 3, 0, -2 }, { 1, 1, 1 }, { 1.0f, 0.5f } }, - Vertex{ {-3, -3, -2 }, { 1, 1, 1 }, { 0.0f, 1.0f } }, - Vertex{ { 0, -3, -2 }, { 1, 1, 1 }, { 0.5f, 1.0f } }, - Vertex{ { 3, -3, -2 }, { 1, 1, 1 }, { 1.0f, 1.0f } } - }, - { - 3, 0, 1, 1, 4, 3, 4, 1 ,2, - 2, 5, 4, 6, 3, 4, 4, 7, 6, - 7, 4, 5, 5, 8, 7 - }, - PrimitiveTopology::TriangleList - } - }; - std::vector m_meshesWorldStrip - { - Mesh - { - std::vector{ - {{-3, 3, -2}, {colors::White}, {0,0} }, - {{0, 3, -2}, {colors::Red}, {0.5,0}}, - {{3, 3, -2}, {colors::Blue}, {1,0}}, - {{-3, 0, -2}, {colors::Red}, {0,0.5}}, - {{0, 0, -2}, {colors::Yellow}, {0.5, 0.5}}, - {{3, 0, -2}, {colors::White}, {1, 0.5}}, - {{-3, -3, -2}, {colors::White}, {0,1}}, - {{0, -3, -2}, {colors::White}, {0.5,1}}, - {{3, -3, -2}, {colors::White}, {1,1}}, - }, - std::vector{ - 3, 0, 4, 1, 5, 2, - 2, 6, - 6, 3, 7, 4, 8, 5, - }, - PrimitiveTopology::TriangleStrip, - } - }; - //square - std::vector testMesh{ - Mesh{ - std::vector{ - {{-1, 1, 0}, {colors::White}, {0, 0}}, - {{1, 1, 0}, {colors::White}, {1, 0}}, - {{-1, -1, 0}, {colors::White}, {0, 1}}, - {{1, -1, 0}, {colors::White}, {1, 1}}, - }, - std::vector{ - 0, 1, 2, 1, 3, 2 - }, - PrimitiveTopology::TriangleList - } - }; -// std::vector m_verticies_screenSpace{}; + ShadeMode m_ShadeMode{ ShadeMode::Combined }; + bool m_isRotating{ true }; + bool m_useNormals{ true }; + + Mesh m_mesh{}; + + bool m_isDepthBuffer{ false }; + + + std::vector m_worldMeshes{}; float* m_pDepthBufferPixels{}; diff --git a/project/src/Utils.h b/project/src/Utils.h index 6fe2a7b..d4ddbd4 100644 --- a/project/src/Utils.h +++ b/project/src/Utils.h @@ -4,7 +4,7 @@ #include "Maths.h" #include "DataTypes.h" -#define DISABLE_OBJ +//#define DISABLE_OBJ namespace dae { @@ -84,7 +84,7 @@ namespace dae { // OBJ format uses 1-based arrays file >> iPosition; - vertex.position = positions[iPosition - 1]; + vertex.position = positions[iPosition - 1].ToVector4(); if ('/' == file.peek())//is next in buffer == '/' ? { diff --git a/project/src/Vector4.h b/project/src/Vector4.h index 08235df..3bab6ed 100644 --- a/project/src/Vector4.h +++ b/project/src/Vector4.h @@ -6,10 +6,10 @@ namespace dae struct Vector3; struct Vector4 { - float x; - float y; - float z; - float w; + float x{}; + float y{}; + float z{}; + float w{}; Vector4() = default; Vector4(float _x, float _y, float _z, float _w); @@ -20,8 +20,8 @@ namespace dae float Normalize(); Vector4 Normalized() const; - Vector2 GetXY() const; - Vector3 GetXYZ() const; + [[nodiscard]] Vector2 GetXY() const; + [[nodiscard]] Vector3 GetXYZ() const; static float Dot(const Vector4& v1, const Vector4& v2); diff --git a/project/src/main.cpp b/project/src/main.cpp index 2fd9604..6d7e345 100644 --- a/project/src/main.cpp +++ b/project/src/main.cpp @@ -2,8 +2,10 @@ #ifdef ENABLE_VLD #include "vld.h" #endif + #include "SDL.h" #include "SDL_surface.h" + #undef main //Standard includes @@ -15,17 +17,15 @@ using namespace dae; -void ShutDown(SDL_Window* pWindow) -{ +void ShutDown(SDL_Window *pWindow) { SDL_DestroyWindow(pWindow); SDL_Quit(); } -int main(int argc, char* args[]) -{ +int main(int argc, char *args[]) { //Unreferenced parameters - (void)argc; - (void)args; + (void) argc; + (void) args; //Create window + surfaces SDL_Init(SDL_INIT_VIDEO); @@ -33,8 +33,8 @@ int main(int argc, char* args[]) const uint32_t width = 640; const uint32_t height = 480; - SDL_Window* pWindow = SDL_CreateWindow( - "Rasterizer - **Insert Name**", + SDL_Window *pWindow = SDL_CreateWindow( + "Rasterizer - Bram Verhulst", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, 0); @@ -55,14 +55,11 @@ int main(int argc, char* args[]) float printTimer = 0.f; bool isLooping = true; bool takeScreenshot = false; - while (isLooping) - { + while (isLooping) { //--------- Get input events --------- SDL_Event e; - while (SDL_PollEvent(&e)) - { - switch (e.type) - { + while (SDL_PollEvent(&e)) { + switch (e.type) { case SDL_QUIT: isLooping = false; break; @@ -70,6 +67,21 @@ int main(int argc, char* args[]) if (e.key.keysym.scancode == SDL_SCANCODE_X) takeScreenshot = true; break; + case SDL_KEYDOWN: { + auto key = e.key.keysym.scancode; + if (key == SDL_SCANCODE_F4) { + pRenderer->SwitchDepthBuffer(); + } + if (key == SDL_SCANCODE_F5) { + pRenderer->ToggleRotation(); + } + if (key == SDL_SCANCODE_F6) { + pRenderer->ToggleNormals(); + } + if (key == SDL_SCANCODE_F7) { + pRenderer->CycleRenderingMode(); + } + } } } @@ -82,15 +94,13 @@ int main(int argc, char* args[]) //--------- Timer --------- pTimer->Update(); printTimer += pTimer->GetElapsed(); - if (printTimer >= 1.f) - { + if (printTimer >= 1.f) { printTimer = 0.f; std::cout << "dFPS: " << pTimer->GetdFPS() << std::endl; } //Save screenshot after full render - if (takeScreenshot) - { + if (takeScreenshot) { if (!pRenderer->SaveBufferToImage()) std::cout << "Screenshot saved!" << std::endl; else