Rasterizer - Full File-by-File Documentation (Beginner + Pro)
This page is intentionally detailed and educational. Each source file is displayed completely, then explained in short professional paragraphs so beginners can understand what the file does, why it exists, and how it connects to the full software rendering pipeline.
Use this as both a learning guide and a submission-quality documentation reference for the assignment milestones.
include/Maths/vector3.h
Vector3 header. Defines the 3D vector type used everywhere in the renderer. It provides basic algebra operations, normalization, and helper methods that are required by transforms, lighting, and camera movement.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include <cmath>
#include <iostream>
class Vector3
{
public:
Vector3() : x(0.0f), y(0.0f), z(0.0f) {}
Vector3(float x, float y, float z) : x(x), y(y), z(z) {}
float x, y, z;
static Vector3 SetFromComponents(float x, float y, float z);
static Vector3 SetFrom2Points(Vector3 p1, Vector3 p2);
static Vector3 Opposite(Vector3 v);
static Vector3 MidPoint(Vector3 p1, Vector3 p2);
static float Distance(Vector3 p1, Vector3 p2);
static Vector3 Add(Vector3 v1, Vector3 v2);
static float SquaredNorm(Vector3 v);
static float Norm(Vector3 v);
static float DotProduct(Vector3 v1, Vector3 v2);
static Vector3 CrossProduct(Vector3 v1, Vector3 v2);
static float GetAngle(Vector3 v1, Vector3 v2);
static Vector3 Translate(Vector3 p, Vector3 t);
static Vector3 Scale(Vector3 p, float a, Vector3 anchor);
static Vector3 RotateAroundAxis(Vector3 p, Vector3 axis, float theta, Vector3 anchor);
float GetMagnitude() const;
void Normalize();
Vector3 Normalized() const;
Vector3 operator+(const Vector3& other) const;
Vector3 operator-(const Vector3& other) const;
Vector3 operator*(float scalar) const;
Vector3 operator/(float scalar) const;
Vector3 operator-() const;
Vector3& operator+=(const Vector3& other);
Vector3& operator-=(const Vector3& other);
Vector3& operator*=(float scalar);
Vector3& operator/=(float scalar);
bool operator==(const Vector3& other) const;
bool operator!=(const Vector3& other) const;
void Print() const;
};
src/Maths/vector3.cpp
Vector3 implementation. Implements vector math operators and helper functions. This file is performance-sensitive because these operations are called many times per frame.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Maths/vector3.h"
Vector3 Vector3::SetFromComponents(float x, float y, float z)
{
return Vector3(x, y, z);
}
Vector3 Vector3::SetFrom2Points(Vector3 p1, Vector3 p2)
{
return Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
}
Vector3 Vector3::Opposite(Vector3 v)
{
return Vector3(-v.x, -v.y, -v.z);
}
Vector3 Vector3::MidPoint(Vector3 p1, Vector3 p2)
{
return Vector3(
(p1.x + p2.x) * 0.5f,
(p1.y + p2.y) * 0.5f,
(p1.z + p2.z) * 0.5f
);
}
float Vector3::Distance(Vector3 p1, Vector3 p2)
{
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
float dz = p2.z - p1.z;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
Vector3 Vector3::Add(Vector3 v1, Vector3 v2)
{
return Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
}
float Vector3::SquaredNorm(Vector3 v)
{
return v.x * v.x + v.y * v.y + v.z * v.z;
}
float Vector3::Norm(Vector3 v)
{
return std::sqrt(SquaredNorm(v));
}
float Vector3::DotProduct(Vector3 v1, Vector3 v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
Vector3 Vector3::CrossProduct(Vector3 v1, Vector3 v2)
{
return Vector3(
v1.y * v2.z - v1.z * v2.y,
v1.z * v2.x - v1.x * v2.z,
v1.x * v2.y - v1.y * v2.x
);
}
float Vector3::GetAngle(Vector3 v1, Vector3 v2)
{
float norm1 = Norm(v1);
float norm2 = Norm(v2);
if (norm1 == 0.0f || norm2 == 0.0f)
return 0.0f;
float cosTheta = DotProduct(v1, v2) / (norm1 * norm2);
if (cosTheta > 1.0f) cosTheta = 1.0f;
if (cosTheta < -1.0f) cosTheta = -1.0f;
return std::acos(cosTheta);
}
Vector3 Vector3::Translate(Vector3 p, Vector3 t)
{
return Add(p, t);
}
Vector3 Vector3::Scale(Vector3 p, float a, Vector3 anchor)
{
if (a == 0.0f) return anchor;
if (a == 1.0f) return p;
return Vector3(
anchor.x + (p.x - anchor.x) * a,
anchor.y + (p.y - anchor.y) * a,
anchor.z + (p.z - anchor.z) * a
);
}
Vector3 Vector3::RotateAroundAxis(Vector3 p, Vector3 axis, float theta, Vector3 anchor)
{
Vector3 v(p.x - anchor.x, p.y - anchor.y, p.z - anchor.z);
float axisNorm = Norm(axis);
if (axisNorm == 0.0f) return p;
Vector3 k(axis.x / axisNorm, axis.y / axisNorm, axis.z / axisNorm);
float cosT = std::cos(theta);
float sinT = std::sin(theta);
Vector3 cross = CrossProduct(k, v);
float dot = DotProduct(k, v);
Vector3 rotated(
v.x * cosT + cross.x * sinT + k.x * dot * (1.0f - cosT),
v.y * cosT + cross.y * sinT + k.y * dot * (1.0f - cosT),
v.z * cosT + cross.z * sinT + k.z * dot * (1.0f - cosT)
);
return Vector3(anchor.x + rotated.x, anchor.y + rotated.y, anchor.z + rotated.z);
}
float Vector3::GetMagnitude() const
{
return std::sqrt(x * x + y * y + z * z);
}
void Vector3::Normalize()
{
float mag = GetMagnitude();
if (mag > 0.0f) {
x /= mag;
y /= mag;
z /= mag;
}
}
Vector3 Vector3::Normalized() const
{
Vector3 result(*this);
result.Normalize();
return result;
}
Vector3 Vector3::operator+(const Vector3& other) const
{
return Vector3(x + other.x, y + other.y, z + other.z);
}
Vector3 Vector3::operator-(const Vector3& other) const
{
return Vector3(x - other.x, y - other.y, z - other.z);
}
Vector3 Vector3::operator*(float scalar) const
{
return Vector3(x * scalar, y * scalar, z * scalar);
}
Vector3 Vector3::operator/(float scalar) const
{
if (scalar == 0.0f) return *this;
return Vector3(x / scalar, y / scalar, z / scalar);
}
Vector3 Vector3::operator-() const
{
return Vector3(-x, -y, -z);
}
Vector3& Vector3::operator+=(const Vector3& other)
{
x += other.x;
y += other.y;
z += other.z;
return *this;
}
Vector3& Vector3::operator-=(const Vector3& other)
{
x -= other.x;
y -= other.y;
z -= other.z;
return *this;
}
Vector3& Vector3::operator*=(float scalar)
{
x *= scalar;
y *= scalar;
z *= scalar;
return *this;
}
Vector3& Vector3::operator/=(float scalar)
{
if (scalar != 0.0f) {
x /= scalar;
y /= scalar;
z /= scalar;
}
return *this;
}
bool Vector3::operator==(const Vector3& other) const
{
return (x == other.x && y == other.y && z == other.z);
}
bool Vector3::operator!=(const Vector3& other) const
{
return !(*this == other);
}
void Vector3::Print() const
{
std::cout << "(" << x << ", " << y << ", " << z << ")" << std::endl;
}
Vector3 operator*(float scalar, const Vector3& vec)
{
return vec * scalar;
}
include/Maths/vector4.h
Vector4 header. Represents homogeneous coordinates (x,y,z,w). This is mandatory for transformation pipelines and perspective divide.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include <cmath>
#include <iostream>
class Vector3;
class Vector4
{
public:
Vector4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) {}
Vector4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {}
Vector4(const Vector3& vec3, float w = 1.0f);
float x, y, z, w;
static Vector4 SetFromComponents(float x, float y, float z, float w);
static Vector4 SetFrom2Points(Vector4 p1, Vector4 p2);
static Vector4 Opposite(Vector4 v);
static Vector4 MidPoint(Vector4 p1, Vector4 p2);
static float Distance(Vector4 p1, Vector4 p2);
static Vector4 Add(Vector4 v1, Vector4 v2);
static float SquaredNorm(Vector4 v);
static float Norm(Vector4 v);
static float DotProduct(Vector4 v1, Vector4 v2);
static float GetAngle(Vector4 v1, Vector4 v2);
static Vector4 Translate(Vector4 p, Vector4 t);
static Vector4 Scale(Vector4 p, float a, Vector4 anchor);
static Vector4 RotateInPlane(Vector4 p, int axis1, int axis2, float theta, Vector4 anchor);
void Homogenize();
float GetMagnitude() const;
void Normalize();
Vector4 Normalized() const;
Vector3 ToVector3() const;
Vector4 operator+(const Vector4& other) const;
Vector4 operator-(const Vector4& other) const;
Vector4 operator*(float scalar) const;
Vector4 operator/(float scalar) const;
Vector4 operator-() const;
Vector4& operator+=(const Vector4& other);
Vector4& operator-=(const Vector4& other);
Vector4& operator*=(float scalar);
Vector4& operator/=(float scalar);
bool operator==(const Vector4& other) const;
bool operator!=(const Vector4& other) const;
float& operator[](int index);
const float& operator[](int index) const;
void Print() const;
};
Vector4 operator*(float scalar, const Vector4& vec);
src/Maths/vector4.cpp
Vector4 implementation. Contains homogenization and normalization logic. Homogenization is the key step after matrix projection in Step 5.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Maths/vector4.h"
#include "Maths/vector3.h"
Vector4::Vector4(const Vector3& vec3, float _w)
: x(vec3.x), y(vec3.y), z(vec3.z), w(_w)
{
}
Vector4 Vector4::SetFromComponents(float x, float y, float z, float w)
{
return Vector4(x, y, z, w);
}
Vector4 Vector4::SetFrom2Points(Vector4 p1, Vector4 p2)
{
return Vector4(
p2.x - p1.x,
p2.y - p1.y,
p2.z - p1.z,
p2.w - p1.w
);
}
Vector4 Vector4::Opposite(Vector4 v)
{
return Vector4(-v.x, -v.y, -v.z, -v.w);
}
Vector4 Vector4::MidPoint(Vector4 p1, Vector4 p2)
{
return Vector4(
(p1.x + p2.x) * 0.5f,
(p1.y + p2.y) * 0.5f,
(p1.z + p2.z) * 0.5f,
(p1.w + p2.w) * 0.5f
);
}
float Vector4::Distance(Vector4 p1, Vector4 p2)
{
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
float dz = p2.z - p1.z;
float dw = p2.w - p1.w;
return std::sqrt(dx * dx + dy * dy + dz * dz + dw * dw);
}
Vector4 Vector4::Add(Vector4 v1, Vector4 v2)
{
return Vector4(
v1.x + v2.x,
v1.y + v2.y,
v1.z + v2.z,
v1.w + v2.w
);
}
float Vector4::SquaredNorm(Vector4 v)
{
return v.x * v.x + v.y * v.y + v.z * v.z + v.w * v.w;
}
float Vector4::Norm(Vector4 v)
{
return std::sqrt(SquaredNorm(v));
}
float Vector4::DotProduct(Vector4 v1, Vector4 v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z + v1.w * v2.w;
}
float Vector4::GetAngle(Vector4 v1, Vector4 v2)
{
float n1 = Norm(v1);
float n2 = Norm(v2);
if (n1 == 0.0f || n2 == 0.0f)
return 0.0f;
float cosTheta = DotProduct(v1, v2) / (n1 * n2);
if (cosTheta > 1.0f) cosTheta = 1.0f;
if (cosTheta < -1.0f) cosTheta = -1.0f;
return std::acos(cosTheta);
}
Vector4 Vector4::Translate(Vector4 p, Vector4 t)
{
return Add(p, t);
}
Vector4 Vector4::Scale(Vector4 p, float a, Vector4 anchor)
{
if (a == 0.0f) return anchor;
if (a == 1.0f) return p;
return Vector4(
anchor.x + (p.x - anchor.x) * a,
anchor.y + (p.y - anchor.y) * a,
anchor.z + (p.z - anchor.z) * a,
anchor.w + (p.w - anchor.w) * a
);
}
Vector4 Vector4::RotateInPlane(Vector4 p, int axis1, int axis2, float theta, Vector4 anchor)
{
if (axis1 < 0 || axis1 > 3 || axis2 < 0 || axis2 > 3 || axis1 == axis2)
return p;
float coords[4] = {
p.x - anchor.x,
p.y - anchor.y,
p.z - anchor.z,
p.w - anchor.w
};
float cosT = std::cos(theta);
float sinT = std::sin(theta);
float c1 = coords[axis1];
float c2 = coords[axis2];
coords[axis1] = c1 * cosT - c2 * sinT;
coords[axis2] = c1 * sinT + c2 * cosT;
return Vector4(
anchor.x + coords[0],
anchor.y + coords[1],
anchor.z + coords[2],
anchor.w + coords[3]
);
}
void Vector4::Homogenize()
{
if (w != 0.0f)
{
x /= w;
y /= w;
z /= w;
w = 1.0f;
}
}
float Vector4::GetMagnitude() const
{
return std::sqrt(x * x + y * y + z * z + w * w);
}
void Vector4::Normalize()
{
float mag = GetMagnitude();
if (mag > 0.0f) {
x /= mag;
y /= mag;
z /= mag;
w /= mag;
}
}
Vector4 Vector4::Normalized() const
{
Vector4 result(*this);
result.Normalize();
return result;
}
Vector3 Vector4::ToVector3() const
{
if (w != 0.0f)
return Vector3(x / w, y / w, z / w);
return Vector3(x, y, z);
}
Vector4 Vector4::operator+(const Vector4& other) const
{
return Vector4(x + other.x, y + other.y, z + other.z, w + other.w);
}
Vector4 Vector4::operator-(const Vector4& other) const
{
return Vector4(x - other.x, y - other.y, z - other.z, w - other.w);
}
Vector4 Vector4::operator*(float scalar) const
{
return Vector4(x * scalar, y * scalar, z * scalar, w * scalar);
}
Vector4 Vector4::operator/(float scalar) const
{
if (scalar == 0.0f) return *this;
return Vector4(x / scalar, y / scalar, z / scalar, w / scalar);
}
Vector4 Vector4::operator-() const
{
return Vector4(-x, -y, -z, -w);
}
Vector4& Vector4::operator+=(const Vector4& other)
{
x += other.x;
y += other.y;
z += other.z;
w += other.w;
return *this;
}
Vector4& Vector4::operator-=(const Vector4& other)
{
x -= other.x;
y -= other.y;
z -= other.z;
w -= other.w;
return *this;
}
Vector4& Vector4::operator*=(float scalar)
{
x *= scalar;
y *= scalar;
z *= scalar;
w *= scalar;
return *this;
}
Vector4& Vector4::operator/=(float scalar)
{
if (scalar != 0.0f) {
x /= scalar;
y /= scalar;
z /= scalar;
w /= scalar;
}
return *this;
}
bool Vector4::operator==(const Vector4& other) const
{
return (x == other.x && y == other.y && z == other.z && w == other.w);
}
bool Vector4::operator!=(const Vector4& other) const
{
return !(*this == other);
}
float& Vector4::operator[](int index)
{
switch (index) {
case 0: return x;
case 1: return y;
case 2: return z;
case 3: return w;
default: throw std::out_of_range("Vector4 index out of range");
}
}
const float& Vector4::operator[](int index) const
{
switch (index) {
case 0: return x;
case 1: return y;
case 2: return z;
case 3: return w;
default: throw std::out_of_range("Vector4 index out of range");
}
}
void Vector4::Print() const
{
std::cout << "(" << x << ", " << y << ", " << z << ", " << w << ")" << std::endl;
}
Vector4 operator*(float scalar, const Vector4& vec)
{
return vec * scalar;
}
include/Maths/matrix4.h
Matrix4 header. Defines 4x4 matrix storage and multiplication operators for matrix-matrix and matrix-vector operations.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Maths/vector4.h"
#include "Maths/vector3.h"
#include <cmath>
class Matrix4
{
public:
Matrix4();
Matrix4(const float* data);
float m[16];
static Matrix4 Identity();
static Matrix4 Zero();
static Matrix4 CreateTranslationMatrix(const Vector3& translation);
static Matrix4 CreateScaleMatrix(const Vector3& scale);
static Matrix4 CreateXRotationMatrix(float angleDeg);
static Matrix4 CreateYRotationMatrix(float angleDeg);
static Matrix4 CreateZRotationMatrix(float angleDeg);
static Matrix4 CreateRotationMatrix(const Vector3& eulerAnglesDeg);
static Matrix4 CreateTransformMatrix(const Vector3& rotation,
const Vector3& position,
const Vector3& scale);
Matrix4 Transpose() const;
Matrix4 Inverse() const;
float Determinant() const;
Matrix4 operator*(const Matrix4& other) const;
Vector4 operator*(const Vector4& vec) const;
Matrix4 operator*(float scalar) const;
Matrix4 operator+(const Matrix4& other) const;
Matrix4 operator-(const Matrix4& other) const;
Matrix4& operator*=(const Matrix4& other);
Matrix4& operator*=(float scalar);
bool operator==(const Matrix4& other) const;
bool operator!=(const Matrix4& other) const;
float& operator()(int row, int col);
const float& operator()(int row, int col) const;
float& operator[](int index);
const float& operator[](int index) const;
void SetIdentity();
void SetZero();
void Print() const;
const float* Data() const { return m; }
};
Matrix4 operator*(float scalar, const Matrix4& mat);
src/Maths/matrix4.cpp
Matrix4 implementation. Implements transformation matrix builders (translation, rotation, scale, composed transform). This powers entity transforms and camera projection flow.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Maths/matrix4.h"
#include <iostream>
Matrix4::Matrix4()
{
SetIdentity();
}
Matrix4::Matrix4(const float* data)
{
for (int i = 0; i < 16; ++i)
m[i] = data[i];
}
Matrix4 Matrix4::Identity()
{
return Matrix4();
}
Matrix4 Matrix4::Zero()
{
Matrix4 mat;
mat.SetZero();
return mat;
}
void Matrix4::SetIdentity()
{
for (int i = 0; i < 16; ++i)
m[i] = (i % 5 == 0) ? 1.0f : 0.0f;
}
void Matrix4::SetZero()
{
for (int i = 0; i < 16; ++i)
m[i] = 0.0f;
}
Matrix4 Matrix4::CreateTranslationMatrix(const Vector3& translation)
{
Matrix4 mat;
mat.m[12] = translation.x;
mat.m[13] = translation.y;
mat.m[14] = translation.z;
return mat;
}
Matrix4 Matrix4::CreateScaleMatrix(const Vector3& scale)
{
Matrix4 mat;
mat.m[0] = scale.x;
mat.m[5] = scale.y;
mat.m[10] = scale.z;
return mat;
}
Matrix4 Matrix4::CreateXRotationMatrix(float angleDeg)
{
float rad = angleDeg * 3.14159265359f / 180.0f;
float c = std::cos(rad);
float s = std::sin(rad);
Matrix4 mat;
mat.m[5] = c;
mat.m[6] = s;
mat.m[9] = -s;
mat.m[10] = c;
return mat;
}
Matrix4 Matrix4::CreateYRotationMatrix(float angleDeg)
{
float rad = angleDeg * 3.14159265359f / 180.0f;
float c = std::cos(rad);
float s = std::sin(rad);
Matrix4 mat;
mat.m[0] = c;
mat.m[2] = -s;
mat.m[8] = s;
mat.m[10] = c;
return mat;
}
Matrix4 Matrix4::CreateZRotationMatrix(float angleDeg)
{
float rad = angleDeg * 3.14159265359f / 180.0f;
float c = std::cos(rad);
float s = std::sin(rad);
Matrix4 mat;
mat.m[0] = c;
mat.m[1] = s;
mat.m[4] = -s;
mat.m[5] = c;
return mat;
}
Matrix4 Matrix4::CreateRotationMatrix(const Vector3& eulerAnglesDeg)
{
Matrix4 rotY = CreateYRotationMatrix(eulerAnglesDeg.y);
Matrix4 rotX = CreateXRotationMatrix(eulerAnglesDeg.x);
Matrix4 rotZ = CreateZRotationMatrix(eulerAnglesDeg.z);
return rotY * rotX * rotZ;
}
Matrix4 Matrix4::CreateTransformMatrix(const Vector3& rotation,
const Vector3& position,
const Vector3& scale)
{
Matrix4 matTranslation = CreateTranslationMatrix(position);
Matrix4 matRotY = CreateYRotationMatrix(rotation.y);
Matrix4 matRotX = CreateXRotationMatrix(rotation.x);
Matrix4 matRotZ = CreateZRotationMatrix(rotation.z);
Matrix4 matScale = CreateScaleMatrix(scale);
return matTranslation * matRotY * matRotX * matRotZ * matScale;
}
Matrix4 Matrix4::Transpose() const
{
Matrix4 result;
for (int row = 0; row < 4; ++row)
for (int col = 0; col < 4; ++col)
result.m[row * 4 + col] = m[col * 4 + row];
return result;
}
Matrix4 Matrix4::operator*(const Matrix4& other) const
{
Matrix4 result;
result.SetZero();
for (int col = 0; col < 4; ++col)
{
for (int row = 0; row < 4; ++row)
{
float sum = 0.0f;
for (int k = 0; k < 4; ++k)
{
sum += m[k * 4 + row] * other.m[col * 4 + k];
}
result.m[col * 4 + row] = sum;
}
}
return result;
}
Vector4 Matrix4::operator*(const Vector4& vec) const
{
float x = m[0] * vec.x + m[4] * vec.y + m[8] * vec.z + m[12] * vec.w;
float y = m[1] * vec.x + m[5] * vec.y + m[9] * vec.z + m[13] * vec.w;
float z = m[2] * vec.x + m[6] * vec.y + m[10] * vec.z + m[14] * vec.w;
float w = m[3] * vec.x + m[7] * vec.y + m[11] * vec.z + m[15] * vec.w;
return Vector4(x, y, z, w);
}
Matrix4 Matrix4::operator*(float scalar) const
{
Matrix4 result;
for (int i = 0; i < 16; ++i)
result.m[i] = m[i] * scalar;
return result;
}
Matrix4 Matrix4::operator+(const Matrix4& other) const
{
Matrix4 result;
for (int i = 0; i < 16; ++i)
result.m[i] = m[i] + other.m[i];
return result;
}
Matrix4 Matrix4::operator-(const Matrix4& other) const
{
Matrix4 result;
for (int i = 0; i < 16; ++i)
result.m[i] = m[i] - other.m[i];
return result;
}
Matrix4& Matrix4::operator*=(const Matrix4& other)
{
*this = *this * other;
return *this;
}
Matrix4& Matrix4::operator*=(float scalar)
{
for (int i = 0; i < 16; ++i)
m[i] *= scalar;
return *this;
}
bool Matrix4::operator==(const Matrix4& other) const
{
for (int i = 0; i < 16; ++i)
if (m[i] != other.m[i]) return false;
return true;
}
bool Matrix4::operator!=(const Matrix4& other) const
{
return !(*this == other);
}
float& Matrix4::operator()(int row, int col)
{
return m[col * 4 + row];
}
const float& Matrix4::operator()(int row, int col) const
{
return m[col * 4 + row];
}
float& Matrix4::operator[](int index)
{
return m[index];
}
const float& Matrix4::operator[](int index) const
{
return m[index];
}
void Matrix4::Print() const
{
std::cout << "Matrix4:\n";
for (int row = 0; row < 4; ++row)
{
std::cout << "[ ";
for (int col = 0; col < 4; ++col)
{
std::cout << m[col * 4 + row] << " ";
}
std::cout << "]\n";
}
}
Matrix4 operator*(float scalar, const Matrix4& mat)
{
return mat * scalar;
}
include/Rasterizer/vertex.h
Vertex data structure. Holds per-vertex attributes. In the assignment evolution, this grows from position-only to color, normals, and UV.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Maths/vector3.h"
#include "Rasterizer/color.h"
class Vertex
{
public:
Vertex() = default;
explicit Vertex(const Vector3& position, const Color& color = Color(255, 255, 255, 255))
: position(position)
, color(color)
{
}
Vector3 position;
Color color = Color(255, 255, 255, 255);
};
include/Rasterizer/mesh.h
Mesh data structure. Defines immutable vertex/index buffers and factory helpers (cube/sphere). The mesh is shared by entities for memory efficiency.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Rasterizer/vertex.h"
#include <cstdint>
#include <vector>
class Mesh
{
public:
Mesh() = default;
Mesh(std::vector<Vertex> vertices, std::vector<uint32_t> indices)
: vertices_(std::move(vertices))
, indices_(std::move(indices))
{
}
static Mesh CreateCube(float halfExtent = 0.5f);
const std::vector<Vertex>& GetVertices() const { return vertices_; }
const std::vector<uint32_t>& GetIndices() const { return indices_; }
private:
std::vector<Vertex> vertices_;
std::vector<uint32_t> indices_;
};
src/Rasterizer/mesh.cpp
Mesh generation. Contains procedural geometry generation. This is where triangle winding and normals are controlled for culling and lighting.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Rasterizer/mesh.h"
Mesh Mesh::CreateCube(float h)
{
std::vector<Vertex> vertices = {
Vertex(Vector3(-h, -h, -h)), Vertex(Vector3(h, -h, -h)), Vertex(Vector3(h, h, -h)), Vertex(Vector3(-h, h, -h)),
Vertex(Vector3(-h, -h, h)), Vertex(Vector3(h, -h, h)), Vertex(Vector3(h, h, h)), Vertex(Vector3(-h, h, h)),
};
std::vector<uint32_t> indices = {
// back
0, 2, 1, 0, 3, 2,
// front
4, 5, 6, 4, 6, 7,
// left
0, 7, 3, 0, 4, 7,
// right
1, 2, 6, 1, 6, 5,
// bottom
0, 1, 5, 0, 5, 4,
// top
3, 7, 6, 3, 6, 2,
};
return Mesh(std::move(vertices), std::move(indices));
}
include/Rasterizer/entity.h
Entity data structure. Represents one renderable instance in the world. It stores transform and rendering properties while referencing shared mesh data.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Maths/matrix4.h"
#include "Maths/vector3.h"
#include "Rasterizer/color.h"
#include "Rasterizer/mesh.h"
class Entity
{
public:
Entity() = default;
explicit Entity(Mesh* mesh)
: mesh_(mesh)
{
}
Mesh* GetMesh() const { return mesh_; }
void SetMesh(Mesh* mesh) { mesh_ = mesh; }
Matrix4 GetModelMatrix() const
{
return Matrix4::CreateTransformMatrix(rotationDeg_, position_, scale_);
}
const Vector3& GetPosition() const { return position_; }
const Vector3& GetRotationDeg() const { return rotationDeg_; }
const Vector3& GetScale() const { return scale_; }
void SetPosition(const Vector3& position) { position_ = position; }
void SetRotationDeg(const Vector3& rotationDeg) { rotationDeg_ = rotationDeg; }
void SetScale(const Vector3& scale) { scale_ = scale; }
const Color& GetColor() const { return color_; }
void SetColor(const Color& color) { color_ = color; }
private:
Mesh* mesh_ = nullptr;
Vector3 position_ = Vector3(0.0f, 0.0f, 0.0f);
Vector3 rotationDeg_ = Vector3(0.0f, 0.0f, 0.0f);
Vector3 scale_ = Vector3(1.0f, 1.0f, 1.0f);
Color color_ = Color(200, 220, 255);
};
include/Rasterizer/scene.h
Scene container. Stores entities and lights. It is the main render input object passed to the software rasterizer.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Rasterizer/entity.h"
#include <vector>
class Scene
{
public:
void AddEntity(const Entity& entity) { entities_.push_back(entity); }
std::vector<Entity>& GetEntities() { return entities_; }
const std::vector<Entity>& GetEntities() const { return entities_; }
private:
std::vector<Entity> entities_;
};
include/Rasterizer/texture.h
Texture / framebuffer. Represents both render targets and sampled textures. It owns pixel memory and pixel write/read helpers.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Rasterizer/color.h"
#include <vector>
struct SDL_Surface;
struct Pixel
{
int x = 0;
int y = 0;
Color color = Color(255, 255, 255);
};
class Texture
{
public:
Texture() = default;;
Texture(unsigned int width, unsigned int height);
~Texture();
void ClearByColor(const Color& color);
void SetPixelColor(const Pixel& pixel);
unsigned int GetWidth() const;
unsigned int GetHeight() const;
Color GetPixelColor(int x, int y);
uint32_t* GetPixels();
const uint32_t* GetPixels() const;
private:
unsigned int width;
unsigned int height;
std::vector<uint32_t> pixels;
};
src/Rasterizer/texture.cpp
Texture implementation. Implements texture allocation, clear, and pixel access. This is critical for correctness and memory safety.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Rasterizer/texture.h"
Texture::Texture(unsigned int _width, unsigned int _height)
: width(_width)
, height(_height)
, pixels(width* height, Color(0, 0, 0).ToUint32())
{
}
Texture::~Texture()
{
pixels.clear();
}
void Texture::ClearByColor(const Color& color)
{
std::fill(pixels.begin(), pixels.end(), color.ToUint32());
}
void Texture::SetPixelColor(const Pixel& pixel)
{
if (pixel.x < 0 || pixel.y < 0)
return;
if (pixel.x >= static_cast<int>(width) || pixel.y >= static_cast<int>(height))
return;
const uint32_t color = pixel.color.ToUint32();
const size_t index = pixel.y * width + pixel.x;
pixels[index] = color;
}
unsigned int Texture::GetWidth() const
{
return width;
}
unsigned int Texture::GetHeight() const
{
return height;
}
Color Texture::GetPixelColor(int x, int y)
{
uint32_t color = pixels[y * width + x];
return Color(color);
}
uint32_t* Texture::GetPixels()
{
return pixels.data();
}
const uint32_t* Texture::GetPixels() const
{
return pixels.data();
}
include/Rasterizer/window.h
SDL window wrapper. Abstracts SDL setup, input polling, and framebuffer presentation. Keeps platform code out of rasterizer core.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include <SDL2/SDL.h>
#include <string>
#include <vector>
#include "Rasterizer/texture.h"
class Window
{
public:
Window();
bool Init(const std::string& title, unsigned int width, unsigned int height);
void Destroy();
void UpdateInputs();
void Clear(const Color& color);
void PresentTexture(const Texture& texture);
void SwapBuffers();
bool ShouldClose() const;
private:
unsigned int width = 1024;
unsigned int height = 720;
bool shouldClose = false;
SDL_Window* window = nullptr;
SDL_Renderer* renderer = nullptr;
SDL_Event events;
Texture frameBuffer;
void PrintError(const std::string& message) const;
};
src/Rasterizer/window.cpp
SDL window implementation. Implements init/update/present lifecycle and error handling. This is the bridge between software pixels and screen output.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Rasterizer/window.h"
#include <iostream>
Window::Window()
: frameBuffer()
{
}
bool Window::Init(const std::string& title, unsigned int _width, unsigned int _height)
{
width = _width;
height = _height;
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
PrintError("SDL initialization failed.");
return false;
}
window = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_SHOWN);
if (window == NULL)
{
PrintError("Window creation failed.");
Destroy();
return false;
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == nullptr)
{
PrintError("Renderer creation failed.");
Destroy();
return false;
}
frameBuffer = Texture(width, height);
return true;
}
void Window::Destroy()
{
if (renderer)
SDL_DestroyRenderer(renderer);
if (window)
SDL_DestroyWindow(window);
SDL_Quit();
}
void Window::UpdateInputs()
{
SDL_PollEvent(&events);
switch (events.type)
{
case SDL_QUIT:
shouldClose = true;
break;
}
}
void Window::Clear(const Color& color)
{
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderClear(renderer);
frameBuffer.ClearByColor(color);
}
void Window::PresentTexture(const Texture& texture)
{
const unsigned int width = texture.GetWidth();
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormatFrom(
(void*)texture.GetPixels(),
width,
texture.GetHeight(),
32,
4 * width,
SDL_PIXELFORMAT_RGBA32);
if (!surface) {
PrintError("Surface creation failed.");
return;
}
SDL_Texture* sdlTexture = SDL_CreateTextureFromSurface(renderer, surface);
if (!sdlTexture) {
SDL_FreeSurface(surface);
PrintError("Texture creation failed.");
return;
}
SDL_RenderCopy(renderer, sdlTexture, nullptr, nullptr);
SDL_RenderPresent(renderer);
SDL_DestroyTexture(sdlTexture);
SDL_FreeSurface(surface);
}
void Window::SwapBuffers()
{
PresentTexture(frameBuffer);
}
bool Window::ShouldClose() const
{
return shouldClose;
}
void Window::PrintError(const std::string& message) const
{
std::cout << message << " SDL_Error: " << SDL_GetError() << std::endl;
}
include/Rasterizer/rasterizer.h
Rasterizer interface. Declares RenderScene and projection helpers. This is the heart of the assignment and should stay clean and testable.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#pragma once
#include "Rasterizer/scene.h"
#include "Rasterizer/texture.h"
#include "Rasterizer/vertex.h"
class Rasterizer
{
public:
void RenderScene(Scene* scene, Texture* target);
};
src/Rasterizer/rasterizer.cpp
Rasterizer implementation. Contains triangle setup, rasterization, interpolation, and depth handling. This file grows through most assignment steps.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Rasterizer/rasterizer.h"
#include "Maths/vector4.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
namespace {
struct ScreenVertex {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};
float Edge(const ScreenVertex& a, const ScreenVertex& b, float px, float py)
{
return (px - a.x) * (b.y - a.y) - (py - a.y) * (b.x - a.x);
}
bool ProjectVertex(const Vector3& worldPos, unsigned width, unsigned height, ScreenVertex& out)
{
const float nearPlane = 0.1f;
const float fovScale = 700.0f;
const float z = worldPos.z + 3.0f; // simple camera offset
if (z <= nearPlane) {
return false;
}
out.x = (worldPos.x / z) * fovScale + static_cast<float>(width) * 0.5f;
out.y = (-worldPos.y / z) * fovScale + static_cast<float>(height) * 0.5f;
out.z = z;
return true;
}
void DrawFilledTriangle(Texture* target,
std::vector<float>& depthBuffer,
const ScreenVertex& v0,
const ScreenVertex& v1,
const ScreenVertex& v2,
const Color& c0,
const Color& c1,
const Color& c2)
{
const int width = static_cast<int>(target->GetWidth());
const int height = static_cast<int>(target->GetHeight());
const float minXf = std::floor(std::min({ v0.x, v1.x, v2.x }));
const float maxXf = std::ceil(std::max({ v0.x, v1.x, v2.x }));
const float minYf = std::floor(std::min({ v0.y, v1.y, v2.y }));
const float maxYf = std::ceil(std::max({ v0.y, v1.y, v2.y }));
const int minX = std::max(0, static_cast<int>(minXf));
const int maxX = std::min(width - 1, static_cast<int>(maxXf));
const int minY = std::max(0, static_cast<int>(minYf));
const int maxY = std::min(height - 1, static_cast<int>(maxYf));
const float area = Edge(v0, v1, v2.x, v2.y);
if (std::abs(area) < 1e-6f) {
return;
}
for (int y = minY; y <= maxY; ++y) {
for (int x = minX; x <= maxX; ++x) {
const float px = static_cast<float>(x) + 0.5f;
const float py = static_cast<float>(y) + 0.5f;
const float w0 = Edge(v1, v2, px, py);
const float w1 = Edge(v2, v0, px, py);
const float w2 = Edge(v0, v1, px, py);
const bool inside = (area > 0.0f)
? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f)
: (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f);
if (!inside) {
continue;
}
const float alpha = w0 / area;
const float beta = w1 / area;
const float gamma = w2 / area;
const float depth = alpha * v0.z + beta * v1.z + gamma * v2.z;
const size_t idx = static_cast<size_t>(y * width + x);
if (depth < depthBuffer[idx]) {
depthBuffer[idx] = depth;
const float rf = alpha * static_cast<float>(c0.r) + beta * static_cast<float>(c1.r) + gamma * static_cast<float>(c2.r);
const float gf = alpha * static_cast<float>(c0.g) + beta * static_cast<float>(c1.g) + gamma * static_cast<float>(c2.g);
const float bf = alpha * static_cast<float>(c0.b) + beta * static_cast<float>(c1.b) + gamma * static_cast<float>(c2.b);
const float af = alpha * static_cast<float>(c0.a) + beta * static_cast<float>(c1.a) + gamma * static_cast<float>(c2.a);
const Color interpolated(
static_cast<unsigned char>(std::clamp(rf, 0.0f, 255.0f)),
static_cast<unsigned char>(std::clamp(gf, 0.0f, 255.0f)),
static_cast<unsigned char>(std::clamp(bf, 0.0f, 255.0f)),
static_cast<unsigned char>(std::clamp(af, 0.0f, 255.0f)));
target->SetPixelColor(Pixel{ x, y, interpolated });
}
}
}
}
} // namespace
void Rasterizer::RenderScene(Scene* scene, Texture* target)
{
if (!scene || !target) {
return;
}
const unsigned width = target->GetWidth();
const unsigned height = target->GetHeight();
std::vector<float> depthBuffer(width * height, std::numeric_limits<float>::max());
for (const Entity& entity : scene->GetEntities()) {
Mesh* mesh = entity.GetMesh();
if (!mesh) {
continue;
}
const Matrix4 model = entity.GetModelMatrix();
const auto& vertices = mesh->GetVertices();
const auto& indices = mesh->GetIndices();
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const Vertex& a = vertices[indices[i]];
const Vertex& b = vertices[indices[i + 1]];
const Vertex& c = vertices[indices[i + 2]];
const Vector4 wa = model * Vector4(a.position, 1.0f);
const Vector4 wb = model * Vector4(b.position, 1.0f);
const Vector4 wc = model * Vector4(c.position, 1.0f);
ScreenVertex sv0, sv1, sv2;
if (!ProjectVertex(wa.ToVector3(), width, height, sv0)) continue;
if (!ProjectVertex(wb.ToVector3(), width, height, sv1)) continue;
if (!ProjectVertex(wc.ToVector3(), width, height, sv2)) continue;
DrawFilledTriangle(target, depthBuffer, sv0, sv1, sv2, a.color, b.color, c.color);
}
}
}
src/main.cpp
Application entry point. Builds demo scenes and runs the render loop. It is used to validate each assignment milestone interactively.
This file is shown in full below so beginners can compare expected structure with their own implementation and understand exactly how each piece fits into the renderer.
#include "Rasterizer/window.h"
#include "Rasterizer/rasterizer.h"
#include "Rasterizer/scene.h"
#include <SDL2/SDL.h>
int main(int argc, char* args[])
{
Window window;
if (!window.Init("Software Rasterizer", 1024, 768)) {
return 1;
}
Texture frameBuffer(1024, 768);
Scene scene;
Rasterizer rasterizer;
// Step 1 reference triangle: RGB interpolation in screen space around z=2.
std::vector<Vertex> triVertices = {
Vertex(Vector3(-0.5f, -0.5f, 2.0f), Color(255, 0, 0, 255)),
Vertex(Vector3(0.5f, -0.5f, 2.0f), Color(0, 255, 0, 255)),
Vertex(Vector3(0.0f, 0.5f, 2.0f), Color(0, 0, 255, 255)),
};
std::vector<uint32_t> triIndices = { 0, 1, 2 };
Mesh triangleMesh(std::move(triVertices), std::move(triIndices));
Entity triangle(&triangleMesh);
scene.AddEntity(triangle);
while (!window.ShouldClose()) {
window.UpdateInputs();
frameBuffer.ClearByColor(Color(18, 18, 24));
rasterizer.RenderScene(&scene, &frameBuffer);
window.Clear(Color(0, 0, 0));
window.PresentTexture(frameBuffer);
SDL_Delay(16);
}
window.Destroy();
return 0;
}